import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { eq } from "../drizzle.js"; import { z } from "zod"; import { db } from "../db.js"; import { objects } from "../schema.js"; import { toolCatch, toolErr, toolOk } from "./tool-result.js"; /** * create_brief — Phase 2a lifecycle MCP tool. * * Convention (see agent-pipeline plan §7): a brief is a `task` object whose * `parentId` is the convoy (`project`). Status starts at "ready" (the brief * itself is ready to dispatch; the convoy's status governs the lifecycle). * * Local-first: role-architect writes `.convoys//brief-N-*.md` FIRST, * then calls this. If this fails, queues to `.pending-mcp-sync.jsonl`. */ const createBriefInputSchema = z.object({ convoyId: z .string() .uuid() .describe("Convoy project id (returned by create_convoy)."), briefNumber: z.number().int().min(1).max(99), title: z.string().min(1).max(500), filesAllowlist: z .array(z.string()) .min(1) .describe( "Files this brief is allowed to edit. Used by the reviewer to detect scope expansion.", ), dependsOn: z .array(z.number().int().min(1).max(99)) .default([]) .describe( "Brief numbers this brief depends on. Empty = parallel-safe (can fan out via /multitask).", ), acceptanceCriteria: z .array(z.string().min(1)) .min(1) .describe("Acceptance criteria, one per line."), briefMarkdown: z .string() .min(1) .describe( "Brief body. Becomes the task description; the agent reads only this file when implementing.", ), }); function initialDescription(input: z.infer): string { const meta = [ ``, ``, ``, ``, ].join("\n"); const acceptance = [ "## Acceptance criteria", "", ...input.acceptanceCriteria.map((c) => `- [ ] ${c}`), ].join("\n"); return `${meta}\n\n${input.briefMarkdown.trim()}\n\n${acceptance}\n`; } export function registerCreateBriefTool(mcp: McpServer): void { mcp.registerTool( "create_brief", { description: "Create a brief as a `task` child of a convoy `project`. Validates that the parent exists and is a project.", inputSchema: createBriefInputSchema, }, async (args) => { try { const input = createBriefInputSchema.parse(args); const [parent] = await db .select({ id: objects.id, type: objects.type, workspaceId: objects.workspaceId }) .from(objects) .where(eq(objects.id, input.convoyId)) .limit(1); if (!parent) { return toolErr(`Convoy not found: ${input.convoyId}`); } if (parent.type !== "project") { return toolErr( `Object ${input.convoyId} is type '${parent.type}', expected 'project'. Briefs must be children of convoys.`, ); } const [created] = await db .insert(objects) .values({ type: "task", title: `Brief ${input.briefNumber}: ${input.title}`, parentId: parent.id, workspaceId: parent.workspaceId, description: initialDescription(input), status: "ready", }) .returning(); if (!created) { return toolErr("Failed to create brief task"); } return toolOk({ brief: { id: created.id, briefNumber: input.briefNumber, title: created.title, convoyId: parent.id, filesAllowlist: input.filesAllowlist, dependsOn: input.dependsOn, }, }); } catch (e) { return toolCatch(e); } }, ); }