PPactDocs
MCP & AI agents

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

bash
npm install @pact/mcp-client @modelcontextprotocol/sdk

Pact in a Next.js app, in under 10 lines

ts
// 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:

ts
// 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

ts
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.

ts
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.