Concepts¶
Every request flows through the same path. Understanding it is most of understanding pontifex-mcp.
flowchart TB
req["Request"] --> auth["Authenticate<br/>API key or JWT"]
auth --> id["CallerIdentity"]
id --> rl["Rate limit"]
rl --> scope["Scope check"]
scope --> tool["Tool handler"]
tool --> audit["Audit log"]
Authentication¶
Two credential types resolve to one identity:
-
API keys
Tokens prefixed
sk_…, for scripts, CI, and machine-to-machine callers. Hashed at rest; looked up by the resolver. -
OAuth 2.1 JWTs
For interactive clients (Claude Desktop, agents). Validated against your OIDC provider's JWKS — any provider works (Auth0, Entra, Clerk, Keycloak).
Both produce a CallerIdentity — a stable owner_id, the granted scopes, and a rate_limit_rpm.
Downstream code never has to know which credential type was used.
Note
JWT validation is asymmetric-only and rejects alg: none. A caller can't raise their own rate limit
or scopes with a forged claim — limits come from server configuration, not the token.
Scopes¶
Permissions use the format domain:resource:action (e.g. orders:order:read). The scope required
by a tool is declared on its tool_runtime decorator and checked before the handler runs.
| Scope | Grants |
|---|---|
orders:order:read |
one tool |
orders:*:read |
read across the whole domain |
orders:*:* |
full access to the domain |
A caller is granted scopes by their API key or their JWT claims — and can never widen them at runtime.
The tool runtime¶
tool_runtime is the decorator that wraps each handler. Around your code it:
- Checks the scope — denies with a structured error if the caller lacks
domain:resource:action. - Runs your handler — you return plain data;
InvalidInputis the one exception you raise for bad arguments. - Writes the audit row — who called, what, when, which data source, cache hit, and latency.
- Normalizes errors — your return value passes through unchanged on success; a raised error
becomes a structured
ToolError, with no stack traces leaking to the caller.
Data adapters¶
External calls go through the DataAdapter protocol rather than being made directly in a tool. A
DataSourceManager orders adapters by health and tracks their success/failure, so your tool can
iterate the available sources and fail over when one is down.
Tip
Keeping I/O behind adapters is what makes tools testable and resilient — and it's where Cache,
async_retry, and CircuitBreaker plug in.
Connectors¶
When the system you're exposing already has an OpenAPI spec, a connector
generates the tools instead of you writing them — each one still wrapped in tool_runtime with a
derived scope, and still calling through a DataAdapter. Same runtime path as above; only the
authoring changes.
Audit¶
Every tool call produces an AuditRecord, written by an AuditWriter. Use DbAuditWriter to
persist to Postgres (the production default) or NoopAuditWriter in tests. This is the trail you need
for compliance and incident response — answering who touched what, and when.