feat(pitch-deck): MCP server for pitch version management via Claude Code
Some checks failed
Build pitch-deck / build-and-push (push) Failing after 1m8s
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 42s
CI / test-bqas (push) Successful in 40s
CI / Deploy (push) Failing after 3s
Some checks failed
Build pitch-deck / build-and-push (push) Failing after 1m8s
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 42s
CI / test-bqas (push) Successful in 40s
CI / Deploy (push) Failing after 3s
Stdio MCP server that wraps the pitch-deck admin API, exposing 11 tools: list_versions, create_version, get_version, get_table_data, update_table_data, commit_version, fork_version, diff_versions, list_investors, assign_version, invite_investor. Authenticates via PITCH_ADMIN_SECRET bearer token against the deployed pitch-deck API. All existing auth, validation, and audit logging is reused — the MCP server is a thin adapter. Usage: add to ~/.claude/settings.json mcpServers, set PITCH_API_URL and PITCH_ADMIN_SECRET env vars. See mcp-server/README.md (to be added). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1173
pitch-deck/mcp-server/package-lock.json
generated
Normal file
1173
pitch-deck/mcp-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
pitch-deck/mcp-server/package.json
Normal file
19
pitch-deck/mcp-server/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "breakpilot-pitch-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP server for managing BreakPilot pitch versions via Claude Code",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2",
|
||||
"@types/node": "^22.10.2"
|
||||
}
|
||||
}
|
||||
285
pitch-deck/mcp-server/src/index.ts
Normal file
285
pitch-deck/mcp-server/src/index.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/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<unknown> {
|
||||
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);
|
||||
});
|
||||
14
pitch-deck/mcp-server/tsconfig.json
Normal file
14
pitch-deck/mcp-server/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user