PPactDocs
MCP & AI agents

Build & publish MCP tools

Scaffold, run, and publish your own MCP server into Pact's federation marketplace with the pact CLI — consent, cost, and rate-limit guardrails baked in.

Build your own MCP server and federate it into Pact. The author SDK bakes in the guardrails — consent, cost, rate-limit, audit — and the pact CLI scaffolds, runs, and publishes it.

Install the CLI

bash
pip install pact-mcp-server

Scaffold a server

bash
pact init mcp-server my-tools          # Python (FastMCP)
pact init mcp-server my-tools --lang ts # TypeScript (@pact/mcp-server)
cd my-tools
pip install -r requirements.txt        # or: npm install
pact mcp dev                           # hot-reload dev server on :9000

The example tools are already consent-gated, cost-metered, and rate-limited:

python
from pact_mcp_server import PactServer
from pact_mcp_server.guardrails import InMemoryConsentStore

server = PactServer("my-tools", consent_store=InMemoryConsentStore())

@server.tool(cost_cents=0.10, consent_subject="email", rate_limit_per_min=60)
async def lookup_orders(email: str, limit: int = 10) -> dict:
    return {"email": email, "orders": fetch(email, limit)}

app = server.app()  # serve with uvicorn / `pact mcp dev`
  • consent_subject — if your consent store blocks the subject, the call returns {"blocked": true, "reason": "consent_withdrawn"} — the same shape Pact's own server uses, so Pact clients treat it as data, not an error.
  • cost_cents — attributed per call and injected into the result.
  • rate_limit_per_min — a per-tool sliding window; over the limit raises the rate-limit error clients retry on.

In TypeScript the equivalent is createPactServer().tool(name, { costCents, consentSubject, rateLimitPerMin, schema }, handler) from @pact/mcp-server.

Describe governance in pact.toml

The real per-tool governance Pact's federation gateway enforces — allowed roles, rate limit, cost cap, PII policy — lives in pact.toml:

toml
[server]
name = "my-tools"
slug = "custom"
endpoint_url = "https://my-tools.acme.com/mcp"
transport = "streamable_http"   # or "sse"
auth_kind = "bearer"            # none | bearer | header | oauth

[tools.lookup_orders]
enabled = true
allowed_roles = ["owner", "admin", "manager"]
rate_limit_per_min = 60
cost_cap_cents = 5.0
est_cost_cents = 0.10
pii_policy = "redact"           # redact | allow | block

Publish

bash
export PACT_ADMIN_TOKEN=...      # an owner/admin session token
export PACT_MCP_AUTH_SECRET=...  # if auth_kind != none; Pact seals it with your BYOK DEK
pact mcp publish

pact mcp publish registers the server, asks Pact to discover its tools, then applies the pact.toml governance to each:

text
→ Installing 'my-tools' (https://my-tools.acme.com/mcp) …
  server 9f3c… status=pending
→ Discovering tools …
  discovered 2 tool(s): lookup_orders, ping
  ✓ governed 'lookup_orders': roles=['owner', 'admin', 'manager'] pii=redact
  · 1 discovered tool(s) left disabled (no pact.toml entry): ping

Discovered tools without a pact.toml entry stay disabled by default — Pact's safe default. pact mcp delete removes the published server.

Warning

Publishing requires an owner/admin token. A read-only API key resolves to role api and is refused by the admin gate — the CLI tells you so. If your Pact API calls need an explicit tenant, set PACT_TENANT (sent as X-Tenant-ID).