Skip to content

feat(auth): Auth URL and Token URL overrides in Server Settings - #2037

Merged
cliffhall merged 5 commits into
v2/mainfrom
v2/feat/1906-auth-token-url-overrides
Aug 17, 2026
Merged

feat(auth): Auth URL and Token URL overrides in Server Settings#2037
cliffhall merged 5 commits into
v2/mainfrom
v2/feat/1906-auth-token-url-overrides

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #1906

Adds two optional per-server fields — Authorization URL override and Token URL override — that replace the authorization_endpoint / token_endpoint an 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 startAuthorization and the token endpoint in fetchToken, both off the discovered metadata document. Neither is routed through OAuthClientProvider, and AuthOptions has no metadata field — 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 fetchFn the SDK discovers through, so core/auth/endpointOverrides.ts wraps it and patches the metadata document in flight. Two consequences, both documented on the module:

  • It wraps the base fetch rather than only the auth fetch, so it also covers the discovery the SDK runs from inside the transport on the 401/refresh path (which is handed that same fetch).
  • The Network tab therefore shows the metadata as the flow consumed it — the useful reading, since it explains where the subsequent authorize/token requests went.
  • A server whose AS publishes no metadata at all is unaffected: there the SDK falls back to /authorize and /token on 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 issuer REQUIRED, and protected-resource metadata has no issuer — so an unrelated JSON body on the connection is never rewritten.

What's wired

  • oauth.authorizationUrl / oauth.tokenUrl on disk (mcp.json), read/written by serverList.ts with the same omit-when-blank round-trip as the other OAuth fields
  • /api/servers read and write validation (shape only — URL validity is checked where the override is applied)
  • The runner (buildRunnerClientAuthOptions), so CLI and TUI pick the same fields up from the same settings the web client reads
  • Server Settings → Authorization, beside the authorization parameters
  • Value validation shared between the form and the runtime (oauthEndpointUrlError), so they cannot disagree: absolute http(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:

before

After, with both overrides set:

after

A value that is not an absolute http(s) URL is flagged inline (the sibling field is unaffected — the two are independent):

validation

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, stale content-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 via setOAuthConfig
  • Round-trip coverage in serverList, runner, the /api/servers route, ServerSettingsForm, and ServerSettingsModal

Docs updated in docs/mcp-server-configuration.md, plus the core/auth entries in AGENTS.md and the root README.

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>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Aug 17, 2026
@cliffhall
cliffhall requested a balanced review from Copilot August 17, 2026 00:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread core/mcp/inspectorClient.ts
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>
@cliffhall

Copy link
Copy Markdown
Member Author

Review round 1 — responded.

npm run ci green on the updated branch. Requesting another review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 URL accepts embedded credentials, but Fetch rejects HTTP(S) request URLs containing a username or password. A token override such as https://user:pass@example.com/token therefore passes both form and runtime validation, then fails the OAuth exchange instead of being dropped with the documented warning. Reject parsed.username/parsed.password here and cover that case in the validator/normalizer tests.
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
    return `"${trimmed}" is not an http(s) URL.`;
  }

Comment thread core/auth/endpointOverrides.ts Outdated
…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>
@cliffhall

Copy link
Copy Markdown
Member Author

Review round 2 — responded. Both findings were real; fixed in ac8a08e.

  • Rebuilt every JSON response (core/auth/endpointOverrides.ts) — now parses a clone and returns the caller's own Response unless a metadata document is actually patched. Fixes the lost url/redirected/type, and the body-less 204/205 case where rebuilding threw (Fetch forbids a body on those statuses) and could fail an unrelated transport response. Tests now assert identity (toBe(original)) on the pass-through paths, plus a new 204 case.
  • Suppressed comment — embedded credentials — also taken. new URL accepts https://user:pass@host/token while Fetch rejects it, so it passed both the form and the runtime and failed inside the exchange; oauthEndpointUrlError now rejects a username or password, with validator and normalizer tests.

npm run ci green. Requesting another review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.

Comment thread core/auth/endpointOverrides.ts Outdated
Comment thread core/auth/endpointOverrides.ts
… 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>
@cliffhall

Copy link
Copy Markdown
Member Author

Review round 3 — responded. Both findings were real; fixed in e5c2060.

  • application/x-ndjson matched the includes("json") test — the serious one. That is how the transport can serve its long-lived server-push channel (isLongLivedStreamResponse), and awaiting .json() on a clone of a never-closing stream would have hung the MCP connection for as long as an override was configured. Now an exact media-type match (application/json or a +json suffix, parameters stripped), with a regression test driven by a ReadableStream that is deliberately never closed — it hangs rather than passing quietly if the wrapper ever reads a stream again.
  • Issuer is left as discovered — took the second option you offered: the overrides are limited to alternate endpoints of the same logical issuer, and the RFC 9207 / SEP-2352 mix-up defense is not weakened. Stated on the module, in the Authorization URL field description, and in docs/mcp-server-configuration.md. Overriding the issuer as a complete unit (with issuer-scoped stored state) is a much larger feature than this issue asked for and belongs in its own issue.

npm run ci green. Requesting another review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-string authorizationUrl/tokenUrl is therefore copied into InspectorServerSettings, and CLI/TUI later call .trim() in oauthEndpointOverridesFromSettings, crashing while loading the server. Guard the disk values as strings, as this module already does for untrusted authorizationParams.
  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/json response, 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>
@cliffhall

Copy link
Copy Markdown
Member Author

Review round 4 — responded. No new inline comments; both suppressed comments were real and are fixed in 8d68039.

  • endpointOverrides.ts — clone-and-parse ran for every application/json response. Correct, and it was the last of the hot-path concerns: with an override configured, every tools/call result and every resource payload on the direct CLI/TUI transports was buffered and parsed a second time before the SDK could consume it, purely to conclude it was not metadata. The wrapper now tests the request URL for the RFC 8414 / OIDC well-known paths first (/.well-known/oauth-authorization-server, /.well-known/openid-configuration, substring-matched on the pathname so the SDK's path-suffixed and path-prefixed variants are all covered), so ordinary traffic costs one string test and is returned untouched — no clone at all. Tests: a JSON-RPC-shaped response asserts clone is never called, plus a case covering all three request-input forms (string, URL, Request with a tenant path suffix). This holds on the web client too — the browser's remote fetch receives the real AS URL and rewrites it internally, so gating outside it sees the true target.
  • serverList.ts — the read path trusted the compile-time string. Also correct, and the same class this module already guards for authorizationParams: the CLI and TUI read mcp.json directly, and oauthEndpointOverridesFromSettings calls .trim(), so "tokenUrl": 42 would have crashed the server load rather than being ignored. Added cleanEndpointOverride, mirroring cleanAuthorizationParams (drop with a warning, leave URL validity to the point of use), with a test that a non-string is dropped while its valid sibling survives.

npm run ci green. Requesting another review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.

@cliffhall
cliffhall merged commit 4e4aa49 into v2/main Aug 17, 2026
4 checks passed
@cliffhall
cliffhall deleted the v2/feat/1906-auth-token-url-overrides branch August 17, 2026 02:49
@cliffhall cliffhall linked an issue Aug 17, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auth URL and Token URL overrides in Server Settings

2 participants