create_project
Create a new project linked to an existing party in Capsule CRM. Provide at least the party ID and project name; optionally set status, owner, team, stage, description, and expected close date.
Instructions
Create a new project (case) in Capsule CRM linked to a party. Requires partyId and name; description, status, owner, and starting board/stage are optional. To pin a project to a specific board+stage on creation, pass stageId (which uniquely identifies a stage within a board). Discover valid ids via list_boards + list_stages. Returns the created project including its assigned id.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | ||
| partyId | Yes | ID of the party linked to this project | |
| description | No | ||
| status | No | Defaults to OPEN when omitted. | |
| ownerId | No | Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation — e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied. | |
| teamId | No | Assign to team ID (discover via list_teams). Capsule projects must always have at least one of {owner, team} set — Capsule returns 422 'owner or team is required' otherwise. Three ownership shapes are valid: owner alone, team alone, or owner+team (the user must be a member of the team — users can belong to multiple teams; 422 'owner is not a member of the team' otherwise). Tenant-specific board automations may set the team field on project creation (e.g. 'when project enters board X, set team to T'). If you observe a team set despite omitting `teamId`, check the target board's automation rules. | |
| stageId | No | Stage (board column) to place the project on. Discover IDs via list_stages — each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply — any clearing you observe traces to board automations, not the API. | |
| expectedCloseOn | No | YYYY-MM-DD |
Implementation Reference
- src/tools/projects.ts:109-127 (handler)The actual handler function that creates a project by POSTing to Capsule's /kases endpoint. It constructs a body with party, status (defaults to OPEN), owner, team, and stage.
export async function createProject(input: z.infer<typeof createProjectSchema>) { const { partyId, ownerId, teamId, status, stageId, ...rest } = input; // Default applied here (not via zod's .default()) so the inferred // input type keeps `status` optional. Same pattern as listTasks. const body: Record<string, unknown> = { ...rest, status: status ?? "OPEN", party: { id: partyId }, }; if (ownerId) body["owner"] = { id: ownerId }; if (teamId) body["team"] = { id: teamId }; // Capsule's create-case body uses `stage: <integer>` per the docs // example. The GET response uses the object form `stage: {id, name}`, // but we follow the documented request shape on the way in. if (stageId) body["stage"] = stageId; return capsulePost<{ kase: unknown }>("/kases", { kase: body }); } - src/tools/projects.ts:69-107 (schema)Zod schema defining the input validation for create_project. Requires name and partyId; optional fields include description, status, ownerId, teamId, stageId, expectedCloseOn.
export const createProjectSchema = z.object({ name: z.string().min(1), partyId: z.number().int().positive().describe("ID of the party linked to this project"), description: z.string().optional(), status: z.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."), ownerId: z .number() .int() .positive() .optional() .describe( "Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. " + "NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation — e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied.", ), teamId: z .number() .int() .positive() .optional() .describe( "Assign to team ID (discover via list_teams). Capsule projects must always have at least one of {owner, team} set — Capsule returns 422 'owner or team is required' otherwise. " + "Three ownership shapes are valid: owner alone, team alone, or owner+team (the user must be a member of the team — users can belong to multiple teams; 422 'owner is not a member of the team' otherwise). " + "Tenant-specific board automations may set the team field on project creation (e.g. 'when project enters board X, set team to T'). If you observe a team set despite omitting `teamId`, check the target board's automation rules.", ), stageId: z .number() .int() .positive() .optional() .describe( "Stage (board column) to place the project on. Discover IDs via list_stages — each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). " + "NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply — any clearing you observe traces to board automations, not the API.", ), expectedCloseOn: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/) .optional() .describe("YYYY-MM-DD"), }); - src/server.ts:527-533 (registration)Registration of the create_project tool with the MCP server, wiring the schema and handler together under the name 'create_project'.
registerTool( server, "create_project", "Create a new project (case) in Capsule CRM linked to a party. Requires partyId and name; description, status, owner, and starting board/stage are optional. To pin a project to a specific board+stage on creation, pass stageId (which uniquely identifies a stage within a board). Discover valid ids via list_boards + list_stages. Returns the created project including its assigned id.", createProjectSchema, createProject, ); - src/server/register-tool.ts:39-59 (helper)Generic helper that wraps a handler+Zod-schema pair into an MCP tool registration, automatically stringifying the return value as JSON text.
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); }); }