Summary
BearerTokenValidator (0.12.1) can validate an opaque token through its tokenValidator closure, but it requires resourceMetadataURL and resourceIdentifier, and every 401 it emits advertises resource_metadata="…". That makes it OAuth-oriented: a locally-run server that wants the spec's "Require an authorization token" mitigation (security best practices, §Local MCP Server Compromise) with a manually configured secret has to either fabricate metadata URLs or write its own validator.
A request for a small second validator, alongside BearerTokenValidator, that checks a pre-shared token and emits plain RFC 6750 challenges without OAuth discovery parameters.
Motivation
The expected deployment is one macOS user, one app hosting an MCP endpoint on loopback, and one or more client programs (Claude Code, Codex, …) on the same machine. Those clients already send Authorization: Bearer <token> from a static configuration (claude mcp add --transport http … --header "Authorization: Bearer …", codex mcp add … --bearer-token-env-var …), so a static token rides the path clients already implement, and an embedder can later swap in BearerTokenValidator without changing what clients send.
Proposed contract
init(secret: String, realm: String = "mcp") and a store-backed form (e.g. init(secrets: @Sendable () -> Set<String>, realm:)) so an app can issue one token per client and revoke individually; the closure is consulted once per request.
- Parsing per RFC 9110 §11.1 / RFC 6750 §2.1: case-insensitive scheme,
1*SP, strict b64token grammar, byte-exact comparison.
- Responses per RFC 6750 §3.1: no credentials or a foreign scheme → 401 with
WWW-Authenticate: Bearer realm="…" (no error code); well-formed mismatch → 401 with error="invalid_token"; malformed credentials → 400 with error="invalid_request". Bodies never disclose the secret.
- Constant-time comparison of the presented token against every configured secret (e.g. HMAC both under a per-instance key and compare the MACs), with no early exit across secrets.
- Placement documented as after
OriginValidator and before SessionValidator, so an unauthenticated initialize draws 401 rather than a session error.
- Explicitly not in scope: token generation/persistence, expiry, rate limiting, OAuth discovery.
Reference implementation
An implementation with this contract, unit tests (36 cases) and live-pipeline integration tests is in a downstream package: https://github.com/ianegordon/Misconception (Sources/MisconceptionMCP/StaticBearerTokenValidator.swift, commit 1e285db). Happy to adapt it into a pull request here if the maintainers would take it.
Summary
BearerTokenValidator(0.12.1) can validate an opaque token through itstokenValidatorclosure, but it requiresresourceMetadataURLandresourceIdentifier, and every 401 it emits advertisesresource_metadata="…". That makes it OAuth-oriented: a locally-run server that wants the spec's "Require an authorization token" mitigation (security best practices, §Local MCP Server Compromise) with a manually configured secret has to either fabricate metadata URLs or write its own validator.A request for a small second validator, alongside
BearerTokenValidator, that checks a pre-shared token and emits plain RFC 6750 challenges without OAuth discovery parameters.Motivation
The expected deployment is one macOS user, one app hosting an MCP endpoint on loopback, and one or more client programs (Claude Code, Codex, …) on the same machine. Those clients already send
Authorization: Bearer <token>from a static configuration (claude mcp add --transport http … --header "Authorization: Bearer …",codex mcp add … --bearer-token-env-var …), so a static token rides the path clients already implement, and an embedder can later swap inBearerTokenValidatorwithout changing what clients send.Proposed contract
init(secret: String, realm: String = "mcp")and a store-backed form (e.g.init(secrets: @Sendable () -> Set<String>, realm:)) so an app can issue one token per client and revoke individually; the closure is consulted once per request.1*SP, strictb64tokengrammar, byte-exact comparison.WWW-Authenticate: Bearer realm="…"(no error code); well-formed mismatch → 401 witherror="invalid_token"; malformed credentials → 400 witherror="invalid_request". Bodies never disclose the secret.OriginValidatorand beforeSessionValidator, so an unauthenticatedinitializedraws 401 rather than a session error.Reference implementation
An implementation with this contract, unit tests (36 cases) and live-pipeline integration tests is in a downstream package: https://github.com/ianegordon/Misconception (
Sources/MisconceptionMCP/StaticBearerTokenValidator.swift, commit 1e285db). Happy to adapt it into a pull request here if the maintainers would take it.