Add pagination support to get_comments

- Add page parameter to get_comments (default: 1)
- Parse X-Total-Count header for total comments count
- Parse Link header for next_page indicator
- Update FastMCP tool and CLI server to return pagination metadata

Response now includes total_count and next_page fields to help
clients navigate through paginated comments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mammad M.
2026-01-10 00:33:00 +01:00
parent 6265026f57
commit 67d68af1da
4 changed files with 55 additions and 19 deletions

View File

@@ -405,21 +405,46 @@ class BasecampClient:
raise Exception(f"Failed to get schedule: {str(e)}") raise Exception(f"Failed to get schedule: {str(e)}")
# Comments methods # Comments methods
def get_comments(self, project_id, recording_id): def get_comments(self, project_id, recording_id, page=1):
""" """
Get all comments for a recording (todos, message, etc.). Get comments for a recording (todos, message, etc.).
Args: Args:
project_id (int): Project/bucket ID.
recording_id (int): ID of the recording (todos, message, etc.) recording_id (int): ID of the recording (todos, message, etc.)
project_id (int): Project/bucket ID. If not provided, it will be extracted from the recording ID. page (int): Page number for pagination (default: 1).
Basecamp uses geared pagination: page 1 has 15 results,
page 2 has 30, page 3 has 50, page 4+ has 100.
Returns: Returns:
list: Comments for the recording dict: Contains 'comments' list and pagination metadata:
- comments: list of comments
- total_count: total number of comments (from X-Total-Count header)
- next_page: next page number if available, None otherwise
""" """
endpoint = f"buckets/{project_id}/recordings/{recording_id}/comments.json" endpoint = f"buckets/{project_id}/recordings/{recording_id}/comments.json"
response = self.get(endpoint) response = self.get(endpoint, params={"page": page})
if response.status_code == 200: if response.status_code == 200:
return response.json() # Parse pagination headers
total_count = response.headers.get('X-Total-Count')
total_count = int(total_count) if total_count else None
# Parse Link header for next page
next_page = None
link_header = response.headers.get('Link', '')
if 'rel="next"' in link_header:
# Extract page number from Link header
# Format: <https://3.basecampapi.com/.../comments.json?page=2>; rel="next"
import re
match = re.search(r'page=(\d+).*rel="next"', link_header)
if match:
next_page = int(match.group(1))
return {
"comments": response.json(),
"total_count": total_count,
"next_page": next_page
}
else: else:
raise Exception(f"Failed to get comments: {response.status_code} - {response.text}") raise Exception(f"Failed to get comments: {response.status_code} - {response.text}")

View File

@@ -500,23 +500,28 @@ async def global_search(query: str) -> Dict[str, Any]:
} }
@mcp.tool() @mcp.tool()
async def get_comments(recording_id: str, project_id: str) -> Dict[str, Any]: async def get_comments(recording_id: str, project_id: str, page: int = 1) -> Dict[str, Any]:
"""Get comments for a Basecamp item. """Get comments for a Basecamp item.
Args: Args:
recording_id: The item ID recording_id: The item ID
project_id: The project ID project_id: The project ID
page: Page number for pagination (default: 1). Basecamp uses geared pagination:
page 1 has 15 results, page 2 has 30, page 3 has 50, page 4+ has 100.
""" """
client = _get_basecamp_client() client = _get_basecamp_client()
if not client: if not client:
return _get_auth_error_response() return _get_auth_error_response()
try: try:
comments = await _run_sync(client.get_comments, project_id, recording_id) result = await _run_sync(client.get_comments, project_id, recording_id, page)
return { return {
"status": "success", "status": "success",
"comments": comments, "comments": result["comments"],
"count": len(comments) "count": len(result["comments"]),
"page": page,
"total_count": result["total_count"],
"next_page": result["next_page"]
} }
except Exception as e: except Exception as e:
logger.error(f"Error getting comments: {e}") logger.error(f"Error getting comments: {e}")

View File

@@ -193,7 +193,8 @@ class MCPServer:
"type": "object", "type": "object",
"properties": { "properties": {
"recording_id": {"type": "string", "description": "The item ID"}, "recording_id": {"type": "string", "description": "The item ID"},
"project_id": {"type": "string", "description": "The project ID"} "project_id": {"type": "string", "description": "The project ID"},
"page": {"type": "integer", "description": "Page number for pagination (default: 1). Basecamp uses geared pagination: page 1 has 15 results, page 2 has 30, page 3 has 50, page 4+ has 100.", "default": 1}
}, },
"required": ["recording_id", "project_id"] "required": ["recording_id", "project_id"]
} }
@@ -1025,11 +1026,15 @@ class MCPServer:
elif tool_name == "get_comments": elif tool_name == "get_comments":
recording_id = arguments.get("recording_id") recording_id = arguments.get("recording_id")
project_id = arguments.get("project_id") project_id = arguments.get("project_id")
comments = client.get_comments(project_id, recording_id) page = arguments.get("page", 1)
result = client.get_comments(project_id, recording_id, page)
return { return {
"status": "success", "status": "success",
"comments": comments, "comments": result["comments"],
"count": len(comments) "count": len(result["comments"]),
"page": page,
"total_count": result["total_count"],
"next_page": result["next_page"]
} }
elif tool_name == "create_comment": elif tool_name == "create_comment":

View File

@@ -467,7 +467,8 @@ class BasecampSearch:
try: try:
# If both recording_id and bucket_id are provided, get comments for that specific recording # If both recording_id and bucket_id are provided, get comments for that specific recording
if recording_id and bucket_id: if recording_id and bucket_id:
comments = self.client.get_comments(recording_id, bucket_id) result = self.client.get_comments(recording_id, bucket_id)
comments = result["comments"]
# Otherwise we can't search across all comments as there's no endpoint for that # Otherwise we can't search across all comments as there's no endpoint for that
else: else:
logger.warning("Cannot search all comments across Basecamp - both recording_id and bucket_id are required") logger.warning("Cannot search all comments across Basecamp - both recording_id and bucket_id are required")