Refactor card management in Basecamp client and MCP server CLI

- Rename and enhance methods for retrieving card tables and managing cards.
- Introduce new methods for completing and uncompleting cards and card steps.
- Update MCP server CLI to include new card and card step functionalities.
- Revise README to reflect changes in card management tools and usage examples.
This commit is contained in:
George Antonopoulos
2025-06-29 08:22:09 +01:00
parent 4e466c20da
commit a439865ef4
3 changed files with 405 additions and 50 deletions

View File

@@ -351,30 +351,38 @@ class BasecampClient:
return response.json()
# Card Table methods
def get_card_table(self, project_id):
"""Get the card table for a project (Basecamp 3 has one card table per project)."""
def get_card_tables(self, project_id):
"""Get all card tables for a project."""
project = self.get_project(project_id)
try:
return next(_ for _ in project["dock"] if _["name"] == "card_table")
except (IndexError, TypeError, StopIteration):
raise Exception(f"Failed to get card table for project: {project_id}. Project response: {project}")
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."""
response = self.get(f'buckets/{project_id}/card_tables/{card_table_id}/columns.json')
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get columns: {response.status_code} - {response.text}")
# 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."""
@@ -402,10 +410,14 @@ class BasecampClient:
else:
raise Exception(f"Failed to update column: {response.status_code} - {response.text}")
def move_column(self, project_id, column_id, position):
def move_column(self, project_id, column_id, position, card_table_id):
"""Move a column to a new position."""
data = {"position": position}
response = self.post(f'buckets/{project_id}/card_tables/columns/{column_id}/moves.json', data)
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:
@@ -469,39 +481,125 @@ class BasecampClient:
else:
raise Exception(f"Failed to get card: {response.status_code} - {response.text}")
def create_card(self, project_id, column_id, title, content=None):
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):
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=None, position=None):
"""Move a card to a new column and/or position."""
data = {}
if column_id:
data["column_id"] = column_id
if position:
data["position"] = position
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}")