MCP server for tangled

remove cursor from MCP tool interface

- cursor is an implementation detail, not useful for LLM workflows
- LLMs don't naturally paginate - they want all results or a filtered subset
- keep cursor parameter in internal functions for future use
- remove from:
- list_repo_branches tool signature
- list_repo_issues tool signature
- ListBranchesResult response type
- ListIssuesResult response type
- update tests to not check cursor field

Changed files
+8 -28
src
tangled_mcp
tests
+4 -8
src/tangled_mcp/server.py
··· 53 limit: Annotated[ 54 int, Field(ge=1, le=100, description="maximum number of branches to return") 55 ] = 50, 56 - cursor: Annotated[str | None, Field(description="pagination cursor")] = None, 57 ) -> ListBranchesResult: 58 """list branches for a repository 59 60 Args: 61 repo: repository identifier in 'owner/repo' format (e.g., 'zzstoatzz/tangled-mcp') 62 limit: maximum number of branches to return (1-100) 63 - cursor: optional pagination cursor 64 65 Returns: 66 - list of branches with optional cursor for pagination 67 """ 68 # resolve owner/repo to (knot, did/repo) 69 knot, repo_id = _tangled.resolve_repo_identifier(repo) 70 - response = _tangled.list_branches(knot, repo_id, limit, cursor) 71 72 return ListBranchesResult.from_api_response(response) 73 ··· 188 limit: Annotated[ 189 int, Field(ge=1, le=100, description="maximum number of issues to return") 190 ] = 20, 191 - cursor: Annotated[str | None, Field(description="pagination cursor")] = None, 192 ) -> ListIssuesResult: 193 """list issues for a repository 194 195 Args: 196 repo: repository identifier in 'owner/repo' format 197 limit: maximum number of issues to return (1-100) 198 - cursor: optional pagination cursor 199 200 Returns: 201 - ListIssuesResult with list of issues and optional cursor 202 """ 203 # resolve owner/repo to (knot, did/repo) 204 _, repo_id = _tangled.resolve_repo_identifier(repo) 205 # list_repo_issues doesn't need knot (queries atproto records, not XRPC) 206 - response = _tangled.list_repo_issues(repo_id, limit, cursor) 207 208 return ListIssuesResult.from_api_response(response) 209
··· 53 limit: Annotated[ 54 int, Field(ge=1, le=100, description="maximum number of branches to return") 55 ] = 50, 56 ) -> ListBranchesResult: 57 """list branches for a repository 58 59 Args: 60 repo: repository identifier in 'owner/repo' format (e.g., 'zzstoatzz/tangled-mcp') 61 limit: maximum number of branches to return (1-100) 62 63 Returns: 64 + list of branches 65 """ 66 # resolve owner/repo to (knot, did/repo) 67 knot, repo_id = _tangled.resolve_repo_identifier(repo) 68 + response = _tangled.list_branches(knot, repo_id, limit, cursor=None) 69 70 return ListBranchesResult.from_api_response(response) 71 ··· 186 limit: Annotated[ 187 int, Field(ge=1, le=100, description="maximum number of issues to return") 188 ] = 20, 189 ) -> ListIssuesResult: 190 """list issues for a repository 191 192 Args: 193 repo: repository identifier in 'owner/repo' format 194 limit: maximum number of issues to return (1-100) 195 196 Returns: 197 + ListIssuesResult with list of issues 198 """ 199 # resolve owner/repo to (knot, did/repo) 200 _, repo_id = _tangled.resolve_repo_identifier(repo) 201 # list_repo_issues doesn't need knot (queries atproto records, not XRPC) 202 + response = _tangled.list_repo_issues(repo_id, limit, cursor=None) 203 204 return ListIssuesResult.from_api_response(response) 205
+2 -4
src/tangled_mcp/types/_branches.py
··· 16 """result of listing branches""" 17 18 branches: list[BranchInfo] 19 - cursor: str | None = None 20 21 @classmethod 22 def from_api_response(cls, response: dict[str, Any]) -> "ListBranchesResult": ··· 28 "branches": [ 29 {"reference": {"name": "main", "hash": "abc123"}}, 30 ... 31 - ], 32 - "cursor": "optional_cursor" 33 } 34 35 Returns: ··· 46 ) 47 ) 48 49 - return cls(branches=branches, cursor=response.get("cursor"))
··· 16 """result of listing branches""" 17 18 branches: list[BranchInfo] 19 20 @classmethod 21 def from_api_response(cls, response: dict[str, Any]) -> "ListBranchesResult": ··· 27 "branches": [ 28 {"reference": {"name": "main", "hash": "abc123"}}, 29 ... 30 + ] 31 } 32 33 Returns: ··· 44 ) 45 ) 46 47 + return cls(branches=branches)
+2 -4
src/tangled_mcp/types/_issues.py
··· 61 """result of listing issues""" 62 63 issues: list[IssueInfo] 64 - cursor: str | None = None 65 66 @classmethod 67 def from_api_response(cls, response: dict[str, Any]) -> "ListIssuesResult": ··· 80 "createdAt": "..." 81 }, 82 ... 83 - ], 84 - "cursor": "optional_cursor" 85 } 86 87 Returns: 88 ListIssuesResult with parsed issues 89 """ 90 issues = [IssueInfo(**issue_data) for issue_data in response.get("issues", [])] 91 - return cls(issues=issues, cursor=response.get("cursor"))
··· 61 """result of listing issues""" 62 63 issues: list[IssueInfo] 64 65 @classmethod 66 def from_api_response(cls, response: dict[str, Any]) -> "ListIssuesResult": ··· 79 "createdAt": "..." 80 }, 81 ... 82 + ] 83 } 84 85 Returns: 86 ListIssuesResult with parsed issues 87 """ 88 issues = [IssueInfo(**issue_data) for issue_data in response.get("issues", [])] 89 + return cls(issues=issues)
-12
tests/test_types.py
··· 58 {"reference": {"name": "main", "hash": "abc123"}}, 59 {"reference": {"name": "dev", "hash": "def456"}}, 60 ], 61 - "cursor": "next_page", 62 } 63 64 result = ListBranchesResult.from_api_response(response) ··· 68 assert result.branches[0].sha == "abc123" 69 assert result.branches[1].name == "dev" 70 assert result.branches[1].sha == "def456" 71 - assert result.cursor == "next_page" 72 - 73 - def test_handles_missing_cursor(self): 74 - """cursor is optional in API response""" 75 - response = {"branches": [{"reference": {"name": "main", "hash": "abc123"}}]} 76 - 77 - result = ListBranchesResult.from_api_response(response) 78 - 79 - assert len(result.branches) == 1 80 - assert result.cursor is None 81 82 def test_handles_empty_branches(self): 83 """handles empty branches list""" ··· 86 result = ListBranchesResult.from_api_response(response) 87 88 assert result.branches == [] 89 - assert result.cursor is None
··· 58 {"reference": {"name": "main", "hash": "abc123"}}, 59 {"reference": {"name": "dev", "hash": "def456"}}, 60 ], 61 } 62 63 result = ListBranchesResult.from_api_response(response) ··· 67 assert result.branches[0].sha == "abc123" 68 assert result.branches[1].name == "dev" 69 assert result.branches[1].sha == "def456" 70 71 def test_handles_empty_branches(self): 72 """handles empty branches list""" ··· 75 result = ListBranchesResult.from_api_response(response) 76 77 assert result.branches == []