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