Skip to main content
Glama

Basecamp MCP Server

by stefanoverna
kanban.ts11.1 kB
/** * Kanban tools for Basecamp MCP server (cards, columns, steps) */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { asyncPagedToArray } from "basecamp-client"; import { z } from "zod"; import { BasecampIdSchema } from "../schemas/common.js"; import { initializeBasecampClient } from "../utils/auth.js"; import { applyContentOperations, ContentOperationFields, validateContentOperations, } from "../utils/contentOperations.js"; import { handleBasecampError } from "../utils/errorHandlers.js"; export function registerKanbanTools(server: McpServer): void { server.registerTool( "basecamp_list_kanban_columns", { title: "List Kanban Columns", description: "List all columns in a kanban board.", inputSchema: { bucket_id: BasecampIdSchema, card_table_id: BasecampIdSchema, }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const client = await initializeBasecampClient(); const response = await client.cardTables.get({ params: { bucketId: params.bucket_id, cardTableId: params.card_table_id, }, }); if (response.status !== 200 || !response.body) { throw new Error("Failed to fetch card table"); } const columns = response.body.lists || []; return { content: [ { type: "text", text: JSON.stringify( columns.map((col) => ({ id: col.id, title: col.title, position: col.position, cards_count: col.cards_count, type: col.type, description: col.description, })), null, 2, ), }, ], }; } catch (error) { return { content: [{ type: "text", text: handleBasecampError(error) }], }; } }, ); server.registerTool( "basecamp_list_kanban_cards", { title: "List Kanban Cards", description: "List cards in a kanban column.", inputSchema: { bucket_id: BasecampIdSchema, column_id: BasecampIdSchema, }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { try { const client = await initializeBasecampClient(); const cards = await asyncPagedToArray({ fetchPage: client.cardTableCards.list, request: { params: { bucketId: params.bucket_id, columnId: params.column_id }, query: {}, }, }); return { content: [ { type: "text", text: JSON.stringify( cards.map((c) => ({ id: c.id, title: c.title, due_on: c.due_on, })), null, 2, ), }, ], }; } catch (error) { return { content: [{ type: "text", text: handleBasecampError(error) }], }; } }, ); server.registerTool( "basecamp_create_kanban_card", { title: "Create Kanban Card", description: "Create a new card in a kanban column.", inputSchema: { bucket_id: BasecampIdSchema, column_id: BasecampIdSchema, title: z.string().min(1), content: z.string().optional(), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, }, async (params) => { try { const client = await initializeBasecampClient(); const response = await client.cardTableCards.create({ params: { bucketId: params.bucket_id, columnId: params.column_id }, body: { title: params.title, content: params.content }, }); if (response.status !== 201 || !response.body) { throw new Error("Failed to create card"); } return { content: [ { type: "text", text: `Card created!\n\nID: ${response.body.id}\nTitle: ${response.body.title}`, }, ], }; } catch (error) { return { content: [{ type: "text", text: handleBasecampError(error) }], }; } }, ); server.registerTool( "basecamp_update_kanban_card", { title: "Update Kanban Card", description: "Update a kanban card. At least one field (title, content, or partial content operations) must be provided. Returns updated card.", inputSchema: { bucket_id: BasecampIdSchema, card_id: BasecampIdSchema, title: z.string().min(1).optional().describe("New card title"), ...ContentOperationFields, due_on: z .string() .optional() .describe("Due date (YYYY-MM-DD format) or null to clear"), notify: z .boolean() .optional() .describe("Whether to notify assignees of the update"), assignee_ids: z .array(z.number()) .optional() .describe("Array of user IDs to assign to the card"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, }, async (params) => { try { // Validate at least one operation is provided validateContentOperations(params, [ "title", "due_on", "notify", "assignee_ids", ]); const client = await initializeBasecampClient(); let finalContent: string | undefined; // Check if we need to fetch current content for partial operations const hasPartialOps = params.content_append || params.content_prepend || params.search_replace; if (hasPartialOps || params.content !== undefined) { // Fetch current card if needed for partial operations if (hasPartialOps) { const currentResponse = await client.cardTableCards.get({ params: { bucketId: params.bucket_id, cardId: params.card_id, }, }); if (currentResponse.status !== 200 || !currentResponse.body) { throw new Error( `Failed to fetch current card: ${currentResponse.status}`, ); } const currentContent = currentResponse.body.content || ""; finalContent = applyContentOperations(currentContent, params); } else { // Full content replacement finalContent = params.content; } } const response = await client.cardTableCards.update({ params: { bucketId: params.bucket_id, cardId: params.card_id }, body: { ...(params.title ? { title: params.title } : {}), ...(finalContent !== undefined ? { content: finalContent } : {}), ...(params.due_on !== undefined ? { due_on: params.due_on } : {}), ...(params.notify !== undefined ? { notify: params.notify } : {}), ...(params.assignee_ids !== undefined ? { assignee_ids: params.assignee_ids } : {}), }, }); if (response.status !== 200 || !response.body) { throw new Error("Failed to update card"); } return { content: [ { type: "text", text: `Card updated successfully!\n\nID: ${response.body.id}\nTitle: ${response.body.title}`, }, ], }; } catch (error) { return { content: [{ type: "text", text: handleBasecampError(error) }], }; } }, ); server.registerTool( "basecamp_move_kanban_card", { title: "Move Kanban Card", description: "Move a kanban card to a different column and/or position within that column.", inputSchema: { bucket_id: BasecampIdSchema, card_id: BasecampIdSchema, column_id: BasecampIdSchema, position: z .number() .int() .nonnegative() .optional() .describe( "Position within the destination column (zero-indexed). If not specified, card will be added to the end of the column.", ), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, }, async (params) => { try { const client = await initializeBasecampClient(); const response = await client.cardTableCards.move({ params: { bucketId: params.bucket_id, cardId: params.card_id }, body: { column_id: params.column_id, ...(params.position !== undefined ? { position: params.position } : {}), }, }); if (response.status !== 204) { throw new Error("Failed to move card"); } return { content: [ { type: "text", text: `Card moved successfully to column ${params.column_id}${params.position !== undefined ? ` at position ${params.position}` : ""}!`, }, ], }; } catch (error) { return { content: [{ type: "text", text: handleBasecampError(error) }], }; } }, ); server.registerTool( "basecamp_create_kanban_step", { title: "Create Kanban Step", description: "Add a checklist step to a kanban card.", inputSchema: { bucket_id: BasecampIdSchema, card_id: BasecampIdSchema, title: z.string().min(1), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, }, async (params) => { try { const client = await initializeBasecampClient(); const response = await client.cardTableSteps.create({ params: { bucketId: params.bucket_id, cardId: params.card_id }, body: { title: params.title }, }); if (response.status !== 201 || !response.body) { throw new Error("Failed to create step"); } return { content: [ { type: "text", text: `Step created!\n\nID: ${response.body.id}\nTitle: ${response.body.title}`, }, ], }; } catch (error) { return { content: [{ type: "text", text: handleBasecampError(error) }], }; } }, ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/stefanoverna/basecamp-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server