#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const API_URL = process.env.PITCH_API_URL || "https://pitch.breakpilot.com"; const API_SECRET = process.env.PITCH_ADMIN_SECRET || ""; if (!API_SECRET) { console.error("PITCH_ADMIN_SECRET is required"); process.exit(1); } // --- HTTP client --- async function api( method: string, path: string, body?: unknown ): Promise { const url = `${API_URL}${path}`; const res = await fetch(url, { method, headers: { Authorization: `Bearer ${API_SECRET}`, "Content-Type": "application/json", }, body: body ? JSON.stringify(body) : undefined, }); const text = await res.text(); let data: unknown; try { data = JSON.parse(text); } catch { data = text; } if (!res.ok) { const msg = typeof data === "object" && data && "error" in data ? (data as { error: string }).error : `HTTP ${res.status}`; throw new Error(msg); } return data; } const TABLE_NAMES = [ "company", "team", "financials", "market", "competitors", "features", "milestones", "metrics", "funding", "products", "fm_scenarios", "fm_assumptions", ] as const; // --- MCP Server --- const server = new McpServer({ name: "breakpilot-pitch", version: "1.0.0", }); // 1. list_versions server.tool( "list_versions", "List all pitch versions with status, parent chain, and investor assignment counts", {}, async () => { const data = await api("GET", "/api/admin/versions"); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 2. create_version server.tool( "create_version", "Create a new draft version. Optionally fork from a parent version ID, otherwise snapshots current base tables.", { name: z.string().describe("Version name, e.g. 'Conservative Q4'"), description: z .string() .optional() .describe("Optional description"), parent_id: z .string() .uuid() .optional() .describe("UUID of parent version to fork from. Omit to snapshot base tables."), }, async ({ name, description, parent_id }) => { const data = await api("POST", "/api/admin/versions", { name, description, parent_id, }); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 3. get_version server.tool( "get_version", "Get full version detail including all 12 data table snapshots", { version_id: z.string().uuid().describe("Version UUID"), }, async ({ version_id }) => { const data = await api("GET", `/api/admin/versions/${version_id}`); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 4. get_table_data server.tool( "get_table_data", "Get a specific table's data for a version. Tables: company, team, financials, market, competitors, features, milestones, metrics, funding, products, fm_scenarios, fm_assumptions", { version_id: z.string().uuid().describe("Version UUID"), table_name: z .enum(TABLE_NAMES) .describe("Which data table to retrieve"), }, async ({ version_id, table_name }) => { const data = await api( "GET", `/api/admin/versions/${version_id}/data/${table_name}` ); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 5. update_table_data server.tool( "update_table_data", "Replace a table's data in a DRAFT version. Pass the full array of row objects. Single-record tables (company, funding) should still be wrapped in an array.", { version_id: z.string().uuid().describe("Version UUID (must be a draft)"), table_name: z.enum(TABLE_NAMES).describe("Which data table to update"), data: z .string() .describe( "JSON string of the new data — an array of row objects. Example for company: [{\"name\":\"BreakPilot\",\"tagline_en\":\"...\"}]" ), }, async ({ version_id, table_name, data: dataStr }) => { let parsed: unknown; try { parsed = JSON.parse(dataStr); } catch { return { content: [{ type: "text", text: "Error: invalid JSON in data parameter" }], isError: true, }; } const result = await api( "PUT", `/api/admin/versions/${version_id}/data/${table_name}`, { data: parsed } ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } ); // 6. commit_version server.tool( "commit_version", "Commit a draft version, making it immutable and available for investor assignment", { version_id: z.string().uuid().describe("Draft version UUID to commit"), }, async ({ version_id }) => { const data = await api( "POST", `/api/admin/versions/${version_id}/commit` ); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 7. fork_version server.tool( "fork_version", "Create a new draft by forking an existing version (copies all data)", { version_id: z.string().uuid().describe("Version UUID to fork from"), name: z.string().describe("Name for the new forked draft"), }, async ({ version_id, name }) => { const data = await api( "POST", `/api/admin/versions/${version_id}/fork`, { name } ); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 8. diff_versions server.tool( "diff_versions", "Compare two versions and see per-table diffs (added/removed/changed rows and fields)", { version_a: z.string().uuid().describe("First version UUID"), version_b: z.string().uuid().describe("Second version UUID"), }, async ({ version_a, version_b }) => { const data = await api( "GET", `/api/admin/versions/${version_a}/diff/${version_b}` ); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 9. list_investors server.tool( "list_investors", "List all investors with their login stats, assigned version, and activity", {}, async () => { const data = await api("GET", "/api/admin/investors"); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 10. assign_version server.tool( "assign_version", "Assign a committed version to an investor (determines what pitch data they see). Pass null to reset to default base tables.", { investor_id: z.string().uuid().describe("Investor UUID"), version_id: z .string() .uuid() .nullable() .describe("Committed version UUID to assign, or null for default"), }, async ({ investor_id, version_id }) => { const data = await api("PATCH", `/api/admin/investors/${investor_id}`, { assigned_version_id: version_id, }); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // 11. invite_investor server.tool( "invite_investor", "Invite a new investor by email — sends a magic link for passwordless access to the pitch deck", { email: z.string().email().describe("Investor email address"), name: z.string().optional().describe("Investor name"), company: z.string().optional().describe("Investor company"), }, async ({ email, name, company }) => { const data = await api("POST", "/api/admin/invite", { email, name, company, }); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ); // --- Start --- async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((err) => { console.error("MCP server error:", err); process.exit(1); });