Add additional Basecamp API tools

This commit is contained in:
George Antonopoulos
2025-07-20 20:51:47 +01:00
parent 982757c309
commit fc3a5b922a
3 changed files with 344 additions and 16 deletions

View File

@@ -80,6 +80,16 @@ Once configured, you can use these tools in Cursor:
- `get_campfire_lines` - Get recent messages from a Basecamp campfire
- `get_daily_check_ins` - Get project's daily check-in questions
- `get_question_answers` - Get answers to daily check-in questions
- `create_attachment` - Upload a file as an attachment
- `get_events` - Get events for a recording
- `get_webhooks` - List webhooks for a project
- `create_webhook` - Create a webhook
- `delete_webhook` - Delete a webhook
- `get_documents` - List documents in a vault
- `get_document` - Get a single document
- `create_document` - Create a document
- `update_document` - Update a document
- `trash_document` - Move a document to trash
### Card Table Tools

View File

@@ -355,7 +355,7 @@ class BasecampClient:
"""Get all card tables for a project."""
project = self.get_project(project_id)
try:
return [_ for _ in project["dock"] if _["name"] == "kanban_board"]
return [item for item in project["dock"] if item.get("name") in ("kanban_board", "card_table")]
except (IndexError, TypeError):
return []
@@ -603,3 +603,110 @@ class BasecampClient:
return True
else:
raise Exception(f"Failed to uncomplete card step: {response.status_code} - {response.text}")
# New methods for additional Basecamp API functionality
def create_attachment(self, file_path, name, content_type="application/octet-stream"):
"""Upload an attachment and return the attachable sgid."""
with open(file_path, "rb") as f:
data = f.read()
headers = self.headers.copy()
headers["Content-Type"] = content_type
headers["Content-Length"] = str(len(data))
endpoint = f"attachments.json?name={name}"
response = requests.post(f"{self.base_url}/{endpoint}", headers=headers, data=data)
if response.status_code == 201:
return response.json()
else:
raise Exception(f"Failed to create attachment: {response.status_code} - {response.text}")
def get_events(self, project_id, recording_id):
"""Get events for a recording."""
endpoint = f"buckets/{project_id}/recordings/{recording_id}/events.json"
response = self.get(endpoint)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get events: {response.status_code} - {response.text}")
def get_webhooks(self, project_id):
"""List webhooks for a project."""
endpoint = f"buckets/{project_id}/webhooks.json"
response = self.get(endpoint)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get webhooks: {response.status_code} - {response.text}")
def create_webhook(self, project_id, payload_url, types=None):
"""Create a webhook for a project."""
data = {"payload_url": payload_url}
if types:
data["types"] = types
endpoint = f"buckets/{project_id}/webhooks.json"
response = self.post(endpoint, data)
if response.status_code == 201:
return response.json()
else:
raise Exception(f"Failed to create webhook: {response.status_code} - {response.text}")
def delete_webhook(self, project_id, webhook_id):
"""Delete a webhook."""
endpoint = f"buckets/{project_id}/webhooks/{webhook_id}.json"
response = self.delete(endpoint)
if response.status_code == 204:
return True
else:
raise Exception(f"Failed to delete webhook: {response.status_code} - {response.text}")
def get_documents(self, project_id, vault_id):
"""List documents in a vault."""
endpoint = f"buckets/{project_id}/vaults/{vault_id}/documents.json"
response = self.get(endpoint)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get documents: {response.status_code} - {response.text}")
def get_document(self, project_id, document_id):
"""Get a single document."""
endpoint = f"buckets/{project_id}/documents/{document_id}.json"
response = self.get(endpoint)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get document: {response.status_code} - {response.text}")
def create_document(self, project_id, vault_id, title, content, status="active"):
"""Create a document in a vault."""
data = {"title": title, "content": content, "status": status}
endpoint = f"buckets/{project_id}/vaults/{vault_id}/documents.json"
response = self.post(endpoint, data)
if response.status_code == 201:
return response.json()
else:
raise Exception(f"Failed to create document: {response.status_code} - {response.text}")
def update_document(self, project_id, document_id, title=None, content=None):
"""Update a document's title or content."""
data = {}
if title:
data["title"] = title
if content:
data["content"] = content
endpoint = f"buckets/{project_id}/documents/{document_id}.json"
response = self.put(endpoint, data)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to update document: {response.status_code} - {response.text}")
def trash_document(self, project_id, document_id):
"""Trash a document."""
endpoint = f"buckets/{project_id}/recordings/{document_id}/status/trashed.json"
response = self.put(endpoint)
if response.status_code == 204:
return True
else:
raise Exception(f"Failed to trash document: {response.status_code} - {response.text}")

View File

@@ -490,6 +490,131 @@ class MCPServer:
},
"required": ["project_id", "step_id"]
}
},
{
"name": "create_attachment",
"description": "Upload a file as an attachment",
"inputSchema": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Local path to file"},
"name": {"type": "string", "description": "Filename for Basecamp"},
"content_type": {"type": "string", "description": "MIME type"}
},
"required": ["file_path", "name"]
}
},
{
"name": "get_events",
"description": "Get events for a recording",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"recording_id": {"type": "string", "description": "Recording ID"}
},
"required": ["project_id", "recording_id"]
}
},
{
"name": "get_webhooks",
"description": "List webhooks for a project",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"}
},
"required": ["project_id"]
}
},
{
"name": "create_webhook",
"description": "Create a webhook",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"payload_url": {"type": "string", "description": "Payload URL"},
"types": {"type": "array", "items": {"type": "string"}, "description": "Event types"}
},
"required": ["project_id", "payload_url"]
}
},
{
"name": "delete_webhook",
"description": "Delete a webhook",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"webhook_id": {"type": "string", "description": "Webhook ID"}
},
"required": ["project_id", "webhook_id"]
}
},
{
"name": "get_documents",
"description": "List documents in a vault",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"vault_id": {"type": "string", "description": "Vault ID"}
},
"required": ["project_id", "vault_id"]
}
},
{
"name": "get_document",
"description": "Get a single document",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"document_id": {"type": "string", "description": "Document ID"}
},
"required": ["project_id", "document_id"]
}
},
{
"name": "create_document",
"description": "Create a document in a vault",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"vault_id": {"type": "string", "description": "Vault ID"},
"title": {"type": "string", "description": "Document title"},
"content": {"type": "string", "description": "Document HTML content"}
},
"required": ["project_id", "vault_id", "title", "content"]
}
},
{
"name": "update_document",
"description": "Update a document",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"document_id": {"type": "string", "description": "Document ID"},
"title": {"type": "string", "description": "New title"},
"content": {"type": "string", "description": "New HTML content"}
},
"required": ["project_id", "document_id"]
}
},
{
"name": "trash_document",
"description": "Move a document to trash",
"inputSchema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"document_id": {"type": "string", "description": "Document ID"}
},
"required": ["project_id", "document_id"]
}
}
]
@@ -777,23 +902,11 @@ class MCPServer:
elif tool_name == "get_card_table":
project_id = arguments.get("project_id")
try:
# First, let's see what card tables we find
card_tables = client.get_card_tables(project_id)
if not card_tables:
return {
"status": "error",
"message": "No card tables found in project",
"debug": f"Found {len(card_tables)} card tables"
}
card_table = card_tables[0] # Get the first card table
# Get the full details
card_table = client.get_card_table(project_id)
card_table_details = client.get_card_table_details(project_id, card_table['id'])
return {
"status": "success",
"card_table": card_table_details,
"debug": f"Found {len(card_tables)} card tables, using first one with ID {card_table['id']}"
"card_table": card_table_details
}
except Exception as e:
error_msg = str(e)
@@ -1052,6 +1165,104 @@ class MCPServer:
"message": "Step marked as incomplete"
}
elif tool_name == "create_attachment":
file_path = arguments.get("file_path")
name = arguments.get("name")
content_type = arguments.get("content_type", "application/octet-stream")
result = client.create_attachment(file_path, name, content_type)
return {
"status": "success",
"attachment": result
}
elif tool_name == "get_events":
project_id = arguments.get("project_id")
recording_id = arguments.get("recording_id")
events = client.get_events(project_id, recording_id)
return {
"status": "success",
"events": events,
"count": len(events)
}
elif tool_name == "get_webhooks":
project_id = arguments.get("project_id")
hooks = client.get_webhooks(project_id)
return {
"status": "success",
"webhooks": hooks,
"count": len(hooks)
}
elif tool_name == "create_webhook":
project_id = arguments.get("project_id")
payload_url = arguments.get("payload_url")
types = arguments.get("types")
hook = client.create_webhook(project_id, payload_url, types)
return {
"status": "success",
"webhook": hook
}
elif tool_name == "delete_webhook":
project_id = arguments.get("project_id")
webhook_id = arguments.get("webhook_id")
client.delete_webhook(project_id, webhook_id)
return {
"status": "success",
"message": "Webhook deleted"
}
elif tool_name == "get_documents":
project_id = arguments.get("project_id")
vault_id = arguments.get("vault_id")
docs = client.get_documents(project_id, vault_id)
return {
"status": "success",
"documents": docs,
"count": len(docs)
}
elif tool_name == "get_document":
project_id = arguments.get("project_id")
document_id = arguments.get("document_id")
doc = client.get_document(project_id, document_id)
return {
"status": "success",
"document": doc
}
elif tool_name == "create_document":
project_id = arguments.get("project_id")
vault_id = arguments.get("vault_id")
title = arguments.get("title")
content = arguments.get("content")
doc = client.create_document(project_id, vault_id, title, content)
return {
"status": "success",
"document": doc
}
elif tool_name == "update_document":
project_id = arguments.get("project_id")
document_id = arguments.get("document_id")
title = arguments.get("title")
content = arguments.get("content")
doc = client.update_document(project_id, document_id, title, content)
return {
"status": "success",
"document": doc
}
elif tool_name == "trash_document":
project_id = arguments.get("project_id")
document_id = arguments.get("document_id")
client.trash_document(project_id, document_id)
return {
"status": "success",
"message": "Document trashed"
}
else:
return {
"error": "Unknown tool",