feat(auth): Auth URL and Token URL overrides in Server Settings - #2037
Conversation
Adds two optional per-server fields — `oauth.authorizationUrl` and `oauth.tokenUrl` — that override the endpoints authorization-server metadata discovery resolved, so a server can be pointed at a development or staging authorization server without changing what it advertises. SDK v2 routes both endpoints through the discovered metadata document and neither through OAuthClientProvider, so the override is applied by wrapping the client's base fetch and patching the metadata document in flight (core/auth/endpointOverrides.ts). Wrapping the base fetch — rather than only the auth fetch — also covers the discovery the SDK runs from inside the transport on the 401/refresh path. The fields round-trip through mcp.json, the /api/servers routes, the runner (CLI/TUI), and the Server Settings → Authorization panel, where a value that is not an absolute http(s) URL is flagged inline; at runtime such a value is dropped with a single warning rather than failing the connection. Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
Pull request overview
Adds per-server OAuth authorization and token endpoint overrides across Web, CLI, and TUI.
Changes:
- Persists and validates endpoint override settings.
- Rewrites discovered authorization-server metadata through a fetch wrapper.
- Adds UI, documentation, and comprehensive tests.
- Review found the wrapper also incorrectly rewrites enterprise IdP metadata in EMA flows.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
README.md |
Documents endpoint overrides. |
docs/mcp-server-configuration.md |
Describes configuration and behavior. |
core/mcp/types.ts |
Adds override fields to shared types. |
core/mcp/serverList.ts |
Converts and persists override settings. |
core/mcp/remote/node/server.ts |
Validates remote API payloads. |
core/mcp/oauthManager.ts |
Exposes live endpoint overrides. |
core/mcp/inspectorClient.ts |
Installs the metadata-rewriting fetch wrapper. |
core/client/runner.ts |
Wires overrides into CLI/TUI clients. |
core/auth/index.ts |
Exports override utilities. |
core/auth/endpointOverrides.ts |
Implements validation and metadata rewriting. |
clients/web/src/test/integration/mcp/remote/servers-route.test.ts |
Tests API persistence and validation. |
clients/web/src/test/core/mcp/serverList.test.ts |
Tests settings conversion. |
clients/web/src/test/core/mcp/oauthManager.test.ts |
Tests live override retrieval. |
clients/web/src/test/core/mcp/inspectorClient-oauth-endpoint-overrides.test.ts |
Tests client fetch wiring. |
clients/web/src/test/core/client/runner.test.ts |
Tests runner forwarding. |
clients/web/src/test/core/auth/endpointOverrides.test.ts |
Tests override utilities and wrapper behavior. |
clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.tsx |
Maps form values into settings. |
clients/web/src/components/groups/ServerSettingsModal/ServerSettingsModal.test.tsx |
Tests persistence and clearing. |
clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx |
Adds override inputs and validation. |
clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx |
Tests input, validation, and clearing. |
clients/web/src/App.tsx |
Wires saved overrides into Web clients. |
AGENTS.md |
Updates architecture documentation. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
The overrides are per-server configuration for the *resource* server's authorization server, but the fetch wrapper that applies them is shared with the enterprise-managed (EMA) flow, whose IdP OIDC discovery runs through the same fetch and satisfies the metadata predicate. An authorization override could therefore redirect the IdP login to the resource AS, and a token override could send the IdP code or refresh token there. OAuthManager.getEndpointOverrides() now returns nothing when the server is enterprise-managed — the same boundary redirectToExternalAuthorization draws for the custom authorization parameters (#2018) — with a regression test, the note surfaced on both form fields, and the rule documented. Signed-off-by: cliffhall <cliff@futurescale.com>
|
Review round 1 — responded.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
core/auth/endpointOverrides.ts:87
new URLaccepts embedded credentials, but Fetch rejects HTTP(S) request URLs containing a username or password. A token override such ashttps://user:pass@example.com/tokentherefore passes both form and runtime validation, then fails the OAuth exchange instead of being dropped with the documented warning. Rejectparsed.username/parsed.passwordhere and cover that case in the validator/normalizer tests.
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return `"${trimmed}" is not an http(s) URL.`;
}
…lied Two review findings on the fetch wrapper: Reading the body with `response.text()` meant every successful JSON response was rebuilt while overrides were configured, even when it was not a metadata document. That dropped native properties a synthesized `Response` cannot carry (`url`, `redirected`, `type`) from responses the wrapper has no business altering, and threw outright on a body-less 204/205, whose status Fetch forbids a body on — so an unrelated transport response could fail. It now parses a clone and returns the caller's own response unless a document is actually patched. `new URL` also accepts embedded credentials while Fetch rejects a request URL carrying them, so `https://user:pass@host/token` passed both the form and the runtime and then failed deep in the OAuth exchange. `oauthEndpointUrlError` now rejects a username or password. Signed-off-by: cliffhall <cliff@futurescale.com>
|
Review round 2 — responded. Both findings were real; fixed in ac8a08e.
|
… not do Two review findings on the fetch wrapper: The content-type test was a substring match for "json", which also matches `application/x-ndjson` — how the streamable-HTTP transport can serve its long-lived server-push channel (`isLongLivedStreamResponse`). Awaiting `.json()` on a clone of an unbounded stream never resolves, so the MCP connection would hang for as long as an override was configured. It now matches the media type exactly (`application/json` or a `+json` suffix), with a regression test driven by a stream that never closes. The overrides also leave `issuer` as discovery returned it, so pointing one at an authorization server that advertises a different issuer is rejected on callback by the RFC 9207 / SEP-2352 mix-up defense. That check is deliberately not weakened; the limit is now stated on the module, in the field description, and in the configuration guide. Signed-off-by: cliffhall <cliff@futurescale.com>
|
Review round 3 — responded. Both findings were real; fixed in e5c2060.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (2)
core/mcp/serverList.ts:387
- These values come from a hand-editable
mcp.json, but this read path trusts the compile-time type. A non-stringauthorizationUrl/tokenUrlis therefore copied intoInspectorServerSettings, and CLI/TUI later call.trim()inoauthEndpointOverridesFromSettings, crashing while loading the server. Guard the disk values as strings, as this module already does for untrustedauthorizationParams.
if (stored.oauth?.authorizationUrl)
settings.oauthAuthorizationUrl = stored.oauth.authorizationUrl;
if (stored.oauth?.tokenUrl) settings.oauthTokenUrl = stored.oauth.tokenUrl;
core/auth/endpointOverrides.ts:284
- When an override is configured, this clone-and-parse runs for every successful
application/jsonresponse, including normal Streamable HTTP JSON-RPC/tool responses in the direct CLI/TUI transports. Large tool/resource payloads are therefore fully buffered and parsed a second time before the SDK can consume them, even though they cannot be authorization-server metadata. Gate this work on the RFC 8414/OIDC discovery request URL (or otherwise scope the wrapper to discovery responses) before cloning the body.
let parsed: unknown;
try {
parsed = await response.clone().json();
…uard disk values Two suppressed review findings. With an override configured, the wrapper cloned and parsed every successful application/json response — every tools/call result and every resource payload on the direct CLI/TUI transports — only to conclude it was not a metadata document. It now tests the request URL for the RFC 8414 / OIDC well-known paths first, so ordinary traffic costs one string test and is returned untouched. The read path also trusted StoredMCPServer's `string` type for the two override fields, which a hand-edited mcp.json does not honor: the CLI and TUI read the file directly and then `.trim()` these, so a number would crash the server load. Guarded with cleanEndpointOverride, mirroring cleanAuthorizationParams. Signed-off-by: cliffhall <cliff@futurescale.com>
|
Review round 4 — responded. No new inline comments; both suppressed comments were real and are fixed in 8d68039.
|
Closes #1906
Adds two optional per-server fields — Authorization URL override and Token URL override — that replace the
authorization_endpoint/token_endpointan authorization server's metadata document advertises. The Inspector still resolves both by discovery out of the box (as a real MCP host does); these exist so a server that advertises its production authorization server can be pointed at a development or staging one while debugging, which was the ask in the issue.Where the override is applied, and why there
SDK v2 reads the authorization endpoint in
startAuthorizationand the token endpoint infetchToken, both off the discovered metadata document. Neither is routed throughOAuthClientProvider, andAuthOptionshas nometadatafield — so the provider seam that carries the custom authorization parameters (#2018) cannot reach the token endpoint at all.The one seam that sees both is the
fetchFnthe SDK discovers through, socore/auth/endpointOverrides.tswraps it and patches the metadata document in flight. Two consequences, both documented on the module:/authorizeand/tokenon the AS origin and there is no document to patch. That matches the feature as asked for ("override whatever urls the authorization server returns").The metadata test is deliberately narrow — RFC 8414 §2 makes
issuerREQUIRED, and protected-resource metadata has noissuer— so an unrelated JSON body on the connection is never rewritten.What's wired
oauth.authorizationUrl/oauth.tokenUrlon disk (mcp.json), read/written byserverList.tswith the same omit-when-blank round-trip as the other OAuth fields/api/serversread and write validation (shape only — URL validity is checked where the override is applied)buildRunnerClientAuthOptions), so CLI and TUI pick the same fields up from the same settings the web client readsoauthEndpointUrlError), so they cannot disagree: absolutehttp(s)only (http:is allowed — reaching a local/staging AS is the point). A bad value is flagged inline in the form, and at runtime it is dropped with a single warning (normalization is memoized per raw value) rather than failing the connection.Screenshots
Server Settings → Authorization, before:
After, with both overrides set:
A value that is not an absolute http(s) URL is flagged inline (the sibling field is unaffected — the two are independent):
Tests
core/auth/endpointOverrides.ts— full unit coverage of the validator, the normalizer (including the warn-once memo), the metadata predicate, the patch, and the fetch wrapper's pass-through cases (non-2xx, non-JSON, non-metadata JSON, invalid JSON, stalecontent-length)inspectorClient-oauth-endpoint-overrides.test.ts— asserts through the fetch the client actually hands the transport, so the wiring cannot be removed silently, including an override applied after construction viasetOAuthConfigserverList,runner, the/api/serversroute,ServerSettingsForm, andServerSettingsModalDocs updated in
docs/mcp-server-configuration.md, plus thecore/authentries inAGENTS.mdand the root README.