17b9006b88
Build pitch-deck / build-push-deploy (push) Successful in 1m55s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 36s
CI / test-python-voice (push) Successful in 35s
CI / test-bqas (push) Successful in 35s
- Add English email template variants (greeting, message, closing, subject, CTA copy) - Add `preferred_lang` column to `pitch_investors` — stored per investor, deck opens in that language by default - Invite form: DE/EN language toggle that switches email defaults and pitch language setting - Invite form: "Send email" toggle — when off, creates investor + returns magic link without sending email (for cold outreach attachment) - `app/page.tsx`: initializes pitch language from investor's `preferred_lang` before first render (no flash) - Migration 007 added to `/api/admin/migrate` route for production rollout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
186 lines
7.4 KiB
JavaScript
186 lines
7.4 KiB
JavaScript
#!/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, path, body) {
|
|
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;
|
|
try {
|
|
data = JSON.parse(text);
|
|
}
|
|
catch {
|
|
data = text;
|
|
}
|
|
if (!res.ok) {
|
|
const msg = typeof data === "object" && data && "error" in data
|
|
? data.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",
|
|
];
|
|
// --- 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;
|
|
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);
|
|
});
|