Merge pull request #9 from georgeantonopoulos/feature/card-tables
Feature/card tables
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -57,3 +57,5 @@ composio_client_example.py
|
|||||||
.composio/
|
.composio/
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
card_table_implementation.md
|
||||||
|
card-tables-docs.md
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -77,6 +77,37 @@ Once configured, you can use these tools in Cursor:
|
|||||||
- `get_todos` - Get todos from a todo list
|
- `get_todos` - Get todos from a todo list
|
||||||
- `search_basecamp` - Search across projects, todos, and messages
|
- `search_basecamp` - Search across projects, todos, and messages
|
||||||
- `get_comments` - Get comments for a Basecamp item
|
- `get_comments` - Get comments for a Basecamp item
|
||||||
|
- `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
|
||||||
|
|
||||||
|
### Card Table Tools
|
||||||
|
|
||||||
|
- `get_card_table` - Get the card table details for a project
|
||||||
|
- `get_columns` - Get all columns in a card table
|
||||||
|
- `get_column` - Get details for a specific column
|
||||||
|
- `create_column` - Create a new column in a card table
|
||||||
|
- `update_column` - Update a column title
|
||||||
|
- `move_column` - Move a column to a new position
|
||||||
|
- `update_column_color` - Update a column color
|
||||||
|
- `put_column_on_hold` - Put a column on hold (freeze work)
|
||||||
|
- `remove_column_hold` - Remove hold from a column (unfreeze work)
|
||||||
|
- `watch_column` - Subscribe to notifications for changes in a column
|
||||||
|
- `unwatch_column` - Unsubscribe from notifications for a column
|
||||||
|
- `get_cards` - Get all cards in a column
|
||||||
|
- `get_card` - Get details for a specific card
|
||||||
|
- `create_card` - Create a new card in a column
|
||||||
|
- `update_card` - Update a card
|
||||||
|
- `move_card` - Move a card to a new column
|
||||||
|
- `complete_card` - Mark a card as complete
|
||||||
|
- `uncomplete_card` - Mark a card as incomplete
|
||||||
|
- `get_card_steps` - Get all steps (sub-tasks) for a card
|
||||||
|
- `create_card_step` - Create a new step (sub-task) for a card
|
||||||
|
- `get_card_step` - Get details for a specific card step
|
||||||
|
- `update_card_step` - Update a card step
|
||||||
|
- `delete_card_step` - Delete a card step
|
||||||
|
- `complete_card_step` - Mark a card step as complete
|
||||||
|
- `uncomplete_card_step` - Mark a card step as incomplete
|
||||||
|
|
||||||
### Example Cursor Usage
|
### Example Cursor Usage
|
||||||
|
|
||||||
@@ -85,6 +116,14 @@ Ask Cursor things like:
|
|||||||
- "What todos are in project X?"
|
- "What todos are in project X?"
|
||||||
- "Search for messages containing 'deadline'"
|
- "Search for messages containing 'deadline'"
|
||||||
- "Get details for the Technology project"
|
- "Get details for the Technology project"
|
||||||
|
- "Show me the card table for project X"
|
||||||
|
- "Create a new card in the 'In Progress' column"
|
||||||
|
- "Move this card to the 'Done' column"
|
||||||
|
- "Update the color of the 'Urgent' column to red"
|
||||||
|
- "Mark card as complete"
|
||||||
|
- "Show me all steps for this card"
|
||||||
|
- "Create a sub-task for this card"
|
||||||
|
- "Mark this card step as complete"
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ class BasecampClient:
|
|||||||
url = f"{self.base_url}/{endpoint}"
|
url = f"{self.base_url}/{endpoint}"
|
||||||
return requests.delete(url, auth=self.auth, headers=self.headers)
|
return requests.delete(url, auth=self.auth, headers=self.headers)
|
||||||
|
|
||||||
|
def patch(self, endpoint, data=None):
|
||||||
|
"""Make a PATCH request to the Basecamp API."""
|
||||||
|
url = f"{self.base_url}/{endpoint}"
|
||||||
|
return requests.patch(url, auth=self.auth, headers=self.headers, json=data)
|
||||||
|
|
||||||
# Project methods
|
# Project methods
|
||||||
def get_projects(self):
|
def get_projects(self):
|
||||||
"""Get all projects."""
|
"""Get all projects."""
|
||||||
@@ -344,3 +349,257 @@ class BasecampClient:
|
|||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise Exception("Failed to read question answers")
|
raise Exception("Failed to read question answers")
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
# Card Table methods
|
||||||
|
def get_card_tables(self, project_id):
|
||||||
|
"""Get all card tables for a project."""
|
||||||
|
project = self.get_project(project_id)
|
||||||
|
try:
|
||||||
|
return [_ for _ in project["dock"] if _["name"] == "kanban_board"]
|
||||||
|
except (IndexError, TypeError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_card_table(self, project_id):
|
||||||
|
"""Get the first card table for a project (Basecamp 3 can have multiple card tables per project)."""
|
||||||
|
card_tables = self.get_card_tables(project_id)
|
||||||
|
if not card_tables:
|
||||||
|
raise Exception(f"No card tables found for project: {project_id}")
|
||||||
|
return card_tables[0] # Return the first card table
|
||||||
|
|
||||||
|
def get_card_table_details(self, project_id, card_table_id):
|
||||||
|
"""Get details for a specific card table."""
|
||||||
|
response = self.get(f'buckets/{project_id}/card_tables/{card_table_id}.json')
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
elif response.status_code == 204:
|
||||||
|
# 204 means "No Content" - return an empty structure
|
||||||
|
return {"lists": [], "id": card_table_id, "status": "empty"}
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to get card table: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
# Card Table Column methods
|
||||||
|
def get_columns(self, project_id, card_table_id):
|
||||||
|
"""Get all columns in a card table."""
|
||||||
|
# Get the card table details which includes the lists (columns)
|
||||||
|
card_table_details = self.get_card_table_details(project_id, card_table_id)
|
||||||
|
return card_table_details.get('lists', [])
|
||||||
|
|
||||||
|
def get_column(self, project_id, column_id):
|
||||||
|
"""Get a specific column."""
|
||||||
|
response = self.get(f'buckets/{project_id}/card_tables/columns/{column_id}.json')
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to get column: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def create_column(self, project_id, card_table_id, title):
|
||||||
|
"""Create a new column in a card table."""
|
||||||
|
data = {"title": title}
|
||||||
|
response = self.post(f'buckets/{project_id}/card_tables/{card_table_id}/columns.json', data)
|
||||||
|
if response.status_code == 201:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to create column: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def update_column(self, project_id, column_id, title):
|
||||||
|
"""Update a column title."""
|
||||||
|
data = {"title": title}
|
||||||
|
response = self.put(f'buckets/{project_id}/card_tables/columns/{column_id}.json', data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to update column: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def move_column(self, project_id, column_id, position, card_table_id):
|
||||||
|
"""Move a column to a new position."""
|
||||||
|
data = {
|
||||||
|
"source_id": column_id,
|
||||||
|
"target_id": card_table_id,
|
||||||
|
"position": position
|
||||||
|
}
|
||||||
|
response = self.post(f'buckets/{project_id}/card_tables/{card_table_id}/moves.json', data)
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to move column: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def update_column_color(self, project_id, column_id, color):
|
||||||
|
"""Update a column color."""
|
||||||
|
data = {"color": color}
|
||||||
|
response = self.patch(f'buckets/{project_id}/card_tables/columns/{column_id}/color.json', data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to update column color: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def put_column_on_hold(self, project_id, column_id):
|
||||||
|
"""Put a column on hold."""
|
||||||
|
response = self.post(f'buckets/{project_id}/card_tables/columns/{column_id}/on_hold.json')
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to put column on hold: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def remove_column_hold(self, project_id, column_id):
|
||||||
|
"""Remove hold from a column."""
|
||||||
|
response = self.delete(f'buckets/{project_id}/card_tables/columns/{column_id}/on_hold.json')
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to remove column hold: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def watch_column(self, project_id, column_id):
|
||||||
|
"""Subscribe to column notifications."""
|
||||||
|
response = self.post(f'buckets/{project_id}/card_tables/lists/{column_id}/subscription.json')
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to watch column: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def unwatch_column(self, project_id, column_id):
|
||||||
|
"""Unsubscribe from column notifications."""
|
||||||
|
response = self.delete(f'buckets/{project_id}/card_tables/lists/{column_id}/subscription.json')
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to unwatch column: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
# Card Table Card methods
|
||||||
|
def get_cards(self, project_id, column_id):
|
||||||
|
"""Get all cards in a column."""
|
||||||
|
response = self.get(f'buckets/{project_id}/card_tables/lists/{column_id}/cards.json')
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to get cards: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def get_card(self, project_id, card_id):
|
||||||
|
"""Get a specific card."""
|
||||||
|
response = self.get(f'buckets/{project_id}/card_tables/cards/{card_id}.json')
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to get card: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def create_card(self, project_id, column_id, title, content=None, due_on=None, notify=False):
|
||||||
|
"""Create a new card in a column."""
|
||||||
|
data = {"title": title}
|
||||||
|
if content:
|
||||||
|
data["content"] = content
|
||||||
|
if due_on:
|
||||||
|
data["due_on"] = due_on
|
||||||
|
if notify:
|
||||||
|
data["notify"] = notify
|
||||||
|
response = self.post(f'buckets/{project_id}/card_tables/lists/{column_id}/cards.json', data)
|
||||||
|
if response.status_code == 201:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to create card: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def update_card(self, project_id, card_id, title=None, content=None, due_on=None, assignee_ids=None):
|
||||||
|
"""Update a card."""
|
||||||
|
data = {}
|
||||||
|
if title:
|
||||||
|
data["title"] = title
|
||||||
|
if content:
|
||||||
|
data["content"] = content
|
||||||
|
if due_on:
|
||||||
|
data["due_on"] = due_on
|
||||||
|
if assignee_ids:
|
||||||
|
data["assignee_ids"] = assignee_ids
|
||||||
|
response = self.put(f'buckets/{project_id}/card_tables/cards/{card_id}.json', data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to update card: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def move_card(self, project_id, card_id, column_id):
|
||||||
|
"""Move a card to a new column."""
|
||||||
|
data = {"column_id": column_id}
|
||||||
|
response = self.post(f'buckets/{project_id}/card_tables/cards/{card_id}/moves.json', data)
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to move card: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def complete_card(self, project_id, card_id):
|
||||||
|
"""Mark a card as complete."""
|
||||||
|
response = self.post(f'buckets/{project_id}/todos/{card_id}/completion.json')
|
||||||
|
if response.status_code == 201:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to complete card: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def uncomplete_card(self, project_id, card_id):
|
||||||
|
"""Mark a card as incomplete."""
|
||||||
|
response = self.delete(f'buckets/{project_id}/todos/{card_id}/completion.json')
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to uncomplete card: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
# Card Steps methods
|
||||||
|
def get_card_steps(self, project_id, card_id):
|
||||||
|
"""Get all steps (sub-tasks) for a card."""
|
||||||
|
card = self.get_card(project_id, card_id)
|
||||||
|
return card.get('steps', [])
|
||||||
|
|
||||||
|
def create_card_step(self, project_id, card_id, title, due_on=None, assignee_ids=None):
|
||||||
|
"""Create a new step (sub-task) for a card."""
|
||||||
|
data = {"title": title}
|
||||||
|
if due_on:
|
||||||
|
data["due_on"] = due_on
|
||||||
|
if assignee_ids:
|
||||||
|
data["assignee_ids"] = assignee_ids
|
||||||
|
response = self.post(f'buckets/{project_id}/card_tables/cards/{card_id}/steps.json', data)
|
||||||
|
if response.status_code == 201:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to create card step: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def get_card_step(self, project_id, step_id):
|
||||||
|
"""Get a specific card step."""
|
||||||
|
response = self.get(f'buckets/{project_id}/card_tables/steps/{step_id}.json')
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to get card step: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def update_card_step(self, project_id, step_id, title=None, due_on=None, assignee_ids=None):
|
||||||
|
"""Update a card step."""
|
||||||
|
data = {}
|
||||||
|
if title:
|
||||||
|
data["title"] = title
|
||||||
|
if due_on:
|
||||||
|
data["due_on"] = due_on
|
||||||
|
if assignee_ids:
|
||||||
|
data["assignee_ids"] = assignee_ids
|
||||||
|
response = self.put(f'buckets/{project_id}/card_tables/steps/{step_id}.json', data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to update card step: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def delete_card_step(self, project_id, step_id):
|
||||||
|
"""Delete a card step."""
|
||||||
|
response = self.delete(f'buckets/{project_id}/card_tables/steps/{step_id}.json')
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to delete card step: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def complete_card_step(self, project_id, step_id):
|
||||||
|
"""Mark a card step as complete."""
|
||||||
|
response = self.post(f'buckets/{project_id}/todos/{step_id}/completion.json')
|
||||||
|
if response.status_code == 201:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to complete card step: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
def uncomplete_card_step(self, project_id, step_id):
|
||||||
|
"""Mark a card step as incomplete."""
|
||||||
|
response = self.delete(f'buckets/{project_id}/todos/{step_id}/completion.json')
|
||||||
|
if response.status_code == 204:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to uncomplete card step: {response.status_code} - {response.text}")
|
||||||
|
|||||||
153
examples/card_table_example.py
Normal file
153
examples/card_table_example.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Example of using the Basecamp Card Table API through the MCP integration.
|
||||||
|
|
||||||
|
This example demonstrates how to:
|
||||||
|
1. Get a card table for a project
|
||||||
|
2. List columns
|
||||||
|
3. Create a new column
|
||||||
|
4. Create cards in columns
|
||||||
|
5. Move cards between columns
|
||||||
|
6. Update column properties
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def send_mcp_request(method, params=None):
|
||||||
|
"""Send a request to the MCP server and return the response."""
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": method,
|
||||||
|
"params": params or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the MCP server with the request
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "mcp_server_cli.py"],
|
||||||
|
input=json.dumps(request),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error: {result.stderr}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
stdout = result.stdout
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(stdout)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"Failed to parse response: {e}")
|
||||||
|
print(f"Response: {stdout}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Demonstrate card table functionality."""
|
||||||
|
|
||||||
|
# Example project ID - replace with your actual project ID
|
||||||
|
project_id = "123456"
|
||||||
|
|
||||||
|
print("Basecamp Card Table Example")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 1. Get the card table for a project
|
||||||
|
print("\n1. Getting card table for project...")
|
||||||
|
response = send_mcp_request("tools/call", {
|
||||||
|
"name": "get_card_table",
|
||||||
|
"arguments": {"project_id": project_id}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response and "result" in response:
|
||||||
|
result = json.loads(response["result"]["content"][0]["text"])
|
||||||
|
if result.get("status") == "success":
|
||||||
|
card_table = result["card_table"]
|
||||||
|
card_table_id = card_table["id"]
|
||||||
|
print(f"Card table found: {card_table['title']} (ID: {card_table_id})")
|
||||||
|
else:
|
||||||
|
print("No card table found. Make sure the Card Table tool is enabled in your project.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. List existing columns
|
||||||
|
print("\n2. Listing columns...")
|
||||||
|
response = send_mcp_request("tools/call", {
|
||||||
|
"name": "get_columns",
|
||||||
|
"arguments": {
|
||||||
|
"project_id": project_id,
|
||||||
|
"card_table_id": card_table_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response and "result" in response:
|
||||||
|
result = json.loads(response["result"]["content"][0]["text"])
|
||||||
|
columns = result.get("columns", [])
|
||||||
|
print(f"Found {len(columns)} columns:")
|
||||||
|
for col in columns:
|
||||||
|
print(f" - {col['title']} (ID: {col['id']})")
|
||||||
|
|
||||||
|
# 3. Create a new column
|
||||||
|
print("\n3. Creating a new column...")
|
||||||
|
response = send_mcp_request("tools/call", {
|
||||||
|
"name": "create_column",
|
||||||
|
"arguments": {
|
||||||
|
"project_id": project_id,
|
||||||
|
"card_table_id": card_table_id,
|
||||||
|
"title": "Testing"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response and "result" in response:
|
||||||
|
result = json.loads(response["result"]["content"][0]["text"])
|
||||||
|
if result.get("status") == "success":
|
||||||
|
new_column = result["column"]
|
||||||
|
print(f"Created column: {new_column['title']} (ID: {new_column['id']})")
|
||||||
|
|
||||||
|
# 4. Create a card in the first column
|
||||||
|
if columns:
|
||||||
|
first_column_id = columns[0]['id']
|
||||||
|
print(f"\n4. Creating a card in column '{columns[0]['title']}'...")
|
||||||
|
|
||||||
|
response = send_mcp_request("tools/call", {
|
||||||
|
"name": "create_card",
|
||||||
|
"arguments": {
|
||||||
|
"project_id": project_id,
|
||||||
|
"column_id": first_column_id,
|
||||||
|
"title": "Test Card",
|
||||||
|
"content": "This is a test card created via the MCP API"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response and "result" in response:
|
||||||
|
result = json.loads(response["result"]["content"][0]["text"])
|
||||||
|
if result.get("status") == "success":
|
||||||
|
new_card = result["card"]
|
||||||
|
print(f"Created card: {new_card['title']} (ID: {new_card['id']})")
|
||||||
|
|
||||||
|
# 5. Update column color
|
||||||
|
if columns:
|
||||||
|
print(f"\n5. Updating color of column '{columns[0]['title']}'...")
|
||||||
|
|
||||||
|
response = send_mcp_request("tools/call", {
|
||||||
|
"name": "update_column_color",
|
||||||
|
"arguments": {
|
||||||
|
"project_id": project_id,
|
||||||
|
"column_id": columns[0]['id'],
|
||||||
|
"color": "#FF0000" # Red
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response and "result" in response:
|
||||||
|
result = json.loads(response["result"]["content"][0]["text"])
|
||||||
|
if result.get("status") == "success":
|
||||||
|
print(f"Updated column color to red")
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("Example completed!")
|
||||||
|
print("\nNote: Replace the project_id with your actual Basecamp project ID to run this example.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -159,6 +159,337 @@ class MCPServer:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["project_id", "question_id"]
|
"required": ["project_id", "question_id"]
|
||||||
|
},
|
||||||
|
# Card Table tools
|
||||||
|
{
|
||||||
|
"name": "get_card_tables",
|
||||||
|
"description": "Get all card tables for a project",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_card_table",
|
||||||
|
"description": "Get the card table details for a project",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_columns",
|
||||||
|
"description": "Get all columns in a card table",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_table_id": {"type": "string", "description": "The card table ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_table_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_column",
|
||||||
|
"description": "Get details for a specific column",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_column",
|
||||||
|
"description": "Create a new column in a card table",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_table_id": {"type": "string", "description": "The card table ID"},
|
||||||
|
"title": {"type": "string", "description": "The column title"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_table_id", "title"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_column",
|
||||||
|
"description": "Update a column title",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"},
|
||||||
|
"title": {"type": "string", "description": "The new column title"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id", "title"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "move_column",
|
||||||
|
"description": "Move a column to a new position",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_table_id": {"type": "string", "description": "The card table ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"},
|
||||||
|
"position": {"type": "integer", "description": "The new 1-based position"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_table_id", "column_id", "position"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_column_color",
|
||||||
|
"description": "Update a column color",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"},
|
||||||
|
"color": {"type": "string", "description": "The hex color code (e.g., #FF0000)"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id", "color"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "put_column_on_hold",
|
||||||
|
"description": "Put a column on hold (freeze work)",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_column_hold",
|
||||||
|
"description": "Remove hold from a column (unfreeze work)",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "watch_column",
|
||||||
|
"description": "Subscribe to notifications for changes in a column",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unwatch_column",
|
||||||
|
"description": "Unsubscribe from notifications for a column",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_cards",
|
||||||
|
"description": "Get all cards in a column",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_card",
|
||||||
|
"description": "Get details for a specific card",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_id": {"type": "string", "description": "The card ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_card",
|
||||||
|
"description": "Create a new card in a column",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The column ID"},
|
||||||
|
"title": {"type": "string", "description": "The card title"},
|
||||||
|
"content": {"type": "string", "description": "Optional card content/description"},
|
||||||
|
"due_on": {"type": "string", "description": "Optional due date (ISO 8601 format)"},
|
||||||
|
"notify": {"type": "boolean", "description": "Whether to notify assignees (default: false)"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "column_id", "title"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_card",
|
||||||
|
"description": "Update a card",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_id": {"type": "string", "description": "The card ID"},
|
||||||
|
"title": {"type": "string", "description": "The new card title"},
|
||||||
|
"content": {"type": "string", "description": "The new card content/description"},
|
||||||
|
"due_on": {"type": "string", "description": "Due date (ISO 8601 format)"},
|
||||||
|
"assignee_ids": {"type": "array", "items": {"type": "string"}, "description": "Array of person IDs to assign to the card"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "move_card",
|
||||||
|
"description": "Move a card to a new column",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_id": {"type": "string", "description": "The card ID"},
|
||||||
|
"column_id": {"type": "string", "description": "The destination column ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_id", "column_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "complete_card",
|
||||||
|
"description": "Mark a card as complete",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_id": {"type": "string", "description": "The card ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "uncomplete_card",
|
||||||
|
"description": "Mark a card as incomplete",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_id": {"type": "string", "description": "The card ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_card_steps",
|
||||||
|
"description": "Get all steps (sub-tasks) for a card",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_id": {"type": "string", "description": "The card ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_card_step",
|
||||||
|
"description": "Create a new step (sub-task) for a card",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"card_id": {"type": "string", "description": "The card ID"},
|
||||||
|
"title": {"type": "string", "description": "The step title"},
|
||||||
|
"due_on": {"type": "string", "description": "Optional due date (ISO 8601 format)"},
|
||||||
|
"assignee_ids": {"type": "array", "items": {"type": "string"}, "description": "Array of person IDs to assign to the step"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "card_id", "title"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_card_step",
|
||||||
|
"description": "Get details for a specific card step",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"step_id": {"type": "string", "description": "The step ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "step_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_card_step",
|
||||||
|
"description": "Update a card step",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"step_id": {"type": "string", "description": "The step ID"},
|
||||||
|
"title": {"type": "string", "description": "The step title"},
|
||||||
|
"due_on": {"type": "string", "description": "Due date (ISO 8601 format)"},
|
||||||
|
"assignee_ids": {"type": "array", "items": {"type": "string"}, "description": "Array of person IDs to assign to the step"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "step_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete_card_step",
|
||||||
|
"description": "Delete a card step",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"step_id": {"type": "string", "description": "The step ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "step_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "complete_card_step",
|
||||||
|
"description": "Mark a card step as complete",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"step_id": {"type": "string", "description": "The step ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "step_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "uncomplete_card_step",
|
||||||
|
"description": "Mark a card step as incomplete",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "The project ID"},
|
||||||
|
"step_id": {"type": "string", "description": "The step ID"}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "step_id"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -199,7 +530,7 @@ class MCPServer:
|
|||||||
logger.error(f"Error creating Basecamp client: {e}")
|
logger.error(f"Error creating Basecamp client: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
def handle_request(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
"""Handle an MCP request."""
|
"""Handle an MCP request."""
|
||||||
method = request.get("method")
|
method = request.get("method")
|
||||||
# Normalize method name for cursor compatibility
|
# Normalize method name for cursor compatibility
|
||||||
@@ -411,7 +742,9 @@ class MCPServer:
|
|||||||
}
|
}
|
||||||
elif tool_name == "get_daily_check_ins":
|
elif tool_name == "get_daily_check_ins":
|
||||||
project_id = arguments.get("project_id")
|
project_id = arguments.get("project_id")
|
||||||
page = arguments.get("page")
|
page = arguments.get("page", 1)
|
||||||
|
if page is not None and not isinstance(page, int):
|
||||||
|
page = 1
|
||||||
answers = client.get_daily_check_ins(project_id, page=page)
|
answers = client.get_daily_check_ins(project_id, page=page)
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -421,7 +754,9 @@ class MCPServer:
|
|||||||
elif tool_name == "get_question_answers":
|
elif tool_name == "get_question_answers":
|
||||||
project_id = arguments.get("project_id")
|
project_id = arguments.get("project_id")
|
||||||
question_id = arguments.get("question_id")
|
question_id = arguments.get("question_id")
|
||||||
page = arguments.get("page")
|
page = arguments.get("page", 1)
|
||||||
|
if page is not None and not isinstance(page, int):
|
||||||
|
page = 1
|
||||||
answers = client.get_question_answers(project_id, question_id, page=page)
|
answers = client.get_question_answers(project_id, question_id, page=page)
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -429,6 +764,294 @@ class MCPServer:
|
|||||||
"count": len(answers)
|
"count": len(answers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Card Table tools implementation
|
||||||
|
elif tool_name == "get_card_tables":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_tables = client.get_card_tables(project_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"card_tables": card_tables,
|
||||||
|
"count": len(card_tables)
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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']}"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Error getting card table: {error_msg}",
|
||||||
|
"debug": error_msg
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "get_columns":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_table_id = arguments.get("card_table_id")
|
||||||
|
columns = client.get_columns(project_id, card_table_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"columns": columns,
|
||||||
|
"count": len(columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "get_column":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
column = client.get_column(project_id, column_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"column": column
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "create_column":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_table_id = arguments.get("card_table_id")
|
||||||
|
title = arguments.get("title")
|
||||||
|
column = client.create_column(project_id, card_table_id, title)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"column": column,
|
||||||
|
"message": f"Column '{title}' created successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "update_column":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
title = arguments.get("title")
|
||||||
|
column = client.update_column(project_id, column_id, title)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"column": column,
|
||||||
|
"message": "Column updated successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "move_column":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_table_id = arguments.get("card_table_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
position = arguments.get("position")
|
||||||
|
client.move_column(project_id, column_id, position, card_table_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Column moved to position {position}"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "update_column_color":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
color = arguments.get("color")
|
||||||
|
column = client.update_column_color(project_id, column_id, color)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"column": column,
|
||||||
|
"message": f"Column color updated to {color}"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "put_column_on_hold":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
client.put_column_on_hold(project_id, column_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Column put on hold"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "remove_column_hold":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
client.remove_column_hold(project_id, column_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Column hold removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "watch_column":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
client.watch_column(project_id, column_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Column notifications enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "unwatch_column":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
client.unwatch_column(project_id, column_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Column notifications disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "get_cards":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
cards = client.get_cards(project_id, column_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"cards": cards,
|
||||||
|
"count": len(cards)
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "get_card":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_id = arguments.get("card_id")
|
||||||
|
card = client.get_card(project_id, card_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"card": card
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "create_card":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
title = arguments.get("title")
|
||||||
|
content = arguments.get("content")
|
||||||
|
due_on = arguments.get("due_on")
|
||||||
|
notify = bool(arguments.get("notify", False))
|
||||||
|
card = client.create_card(project_id, column_id, title, content, due_on, notify)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"card": card,
|
||||||
|
"message": f"Card '{title}' created successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "update_card":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_id = arguments.get("card_id")
|
||||||
|
title = arguments.get("title")
|
||||||
|
content = arguments.get("content")
|
||||||
|
due_on = arguments.get("due_on")
|
||||||
|
assignee_ids = arguments.get("assignee_ids")
|
||||||
|
card = client.update_card(project_id, card_id, title, content, due_on, assignee_ids)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"card": card,
|
||||||
|
"message": "Card updated successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "move_card":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_id = arguments.get("card_id")
|
||||||
|
column_id = arguments.get("column_id")
|
||||||
|
client.move_card(project_id, card_id, column_id)
|
||||||
|
message = "Card moved"
|
||||||
|
if column_id:
|
||||||
|
message = f"Card moved to column {column_id}"
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "complete_card":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_id = arguments.get("card_id")
|
||||||
|
client.complete_card(project_id, card_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Card marked as complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "uncomplete_card":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_id = arguments.get("card_id")
|
||||||
|
client.uncomplete_card(project_id, card_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Card marked as incomplete"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "get_card_steps":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_id = arguments.get("card_id")
|
||||||
|
steps = client.get_card_steps(project_id, card_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"steps": steps,
|
||||||
|
"count": len(steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "create_card_step":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
card_id = arguments.get("card_id")
|
||||||
|
title = arguments.get("title")
|
||||||
|
due_on = arguments.get("due_on")
|
||||||
|
assignee_ids = arguments.get("assignee_ids")
|
||||||
|
step = client.create_card_step(project_id, card_id, title, due_on, assignee_ids)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"step": step,
|
||||||
|
"message": f"Step '{title}' created successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "get_card_step":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
step_id = arguments.get("step_id")
|
||||||
|
step = client.get_card_step(project_id, step_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"step": step
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "update_card_step":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
step_id = arguments.get("step_id")
|
||||||
|
title = arguments.get("title")
|
||||||
|
due_on = arguments.get("due_on")
|
||||||
|
assignee_ids = arguments.get("assignee_ids")
|
||||||
|
step = client.update_card_step(project_id, step_id, title, due_on, assignee_ids)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"step": step,
|
||||||
|
"message": f"Step '{title}' updated successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "delete_card_step":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
step_id = arguments.get("step_id")
|
||||||
|
client.delete_card_step(project_id, step_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Step deleted successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "complete_card_step":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
step_id = arguments.get("step_id")
|
||||||
|
client.complete_card_step(project_id, step_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Step marked as complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
elif tool_name == "uncomplete_card_step":
|
||||||
|
project_id = arguments.get("project_id")
|
||||||
|
step_id = arguments.get("step_id")
|
||||||
|
client.uncomplete_card_step(project_id, step_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Step marked as incomplete"
|
||||||
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"error": "Unknown tool",
|
"error": "Unknown tool",
|
||||||
|
|||||||
207
tests/test_card_tables.py
Normal file
207
tests/test_card_tables.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for Card Table functionality in Basecamp MCP integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
|
|
||||||
|
# Add parent directory to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from mcp_server_cli import MCPServer
|
||||||
|
from basecamp_client import BasecampClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestCardTableTools(unittest.TestCase):
|
||||||
|
"""Test Card Table MCP tools."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.server = MCPServer()
|
||||||
|
|
||||||
|
def test_card_table_tools_registered(self):
|
||||||
|
"""Test that all card table tools are registered."""
|
||||||
|
tool_names = [tool['name'] for tool in self.server.tools]
|
||||||
|
|
||||||
|
expected_tools = [
|
||||||
|
'get_card_table',
|
||||||
|
'get_columns',
|
||||||
|
'get_column',
|
||||||
|
'create_column',
|
||||||
|
'update_column',
|
||||||
|
'move_column',
|
||||||
|
'update_column_color',
|
||||||
|
'put_column_on_hold',
|
||||||
|
'remove_column_hold',
|
||||||
|
'watch_column',
|
||||||
|
'unwatch_column',
|
||||||
|
'get_cards',
|
||||||
|
'get_card',
|
||||||
|
'create_card',
|
||||||
|
'update_card',
|
||||||
|
'move_card'
|
||||||
|
]
|
||||||
|
|
||||||
|
for tool in expected_tools:
|
||||||
|
self.assertIn(tool, tool_names, f"Tool '{tool}' not found in registered tools")
|
||||||
|
|
||||||
|
def test_tool_schemas(self):
|
||||||
|
"""Test that tool schemas are properly defined."""
|
||||||
|
tools_by_name = {tool['name']: tool for tool in self.server.tools}
|
||||||
|
|
||||||
|
# Test get_card_table schema
|
||||||
|
schema = tools_by_name['get_card_table']['inputSchema']
|
||||||
|
self.assertEqual(schema['type'], 'object')
|
||||||
|
self.assertIn('project_id', schema['properties'])
|
||||||
|
self.assertIn('project_id', schema['required'])
|
||||||
|
|
||||||
|
# Test create_card schema
|
||||||
|
schema = tools_by_name['create_card']['inputSchema']
|
||||||
|
self.assertEqual(schema['type'], 'object')
|
||||||
|
self.assertIn('project_id', schema['properties'])
|
||||||
|
self.assertIn('column_id', schema['properties'])
|
||||||
|
self.assertIn('title', schema['properties'])
|
||||||
|
self.assertIn('content', schema['properties'])
|
||||||
|
self.assertIn('project_id', schema['required'])
|
||||||
|
self.assertIn('column_id', schema['required'])
|
||||||
|
self.assertIn('title', schema['required'])
|
||||||
|
self.assertNotIn('content', schema['required']) # content is optional
|
||||||
|
|
||||||
|
@patch('mcp_server_cli.token_storage.get_token')
|
||||||
|
@patch('mcp_server_cli.token_storage.is_token_expired')
|
||||||
|
def test_execute_get_card_table(self, mock_expired, mock_get_token):
|
||||||
|
"""Test executing get_card_table tool."""
|
||||||
|
mock_expired.return_value = False
|
||||||
|
mock_get_token.return_value = {
|
||||||
|
'access_token': 'test_token',
|
||||||
|
'account_id': '12345'
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(BasecampClient, 'get_card_table') as mock_get_table:
|
||||||
|
with patch.object(BasecampClient, 'get_card_table_details') as mock_get_details:
|
||||||
|
mock_get_table.return_value = {'id': '123', 'name': 'card_table'}
|
||||||
|
mock_get_details.return_value = {
|
||||||
|
'id': '123',
|
||||||
|
'name': 'card_table',
|
||||||
|
'title': 'Card Table',
|
||||||
|
'columns_count': 4
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.server._execute_tool('get_card_table', {'project_id': '456'})
|
||||||
|
|
||||||
|
self.assertEqual(result['status'], 'success')
|
||||||
|
self.assertIn('card_table', result)
|
||||||
|
self.assertEqual(result['card_table']['id'], '123')
|
||||||
|
|
||||||
|
@patch('mcp_server_cli.token_storage.get_token')
|
||||||
|
@patch('mcp_server_cli.token_storage.is_token_expired')
|
||||||
|
def test_execute_create_card(self, mock_expired, mock_get_token):
|
||||||
|
"""Test executing create_card tool."""
|
||||||
|
mock_expired.return_value = False
|
||||||
|
mock_get_token.return_value = {
|
||||||
|
'access_token': 'test_token',
|
||||||
|
'account_id': '12345'
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(BasecampClient, 'create_card') as mock_create:
|
||||||
|
mock_create.return_value = {
|
||||||
|
'id': '789',
|
||||||
|
'title': 'New Card',
|
||||||
|
'content': 'Card content',
|
||||||
|
'column_id': '456'
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.server._execute_tool('create_card', {
|
||||||
|
'project_id': '123',
|
||||||
|
'column_id': '456',
|
||||||
|
'title': 'New Card',
|
||||||
|
'content': 'Card content'
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(result['status'], 'success')
|
||||||
|
self.assertIn('card', result)
|
||||||
|
self.assertEqual(result['card']['title'], 'New Card')
|
||||||
|
self.assertIn('created successfully', result['message'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestBasecampClientCardTables(unittest.TestCase):
|
||||||
|
"""Test BasecampClient card table methods."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.client = BasecampClient(
|
||||||
|
access_token='test_token',
|
||||||
|
account_id='12345',
|
||||||
|
user_agent='Test Agent',
|
||||||
|
auth_mode='oauth'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_patch_method_exists(self):
|
||||||
|
"""Test that patch method exists."""
|
||||||
|
self.assertTrue(hasattr(self.client, 'patch'))
|
||||||
|
|
||||||
|
@patch('requests.get')
|
||||||
|
def test_get_card_table(self, mock_get):
|
||||||
|
"""Test getting card table from project dock."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
'id': '123',
|
||||||
|
'dock': [
|
||||||
|
{'name': 'todoset', 'id': '111'},
|
||||||
|
{'name': 'card_table', 'id': '222'},
|
||||||
|
{'name': 'message_board', 'id': '333'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.client.get_card_table('123')
|
||||||
|
|
||||||
|
self.assertEqual(result['name'], 'card_table')
|
||||||
|
self.assertEqual(result['id'], '222')
|
||||||
|
|
||||||
|
@patch('requests.post')
|
||||||
|
def test_create_column(self, mock_post):
|
||||||
|
"""Test creating a column."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.status_code = 201
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
'id': '456',
|
||||||
|
'title': 'New Column',
|
||||||
|
'position': 5
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.client.create_column('123', '456', 'New Column')
|
||||||
|
|
||||||
|
self.assertEqual(result['title'], 'New Column')
|
||||||
|
mock_post.assert_called_once()
|
||||||
|
call_args = mock_post.call_args
|
||||||
|
self.assertEqual(call_args[1]['json'], {'title': 'New Column'})
|
||||||
|
|
||||||
|
@patch('requests.patch')
|
||||||
|
def test_update_column_color(self, mock_patch):
|
||||||
|
"""Test updating column color."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
'id': '456',
|
||||||
|
'title': 'Column',
|
||||||
|
'color': '#FF0000'
|
||||||
|
}
|
||||||
|
mock_patch.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.client.update_column_color('123', '456', '#FF0000')
|
||||||
|
|
||||||
|
self.assertEqual(result['color'], '#FF0000')
|
||||||
|
mock_patch.assert_called_once()
|
||||||
|
call_args = mock_patch.call_args
|
||||||
|
self.assertEqual(call_args[1]['json'], {'color': '#FF0000'})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Reference in New Issue
Block a user