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