Skip to content

Errors & scopes

The two contracts a caller has to reason about: the scopes that grant access, and the errors returned when something is wrong.

Scopes

A scope is namespace:resource:action, lowercase, colon-separated. For example, orders:order:read.

A tool declares the scope it requires. scopes_match checks the caller's scopes against it, case-insensitively, accepting any of four patterns:

Pattern Grants
namespace:*:* every resource and action in the namespace
namespace:*:action one action across every resource (e.g. read-only)
namespace:resource:* every action on one resource
namespace:resource:action exactly one action on one resource

A caller satisfies the check if any of their scopes matches one of these patterns for the required namespace / resource / action. If none match, the call is rejected with scope_denied.

from pontifex_mcp import scopes_match

scopes_match(["orders:*:read"], "orders", "order", "read")    # True
scopes_match(["orders:order:read"], "orders", "order", "write")  # False

Scopes are granted by the caller's API key or their verified JWT claims, and are never expanded at runtime. Issuing them: Authenticate callers.

Error codes

A tool error is returned as a structured ToolError, not a stack trace. Messages are written for an agent to act on: what went wrong, and what to do next.

Code Status Retry Meaning
auth_failed 401 No No valid identity: missing, invalid, expired, or revoked credential.
scope_denied 403 No Valid identity, but missing the required scope.
rate_limited 429 Yes Caller's request rate exceeded. Returned by the middleware before the tool runs.
invalid_input 400 No Bad argument, e.g. an unknown value, or an API-key caller hitting a token-exchange connector.
source_unavailable 503 Yes All data sources failed or are circuit-broken. Includes retry_after_seconds (30).
internal_error 500 Yes Unexpected server error.

auth_failed, scope_denied, and invalid_input are caller errors: retrying without changing the request won't help. rate_limited, source_unavailable, and internal_error are transient: retry, honoring retry_after_seconds when present.

The error envelope

class ToolError:
    error_code: str                  # one of the codes above
    message: str                     # human- and agent-readable
    status: int                      # HTTP-style status
    retry: bool                      # should the caller retry?
    retry_after_seconds: int | None  # set on source_unavailable
    detail: str | None               # optional extra context

Raise InvalidInput inside a handler to return invalid_input cleanly. The runtime produces every other code; you don't construct them by hand.