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:
@@ -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()
|
||||||
@@ -329,4 +327,20 @@ class BasecampClient:
|
|||||||
if response.status_code == 204:
|
if response.status_code == 204:
|
||||||
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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user