Skip to main content
Glama

basecamp_create_kanban_card

Create a new kanban card in Basecamp to organize tasks and track work progress across project columns.

Instructions

Create a new card in a kanban column.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
bucket_idYesBasecamp resource identifier
column_idYes
contentNo
titleYes

Implementation Reference

  • Handler function that creates a new kanban card using the Basecamp API client. It handles title, content, due date, assignees, notifications, and creates optional checklist steps using the processStepOperations helper.
    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, ...(params.due_on ? { due_on: params.due_on } : {}), ...(params.assignee_ids ? { assignee_ids: params.assignee_ids } : {}), ...(params.notify !== undefined ? { notify: params.notify } : {}), }, }); if (response.status !== 201 || !response.body) { throw new Error("Failed to create card"); } // Process step operations if provided (for new card, currentSteps is empty) if (params.steps) { await processStepOperations( client, params.bucket_id, response.body.id, [], // No current steps for a new card params.steps, ); } 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) }], }; }
  • Zod-based input schema defining parameters for creating a kanban card: bucket_id, column_id, title (required), content, due_on, assignee_ids, notify, and optional steps array.
    inputSchema: { bucket_id: BasecampIdSchema, column_id: BasecampIdSchema, title: z.string().min(1), content: z.string().optional(), due_on: z.string().optional().describe("Due date in YYYY-MM-DD format"), assignee_ids: z .array(z.number()) .optional() .describe("Array of user IDs to assign to the card"), notify: z.boolean().optional().describe("Whether to notify assignees"), steps: z .array( z.object({ title: z.string().describe("Step title"), due_on: z .string() .nullable() .optional() .describe("Due date (YYYY-MM-DD) or null"), assignee_ids: z .array(z.number()) .optional() .describe("Array of user IDs to assign"), completed: z .boolean() .optional() .describe("Whether step is completed"), }), ) .optional() .describe("Array of steps to create. Array order defines position."), },
  • Registers the 'basecamp_create_kanban_card' tool with the MCP server inside the registerKanbanTools function, including title, description, input schema, annotations, and handler.
    server.registerTool( "basecamp_create_kanban_card", { title: "Create Kanban Card", description: `Create a new card in a kanban column with optional checklist steps. ${htmlRules}`, inputSchema: { bucket_id: BasecampIdSchema, column_id: BasecampIdSchema, title: z.string().min(1), content: z.string().optional(), due_on: z.string().optional().describe("Due date in YYYY-MM-DD format"), assignee_ids: z .array(z.number()) .optional() .describe("Array of user IDs to assign to the card"), notify: z.boolean().optional().describe("Whether to notify assignees"), steps: z .array( z.object({ title: z.string().describe("Step title"), due_on: z .string() .nullable() .optional() .describe("Due date (YYYY-MM-DD) or null"), assignee_ids: z .array(z.number()) .optional() .describe("Array of user IDs to assign"), completed: z .boolean() .optional() .describe("Whether step is completed"), }), ) .optional() .describe("Array of steps to create. Array order defines position."), }, 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, ...(params.due_on ? { due_on: params.due_on } : {}), ...(params.assignee_ids ? { assignee_ids: params.assignee_ids } : {}), ...(params.notify !== undefined ? { notify: params.notify } : {}), }, }); if (response.status !== 201 || !response.body) { throw new Error("Failed to create card"); } // Process step operations if provided (for new card, currentSteps is empty) if (params.steps) { await processStepOperations( client, params.bucket_id, response.body.id, [], // No current steps for a new card params.steps, ); } 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) }], }; } }, );
  • Supporting helper function that handles all step operations (create new steps, update existing, delete missing, reposition) for kanban cards. Used by both create and update card tools.
    async function processStepOperations( client: Awaited<ReturnType<typeof initializeBasecampClient>>, bucketId: number, cardId: number, currentSteps: CurrentStep[], desiredSteps: StepInput[], ): Promise<void> { // ===== VALIDATION ===== // Validate new steps have titles for (const step of desiredSteps) { if (!step.id && !step.title) { throw new Error("New steps (without id) must have a title"); } } // Check for duplicate IDs const stepIds = desiredSteps.filter((s) => s.id).map((s) => s.id!); if (new Set(stepIds).size !== stepIds.length) { throw new Error("Duplicate step IDs found in steps array"); } // Validate all step IDs exist on current card const currentStepIds = new Set(currentSteps.map((s) => s.id)); for (const id of stepIds) { if (!currentStepIds.has(id)) { const available = Array.from(currentStepIds).join(", "); throw new Error( `Step ID ${id} not found on card. ${ available ? `Available IDs: ${available}` : "No steps exist on this card." }`, ); } } // ===== IDENTIFY OPERATIONS ===== const desiredStepIds = new Set(stepIds); // Steps to delete: in current but not in desired const toDelete = currentSteps.filter((s) => !desiredStepIds.has(s.id)); // Steps to create: no ID provided const toCreate = desiredSteps.filter((s) => !s.id); // Steps to update/reposition/complete const toProcess = desiredSteps.filter((s) => s.id); // ===== DELETE OPERATIONS ===== for (const step of toDelete) { await client.recordings.trash({ params: { bucketId, recordingId: step.id }, }); } // ===== CREATE OPERATIONS ===== const createdStepIds: number[] = []; // Track new IDs for repositioning for (const step of toCreate) { const response = await client.cardTableSteps.create({ params: { bucketId, cardId }, body: { title: step.title, ...(step.due_on !== undefined ? { due_on: step.due_on || undefined } : {}), ...(step.assignee_ids ? { assignees: step.assignee_ids.join(",") } : {}), }, }); if (response.status !== 201 || !response.body) { throw new Error(`Failed to create step: ${step.title}`); } createdStepIds.push(response.body.id); // Set completion if specified if (step.completed !== undefined && step.completed) { await client.cardTableSteps.setCompletion({ params: { bucketId, stepId: response.body.id }, body: { completion: "on" }, }); } } // ===== UPDATE OPERATIONS ===== // Build a map of current steps for comparison const currentStepMap = new Map(currentSteps.map((s) => [s.id, s])); for (const step of toProcess) { const currentStep = currentStepMap.get(step.id!); if (!currentStep) continue; // Should not happen due to validation // Check what changed const titleChanged = step.title && step.title !== currentStep.title; const dueOnChanged = step.due_on !== undefined && step.due_on !== currentStep.due_on; const currentAssigneeIds = currentStep.assignees?.map((a) => a.id) || []; const desiredAssigneeIds = step.assignee_ids || []; const assigneesChanged = step.assignee_ids !== undefined && (currentAssigneeIds.length !== desiredAssigneeIds.length || !currentAssigneeIds.every((id, i) => id === desiredAssigneeIds[i])); // Only update if something changed if (titleChanged || dueOnChanged || assigneesChanged) { await client.cardTableSteps.update({ params: { bucketId, stepId: step.id! }, body: { ...(step.title ? { title: step.title } : {}), ...(step.due_on !== undefined ? { due_on: step.due_on || undefined } : {}), ...(step.assignee_ids ? { assignees: step.assignee_ids.join(",") } : {}), }, }); } // Handle completion status changes if ( step.completed !== undefined && step.completed !== currentStep.completed ) { await client.cardTableSteps.setCompletion({ params: { bucketId, stepId: step.id! }, body: { completion: step.completed ? "on" : "off" }, }); } } // ===== REPOSITION OPERATIONS ===== // Build final list of step IDs in desired order (mix of existing and new) const finalStepIds: number[] = []; let createIndex = 0; for (const step of desiredSteps) { if (step.id) { finalStepIds.push(step.id); } else { // This was a created step, use the ID we tracked if (createIndex < createdStepIds.length) { finalStepIds.push(createdStepIds[createIndex++]); } } } // Get current positions after all operations // We need to reposition if the order doesn't match const currentPositions = new Map( currentSteps .filter((s) => finalStepIds.includes(s.id)) .map((s, i) => [s.id, i]), ); // Reposition each step to match the desired array order for (let i = 0; i < finalStepIds.length; i++) { const stepId = finalStepIds[i]; const currentPos = currentPositions.get(stepId); // Only reposition if needed if (currentPos !== undefined && currentPos !== i) { await client.cardTableSteps.reposition({ params: { bucketId, cardId }, body: { source_id: stepId, position: i }, }); } } }

Latest Blog Posts

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