/** * The PIG MCP server. * * PIG is a first-class application for agents as well as for people, and this * is that surface. Any MCP client connects — Claude Code, Codex, prime-agent, * or a Buzz workspace agent through its ACP bridge — so a team member works * from the terminal they already live in rather than being made to visit a web * app to log a call. * * Design rules, learned from tool surfaces that went wrong: * * **Keep it small.** Nine tools, each doing one thing. A sprawling tool list * degrades model performance more than it adds capability; anything genuinely * niche belongs behind `pig_search` or the HTTP API. * * **Every tool is an authenticated API call.** This process holds a PIG API * key and talks to the same HTTP API the browser uses. It has no database * credentials and no privileged path, so an agent can never reach further than * the person it acts for. * * **Return prose, not just JSON.** Results are formatted for a model to read * and quote back to a human. A wall of raw JSON forces the model to re-derive * meaning that the server already knows. */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; export interface PigMcpOptions { /** Base URL of the PIG API, e.g. https://primeintellectgrowth.com */ baseUrl: string; /** A PIG API key (`pig_…`). Scope it to `read` unless writes are wanted. */ apiKey: string; fetchImpl?: typeof fetch; } class PigApi { constructor(private readonly options: PigMcpOptions) {} private get fetchImpl() { return this.options.fetchImpl ?? fetch; } async request(path: string, init: RequestInit = {}): Promise { const response = await this.fetchImpl(`${this.options.baseUrl}${path}`, { ...init, headers: { authorization: `Bearer ${this.options.apiKey}`, 'content-type': 'application/json', accept: 'application/json', ...(init.headers ?? {}), }, }); if (!response.ok) { const body = await response.text().catch(() => ''); // Surface the real reason. An agent that is told "403 needs_profile" can // tell its user to get an invite; one told "request failed" cannot. throw new Error( `PIG API ${response.status}: ${body.slice(0, 300) || response.statusText}`, ); } return (await response.json()) as T; } } const money = (cents: number | null | undefined, currency = 'USD') => cents == null ? '—' : new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(cents / 100); const pct = (v: number | null | undefined) => v == null ? '—' : `${(v * 100).toFixed(1)}%`; /** Wrap a handler so a thrown error becomes a readable tool error. */ function ok(text: string) { return { content: [{ type: 'text' as const, text }] }; } export function createPigMcpServer(options: PigMcpOptions): McpServer { const api = new PigApi(options); const server = new McpServer({ name: 'pig', version: '0.1.0' }); // ------------------------------------------------------------------ whoami server.registerTool( 'pig_whoami', { title: 'Who am I in PIG', description: 'Identify the PIG user this agent is acting for, and which teams they belong to ' + '(supply, demand, research). Call this first when you need to know whose pipeline ' + 'to look at or whether the user may see supply-side economics.', inputSchema: {}, }, async () => { const me = await api.request<{ name: string; email: string; isPlatformAdmin: boolean; teams: { team: string; role: string }[]; }>('/api/me'); const teams = me.teams.length ? me.teams.map((t) => `${t.team} (${t.role})`).join(', ') : 'no team membership'; return ok( `${me.name} <${me.email}>\nTeams: ${teams}${me.isPlatformAdmin ? '\nPlatform admin.' : ''}`, ); }, ); // -------------------------------------------------------------- my pipeline server.registerTool( 'pig_my_pipeline', { title: 'My pipeline', description: 'Summarise the current state of the business: margin across the capacity book, ' + 'open deals on both sides, and any idle-capacity alerts. Good opening call when ' + 'the user asks "where are we?" or "what needs attention?".', inputSchema: {}, }, async () => { const d = await api.request<{ margin: { revenueCents: number; costCents: number; grossMarginCents: number; grossMarginPct: number | null; utilisation: number; idleGpuHours: number; }; blocks: number; openDemandDeals: number; openSupplyDeals: number; idleAlerts: { name: string; gpuType: string; idleCostCents?: number }[]; }>('/api/dashboard'); const lines = [ `Capacity book: ${d.blocks} commitment(s), ${pct(d.margin.utilisation)} utilised.`, `Revenue ${money(d.margin.revenueCents)} against cost ${money(d.margin.costCents)}.`, `Gross margin ${money(d.margin.grossMarginCents)} (${pct(d.margin.grossMarginPct)}).`, `Idle: ${Math.round(d.margin.idleGpuHours).toLocaleString()} GPU-hours bought and unsold.`, '', `Open deals — demand ${d.openDemandDeals}, supply ${d.openSupplyDeals}.`, ]; if (d.idleAlerts.length) { lines.push('', 'Idle capacity worth attention:'); for (const a of d.idleAlerts) { lines.push(` • ${a.name} (${a.gpuType}) — ${money(a.idleCostCents)} unsold`); } } return ok(lines.join('\n')); }, ); // ------------------------------------------------------------ capacity match server.registerTool( 'pig_capacity_match', { title: 'Match capacity to a requirement', description: 'THE most useful tool here. Given what a customer needs — GPU type, count, dates, ' + 'whether they need high-speed interconnect for distributed training — find capacity ' + 'we have already committed to and could sell them, ranked by fit. Prefers blocks ' + 'sitting idle, because those hours are already paid for. Warns when a match would ' + 'sell below break-even.', inputSchema: { gpuCount: z.number().int().positive().describe('Number of GPUs required'), gpuType: z .string() .optional() .describe('Preferred GPU, e.g. H100_80GB, H200, B200. Omit to consider all.'), gpuTypeAlternatives: z .array(z.string()) .optional() .describe('Acceptable substitutes, in preference order'), totalGpuHours: z.number().positive().optional().describe('Total GPU-hours needed'), requiresHighSpeedInterconnect: z .boolean() .optional() .describe( 'True for distributed training. Excludes Ethernet-only and unverified fabric.', ), startsAt: z.string().optional().describe('ISO-8601 start date'), endsAt: z.string().optional().describe('ISO-8601 end date'), maxPricePerGpuHourCents: z .number() .int() .positive() .optional() .describe("Customer's price ceiling, in cents per GPU-hour"), }, }, async (input) => { const matches = await api.request< { name: string; gpuType: string; gpuCount: number; interconnectType: string; availableGpuHours: number; utilisation: number; costPerGpuHourCents: number; breakEvenPriceCents: number | null; score: number; rationale: string[]; }[] >('/api/capacity/match', { method: 'POST', body: JSON.stringify(input) }); if (matches.length === 0) { return ok( 'No committed capacity matches that requirement.\n\n' + 'Consider `pig_inventory_search` to find capacity available to buy from ' + 'providers, which would need a new supply deal.', ); } const lines = [`${matches.length} match(es), best first:\n`]; for (const m of matches.slice(0, 8)) { lines.push( `▸ ${m.name} — ${m.gpuCount}× ${m.gpuType}, ${m.interconnectType}`, ` ${Math.round(m.availableGpuHours).toLocaleString()} GPU-hours free · ` + `${pct(m.utilisation)} utilised · cost ${money(m.costPerGpuHourCents)}/GPU-hr`, ` Break-even on remaining hours: ${money(m.breakEvenPriceCents)}/GPU-hr`, ...m.rationale.map((r) => ` ${r}`), '', ); } return ok(lines.join('\n')); }, ); // ------------------------------------------------------------ margin report server.registerTool( 'pig_margin_report', { title: 'Margin across the capacity book', description: 'Per-commitment and total margin: what each block of capacity cost, what it earned, ' + 'and how much of it is sold. Cost is charged against the FULL commitment, not only ' + 'the hours that sold, because unsold hours are already paid for.', inputSchema: {}, }, async () => { const report = await api.request<{ totals: { revenueCents: number; costCents: number; grossMarginCents: number; grossMarginPct: number | null; utilisation: number; idleGpuHours: number; marginPerAllocatedGpuHourCents: number | null; }; blocks: { name: string; gpuType: string; utilisation: number; soldGpuHours: number; totalGpuHours: number; costPerGpuHourCents: number; }[]; }>('/api/capacity/margin'); const t = report.totals; const lines = [ 'Book totals', ` Revenue ${money(t.revenueCents)}`, ` Cost ${money(t.costCents)}`, ` Gross margin ${money(t.grossMarginCents)} (${pct(t.grossMarginPct)})`, ` Per sold hour ${money(t.marginPerAllocatedGpuHourCents)}`, ` Utilisation ${pct(t.utilisation)}`, ` Idle ${Math.round(t.idleGpuHours).toLocaleString()} GPU-hours`, '', 'By commitment', ]; for (const b of report.blocks) { lines.push( ` ${b.name} (${b.gpuType}) — ${pct(b.utilisation)} sold ` + `(${Math.round(b.soldGpuHours).toLocaleString()}/${Math.round(b.totalGpuHours).toLocaleString()} hrs) ` + `at ${money(b.costPerGpuHourCents)}/hr cost`, ); } return ok(lines.join('\n')); }, ); // ------------------------------------------------------------ idle capacity server.registerTool( 'pig_idle_capacity', { title: 'Idle capacity', description: 'Committed capacity that is not sold, ranked by what it is costing. These hours are ' + 'already paid for, so this is money leaving the business every hour it stays unsold. ' + 'Use it to decide what to push.', inputSchema: { thresholdPct: z .number() .min(0) .max(1) .optional() .describe('Minimum idle fraction to report. Default 0.25.'), withinDays: z .number() .int() .positive() .optional() .describe('Only blocks live or starting within this many days. Default 30.'), }, }, async (input) => { const params = new URLSearchParams(); if (input.thresholdPct != null) params.set('threshold', String(input.thresholdPct)); if (input.withinDays != null) params.set('withinDays', String(input.withinDays)); const rows = await api.request< { name: string; gpuType: string; gpuCount: number; idleGpuHours: number; idleCostCents: number; utilisation: number; breakEvenPriceCents: number | null; endsAt: string; }[] >(`/api/capacity/idle?${params}`); if (rows.length === 0) return ok('No idle capacity above the threshold. The book is tight.'); const total = rows.reduce((s, r) => s + r.idleCostCents, 0); const lines = [`${rows.length} block(s) with idle capacity — ${money(total)} at stake:\n`]; for (const r of rows) { lines.push( `▸ ${r.name} — ${r.gpuCount}× ${r.gpuType}`, ` ${Math.round(r.idleGpuHours).toLocaleString()} GPU-hours unsold, costing ${money(r.idleCostCents)}`, ` ${pct(r.utilisation)} utilised · sell above ${money(r.breakEvenPriceCents)}/GPU-hr to break even`, ` Block ends ${new Date(r.endsAt).toISOString().slice(0, 10)}`, '', ); } return ok(lines.join('\n')); }, ); // -------------------------------------------------------- inventory search server.registerTool( 'pig_inventory_search', { title: 'Search provider inventory', description: 'Search GPU capacity available to BUY from providers — synced live from the compute ' + 'marketplace. This is capacity we do not yet hold; use it when demand exists that ' + 'committed capacity cannot cover, then open a supply deal.', inputSchema: { gpuType: z.string().optional().describe('e.g. H100_80GB, H200, B200'), minGpuCount: z.number().int().positive().optional(), maxPriceCents: z .number() .int() .positive() .optional() .describe('Ceiling on on-demand price, in cents per GPU-hour'), requiresHighSpeedInterconnect: z.boolean().optional(), limit: z.number().int().positive().max(100).optional(), }, }, async (input) => { const params = new URLSearchParams(); if (input.gpuType) params.set('gpuType', input.gpuType); if (input.minGpuCount) params.set('minGpuCount', String(input.minGpuCount)); if (input.maxPriceCents) params.set('maxPriceCents', String(input.maxPriceCents)); if (input.requiresHighSpeedInterconnect) params.set('fastFabric', 'true'); if (input.limit) params.set('limit', String(input.limit)); const rows = await api.request< { providerSlug: string | null; gpuType: string; gpuCount: number; socket: string | null; interconnectType: string; region: string | null; country: string | null; securityTier: string; stockStatus: string; onDemandPriceCents: number | null; provisioningMinutes: number | null; observedAt: string; }[] >(`/api/inventory?${params}`); if (rows.length === 0) { return ok( 'No matching inventory. Note that sync may be disabled, or the filters may be ' + 'narrower than current market supply.', ); } const lines = [`${rows.length} listing(s):\n`]; for (const r of rows.slice(0, 25)) { lines.push( `▸ ${r.gpuCount}× ${r.gpuType}${r.socket ? ` ${r.socket}` : ''} — ` + `${money(r.onDemandPriceCents)}/GPU-hr`, ` ${r.providerSlug ?? 'unknown provider'} · ${r.region ?? r.country ?? 'region unknown'} · ` + `${r.interconnectType} · ${r.securityTier} · ${r.stockStatus}` + (r.provisioningMinutes ? ` · ~${r.provisioningMinutes}m to provision` : ''), ); } lines.push( '', `Prices last observed ${new Date(rows[0]!.observedAt).toISOString().slice(0, 16)}Z.`, ); return ok(lines.join('\n')); }, ); // ------------------------------------------------------------------ search server.registerTool( 'pig_search', { title: 'Search accounts', description: 'Find accounts by name. Returns which side of the market they sit on (supply, demand, ' + 'or both) so you know whether they are a provider, a customer, or each in turn.', inputSchema: { query: z.string().min(1).describe('Name or partial name'), side: z.enum(['supply', 'demand', 'all']).optional(), }, }, async (input) => { const params = new URLSearchParams({ q: input.query }); if (input.side) params.set('side', input.side); const rows = await api.request< { id: string; name: string; domain: string | null; side: string; supplierType: string | null; customerSegment: string | null; country: string | null; confidence: string; }[] >(`/api/accounts?${params}`); if (rows.length === 0) return ok(`No accounts matching "${input.query}".`); return ok( rows .slice(0, 25) .map( (r) => `▸ ${r.name}${r.domain ? ` (${r.domain})` : ''} — ${r.side}` + `${r.supplierType ? `, ${r.supplierType}` : ''}` + `${r.customerSegment ? `, ${r.customerSegment}` : ''}` + `${r.confidence !== 'confirmed' ? ` [${r.confidence}]` : ''}\n id: ${r.id}`, ) .join('\n'), ); }, ); // ------------------------------------------------------------- get account server.registerTool( 'pig_get_account', { title: 'Get an account', description: 'Full detail for one account: contacts, deals on both sides, contracts, and recent ' + 'activity. Use `pig_search` first to find the id.', inputSchema: { accountId: z.string().uuid() }, }, async (input) => { const d = await api.request<{ account: Record & { name: string; side: string }; contacts: { fullName: string; title: string | null; confidence: string }[]; demandDeals: { name: string; stage: string; acvCents: number | null }[]; supplyDeals: { name: string; stage: string }[]; contracts: { title: string; type: string; status: string }[]; activities: { type: string; subject: string | null; occurredAt: string }[]; }>(`/api/accounts/${input.accountId}`); const lines = [`${d.account.name} — ${d.account.side} side`, '']; if (d.contacts.length) { lines.push('Contacts'); for (const c of d.contacts) { lines.push( ` • ${c.fullName}${c.title ? ` — ${c.title}` : ''}` + // Surface weak provenance rather than presenting every record as // equally solid; some of these are single-source claims. (c.confidence !== 'confirmed' ? ` [${c.confidence}]` : ''), ); } lines.push(''); } if (d.demandDeals.length) { lines.push('Demand deals'); for (const x of d.demandDeals) { lines.push(` • ${x.name} — ${x.stage}${x.acvCents ? ` · ${money(x.acvCents)} ACV` : ''}`); } lines.push(''); } if (d.supplyDeals.length) { lines.push('Supply deals'); for (const x of d.supplyDeals) lines.push(` • ${x.name} — ${x.stage}`); lines.push(''); } if (d.contracts.length) { lines.push('Contracts'); for (const x of d.contracts) { lines.push(` • ${x.title} — ${x.type.toUpperCase()}, ${x.status}`); } lines.push(''); } if (d.activities.length) { lines.push('Recent activity'); for (const a of d.activities.slice(0, 8)) { lines.push( ` • ${new Date(a.occurredAt).toISOString().slice(0, 10)} ${a.type}` + `${a.subject ? ` — ${a.subject}` : ''}`, ); } } return ok(lines.join('\n')); }, ); // ------------------------------------------------------------- log activity server.registerTool( 'pig_log_activity', { title: 'Log an activity', description: 'Record a call, meeting, email or note against an account. Use this after a ' + 'conversation so the CRM reflects what actually happened rather than what someone ' + 'remembers to type in later.', inputSchema: { accountId: z.string().uuid(), type: z.enum(['note', 'email', 'call', 'meeting', 'slack', 'buzz']), subject: z.string().min(1).max(200), body: z.string().max(8000).optional(), occurredAt: z.string().optional().describe('ISO-8601. Defaults to now.'), }, }, async (input) => { await api.request('/api/activities', { method: 'POST', body: JSON.stringify(input), }); return ok(`Logged: ${input.type} — ${input.subject}`); }, ); return server; }