diff --git a/AGENTS.md b/AGENTS.md
index d07e84f4d..3deca03ba 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -45,7 +45,13 @@ v2/main/
│ │ # providers.redirectToAuthorization, the only
│ │ # seam that sees the SDK-built authorize URL;
│ │ # authorization request only, never the token
-│ │ # request — #2018)
+│ │ # request — #2018;
+│ │ # endpointOverrides.ts per-server
+│ │ # authorization/token URL overrides — a fetch
+│ │ # wrapper that rewrites the discovered AS
+│ │ # metadata document, the one seam SDK v2 routes
+│ │ # BOTH endpoints through (neither reaches the
+│ │ # OAuthClientProvider) — #1906)
│ │ ├── browser/ # Browser-side OAuth (sessionStorage, BrowserNavigation)
│ │ ├── node/ # Node-side OAuth (NodeOAuthStorage, OAuthCallbackServer,
│ │ │ # runner-interactive-oauth loopback callback flow)
diff --git a/README.md b/README.md
index cb57dd359..a9fab0c1e 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ inspector/
│ ├── tui/ # TUI client (Ink + React, tsup bundle)
│ └── launcher/ # Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
├── core/ # Shared code consumed via the `@inspector/core` alias (no package.json)
-│ ├── auth/ # OAuth: providers, discovery, storage, mid-session recovery (browser/node/remote backends)
+│ ├── auth/ # OAuth: providers, discovery, storage, endpoint overrides, mid-session recovery (browser/node/remote backends)
│ ├── client/ # Install-level client config (`client.json`): browser-safe parse/validate + Node load/save, remote backend, secrets
│ ├── json/ # JSON + parameter/argument conversion utilities, and the nullable-union
│ │ # schema collapse shared by the web and TUI form builders
diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx
index 72d8bd1f3..5837f27f5 100644
--- a/clients/web/src/App.tsx
+++ b/clients/web/src/App.tsx
@@ -64,6 +64,7 @@ import { ResourceSubscriptionsState } from "@inspector/core/mcp/state/resourceSu
import {
cleanRoots,
oauthAuthorizationParamsFromSettings,
+ oauthEndpointOverridesFromSettings,
serializeMcpConfig,
} from "@inspector/core/mcp/serverList.js";
import type { ClientConfig } from "@inspector/core/client/types.js";
@@ -2315,12 +2316,16 @@ function App() {
const serverAuthorizationParams = savedSettings
? oauthAuthorizationParamsFromSettings(savedSettings)
: undefined;
+ const serverEndpointOverrides = savedSettings
+ ? oauthEndpointOverridesFromSettings(savedSettings)
+ : undefined;
const oauthFromServer =
savedSettings &&
(savedSettings.oauthClientId ||
savedSettings.oauthClientSecret ||
savedSettings.oauthScopes ||
serverAuthorizationParams ||
+ serverEndpointOverrides ||
savedSettings.enterpriseManaged)
? {
...(savedSettings.oauthClientId && {
@@ -2335,6 +2340,7 @@ function App() {
...(serverAuthorizationParams && {
authorizationParams: serverAuthorizationParams,
}),
+ ...serverEndpointOverrides,
...(savedSettings.enterpriseManaged && {
enterpriseManaged: true,
}),
diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx
index d238228a1..09ded1716 100644
--- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx
+++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx
@@ -701,6 +701,8 @@ describe("ServerSettingsForm", () => {
clientSecret: "",
scopes: "",
authorizationParams: [],
+ authorizationUrl: "",
+ tokenUrl: "",
enterpriseManaged: false,
});
});
@@ -966,6 +968,104 @@ describe("ServerSettingsForm", () => {
expect(screen.getByText(/state, scope are set/)).toBeInTheDocument();
});
+ // #1906 — the endpoint overrides ride the same `onOAuthChange` callback as
+ // the rest of the Authorization section.
+ it("invokes onOAuthChange when an endpoint override is typed", async () => {
+ const user = userEvent.setup();
+ const onOAuthChange = vi.fn();
+ renderWithMantine(
+ ,
+ );
+ await user.type(
+ screen.getByRole("textbox", { name: /Authorization URL override/i }),
+ "h",
+ );
+ expect(onOAuthChange).toHaveBeenCalledWith(
+ expect.objectContaining({ authorizationUrl: "h" }),
+ );
+
+ onOAuthChange.mockClear();
+ await user.type(
+ screen.getByRole("textbox", { name: /Token URL override/i }),
+ "h",
+ );
+ expect(onOAuthChange).toHaveBeenCalledWith(
+ expect.objectContaining({ tokenUrl: "h" }),
+ );
+ });
+
+ it("says the endpoint overrides are unused under enterprise-managed auth", () => {
+ const { rerender } = renderWithMantine(
+ ,
+ );
+ expect(
+ screen.getAllByText(
+ /Not applied while Enterprise-managed authorization is on/,
+ ),
+ ).toHaveLength(2);
+
+ rerender(
+ ,
+ );
+ expect(
+ screen.queryByText(
+ /Not applied while Enterprise-managed authorization is on/,
+ ),
+ ).not.toBeInTheDocument();
+ });
+
+ it("flags an endpoint override that is not an absolute http(s) URL", () => {
+ renderWithMantine(
+ ,
+ );
+ expect(
+ screen.getByText('"/authorize" is not an absolute URL.'),
+ ).toBeInTheDocument();
+ expect(screen.queryByText(/is not an http\(s\) URL/)).toBeNull();
+ });
+
+ it("clears an endpoint override through its clear button", async () => {
+ const user = userEvent.setup();
+ const onOAuthChange = vi.fn();
+ renderWithMantine(
+ ,
+ );
+ const clearButtons = screen.getAllByRole("button", { name: /clear/i });
+ await user.click(clearButtons[clearButtons.length - 1]);
+ expect(onOAuthChange).toHaveBeenCalledWith(
+ expect.objectContaining({ tokenUrl: "" }),
+ );
+ });
+
it("invokes onOAuthChange with the chosen insufficient-scope policy (SEP-2350)", async () => {
const user = userEvent.setup();
const onOAuthChange = vi.fn();
@@ -1007,6 +1107,8 @@ describe("ServerSettingsForm", () => {
clientSecret: "",
scopes: "",
authorizationParams: [],
+ authorizationUrl: "",
+ tokenUrl: "",
enterpriseManaged: true,
});
});
@@ -1112,6 +1214,8 @@ describe("ServerSettingsForm", () => {
clientSecret: "z",
scopes: "",
authorizationParams: [],
+ authorizationUrl: "",
+ tokenUrl: "",
enterpriseManaged: false,
});
});
diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx
index 276c9cf9d..b2a786c64 100644
--- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx
+++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx
@@ -32,6 +32,7 @@ import {
authorizationParamKeyError,
isReservedAuthorizationParam,
} from "@inspector/core/auth/authorizationParams.js";
+import { oauthEndpointUrlError } from "@inspector/core/auth/endpointOverrides.js";
import { ADVERTISABLE_EXTENSIONS } from "@inspector/core/mcp/extensions.js";
import type { Root } from "@modelcontextprotocol/client";
@@ -478,11 +479,20 @@ export function ServerSettingsForm({
clientSecret: settings.oauthClientSecret ?? "",
scopes: settings.oauthScopes ?? "",
authorizationParams,
+ authorizationUrl: settings.oauthAuthorizationUrl ?? "",
+ tokenUrl: settings.oauthTokenUrl ?? "",
enterpriseManaged: settings.enterpriseManaged ?? false,
onInsufficientScope: settings.oauthOnInsufficientScope,
};
}
+ // #1906 — the overrides are suppressed under EMA for the same reason the
+ // custom authorization parameters are: that leg authorizes against the
+ // enterprise IdP, a different authorization server.
+ const endpointOverrideEmaNote = settings.enterpriseManaged
+ ? " Not applied while Enterprise-managed authorization is on: that flow authorizes against the enterprise IdP, a different authorization server."
+ : "";
+
const rejectedParamKeys = authorizationParams
.map((p) => p.key.trim())
.filter((key) => isReservedAuthorizationParam(key));
@@ -863,6 +873,55 @@ export function ServerSettingsForm({
+ Add Parameter
+
+ onOAuthChange({
+ ...currentOAuth(),
+ authorizationUrl: e.currentTarget.value,
+ })
+ }
+ rightSection={
+ settings.oauthAuthorizationUrl ? (
+
+ onOAuthChange({
+ ...currentOAuth(),
+ authorizationUrl: "",
+ })
+ }
+ />
+ ) : null
+ }
+ />
+
+ onOAuthChange({
+ ...currentOAuth(),
+ tokenUrl: e.currentTarget.value,
+ })
+ }
+ rightSection={
+ settings.oauthTokenUrl ? (
+
+ onOAuthChange({ ...currentOAuth(), tokenUrl: "" })
+ }
+ />
+ ) : null
+ }
+ />