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
pip install pact-mcp-server
Scaffold a server
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:
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:
[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
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:
→ 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).