Add additional Basecamp API tools
This commit is contained in:
10
README.md
10
README.md
@@ -80,6 +80,16 @@ Once configured, you can use these tools in Cursor:
|
|||||||
- `get_campfire_lines` - Get recent messages from a Basecamp campfire
|
- `get_campfire_lines` - Get recent messages from a Basecamp campfire
|
||||||
- `get_daily_check_ins` - Get project's daily check-in questions
|
- `get_daily_check_ins` - Get project's daily check-in questions
|
||||||
- `get_question_answers` - Get answers to 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
|
### Card Table Tools
|
||||||
|
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ class BasecampClient:
|
|||||||
"""Get all card tables for a project."""
|
"""Get all card tables for a project."""
|
||||||
project = self.get_project(project_id)
|
project = self.get_project(project_id)
|
||||||
try:
|
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):
|
except (IndexError, TypeError):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -603,3 +603,110 @@ class BasecampClient:
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Failed to uncomplete card step: {response.status_code} - {response.text}")
|
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}")
|
||||||
|
|||||||
@@ -490,6 +490,131 @@ class MCPServer:
|
|||||||
},
|
},
|
||||||
"required": ["project_id", "step_id"]
|
"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,28 +902,16 @@ class MCPServer:
|
|||||||
elif tool_name == "get_card_table":
|
elif tool_name == "get_card_table":
|
||||||
project_id = arguments.get("project_id")
|
project_id = arguments.get("project_id")
|
||||||
try:
|
try:
|
||||||
# First, let's see what card tables we find
|
card_table = client.get_card_table(project_id)
|
||||||
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_details = client.get_card_table_details(project_id, card_table['id'])
|
card_table_details = client.get_card_table_details(project_id, card_table['id'])
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"card_table": card_table_details,
|
"card_table": card_table_details
|
||||||
"debug": f"Found {len(card_tables)} card tables, using first one with ID {card_table['id']}"
|
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
return {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"message": f"Error getting card table: {error_msg}",
|
"message": f"Error getting card table: {error_msg}",
|
||||||
"debug": error_msg
|
"debug": error_msg
|
||||||
}
|
}
|
||||||
@@ -1052,6 +1165,104 @@ class MCPServer:
|
|||||||
"message": "Step marked as incomplete"
|
"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:
|
else:
|
||||||
return {
|
return {
|
||||||
"error": "Unknown tool",
|
"error": "Unknown tool",
|
||||||
|
|||||||
Reference in New Issue
Block a user