Add Prime Intellect client, API, and MCP server

packages/prime — a hand-written typed client, because the first-party SDK is
Python only. Deliberately narrow: PIG reads availability and nothing else, and
the key it holds should be scoped so it could not provision even if the code
tried. Rate limits are undocumented upstream, so it backs off empirically with
full jitter and honours Retry-After. Unknown fields survive in `raw` rather
than being dropped.

apps/api — Hono, with authentication and authorization kept firmly apart. A
verified JWT proves someone has an account in the identity project, which may
be shared with other applications; it does NOT prove they belong here. Access
requires a row in PIG's own users table, and a token without one gets 403
needs_profile rather than entry.

The capacity service is the business logic: availability counts sold and held
separately, so a live hold removes inventory from everyone else's availability
without inflating utilisation. Expired holds are ignored at read time, so the
numbers stay right even when the sweeper is behind. Matching treats
interconnect as a hard filter and excludes Unknown as well as Ethernet —
unverified is not the same as adequate.

apps/mcp — nine tools over stdio, so a team member drives PIG from Claude
Code, Codex, prime-agent, or a Buzz agent. It holds an API key and calls the
same HTTP API the browser does, with no database credentials, so an agent can
never reach further than the person it acts for. Results are formatted as
prose rather than raw JSON.

Theme preferences live in the database rather than localStorage, so a chosen
accent follows someone from laptop to phone. Status colours stay independent
of the accent: if "at risk" re-tinted to whatever a user picked, the signal
would be gone.

Note on the SDK import: its package exports use a `./*` wildcard whose types
entry resolves server/mcp.js to server/mcp.js.d.ts, which does not exist. The
runtime specifier must keep the .js suffix, so the types are mapped via
tsconfig paths rather than by writing an import that would fail at runtime.

Verified: all five packages typecheck; the MCP server constructs and registers
its tools.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:02:45 -07:00
parent d36762f264
commit 7aeec0c632
21 changed files with 3997 additions and 2 deletions
+573
View File
@@ -0,0 +1,573 @@
/**
* 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<T>(path: string, init: RequestInit = {}): Promise<T> {
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<string, unknown> & { 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;
}
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env node
/**
* stdio entry point — how a local agent connects.
*
* claude mcp add pig -- npx -y @pig/mcp
*
* Configuration comes from the environment because stdio servers have no other
* channel: PIG_URL and PIG_API_KEY. The key is minted in PIG under Settings →
* API keys, and should carry the narrowest scope that does the job.
*/
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createPigMcpServer } from './server';
const baseUrl = process.env.PIG_URL ?? 'http://localhost:8920';
const apiKey = process.env.PIG_API_KEY;
if (!apiKey) {
// stdout is the protocol channel — diagnostics must go to stderr or they
// corrupt the stream and the client reports an unhelpful parse error.
process.stderr.write(
'PIG_API_KEY is not set.\n' +
'Create a key in PIG under Settings → API keys, then:\n' +
' export PIG_API_KEY=pig_...\n' +
' export PIG_URL=https://your-pig-host\n',
);
process.exit(1);
}
const server = createPigMcpServer({ baseUrl, apiKey });
await server.connect(new StdioServerTransport());