basecamp_create_kanban_card
Create a new card in a Basecamp kanban column with optional checklist steps, due dates, and assignees to organize project tasks.
Instructions
Create a new card in a kanban column with optional checklist steps.
HTML rules for content:
Allowed tags: div, span, h1, br, strong, em, strike, a (with an href attribute), pre, ol, ul, li, blockquote, bc-attachment (with sgid attribute).
Try to be semantic despite the limitations of tags. Use double to create paragraph spacing
To mention people:
To consume less tokens, existing tags can be rewritten by dropping any attributes/inner content and just leave the "sgid" and "caption" attributes, without loosing any information
You can highlight parts of the content in this format ... valid colors are:
red: 255, 229, 229
yellow: 250, 247, 133
green: 228, 248, 226
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| bucket_id | Yes | Basecamp resource identifier | |
| column_id | Yes | ||
| title | Yes | ||
| content | No | ||
| due_on | No | Due date in YYYY-MM-DD format | |
| assignee_ids | No | Array of user IDs to assign to the card | |
| notify | No | Whether to notify assignees | |
| steps | No | Array of steps to create. Array order defines position. |
Implementation Reference
- src/tools/kanban.ts:454-497 (handler)The handler function that creates a new kanban card in the specified Basecamp bucket and column using the API client. Handles optional steps by calling the processStepOperations helper. Returns structured content with success message including new card ID or error text.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) }], }; }
- src/tools/kanban.ts:411-453 (schema)Input schema using Zod for validating tool parameters: bucket_id, column_id, title (required), content, due_on, assignee_ids, notify, and optional steps array with their properties.{ 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, }, },
- src/tools/kanban.ts:409-499 (registration)Registers the tool with the MCP server inside the registerKanbanTools function, which is exported and called from src/index.ts.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) }], }; } }, );
- src/tools/kanban.ts:41-219 (helper)Helper function called by the handler (and update tool) to manage kanban card steps: validates inputs, performs CRUD and reposition operations via Basecamp API based on desired vs current steps.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 }, }); } } }