From cb82df454770902653af3ac6ddb597271ae90c14 Mon Sep 17 00:00:00 2001 From: Alexander Anikeev Date: Thu, 5 Jun 2025 18:51:36 +0700 Subject: [PATCH] Add daily check-ins functionality, fix some issues with getting todo lists/sets --- basecamp_client.py | 54 +++++++++++++++++++++++++---------------- mcp_server_cli.py | 60 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 27 deletions(-) diff --git a/basecamp_client.py b/basecamp_client.py index 750cb86..c03cf39 100644 --- a/basecamp_client.py +++ b/basecamp_client.py @@ -1,7 +1,9 @@ import os + import requests from dotenv import load_dotenv + class BasecampClient: """ 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}") # 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).""" - response = self.get(f'projects/{project_id}/todoset.json') - if response.status_code == 200: - return response.json() - else: - raise Exception(f"Failed to get todoset: {response.status_code} - {response.text}") + project = self.get_project(project_id) + try: + return next(_ for _ in project["dock"] if _["name"] == "todoset") + except (IndexError, TypeError, StopIteration): + raise Exception(f"Failed to get todoset for project: {project.id}. Project response: {project}") def get_todolists(self, project_id): """Get all todolists for a 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'] # 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: return response.json() else: @@ -136,9 +138,9 @@ class BasecampClient: raise Exception(f"Failed to get todolist: {response.status_code} - {response.text}") # To-do methods - def get_todos(self, todolist_id): + def get_todos(self, project_id, todolist_id): """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: return response.json() else: @@ -233,22 +235,18 @@ class BasecampClient: raise Exception(f"Failed to get schedule: {str(e)}") # 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: - recording_id (int): ID of the recording (todo, message, etc.) - bucket_id (int, optional): Project/bucket ID. If not provided, it will be extracted from the recording ID. + 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. Returns: list: Comments for the recording """ - if bucket_id is None: - # 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" + endpoint = f"buckets/{project_id}/recordings/{recording_id}/comments.json" response = self.get(endpoint) if response.status_code == 200: return response.json() @@ -329,4 +327,20 @@ class BasecampClient: if response.status_code == 204: return True else: - raise Exception(f"Failed to delete comment: {response.status_code} - {response.text}") \ No newline at end of file + 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() diff --git a/mcp_server_cli.py b/mcp_server_cli.py index 122af21..8c98b38 100755 --- a/mcp_server_cli.py +++ b/mcp_server_cli.py @@ -82,9 +82,10 @@ class MCPServer: "inputSchema": { "type": "object", "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", "properties": { "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"] } + }, + { + "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": 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 { "status": "success", "todos": todos, @@ -344,8 +371,8 @@ class MCPServer: elif tool_name == "get_comments": recording_id = arguments.get("recording_id") - bucket_id = arguments.get("bucket_id") - comments = client.get_comments(recording_id, bucket_id) + project_id = arguments.get("project_id") + comments = client.get_comments(project_id, recording_id) return { "status": "success", "comments": comments, @@ -361,6 +388,25 @@ class MCPServer: "campfire_lines": 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: return {