Submatter/apps/mcp-server/src/tools/create-brief.ts
Randall Stillwell 9405da8a45 feat(convoys): convoy lifecycle MCP and state machine (Epic 4)
Adds server-enforced convoy transitions with gate actor guards, convoy_events
logging, and six MCP tools (create, brief, transition, log, query, reconcile).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 14:51:19 -05:00

76 lines
2.3 KiB
TypeScript

import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { briefObjectContentSchema } from "@tasks/shared/convoy-lifecycle";
import { db } from "../db.js";
import { objects } from "../schema.js";
import { loadConvoyProject } from "../lib/convoy-objects.js";
import { resolveWorkspaceHandle } from "../lib/resolve-workspace.js";
import { toolCatch, toolErr, toolOk } from "./tool-result.js";
const createBriefInputSchema = z.object({
workspace: z.string().min(1),
convoyId: z.string().uuid(),
briefNumber: z.number().int().positive(),
title: z.string().min(1).max(500),
files: z.array(z.string()).default([]),
sliceDependencies: z.array(z.number().int().positive()).default([]),
acceptance: z.string().optional(),
bodyMarkdown: z.string().optional(),
});
export function registerCreateBriefTool(mcp: McpServer): void {
mcp.registerTool(
"create_brief",
{
description:
"Create an implementer brief as a task object child of a convoy project.",
inputSchema: createBriefInputSchema,
},
async (args) => {
try {
const input = createBriefInputSchema.parse(args);
const ws = await resolveWorkspaceHandle(input.workspace);
const convoy = await loadConvoyProject(input.convoyId, ws.id);
if (!convoy) {
return toolErr(`Convoy ${input.convoyId} not found in workspace ${ws.slug}`);
}
const content = briefObjectContentSchema.parse({
briefNumber: input.briefNumber,
files: input.files,
sliceDependencies: input.sliceDependencies,
acceptance: input.acceptance,
});
const [created] = await db
.insert(objects)
.values({
type: "task",
title: input.title,
parentId: convoy.id,
workspaceId: ws.id,
description: input.bodyMarkdown ?? input.acceptance ?? null,
status: "ready",
content,
sortOrder: input.briefNumber,
})
.returning();
if (!created) {
return toolErr("Failed to create brief");
}
return toolOk({
workspace: { id: ws.id, slug: ws.slug, name: ws.name },
convoyId: convoy.id,
brief: created,
});
} catch (e) {
return toolCatch(e);
}
},
);
}