Skip to main content
Glama

basecamp_update_kanban_card

Modify a Basecamp kanban card's title, content, due date, assignees, or steps using partial updates to minimize token usage.

Instructions

Update a kanban card including its steps. At least one field (title, content, partial content operations, or steps) must be provided. Use partial content operations when possible to save on token usage.

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

TableJSON Schema
NameRequiredDescriptionDefault
bucket_idYesBasecamp resource identifier
card_idYes
titleNoNew card title
contentNoIf provided, replaces entire HTML content. Cannot be used with content_append, content_prepend, or search_replace.
content_appendNoText to append to the end of current content. Cannot be used with content.
content_prependNoText to prepend to the beginning of current content. Cannot be used with content.
search_replaceNoArray of search-replace operations to apply to current content. Cannot be used with content.
due_onNoDue date (YYYY-MM-DD format) or null to clear
notifyNoWhether to notify assignees of the update
assignee_idsNoArray of user IDs to assign to the card
stepsNoComplete array of desired steps. Array order defines position. Steps not in array will be deleted.

Implementation Reference

  • Handler function that updates kanban card properties (title, content with partial operations support, due_on, assignees, notify) and processes steps (create/update/delete/reposition/complete) using processStepOperations helper.
    async (params) => { try { // Validate at least one operation is provided validateContentOperations(params, [ "title", "due_on", "notify", "assignee_ids", "steps", ]); const client = await initializeBasecampClient(); let finalContent: string | undefined; let currentCard: any = null; // 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 || params.steps) { // Fetch current card if needed for partial operations or steps if (hasPartialOps || params.steps) { 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}`, ); } currentCard = currentResponse.body; if (hasPartialOps) { const currentContent = currentCard.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"); } // Process step operations if provided if (params.steps) { await processStepOperations( client, params.bucket_id, params.card_id, currentCard?.steps || [], params.steps, ); } 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) }], }; } },
  • Input schema for the tool using Zod, including bucket_id, card_id, optional title, content operations, due_on, notify, assignee_ids, and steps array with full CRUD semantics.
    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"), steps: z .array( z.object({ id: z .number() .optional() .describe("Step ID for updates. Omit for new steps."), title: z.string().describe("Step title. Required for new steps."), due_on: z .string() .nullable() .optional() .describe("Due date (YYYY-MM-DD) or null to clear"), 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( "Complete array of desired steps. Array order defines position. Steps not in array will be deleted.", ), },
  • Tool registration call within registerKanbanTools function, specifying title, description, inputSchema, annotations, and handler.
    server.registerTool( "basecamp_update_kanban_card", { title: "Update Kanban Card", description: `Update a kanban card including its steps. At least one field (title, content, partial content operations, or steps) must be provided. Use partial content operations when possible to save on token usage. ${htmlRules}`, 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"), steps: z .array( z.object({ id: z .number() .optional() .describe("Step ID for updates. Omit for new steps."), title: z.string().describe("Step title. Required for new steps."), due_on: z .string() .nullable() .optional() .describe("Due date (YYYY-MM-DD) or null to clear"), 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( "Complete array of desired steps. Array order defines position. Steps not in array will be deleted.", ), }, 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", "steps", ]); const client = await initializeBasecampClient(); let finalContent: string | undefined; let currentCard: any = null; // 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 || params.steps) { // Fetch current card if needed for partial operations or steps if (hasPartialOps || params.steps) { 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}`, ); } currentCard = currentResponse.body; if (hasPartialOps) { const currentContent = currentCard.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"); } // Process step operations if provided if (params.steps) { await processStepOperations( client, params.bucket_id, params.card_id, currentCard?.steps || [], params.steps, ); } 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) }], }; } }, );
  • Helper function to process step operations on a kanban card: validates inputs, identifies create/update/delete/reposition/completion changes, executes API calls accordingly, using complete array replacement semantics.
    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 }, }); } } }
  • src/index.ts:66-66 (registration)
    Invocation of registerKanbanTools during server initialization, which registers the kanban tools including basecamp_update_kanban_card.
    registerKanbanTools(server);

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