Merge pull request #4 from iamanikeev/fix-basecamp-api-issues

Add daily check-ins functionality, fix some issues with getting todo lists/sets
This commit is contained in:
George Antonopoulos
2025-06-06 10:24:46 +01:00
committed by GitHub
2 changed files with 87 additions and 27 deletions

View File

@@ -1,7 +1,9 @@
import os import os
import requests import requests
from dotenv import load_dotenv from dotenv import load_dotenv
class BasecampClient: class BasecampClient:
""" """
Client for interacting with Basecamp 3 API using Basic Authentication or OAuth 2.0. Client for interacting with Basecamp 3 API using Basic Authentication or OAuth 2.0.
@@ -106,22 +108,22 @@ class BasecampClient:
raise Exception(f"Failed to get project: {response.status_code} - {response.text}") raise Exception(f"Failed to get project: {response.status_code} - {response.text}")
# To-do list methods # To-do list methods
def get_todosets(self, project_id): def get_todoset(self, project_id):
"""Get the todoset for a project (Basecamp 3 has one todoset per project).""" """Get the todoset for a project (Basecamp 3 has one todoset per project)."""
response = self.get(f'projects/{project_id}/todoset.json') project = self.get_project(project_id)
if response.status_code == 200: try:
return response.json() return next(_ for _ in project["dock"] if _["name"] == "todoset")
else: except (IndexError, TypeError, StopIteration):
raise Exception(f"Failed to get todoset: {response.status_code} - {response.text}") raise Exception(f"Failed to get todoset for project: {project.id}. Project response: {project}")
def get_todolists(self, project_id): def get_todolists(self, project_id):
"""Get all todolists for a project.""" """Get all todolists for a project."""
# First get the todoset ID for this project # First get the todoset ID for this project
todoset = self.get_todosets(project_id) todoset = self.get_todoset(project_id)
todoset_id = todoset['id'] todoset_id = todoset['id']
# Then get all todolists in this todoset # Then get all todolists in this todoset
response = self.get(f'todolists.json', {'todoset_id': todoset_id}) response = self.get(f'buckets/{project_id}/todosets/{todoset_id}/todolists.json')
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: else:
@@ -136,9 +138,9 @@ class BasecampClient:
raise Exception(f"Failed to get todolist: {response.status_code} - {response.text}") raise Exception(f"Failed to get todolist: {response.status_code} - {response.text}")
# To-do methods # To-do methods
def get_todos(self, todolist_id): def get_todos(self, project_id, todolist_id):
"""Get all todos in a todolist.""" """Get all todos in a todolist."""
response = self.get(f'todolists/{todolist_id}/todos.json') response = self.get(f'buckets/{project_id}/todolists/{todolist_id}/todos.json')
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: else:
@@ -233,22 +235,18 @@ 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, recording_id, bucket_id=None): def get_comments(self, project_id, recording_id):
""" """
Get all comments for a recording (todo, message, etc.). Get all comments for a recording (todos, message, etc.).
Args: Args:
recording_id (int): ID of the recording (todo, message, etc.) recording_id (int): ID of the recording (todos, message, etc.)
bucket_id (int, optional): Project/bucket ID. If not provided, it will be extracted from the recording ID. project_id (int): Project/bucket ID. If not provided, it will be extracted from the recording ID.
Returns: Returns:
list: Comments for the recording list: Comments for the recording
""" """
if bucket_id is None: endpoint = f"buckets/{project_id}/recordings/{recording_id}/comments.json"
# Try to get the recording first to extract the bucket_id
raise ValueError("bucket_id is required")
endpoint = f"buckets/{bucket_id}/recordings/{recording_id}/comments.json"
response = self.get(endpoint) response = self.get(endpoint)
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
@@ -330,3 +328,19 @@ class BasecampClient:
return True return True
else: else:
raise Exception(f"Failed to delete comment: {response.status_code} - {response.text}") raise Exception(f"Failed to delete comment: {response.status_code} - {response.text}")
def get_daily_check_ins(self, project_id, page=1):
project = self.get_project(project_id)
questionnaire = next(_ for _ in project["dock"] if _["name"] == "questionnaire")
endpoint = f"buckets/{project_id}/questionnaires/{questionnaire['id']}/questions.json"
response = self.get(endpoint, params={"page": page})
if response.status_code != 200:
raise Exception("Failed to read questions")
return response.json()
def get_question_answers(self, project_id, question_id, page=1):
endpoint = f"buckets/{project_id}/questions/{question_id}/answers.json"
response = self.get(endpoint, params={"page": page})
if response.status_code != 200:
raise Exception("Failed to read question answers")
return response.json()

View File

@@ -82,9 +82,10 @@ class MCPServer:
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
"todolist_id": {"type": "string", "description": "The todo list ID"} "project_id": {"type": "string", "description": "Project ID"},
"todolist_id": {"type": "string", "description": "The todo list ID"},
}, },
"required": ["todolist_id"] "required": ["project_id", "todolist_id"]
} }
}, },
{ {
@@ -106,9 +107,9 @@ class MCPServer:
"type": "object", "type": "object",
"properties": { "properties": {
"recording_id": {"type": "string", "description": "The item ID"}, "recording_id": {"type": "string", "description": "The item ID"},
"bucket_id": {"type": "string", "description": "The bucket/project ID"} "project_id": {"type": "string", "description": "The project ID"}
}, },
"required": ["recording_id", "bucket_id"] "required": ["recording_id", "project_id"]
} }
}, },
{ {
@@ -122,6 +123,31 @@ class MCPServer:
}, },
"required": ["project_id", "campfire_id"] "required": ["project_id", "campfire_id"]
} }
},
{
"name": "get_daily_check_ins",
"description": "Get project's daily checking questionnaire",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "The project ID"},
"page": {"type": "integer", "description": "Page number paginated response"}
}
},
"required": ["project_id"]
},
{
"name": "get_question_answers",
"description": "Get answers on daily check-in question",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "The project ID"},
"question_id": {"type": "string", "description": "The question ID"},
"page": {"type": "integer", "description": "Page number paginated response"}
}
},
"required": ["project_id", "question_id"]
} }
] ]
@@ -312,7 +338,8 @@ class MCPServer:
elif tool_name == "get_todos": elif tool_name == "get_todos":
todolist_id = arguments.get("todolist_id") todolist_id = arguments.get("todolist_id")
todos = client.get_todos(todolist_id) project_id = arguments.get("project_id")
todos = client.get_todos(project_id, todolist_id)
return { return {
"status": "success", "status": "success",
"todos": todos, "todos": todos,
@@ -344,8 +371,8 @@ class MCPServer:
elif tool_name == "get_comments": elif tool_name == "get_comments":
recording_id = arguments.get("recording_id") recording_id = arguments.get("recording_id")
bucket_id = arguments.get("bucket_id") project_id = arguments.get("project_id")
comments = client.get_comments(recording_id, bucket_id) comments = client.get_comments(project_id, recording_id)
return { return {
"status": "success", "status": "success",
"comments": comments, "comments": comments,
@@ -361,6 +388,25 @@ class MCPServer:
"campfire_lines": lines, "campfire_lines": lines,
"count": len(lines) "count": len(lines)
} }
elif tool_name == "get_daily_check_ins":
project_id = arguments.get("project_id")
page = arguments.get("page")
answers = client.get_daily_check_ins(project_id, page=page)
return {
"status": "success",
"campfire_lines": answers,
"count": len(answers)
}
elif tool_name == "get_question_answers":
project_id = arguments.get("project_id")
question_id = arguments.get("question_id")
page = arguments.get("page")
answers = client.get_question_answers(project_id, question_id, page=page)
return {
"status": "success",
"campfire_lines": answers,
"count": len(answers)
}
else: else:
return { return {