TypeScript client (@pact/mcp-client)
Call Pact's MCP server from TypeScript — typed tool wrappers, OAuth + API-key auth, cost-aware retries, consent-aware results.
@pact/mcp-client is the official TypeScript client for Pact's MCP server. It
wraps the official @modelcontextprotocol/sdk transport and adds typed tool
wrappers, auth, cost attribution, and rate-limit-aware retries.
Install
npm install @pact/mcp-client @modelcontextprotocol/sdk
Pact in a Next.js app, in under 10 lines
// app/api/ask/route.ts
import { PactMcpClient } from "@pact/mcp-client";
export async function POST(req: Request): Promise<Response> {
const { question } = await req.json();
const pact = new PactMcpClient({ token: process.env.PACT_API_KEY! });
try {
return Response.json(await pact.askWorkspace({ question }));
} finally {
await pact.close();
}
}
The response is the consent-filtered, cited answer: { answer, citations, cost_cents, consent_filtered, ... }.
Authentication
Pass a scoped API key as token, or OAuth client credentials as oauth:
// API key (created at Settings → API keys)
const pact = new PactMcpClient({ token: process.env.PACT_API_KEY! });
// OAuth client credentials (a tenant-scoped app) — exchanged + refreshed for you
const pact = new PactMcpClient({
oauth: { clientId: process.env.PACT_CLIENT_ID!, clientSecret: process.env.PACT_CLIENT_SECRET! },
});
The default endpoint is https://api.pact.place/mcp; override with baseUrl.
Read tools need a scope (read:accounts, read:contacts, read:deals,
read:activities, read:workflows); agent tools need read:workflows. A key
missing a tool's scope throws PactScopeError.
Typed tool wrappers
const accounts = await pact.queryAccounts({ search: "acme", limit: 10 });
const contacts = await pact.queryContacts({ company: "Acme" });
const deals = await pact.queryDeals({ forecast_cat: "Commit" });
const health = await pact.queryPipelineHealth();
const why = await pact.getMetricExplanation({ metric: "win_rate", range: "30d" });
const answer = await pact.askWorkspace({ question: "Where is pipeline slipping?" });
const agents = await pact.listAgents();
const run = await pact.fireAgent({ agent_id: "deal_coach_agent", target_type: "deal", target_id: "123" });
const briefing = await pact.readBriefing();
callTool(name, args) is the raw escape hatch for anything not yet wrapped.
Consent, cost, retries
const pact = new PactMcpClient({
token,
onCost: ({ tool, costCents, totalCents }) => meter.record(tool, costCents),
onRateLimit: ({ attempt, delayMs }) => (attempt > 2 ? false : undefined), // abort after 2
retry: { maxAttempts: 4 },
});
const { items, consent_filtered } = await pact.queryContacts({ search: "vp" });
// consent_filtered records were withheld; the client never sees them.
const res = await pact.fireAgent({ agent_id: "deal_coach_agent", target_type: "contact", target_id: "c_1" });
if (res.blocked) console.log(res.reason); // "consent_withdrawn"
Errors
Every failure throws a PactMcpError subclass: PactAuthError, PactScopeError,
PactRateLimitError (retried automatically), PactCostCapError, PactToolError.
Note
askWorkspaceStream returns an async-iterable that yields a start event then an
answer event. Today the server returns the full answer in one response — this is
an honest place to render a thinking state, not a token stream.