Audience: Engineers implementing or reviewing MCP authentication, authorization, and error disclosure behavior.
Table of Contents
- Executive Overview
- Request Context and Identity
- Authentication Modes
- Tool Visibility and Call Policy
- Namespace Authorization (Pluggable)
- Usage Metering and Quotas (Pluggable)
- Auth Audit Events
- Disclosure Posture (JSON-RPC and HTTP)
- Rate Limiting and Overload Responses
- File-by-File Implementation Anchors
Executive Overview
Decision Gate MCP enforces strict, fail-closed authentication and separately
owned authorization policy. Authentication is transport-aware (stdio, HTTP,
SSE), configured via server.auth, and owned by RequestAuthenticator.
RequestAuthenticator receives request identity only; it does not receive a
tool action and cannot become a second tool-policy owner. ToolVisibilityResolver
is the sole owner of static tool discovery and invocation policy, configured by
server.tools. A separate, pluggable namespace authorization layer enforces
namespace scoping before tool execution.
Every supported semantic call passes its exact namespace into the authorization
seam. The initial profile has no provider-scoped tool call or standalone
evidence-query authority. Caller evidence enters only through the scenario
precheck/evaluation carriers; source and assurance claims are constructed by
the server, not accepted from caller fields. Named time, immutable environment,
and rooted-document acquisition use operator-registered local authorities and
the scenario-owned binding relation rather than provider-scoped RBAC.
Auth decisions emit structured audit events, and request failures are mapped to
stable JSON-RPC error codes and HTTP status codes for deterministic disclosure
and metrics labeling. Internally, MCP now retains catalog-backed exact
symbolic/numeric identity plus projection-safe public codes for audit and
telemetry while keeping the external JSON-RPC envelope projection-first.
The generated DG authority artifacts under Docs/generated/decision-gate/
freeze those OSS error and telemetry semantics in machine-readable form so
private verification tooling consumes DG-owned contracts rather than inferring
them ad hoc from implementation details.
F:crates/decision-gate-mcp/src/auth.rs L293-L372 F:crates/decision-gate-mcp/src/tools/policy.rs L266-L281 F:crates/decision-gate-mcp/src/tools/policy.rs L36-L72 F:crates/decision-gate-mcp/src/server.rs L1984-L2017
Scope and Non-Goals
Scope is MCP ingress identity, authentication, authorization, namespace authorization hooks, disclosure, audit, and overload handling. This document does not define namespace placement, evaluator ownership, fencing, or replication; authorization scope is distinct from those future control-plane responsibilities.
Layer Responsibilities
- Ingress normalizes untrusted transport identity and request metadata.
- Authentication establishes the principal context.
ToolVisibilityResolverand namespace authorization decide whether a request may execute.- Disclosure and audit project the result without weakening the decision.
Request Context and Identity
Request Context
Incoming requests are normalized into a RequestContext that records transport,
peer IP, auth header, and optional authoritative/caller request identity
metadata. For HTTP/SSE transports, the sole application credential carrier is
the Authorization header. Caller-asserted certificate subjects are not an
admitted identity channel. Caller-provided provenance arrives via
x-caller-request-id and is treated as unsafe input: it is strictly
validated and rejected if invalid. The server always issues its own canonical
UUIDv7 x-request-id and returns it on responses, providing a stable,
auditable identifier even when caller provenance is missing. MCP additionally
tracks the per-message JSON-RPC id as an internal request-context field, but
that protocol identifier is kept separate from both caller provenance and the
authoritative server request ID. It is not returned as an HTTP/SSE header and
it does not replace audit or telemetry request identity channels.
F:crates/decision-gate-mcp/src/auth.rs L82-L173 F:crates/decision-gate-mcp/src/server.rs L993-L1072 F:crates/decision-gate-mcp/src/server.rs L1648-L1734
Principal Identity
AuthContext is a sealed tagged principal carrier. Its variants bind a
transport-derived local subject or canonical bearer-token digest directly to the matching
authentication method; callers cannot construct mismatched option fields or an
anonymous/malformed “authenticated” context. Local admission assigns the exact
stdio or loopback subject from transport. Bearer admission stores the typed
sha256 ContentDigest, and ACL/usage identity is projected only from that sealed
variant. There is no fabricated fallback principal for invalid state.
F:crates/decision-gate-mcp/src/auth.rs L181-L216 F:crates/decision-gate-mcp/src/auth.rs L503-L517
Authentication Modes
Auth mode is configured via server.auth.mode:
local_only: stdio is allowed; HTTP/SSE are only allowed for loopback IPs.bearer_token: bearer token verifier material must resolve from every configuredserver.auth.bearer_tokenssecret reference before publication.
Configuration surface:
server.auth.mode,bearer_tokens, andprincipals. F:crates/decision-gate-config/src/config.rs L789-L937
Implementation details:
- Local-only rejects non-loopback HTTP/SSE.
- Bearer secret references resolve atomically at startup; raw material is bounded, validated, hashed into verifier digests, and dropped. Request credentials are parsed with size and scheme validation and compared against every verifier digest without an early-exit match oracle.
- Server TLS protects the transport. It does not mint application identity. F:crates/decision-gate-mcp/src/auth.rs L479-L552
Tool Visibility and Call Policy
ToolVisibilityResolver is the sole static tool-policy owner. The
server.tools.allowlist and server.tools.denylist sets govern both
tools/list and direct invocation. A tool that is not callable is projected as
UnknownTool, preventing discovery through different call/list disclosure.
Unknown tool names and oversized policy sets fail configuration admission.
The removed server.auth.allowed_tools surface has no parser alias or runtime
bridge; its presence is an unknown-field error.
F:crates/decision-gate-mcp/src/tools/visibility.rs
F:crates/decision-gate-config/src/config.rs
Authentication results are emitted by the tool router before tool policy:
AuthAuditEvent::allowedon successAuthAuditEvent::deniedon failure F:crates/decision-gate-mcp/src/tools/policy.rs F:crates/decision-gate-mcp/src/auth.rs
Primary Flow/Sequence Description
- The server normalizes request context and rejects malformed identity input.
- Authentication establishes a principal or rejects the request.
- Tool policy, namespace authorization, and applicable quota checks run before the tool’s side effect.
- The result is audited and projected through the JSON-RPC/HTTP disclosure policy.
Namespace Authorization (Pluggable)
Namespace authorization is enforced by a pluggable NamespaceAuthorizer
hook. The standalone implementation permits only local authentication carrying
a durable authentication-governance receipt and requires namespace context for
every namespace-bearing tool. Enterprise deployments supply an authorizer that
binds principals to namespace scopes.
Policy allow and deny are disjoint NamespaceAuthzDecision variants. Failure
to obtain an authoritative decision is a separate
NamespaceAuthorizationError that preserves the exact source chain; the router
records a fail-closed denial event and returns the authority failure rather than
mislabeling it as a policy deny. If that required denial audit also fails, both
failures are preserved in a composite tool error. Namespace authorization runs
after static tool-policy checks and before tool execution. All outcomes emit
dedicated audit events (namespace_authz).
Implementation references:
- Namespace authz interface: F:crates/decision-gate-mcp/src/namespace_authz.rs L29-L65
- Enforcement and audit emission: F:crates/decision-gate-mcp/src/tools/policy.rs L36-L119
Usage Metering and Quotas (Pluggable)
Usage metering and quota checks are enforced by a pluggable UsageMeter hook.
The admitted OSS composition uses the same durable governance sink as request
audit; explicit no-op markers reject every operation and cannot satisfy durable
startup wiring. Enterprise deployments supply the platform-quota adapter.
Usage reservations run before tool execution; denials emit usage_audit
events. A sealed admission retains the authenticated principal. Resolution
rejects principal substitution, commits a distinct resolution_attempt, asks
the quota authority for the idempotent transition, and emits
resolution_finalized only after authority confirmation. The usage seam
receives three distinct identity inputs: caller provenance
(caller_request_id), authoritative server request identity (request_id),
and a dedicated tool-call idempotency_key. Decision Gate derives that
idempotency key from the normalized JSON-RPC id when available, falling back
to the server request_id only when the protocol message does not carry a
usable JSON-RPC identifier. Safe JSON-RPC identifiers pass through directly;
unsafe identifiers are deterministically transformed into
jsonrpc-sha256:<hex> to preserve fail-closed enterprise quota/idempotency
behavior without leaking JSON-RPC identity into transport headers, audit
request IDs, or telemetry request IDs.
Implementation references:
- Usage metering interface: F:crates/decision-gate-mcp/src/usage.rs L28-L105
- Enforcement and audit emission: F:crates/decision-gate-mcp/src/tools/policy.rs L121-L213
Auth Audit Events
Authentication decisions emit structured mcp_request_authentication audit
events with action context, transport, subject, method, and failure details.
The action identifies the attempted operation in evidence; it is not an input
to authentication policy. The default audit sink logs JSON lines to stderr;
tests can use a no-op sink.
F:crates/decision-gate-mcp/src/auth.rs L379-L445
Disclosure Posture (JSON-RPC and HTTP)
Stage Evaluation Disclosure
scenario_evaluate_stage returns the accepted run view, not raw observation
values. Attempt/evidence material remains in accepted history and runpack
families according to evidence disclosure policy. scenario_precheck_stage
returns only semantic outcome and trace for caller-submitted or explicit local evidence
and makes no accepted-progress claim. The removed cursor feedback configuration
has no compatibility alias.
JSON-RPC Error Envelope
The MCP server responds using JSON-RPC error codes and structured metadata
(kind, retryable, request_id, optional retry_after_ms). Error kinds are
stable labels used for metrics and audit categorization. Internally, the server
also retains a projection-safe public code, optional reason code, and canonical
exact identity on the error object for audit/telemetry sinks, but those fields
are not serialized onto the public JSON-RPC wire by default.
The public MCP projection remains Docs/generated/decision-gate/mcp_errors.json,
while the richer canonical OSS completeness source is now
Docs/generated/decision-gate/error_catalog.json.
F:crates/decision-gate-mcp/src/server.rs L1268-L1283 F:crates/decision-gate-mcp/src/server.rs L1672-L1707 F:crates/decision-gate-mcp/src/server.rs L2140-L2198 F:crates/decision-gate-mcp/src/audit.rs L48-L78 F:crates/decision-gate-mcp/src/telemetry.rs L102-L120
Telemetry Authority Artifacts
Decision Gate publishes generated telemetry event and operator-seam authority artifacts:
Docs/generated/decision-gate/telemetry_event_catalog.jsonDocs/generated/decision-gate/telemetry_operator_seams.json
These artifacts are generated from DG-owned source manifests in
crates/decision-gate-contract/catalogs/ and freeze event codes and high-risk
operator seams that observability governance is expected to protect. Metric
family identity, labels, help text, units, kind, cardinality, and histogram
bucket profiles are owned by a platform telemetry catalog supplied by the
integrating workspace; that external authority is not a Decision Gate
repository path, and DG must not carry a row-bearing metric catalog mirror.
Error Mapping (Tool Errors)
Tool errors are mapped to HTTP status + JSON-RPC error codes:
| ToolError | HTTP | JSON-RPC Code | Message |
|---|---|---|---|
| Unauthenticated | 401 | -32001 | unauthenticated |
| Unauthorized | 403 | -32003 | unauthorized |
| InvalidParams | 400 | -32602 | provided message |
| CapabilityViolation | 400 | -32602 | code: message |
| UnknownTool | 400 | -32601 | unknown tool |
| ResponseTooLarge | 200 | -32070 | provided message |
| RateLimited | 200 | -32071 | provided message |
| NotFound | 200 | -32004 | provided message |
| Conflict | 200 | -32009 | provided message |
| Evidence | 200 | -32020 | provided message |
| ControlPlane | 200 | -32030 | provided message |
| Runpack | 200 | -32040 | provided message |
| RateLimitAuthority | 200 | -32050 | rate-limit authority failed |
| Internal | 200 | -32050 | provided message |
| Serialization | 200 | -32060 | serialization failed |
These mappings are implemented in jsonrpc_error.
F:crates/decision-gate-mcp/src/server.rs L1984-L2015
Exact Identity Backing
The JSON-RPC mapping remains the stable public contract, but it is now backed by
catalog-owned exact identities. Routed ToolError variants and server ingress
rejections resolve to canonical OSS symbolic/numeric identities first and only
then project onto the public JSON-RPC taxonomy.
F:crates/decision-gate-mcp/src/tools/error.rs L126-L189 F:crates/decision-gate-mcp/src/server.rs L2270-L2307
Auth Challenge Header (RFC 6750)
HTTP/SSE responses for unauthenticated requests include a WWW-Authenticate
header with a Bearer realm when bearer token auth is enabled. This aligns with
RFC 6750 and keeps auth challenges explicit without leaking token validation
details.
F:crates/decision-gate-mcp/src/auth.rs L46-L75 F:crates/decision-gate-mcp/src/server.rs L1706-L1718
Request-Identity Headers
HTTP/SSE responses always include a server-issued canonical lowercase
hyphenated UUIDv7 in x-request-id. If the caller supplied a valid
x-caller-request-id, it is echoed back as caller provenance, but it never
replaces the authoritative server request identifier. Invalid caller request
IDs are rejected before request parsing and are not echoed. The rejection
uses HTTP 400 with JSON-RPC error code -32073
(invalid_caller_request_id).
F:crates/decision-gate-mcp/src/server.rs L993-L1072 F:crates/decision-gate-mcp/src/server.rs L1648-L1749
Request Parsing Failures
Invalid JSON-RPC versions, unknown methods, and malformed request bodies are rejected with standard JSON-RPC error codes and HTTP 400. F:crates/decision-gate-mcp/src/server.rs L1505-L1583
Rate Limiting and Overload Responses
decision-gate-mcp::rate_limit is the sole Decision Gate owner of the
in-process fixed-window algorithm. MCP ingress and account-scoped enterprise
docs supply different key and outward-policy types, but they do not own copies
of the clock, bucket, eviction, counter, or publication relation. The admitted
policy seals non-zero request count, window, and key capacity and proves the
two-window retention horizon representable before a limiter can exist.
For a key k, policy (m, w, c), serialized monotonic sample t, and retained
state S, admission is a partial deterministic transition
step(S, k, t) -> Result<(S', Allow | Limited(retry)), E>. A successful
transition publishes the complete next state and advances one global monotonic
watermark. Synchronization failure, clock regression, capacity exhaustion,
counter failure, or retry projection failure publishes no state. Any positive
sub-millisecond retry remainder projects to the named one-millisecond public
floor; it cannot become a zero retry hint.
The server enforces:
- Inflight request limits (reject with 503 and
-32072, kindinflight_limit, reason codedg.server.inflight_limit_exhausted). - Rate-window limiting (reject with 429 and
-32071, kindrate_limited, reason codedg.server.rate_limit_window_exhausted, including retry-after hints). - Rate-limiter capacity saturation (reject with 503 and
-32074, kindrate_limiter_capacity, reason codedg.server.rate_limiter_capacity_exhausted). - Payload size limits (reject with 413 and
-32070).
The fixed-window authority owns a bounded map plus one globally published monotonic watermark. It samples only after serializing access, validates clock order and every fallible calculation before publication, and leaves the exact prior state unchanged on clock regression, capacity exhaustion, arithmetic failure, or poisoned synchronization. Ordinary scheduler reordering therefore cannot be misclassified as clock regression, while switching attribution keys cannot hide a real regression.
Local acquisition has its own validated operation and concurrency budget. The initial profile contains no outbound network acquisition path.
These failures are reported with structured JSON-RPC error metadata and are marked retryable when appropriate. F:crates/decision-gate-mcp/src/rate_limit.rs F:crates/decision-gate-mcp/src/server.rs
Invariants
- Untrusted request identity never becomes authoritative without validation.
- Missing or denied authentication/authorization fails closed before tool work.
- Namespace authorization is an access decision, not a placement decision.
- Disclosure preserves stable public errors without exposing sensitive detail.
Failure Modes and Recovery Matrix
| Failure mode | Fail-closed behavior | Recovery |
|---|---|---|
| Invalid request identity | Reject before authorization/tool execution. | Correct caller input. |
| Authentication unavailable or denied | Reject request. | Restore auth configuration/backend or credentials. |
| Namespace authorization unavailable | Deny namespace-scoped request. | Restore authority and retry. |
| Rate/usage limit exceeded | Return structured retryable response where applicable. | Wait or restore capacity/quota. |
File-by-File Implementation Anchors
| Area | File | Notes |
|---|---|---|
| Auth config surface | crates/decision-gate-config/src/config.rs | Auth modes, token/subject allowlists, tool allowlist. |
| Auth policy engine | crates/decision-gate-mcp/src/auth.rs | DefaultRequestAuthenticator, auth modes, audit events, token parsing. |
| Tool auth integration | crates/decision-gate-mcp/src/tools/router.rs | Per-call authorization + audit emission. |
| Namespace authz interface | crates/decision-gate-mcp/src/namespace_authz.rs | Pluggable namespace authorization seam. |
| Usage metering interface | crates/decision-gate-mcp/src/usage.rs | Pluggable usage metering + quota enforcement seam. |
| Fixed-window request authority | crates/decision-gate-mcp/src/rate_limit.rs | Sealed policy, monotonic state, bounded retention, and atomic publication. |
| JSON-RPC disclosure | crates/decision-gate-mcp/src/server.rs | Error mapping and response codes. |
Test and Contract Traceability
- Authentication, namespace authorization, disclosure, and server tests under
crates/decision-gate-mcp/cover the current ingress contract. - Generated contract artifacts freeze public error and telemetry projections.
Operational Readiness Hooks
- Monitor authentication denials, namespace authorization denials, quota rejections, and disclosure-safe error categories.
- A healthy authorization path does not prove evaluator placement or writer ownership; those require separate future namespace-cell evidence.
Maintenance Rules
- Preserve the distinction between authorization scope and placement authority.
- Update the generated contract artifacts whenever public disclosure text or schema changes.
Threat Model Delta Statement
Threat Model Delta: none for this terminology correction; no ingress policy or runtime authorization behavior changed.