MCP server for tangled

fix: clean up error messages and handle malformed issues

- add _extract_error_message() to produce concise error output
- skip issues with missing issueId in list_repo_issues
- truncate verbose exception details (HTTP headers, etc.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+28 -8
+22 -7
src/tangled_mcp/_tangled/_client.py
··· 8 8 from tangled_mcp.settings import TANGLED_DID, settings 9 9 10 10 11 + def _extract_error_message(e: Exception) -> str: 12 + """extract a clean, concise error message from an exception""" 13 + # handle atproto Response objects with XrpcError 14 + if hasattr(e, "content") and hasattr(e.content, "message"): 15 + return e.content.message 16 + # handle httpx errors 17 + if hasattr(e, "response") and hasattr(e.response, "text"): 18 + return e.response.text[:200] # truncate long responses 19 + # fallback to string but limit length 20 + msg = str(e) 21 + if len(msg) > 200: 22 + return msg[:200] + "..." 23 + return msg 24 + 25 + 11 26 def resolve_repo_identifier(owner_slash_repo: str) -> tuple[str, str]: 12 27 """resolve owner/repo format to (knot, did/repo) for tangled XRPC 13 28 ··· 44 59 ) 45 60 owner_did = response.did 46 61 except Exception as e: 47 - raise ValueError(f"failed to resolve handle '{owner}': {e}") from e 62 + # extract clean error message 63 + msg = _extract_error_message(e) 64 + raise ValueError(f"failed to resolve handle '{owner}': {msg}") from e 48 65 49 66 # query owner's repo collection to find repo and get knot 50 67 try: ··· 56 73 ) 57 74 ) 58 75 except Exception as e: 59 - raise ValueError(f"failed to list repos for '{owner}': {e}") from e 76 + msg = _extract_error_message(e) 77 + raise ValueError(f"failed to list repos for '{owner}': {msg}") from e 60 78 61 79 # find repo with matching name and extract knot 62 80 for record in records.records: ··· 86 104 try: 87 105 client.login(settings.tangled_handle, settings.tangled_password) 88 106 except Exception as e: 89 - raise RuntimeError( 90 - f"failed to authenticate with handle '{settings.tangled_handle}'. " 91 - f"verify TANGLED_HANDLE and TANGLED_PASSWORD are correct. " 92 - f"error: {e}" 93 - ) from e 107 + msg = _extract_error_message(e) 108 + raise RuntimeError(f"auth failed for '{settings.tangled_handle}': {msg}") from e 94 109 95 110 return client 96 111
+6 -1
src/tangled_mcp/types/_issues.py
··· 85 85 Returns: 86 86 ListIssuesResult with parsed issues 87 87 """ 88 - issues = [IssueInfo(**issue_data) for issue_data in response.get("issues", [])] 88 + issues = [] 89 + for issue_data in response.get("issues", []): 90 + # skip malformed issues (e.g., missing issueId) 91 + if issue_data.get("issueId") is None: 92 + continue 93 + issues.append(IssueInfo(**issue_data)) 89 94 return cls(issues=issues)