Skip to main content
Glama
soil-dev

capsulemcp

update_project

Update specific fields on an existing Capsule project. Change name, status, owner, team, stage, custom fields, or expected close date without affecting other data.

Instructions

Update fields on an existing project. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable — Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
idYes
nameNo
descriptionNo
statusNo
ownerIdNoReassign owner: pass a user ID to set, or `null` to unassign (matches the 'Unassign' option in Capsule's web UI). When you supply `ownerId` and omit `teamId` and/or `stageId`, the connector fetches the project's current omitted fields and includes them in the PUT body — this preserves them across the owner change (without it, Capsule's PUT would clear team; stage carry is defensive against the symmetric clear). Supply `teamId` and/or `stageId` explicitly on the same call to change them instead. `teamId: null` clears the team as part of an owner change. Constraints (Capsule enforces, 422 on violation): owner must be a member of the team if both are set; a project must always have at least one of {owner, team} set (cannot clear both).
teamIdNoReassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_project { teamId }` alone is safe — the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. A project must always have at least one of {owner, team} set — `teamId: null` on a project whose owner is already null returns 422 'owner or team is required'.
stageIdNoMove the project to this stage (board column). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board — passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id.
expectedCloseOnNoYYYY-MM-DD
fieldsNoSet custom field values on this record. PARTIAL UPDATE: only the definitions you list are touched; any field NOT in this array is left unchanged. Discover available definitions via list_custom_fields; read current values via get_project with embed='fields'. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array — use add_tag explicitly if you want it visible via embed=tags.

Implementation Reference

  • The handler function `updateProject` that executes the tool logic. It builds a PUT body, handles the asymmetric Capsule owner/team semantic by fetching current values when only ownerId is provided, and calls capsulePut at /kases/{id}.
    export async function updateProject(input: z.infer<typeof updateProjectSchema>) {
      const { id, ownerId, teamId, stageId, fields, ...rest } = input;
    
      const body: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(rest)) {
        if (v !== undefined) body[k] = v;
      }
    
      // Capsule's PUT on /kases has an asymmetric owner/team semantic:
      //
      //   `owner` in body          → Capsule clears `team` (unless `team` also in body)
      //   `team` in body           → Capsule preserves the existing `owner` (and validates owner ∈ team)
      //
      // To make `update_project { ownerId }` safe (so it doesn't accidentally
      // clear an existing team — or, defensively, stage), the connector reads
      // the current project and carries any omitted `team` / `stage` into the
      // PUT body whenever `ownerId` is being touched. Carrying stage is
      // defensive: alpha.20-era verification didn't directly probe whether
      // owner-in-body PUTs clear stage the way they clear team, but the cost
      // of a redundant `stage: <currentId>` is one extra integer in the body,
      // so we err on the safe side rather than risk a silent stage clear.
      //
      // `null` means "unassign" on either owner/team (matches Capsule's UI
      // "Unassign" option); `undefined` means "don't touch this field".
      let resolvedTeamId: number | null | undefined = teamId;
      let resolvedStageId: number | undefined = stageId;
      if (ownerId !== undefined && (teamId === undefined || stageId === undefined)) {
        const { data } = await capsuleGet<{
          kase: { team?: { id: number } | null; stage?: { id: number } | null };
        }>(`/kases/${id}`);
        // Only carry forward when the project actually has a value; if
        // current team/stage is null, leave the field out of the body entirely
        // (sending `team: null` would be a redundant clear and could surprise
        // on the owner-or-team-required 422 path; same idea for stage).
        if (teamId === undefined) {
          resolvedTeamId = data.kase?.team?.id ?? undefined;
        }
        if (stageId === undefined) {
          resolvedStageId = data.kase?.stage?.id ?? undefined;
        }
      }
    
      if (ownerId === null) body["owner"] = null;
      else if (ownerId !== undefined) body["owner"] = { id: ownerId };
      if (resolvedTeamId === null) body["team"] = null;
      else if (resolvedTeamId !== undefined) body["team"] = { id: resolvedTeamId };
      if (resolvedStageId) body["stage"] = resolvedStageId;
      const mappedFields = mapFieldsForBody(fields);
      if (mappedFields !== undefined) body["fields"] = mappedFields;
    
      return capsulePut<{ kase: unknown }>(`/kases/${id}`, { kase: body });
    }
  • Zod schema `updateProjectSchema` defining input validation for the tool. Fields: id (required), name, description, status (OPEN/CLOSED), ownerId (nullable), teamId (nullable), stageId, expectedCloseOn (YYYY-MM-DD), and fields (custom fields array).
    export const updateProjectSchema = z.object({
      id: z.number().int().positive(),
      name: z.string().min(1).optional(),
      description: z.string().optional(),
      status: z.enum(["OPEN", "CLOSED"]).optional(),
      ownerId: z
        .number()
        .int()
        .positive()
        .nullable()
        .optional()
        .describe(
          "Reassign owner: pass a user ID to set, or `null` to unassign (matches the 'Unassign' option in Capsule's web UI). " +
            "When you supply `ownerId` and omit `teamId` and/or `stageId`, the connector fetches the project's current omitted fields and includes them in the PUT body — this preserves them across the owner change (without it, Capsule's PUT would clear team; stage carry is defensive against the symmetric clear). " +
            "Supply `teamId` and/or `stageId` explicitly on the same call to change them instead. `teamId: null` clears the team as part of an owner change. " +
            "Constraints (Capsule enforces, 422 on violation): owner must be a member of the team if both are set; a project must always have at least one of {owner, team} set (cannot clear both).",
        ),
      teamId: z
        .number()
        .int()
        .positive()
        .nullable()
        .optional()
        .describe(
          "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. " +
            "Capsule preserves the existing owner across a team change (server-side), so `update_project { teamId }` alone is safe — the owner is carried through. " +
            "Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. " +
            "A project must always have at least one of {owner, team} set — `teamId: null` on a project whose owner is already null returns 422 'owner or team is required'.",
        ),
      stageId: z
        .number()
        .int()
        .positive()
        .optional()
        .describe(
          "Move the project to this stage (board column). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). " +
            "WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board — passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id.",
        ),
      expectedCloseOn: z
        .string()
        .regex(/^\d{4}-\d{2}-\d{2}$/)
        .optional()
        .describe("YYYY-MM-DD"),
      fields: z
        .array(CustomFieldWriteSchema)
        .optional()
        .describe(
          fieldsArrayDescriptor("get_project") +
            " Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array — use add_tag explicitly if you want it visible via embed=tags.",
        ),
    });
  • src/server.ts:535-541 (registration)
    Registration of the tool named 'update_project' via the `registerTool` helper in src/server.ts. Passes description, schema (updateProjectSchema), and handler (updateProject).
    registerTool(
      server,
      "update_project",
      "Update fields on an existing project. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable — Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning.",
      updateProjectSchema,
      updateProject,
    );
  • src/server.ts:65-69 (registration)
    Import of `updateProjectSchema` and `updateProject` from ./tools/projects.js into src/server.ts, used for the tool registration.
      updateProjectSchema,
      updateProject,
      deleteProjectSchema,
      deleteProject,
    } from "./tools/projects.js";
  • The `registerTool` helper function that wraps the MCP SDK's server.registerTool to reduce boilerplate. It takes name, description, schema, and handler, and wraps the return value in the standard MCP text-content response.
    export function registerTool<Schema extends z.ZodObject<ZodRawShape>>(
      server: McpServer,
      name: string,
      description: string,
      schema: Schema,
      handler: (input: z.infer<Schema>) => Promise<unknown>,
    ): void {
      // Use the SDK config-form registerTool with the full Zod schema. The
      // deprecated shape overload rebuilds z.object(schema.shape), which drops
      // object-level refinements such as superRefine.
      const registerWithSchema = server.registerTool.bind(server) as (
        toolName: string,
        config: { description: string; inputSchema: Schema },
        callback: (input: z.infer<Schema>) => Promise<CallToolResult>,
      ) => void;
    
      registerWithSchema(name, { description, inputSchema: schema }, async (input) => {
        const result = await handler(input);
        return wrapAsText(result);
      });
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations, so description fully discloses behavior: partial updates, editable CLOSED projects, cross-board stage move risks, custom field quirks. Exceeds transparency expectations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three concise sentences, front-loaded with purpose and key behavioral notes. No fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers update behavior and edge cases well. Lacks return value description, but no output schema is present. Slight gap for agent expectation of result.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 56% but schema descriptions are extensive (ownerId, teamId, stageId, fields). Tool description adds minor context (closed status effect) but doesn't add meaning beyond schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clear verb+resource: 'Update fields on an existing project.' Specific about partial update and status='CLOSED' for closing. Distinguishes from update_task, update_opportunity, etc.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides context for closing projects and editability of CLOSED projects. Lacks explicit when-not-to-use or comparison with sibling tools, but gives sufficient operational guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/soil-dev/capsulemcp'

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