Agent authored:
Summary
This issue tracks a bounded OAuth refactor plus a phase-1 feature addition so HTTP OAuth authorization can succeed for unregistered clients when - and only when - the redirect URI matches an explicitly approved allowlist.
Today, compatibility is partly anchored to special-case client_id behavior (notably the VS Code MCP proxy path). The goal is to replace that narrow exception with a generalized, security-first model that supports localhost and approved custom-scheme callbacks without requiring local client registration on every pod.
This issue is intentionally scoped to phase 1 only. Redis/shared-state support should be implemented later on top of the same boundaries.
Agent-authored by Hermes.
Problem statement
Current OAuth validation is split across multiple layers:
- transport request handling and client lookup
- provider-side redirect checks
- env/profile/runtime config assembly
- legacy compatibility fallback paths
That split makes it harder to reason about the trust boundary and increases the risk that authorize and token flows diverge.
In addition, the current compatibility behavior for VS Code is tied to a special-case client_id, while the desired behavior is broader and more explicit:
- registered clients continue to work normally
- unregistered clients can be accepted only when their
redirect_uri matches a configured allowlist
Goals
- Support phase-1 OAuth authorization for unregistered clients with an explicit redirect allowlist.
- Replace
client_id-specific compatibility as the primary mechanism with redirect-based policy.
- Centralize OAuth validation decisions into explicit reusable components.
- Keep the implementation low-risk, strongly tested, and ready for later Redis/shared-state support.
Non-goals
- No Redis/shared-state implementation in this issue
- No broad OAuth redesign beyond the validation/config boundaries needed here
- No broadening of unregistered-client trust beyond approved redirect URIs
- No permissive wildcard host matching for unregistered clients in phase 1
Desired behavior
Registered clients
- Continue to use the normal registration-based flow.
- Registered redirect URIs must still match existing client registration rules.
- Existing defense-in-depth redirect host checks remain in place.
Unregistered clients
- Are rejected by default.
- Can be accepted only when all of the following are true:
- unregistered-client support is enabled
redirect_uri is present
redirect_uri matches an explicitly approved allowlist
- token exchange later matches the same
client_id, redirect_uri, and registration mode metadata stored with the authorization code
Configuration
Introduce new OAuth config inputs:
MCP4_OAUTH_ALLOW_UNREGISTERED_CLIENTS
MCP4_OAUTH_ALLOWED_UNREGISTERED_REDIRECTS
Expected semantics:
MCP4_OAUTH_ALLOW_UNREGISTERED_CLIENTS=false by default
- invalid boolean values fail fast at startup
- if unregistered-client support is enabled but the allowlist is empty, startup fails fast
- unregistered redirect patterns are parsed and normalized once at startup, not per request
Approved redirect policy for phase 1
Support a conservative allowlist model only:
http://localhost:*
http://127.0.0.1:*
- optionally
http://[::1]:* if IPv6 localhost handling is already consistent
- exact approved custom-scheme callbacks such as
cursor://oauth/callback or vscode://mcp-auth/callback
Do not support in phase 1:
- wildcard hosts for unregistered clients
- CIDR-based unregistered redirect patterns
- broad custom-scheme wildcards such as
cursor://*
- substring or prefix matching instead of parsed URI matching
Architectural direction
Implement the change around three explicit components.
1. src/auth/oauth-runtime-config.ts
Responsibilities:
- resolve final OAuth runtime config from env/profile inputs
- apply precedence in one place
- validate config at startup
- parse and normalize approved unregistered redirect patterns once
Expected output should become the one authoritative runtime OAuth shape used by the transport/provider path.
2. src/auth/redirect-uri-validator.ts
Responsibilities:
- shared redirect parsing, normalization, and dangerous-scheme rejection
- support two explicit validation modes:
- handle exact registered redirect matching plus existing host allowlist defense-in-depth
- handle unregistered redirect allowlist matching
This must be the single shared redirect validation component to avoid duplicated parsing and edge-case drift.
3. src/auth/oauth-client-validation-policy.ts
Responsibilities:
- act as the single source of truth for OAuth client acceptance rules
- decide registered vs unregistered client mode during
authorize
- validate token exchange against stored authorization-code metadata
- keep unregistered-client trust anchored to redirect allowlist + authorization-code metadata, not registry lookup
Trust model
The implementation must keep the trust boundary explicit:
- registered client trust is anchored in the registry
- unregistered client trust is anchored only in approved redirect validation plus authorization-code metadata
Unregistered clients must not be silently promoted into normal registered clients.
Pseudo-registration or temporary insertion into the client registry is out of scope.
Authorization-code metadata
Store the following authoritative metadata with authorization codes:
interface AuthorizationCodeMetadata {
clientId: string;
redirectUri?: string;
registrationMode: 'registered' | 'unregistered';
}
This metadata must be used by the token flow so unregistered-client token exchange does not depend on later registry lookup.
Legacy VS Code compatibility
Keep existing VS Code compatibility working during migration, but isolate it into a clearly bounded compatibility helper/adapter rather than leaving fallback branches scattered across transport/provider logic.
The new generalized unregistered-client flow should become the primary path.
The legacy special-case path should remain temporary and explicitly marked for later cleanup.
Implementation plan
Step 1 - Centralize runtime OAuth config
- Add
src/auth/oauth-runtime-config.ts
- Move OAuth runtime config assembly/validation into that module
- Keep behavior unchanged where possible
- Wire transport/provider consumers to use the new final config shape
Step 2 - Extract redirect validation
- Add
src/auth/redirect-uri-validator.ts
- Move shared redirect parsing, normalization, dangerous-scheme checks, and matching there
- Reuse one validator for both registered and unregistered modes
Step 3 - Introduce canonical client-validation policy
- Add
src/auth/oauth-client-validation-policy.ts
- Move registered/unregistered client acceptance rules there
- Keep transport focused on request parsing and response mapping
- Keep provider focused on protocol execution
Step 4 - Switch authorize flow to policy-driven resolution
- Resolve registered vs unregistered mode through the policy layer
- Reject unregistered clients unless redirect allowlist policy passes
- Create only minimal internal ephemeral client shapes for unregistered flows
Step 5 - Switch token flow to metadata-driven validation
- Persist
clientId, redirectUri, and registrationMode in authorization-code metadata
- Validate token exchange against that metadata for both registered and unregistered modes
- Do not require registry lookup for unregistered flows
Step 6 - Add generalized approved unregistered-client support
- Enable localhost/custom-scheme approved redirect support for unregistered clients
- Ensure legacy VS Code behavior still works through the isolated compatibility layer
Step 7 - Add full regression coverage
- Unit tests for runtime config
- Unit tests for redirect validation
- Unit tests for client-validation policy
- Integration tests for authorize/token parity
- Legacy VS Code regression tests
- Generalized unregistered localhost regression tests
Files likely affected
src/core/cli-config.ts
src/transport/http-transport-config.ts
src/transport/http-transport.ts
src/auth/oauth-provider.ts
src/core/errors.ts if new typed error coverage is needed
docs/OAUTH.md
docs/HTTP-TRANSPORT.md
README.md
CHANGELOG.md
- related OAuth and transport test files
New files:
src/auth/oauth-runtime-config.ts
src/auth/oauth-runtime-config.test.ts
src/auth/redirect-uri-validator.ts
src/auth/redirect-uri-validator.test.ts
src/auth/oauth-client-validation-policy.ts
src/auth/oauth-client-validation-policy.test.ts
Testing requirements
Runtime config
- precedence between env/profile/defaults
- invalid booleans fail fast
- malformed redirect patterns fail fast
- empty allowlist with unregistered-client support enabled fails fast
Redirect validation
- table-driven success/failure coverage for:
- localhost allow
- 127.0.0.1 allow
- optional IPv6 localhost allow
- exact custom-scheme allow
- dangerous scheme rejection
- malformed URI rejection
- userinfo/host confusion rejection
localhost.evil.com rejection
localhost@evil.com rejection
Flow parity
- registered authorize + token parity
- unregistered authorize + token parity
- token rejection when
client_id mismatches code metadata
- token rejection when
redirect_uri mismatches code metadata
Regression
- legacy VS Code compatibility still works during migration
- new generalized unregistered localhost flow works without relying on the old special-case
client_id
Acceptance criteria
- OAuth decision logic is centralized into the three explicit components above
- registered and unregistered client trust models are explicit and separate
- unregistered clients are denied by default
- unregistered clients can be authorized only through the explicit approved redirect allowlist
- authorize and token flows share the same registration-mode model through authorization-code metadata
- redirect parsing/normalization is not duplicated and is not re-parsed per request
- legacy VS Code compatibility still works during migration through an isolated compat path
- docs are updated for the new env/config behavior
- targeted tests pass, plus
npm run typecheck
Related work
This issue should align with those directions and avoid duplicating low-level matcher logic again.
Agent authored:
Summary
This issue tracks a bounded OAuth refactor plus a phase-1 feature addition so HTTP OAuth authorization can succeed for unregistered clients when - and only when - the redirect URI matches an explicitly approved allowlist.
Today, compatibility is partly anchored to special-case
client_idbehavior (notably the VS Code MCP proxy path). The goal is to replace that narrow exception with a generalized, security-first model that supports localhost and approved custom-scheme callbacks without requiring local client registration on every pod.This issue is intentionally scoped to phase 1 only. Redis/shared-state support should be implemented later on top of the same boundaries.
Agent-authored by Hermes.
Problem statement
Current OAuth validation is split across multiple layers:
That split makes it harder to reason about the trust boundary and increases the risk that
authorizeandtokenflows diverge.In addition, the current compatibility behavior for VS Code is tied to a special-case
client_id, while the desired behavior is broader and more explicit:redirect_urimatches a configured allowlistGoals
client_id-specific compatibility as the primary mechanism with redirect-based policy.Non-goals
Desired behavior
Registered clients
Unregistered clients
redirect_uriis presentredirect_urimatches an explicitly approved allowlistclient_id,redirect_uri, and registration mode metadata stored with the authorization codeConfiguration
Introduce new OAuth config inputs:
MCP4_OAUTH_ALLOW_UNREGISTERED_CLIENTSMCP4_OAUTH_ALLOWED_UNREGISTERED_REDIRECTSExpected semantics:
MCP4_OAUTH_ALLOW_UNREGISTERED_CLIENTS=falseby defaultApproved redirect policy for phase 1
Support a conservative allowlist model only:
http://localhost:*http://127.0.0.1:*http://[::1]:*if IPv6 localhost handling is already consistentcursor://oauth/callbackorvscode://mcp-auth/callbackDo not support in phase 1:
cursor://*Architectural direction
Implement the change around three explicit components.
1.
src/auth/oauth-runtime-config.tsResponsibilities:
Expected output should become the one authoritative runtime OAuth shape used by the transport/provider path.
2.
src/auth/redirect-uri-validator.tsResponsibilities:
registeredunregisteredThis must be the single shared redirect validation component to avoid duplicated parsing and edge-case drift.
3.
src/auth/oauth-client-validation-policy.tsResponsibilities:
authorizeTrust model
The implementation must keep the trust boundary explicit:
Unregistered clients must not be silently promoted into normal registered clients.
Pseudo-registration or temporary insertion into the client registry is out of scope.
Authorization-code metadata
Store the following authoritative metadata with authorization codes:
This metadata must be used by the token flow so unregistered-client token exchange does not depend on later registry lookup.
Legacy VS Code compatibility
Keep existing VS Code compatibility working during migration, but isolate it into a clearly bounded compatibility helper/adapter rather than leaving fallback branches scattered across transport/provider logic.
The new generalized unregistered-client flow should become the primary path.
The legacy special-case path should remain temporary and explicitly marked for later cleanup.
Implementation plan
Step 1 - Centralize runtime OAuth config
src/auth/oauth-runtime-config.tsStep 2 - Extract redirect validation
src/auth/redirect-uri-validator.tsStep 3 - Introduce canonical client-validation policy
src/auth/oauth-client-validation-policy.tsStep 4 - Switch authorize flow to policy-driven resolution
Step 5 - Switch token flow to metadata-driven validation
clientId,redirectUri, andregistrationModein authorization-code metadataStep 6 - Add generalized approved unregistered-client support
Step 7 - Add full regression coverage
Files likely affected
src/core/cli-config.tssrc/transport/http-transport-config.tssrc/transport/http-transport.tssrc/auth/oauth-provider.tssrc/core/errors.tsif new typed error coverage is neededdocs/OAUTH.mddocs/HTTP-TRANSPORT.mdREADME.mdCHANGELOG.mdNew files:
src/auth/oauth-runtime-config.tssrc/auth/oauth-runtime-config.test.tssrc/auth/redirect-uri-validator.tssrc/auth/redirect-uri-validator.test.tssrc/auth/oauth-client-validation-policy.tssrc/auth/oauth-client-validation-policy.test.tsTesting requirements
Runtime config
Redirect validation
localhost.evil.comrejectionlocalhost@evil.comrejectionFlow parity
client_idmismatches code metadataredirect_urimismatches code metadataRegression
client_idAcceptance criteria
npm run typecheckRelated work
This issue should align with those directions and avoid duplicating low-level matcher logic again.