Merge branch 'main' of ssh://gitea.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-core into feature/payment-compliance-module
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
|||||||
secrets/
|
secrets/
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
|
.mcp.json
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
123
pitch-deck/mcp-server/README.md
Normal file
123
pitch-deck/mcp-server/README.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# BreakPilot Pitch MCP Server
|
||||||
|
|
||||||
|
MCP server that lets Claude Code directly manage pitch versions, invite investors, and assign versions — without touching the browser admin UI.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
11 tools exposed to Claude Code:
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `list_versions` | List all pitch versions with status + investor counts |
|
||||||
|
| `create_version` | Create a draft (snapshot base tables or fork from parent) |
|
||||||
|
| `get_version` | Get full version detail with all 12 data table snapshots |
|
||||||
|
| `get_table_data` | Get one table's data (company, team, financials, market, etc.) |
|
||||||
|
| `update_table_data` | Replace a table's data in a draft version |
|
||||||
|
| `commit_version` | Lock a draft as immutable |
|
||||||
|
| `fork_version` | Create new draft by copying an existing version |
|
||||||
|
| `diff_versions` | Per-table diff between any two versions |
|
||||||
|
| `list_investors` | List all investors with stats + version assignments |
|
||||||
|
| `assign_version` | Assign a committed version to an investor |
|
||||||
|
| `invite_investor` | Send magic-link email to a new investor |
|
||||||
|
|
||||||
|
All actions go through the existing admin API at `pitch.breakpilot.com`, so they show up in the audit log.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd pitch-deck/mcp-server
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Get the admin secret
|
||||||
|
|
||||||
|
The `PITCH_ADMIN_SECRET` is stored in orca secrets on the server. SSH in and retrieve it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh breakpilot-infra-vm1
|
||||||
|
cat ~/orca/services/breakpilot-dsms/secrets.json | grep PITCH_ADMIN_SECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure
|
||||||
|
|
||||||
|
Edit `.mcp.json` in the **breakpilot-core** repo root (already created):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"pitch-versions": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["pitch-deck/mcp-server/dist/index.js"],
|
||||||
|
"env": {
|
||||||
|
"PITCH_API_URL": "https://pitch.breakpilot.com",
|
||||||
|
"PITCH_ADMIN_SECRET": "paste-your-secret-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important:** `.mcp.json` contains a secret. It's already in `.gitignore` — never commit it.
|
||||||
|
|
||||||
|
### 4. Restart Claude Code
|
||||||
|
|
||||||
|
Exit and reopen Claude Code, or run `/mcp` to check it loaded:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see `pitch-versions` listed with 11 tools.
|
||||||
|
|
||||||
|
## Usage examples
|
||||||
|
|
||||||
|
Just talk to Claude naturally:
|
||||||
|
|
||||||
|
- **"List all pitch versions"** → calls `list_versions`
|
||||||
|
- **"Create a new version called 'Series A Aggressive'"** → calls `create_version`
|
||||||
|
- **"Show me the company data from version X"** → calls `get_table_data`
|
||||||
|
- **"Update the company tagline to 'AI-Powered Compliance' in version X"** → calls `update_table_data`
|
||||||
|
- **"Commit version X"** → calls `commit_version`
|
||||||
|
- **"Fork version X into a new draft called 'Conservative'"** → calls `fork_version`
|
||||||
|
- **"Compare version X with version Y"** → calls `diff_versions`
|
||||||
|
- **"Assign version X to investor jane@vc.com"** → calls `assign_version`
|
||||||
|
- **"Invite john@fund.com from Big Fund"** → calls `invite_investor`
|
||||||
|
|
||||||
|
## Data tables
|
||||||
|
|
||||||
|
Each version stores 12 data tables as JSONB snapshots:
|
||||||
|
|
||||||
|
| Table | Content |
|
||||||
|
|-------|---------|
|
||||||
|
| `company` | Name, tagline (DE/EN), mission (DE/EN), website, HQ city |
|
||||||
|
| `team` | Members with roles, bios, equity, expertise (all bilingual) |
|
||||||
|
| `financials` | Year-by-year revenue, costs, MRR, ARR, customers, employees |
|
||||||
|
| `market` | TAM/SAM/SOM with values, growth rates, sources |
|
||||||
|
| `competitors` | Names, customer counts, pricing, strengths, weaknesses |
|
||||||
|
| `features` | Feature comparison matrix (BreakPilot vs competitors) |
|
||||||
|
| `milestones` | Timeline with dates, titles, descriptions, status (bilingual) |
|
||||||
|
| `metrics` | Key metrics with labels (bilingual) and values |
|
||||||
|
| `funding` | Round details, amount, instrument, use of funds breakdown |
|
||||||
|
| `products` | Product tiers with pricing, LLM specs, features (bilingual) |
|
||||||
|
| `fm_scenarios` | Financial model scenario names, colors, default flag |
|
||||||
|
| `fm_assumptions` | Per-scenario assumptions (growth rate, ARPU, churn, etc.) |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Claude Code ←stdio→ MCP Server ←HTTP→ pitch.breakpilot.com/api/admin/*
|
||||||
|
(local) (deployed on orca)
|
||||||
|
```
|
||||||
|
|
||||||
|
The MCP server is a thin HTTP client. All auth, validation, and audit logging happens on the server side. The bearer token authenticates as a CLI admin actor.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**"PITCH_ADMIN_SECRET is required"** → The env var is missing in `.mcp.json`
|
||||||
|
|
||||||
|
**401 errors** → The secret is wrong or the pitch-deck container isn't running. Check: `curl -s -H "Authorization: Bearer YOUR_SECRET" https://pitch.breakpilot.com/api/admin/investors`
|
||||||
|
|
||||||
|
**MCP server not showing in `/mcp`** → Make sure you're in the `breakpilot-core` directory when you launch Claude Code (`.mcp.json` is project-scoped)
|
||||||
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