diff --git a/README.md b/README.md index 3db0f4c..727401c 100644 --- a/README.md +++ b/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_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 diff --git a/basecamp_client.py b/basecamp_client.py index 4bfeb5b..e8545cb 100644 --- a/basecamp_client.py +++ b/basecamp_client.py @@ -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}") diff --git a/mcp_server_cli.py b/mcp_server_cli.py index 357607d..8a938bc 100755 --- a/mcp_server_cli.py +++ b/mcp_server_cli.py @@ -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,28 +902,16 @@ 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) return { - "status": "error", + "status": "error", "message": f"Error getting card table: {error_msg}", "debug": error_msg } @@ -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",