diff --git a/clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.test.tsx b/clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.test.tsx
index 7f3cbec27..9945bc366 100644
--- a/clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.test.tsx
+++ b/clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.test.tsx
@@ -613,3 +613,73 @@ describe("ClientSettingsForm interactions", () => {
).not.toBeInTheDocument();
});
});
+
+describe("ClientSettingsForm SEP-837 registration UX", () => {
+ it("renders a registration-rejection alert with the RFC 7591 error detail", () => {
+ renderWithMantine(
+ ,
+ );
+ expect(
+ screen.getByText("Client registration was rejected"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/invalid_redirect_uri — .* — HTTP 400/),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/registers as a native client/i),
+ ).toBeInTheDocument();
+ });
+
+ it("falls back to a generic message when no RFC 7591 detail is present", () => {
+ renderWithMantine(
+ ,
+ );
+ expect(
+ screen.getByText(
+ "The authorization server rejected client registration.",
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("omits the rejection alert when there is no registration error", () => {
+ renderWithMantine(
+ ,
+ );
+ expect(
+ screen.queryByText("Client registration was rejected"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("reflects DCR's deprecated-in-favor-of-CIMD status in the CIMD section", () => {
+ renderWithMantine(
+ ,
+ );
+ expect(screen.getByText(/deprecated in favor of/i)).toBeInTheDocument();
+ });
+});
diff --git a/clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.tsx b/clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.tsx
index cf89e2a89..c8d8ca772 100644
--- a/clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.tsx
+++ b/clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.tsx
@@ -1,6 +1,7 @@
import { useState } from "react";
import {
Accordion,
+ Alert,
Badge,
Button,
Checkbox,
@@ -14,6 +15,7 @@ import type { EmaIdpLoginState } from "@inspector/core/auth/ema/idpSession.js";
import {
validateClientSettings,
type ClientSettingsFormValues,
+ type OAuthRegistrationError,
} from "./clientSettingsValues.js";
export type ClientSettingsSection = "ema" | "cimd";
@@ -36,6 +38,12 @@ export interface ClientSettingsFormProps {
* silently dropped without explanation.
*/
revealErrors?: boolean;
+ /**
+ * Last Dynamic Client Registration rejection from an authorization server
+ * (SEP-837), surfaced so the user can see *why* DCR failed and adjust (switch
+ * to CIMD, fix the redirect URI). Cleared by the parent on the next attempt.
+ */
+ registrationError?: OAuthRegistrationError;
}
const HintText = Text.withProps({
@@ -43,6 +51,18 @@ const HintText = Text.withProps({
c: "dimmed",
});
+/** Human-readable one-liner for an RFC 7591 DCR rejection. */
+function registrationRejectionSummary(error: OAuthRegistrationError): string {
+ const parts = [
+ error.error,
+ error.errorDescription,
+ error.status ? `HTTP ${error.status}` : undefined,
+ ].filter(Boolean);
+ return parts.length > 0
+ ? parts.join(" — ")
+ : "The authorization server rejected client registration.";
+}
+
export function ClientSettingsForm({
settings,
expandedSections,
@@ -51,6 +71,7 @@ export function ClientSettingsForm({
emaIdpLoginState = "none",
onEmaIdpLogout,
revealErrors = false,
+ registrationError,
}: ClientSettingsFormProps) {
function patch(partial: Partial) {
onSettingsChange((prev) => ({ ...prev, ...partial }));
@@ -92,174 +113,216 @@ export function ClientSettingsForm({
emaIdpLoginState !== "none";
return (
-
- onExpandedSectionsChange(value as ClientSettingsSection[])
- }
- variant="separated"
- >
-
- Enterprise-Managed Authorization
-
-
- patch({ emaEnabled: e.currentTarget.checked })}
- />
- {settings.emaEnabled && (
- <>
-
- Register this redirect URI with your IdP:{" "}
- {typeof window !== "undefined"
- ? `${window.location.origin}/oauth/callback`
- : "http://localhost:6274/oauth/callback"}
-
- patch({ issuer: e.currentTarget.value })}
- onBlur={() => setIssuerTouched(true)}
- error={showIssuerError}
- rightSectionPointerEvents="auto"
- rightSection={
- settings.issuer ? (
- patch({ issuer: "" })} />
- ) : null
- }
- />
- patch({ clientId: e.currentTarget.value })}
- error={showClientIdError}
- rightSectionPointerEvents="auto"
- rightSection={
- settings.clientId ? (
- patch({ clientId: "" })} />
- ) : null
- }
- />
-
- patch({ clientSecret: e.currentTarget.value })
- }
- rightSectionPointerEvents="auto"
- rightSection={
- settings.clientSecret ? (
- patch({ clientSecret: "" })}
- />
- ) : null
- }
- />
- {settings.issuer.trim() !== "" && (
-
-
-
- IdP sign-in
-
- {emaIdpLoginState === "logged_in" ? (
- <>
-
- Signed in
-
-
- Your enterprise IdP session is active. Connecting to
- EMA-enabled MCP servers will not prompt for IdP
- login until you sign out or the session expires.
-
- >
- ) : emaIdpLoginState === "expired" ? (
- <>
-
- Session expired
-
+
+ {registrationError && (
+
+
+
+ {registrationRejectionSummary(registrationError)}
+
+
+ Inspector registers as a native client (application_type
+ "native") so authorization servers accept its localhost
+ redirect URI (SEP-837). If the server still rejects Dynamic Client
+ Registration, use a Client ID Metadata Document (below) or a
+ preregistered client instead.
+
+
+
+ )}
+
+ onExpandedSectionsChange(value as ClientSettingsSection[])
+ }
+ variant="separated"
+ >
+
+
+ Enterprise-Managed Authorization
+
+
+
+ patch({ emaEnabled: e.currentTarget.checked })}
+ />
+ {settings.emaEnabled && (
+ <>
+
+ Register this redirect URI with your IdP:{" "}
+ {typeof window !== "undefined"
+ ? `${window.location.origin}/oauth/callback`
+ : "http://localhost:6274/oauth/callback"}
+
+ patch({ issuer: e.currentTarget.value })}
+ onBlur={() => setIssuerTouched(true)}
+ error={showIssuerError}
+ rightSectionPointerEvents="auto"
+ rightSection={
+ settings.issuer ? (
+ patch({ issuer: "" })} />
+ ) : null
+ }
+ />
+ patch({ clientId: e.currentTarget.value })}
+ error={showClientIdError}
+ rightSectionPointerEvents="auto"
+ rightSection={
+ settings.clientId ? (
+ patch({ clientId: "" })} />
+ ) : null
+ }
+ />
+
+ patch({ clientSecret: e.currentTarget.value })
+ }
+ rightSectionPointerEvents="auto"
+ rightSection={
+ settings.clientSecret ? (
+ patch({ clientSecret: "" })}
+ />
+ ) : null
+ }
+ />
+ {settings.issuer.trim() !== "" && (
+
+
+
+ IdP sign-in
+
+ {emaIdpLoginState === "logged_in" ? (
+ <>
+
+ Signed in
+
+
+ Your enterprise IdP session is active. Connecting
+ to EMA-enabled MCP servers will not prompt for IdP
+ login until you sign out or the session expires.
+
+ >
+ ) : emaIdpLoginState === "expired" ? (
+ <>
+
+ Session expired
+
+
+ Your cached IdP session has expired. The next
+ connect to an EMA-enabled server will prompt for
+ IdP login.
+
+ >
+ ) : (
- Your cached IdP session has expired. The next
- connect to an EMA-enabled server will prompt for IdP
- login.
+ Not signed in to your enterprise IdP. Connecting to
+ an EMA-enabled MCP server will open IdP login.
- >
- ) : (
-
- Not signed in to your enterprise IdP. Connecting to an
- EMA-enabled MCP server will open IdP login.
-
- )}
-
- {showIdpSession && onEmaIdpLogout ? (
-
- ) : null}
-
- )}
- >
- )}
-
-
-
-
- OAuth Client ID Metadata Document
-
-
- patch({ cimdEnabled: e.currentTarget.checked })}
- />
- {settings.cimdEnabled && (
- <>
-
- The metadata document must be served over HTTPS and list this
- redirect URI:{" "}
- {typeof window !== "undefined"
- ? `${window.location.origin}/oauth/callback`
- : "http://localhost:6274/oauth/callback"}
-
-
- patch({ clientMetadataUrl: e.currentTarget.value })
- }
- onBlur={() => setClientMetadataUrlTouched(true)}
- error={showClientMetadataUrlError}
- rightSectionPointerEvents="auto"
- rightSection={
- settings.clientMetadataUrl ? (
- patch({ clientMetadataUrl: "" })}
- />
- ) : null
- }
- />
- >
- )}
-
-
-
-
+ )}
+
+ {showIdpSession && onEmaIdpLogout ? (
+
+ ) : null}
+
+ )}
+ >
+ )}
+
+
+
+
+
+ OAuth Client ID Metadata Document
+
+
+
+
+ CIMD is the preferred client-identity mechanism in the
+ 2026-07-28 spec. Dynamic Client Registration still works and is
+ used by default when CIMD is off, but it is now deprecated in
+ favor of CIMD (SEP-991).
+
+
+ patch({ cimdEnabled: e.currentTarget.checked })
+ }
+ />
+ {settings.cimdEnabled && (
+ <>
+
+ The metadata document must be served over HTTPS and list
+ this redirect URI:{" "}
+ {typeof window !== "undefined"
+ ? `${window.location.origin}/oauth/callback`
+ : "http://localhost:6274/oauth/callback"}
+
+
+ patch({ clientMetadataUrl: e.currentTarget.value })
+ }
+ onBlur={() => setClientMetadataUrlTouched(true)}
+ error={showClientMetadataUrlError}
+ rightSectionPointerEvents="auto"
+ rightSection={
+ settings.clientMetadataUrl ? (
+ patch({ clientMetadataUrl: "" })}
+ />
+ ) : null
+ }
+ />
+ >
+ )}
+
+
+
+
+
);
}
diff --git a/clients/web/src/components/groups/ClientSettingsForm/clientSettingsValues.ts b/clients/web/src/components/groups/ClientSettingsForm/clientSettingsValues.ts
index 5e5923688..28c967070 100644
--- a/clients/web/src/components/groups/ClientSettingsForm/clientSettingsValues.ts
+++ b/clients/web/src/components/groups/ClientSettingsForm/clientSettingsValues.ts
@@ -22,6 +22,23 @@ export interface ClientSettingsErrors {
clientMetadataUrl?: string;
}
+/**
+ * A Dynamic Client Registration rejection surfaced from the authorization
+ * server (SEP-837). Parsed from the SDK's `RegistrationRejectedError` — the
+ * RFC 7591 error JSON (`error` / `error_description`) plus the HTTP status — so
+ * the form can explain *why* registration failed (commonly a redirect-URI
+ * constraint that the `application_type: "native"` declaration is meant to
+ * satisfy) rather than showing a bare failure.
+ */
+export interface OAuthRegistrationError {
+ /** RFC 7591 `error` code (e.g. `invalid_redirect_uri`, `invalid_client_metadata`). */
+ error?: string;
+ /** RFC 7591 `error_description`, if the AS provided one. */
+ errorDescription?: string;
+ /** HTTP status returned by the registration endpoint. */
+ status?: number;
+}
+
export interface ValidateClientSettingsOptions {
/**
* Also flag required fields left blank. Off by default so inline (as-you-type)
diff --git a/clients/web/src/components/groups/NetworkEntry/NetworkEntry.test.tsx b/clients/web/src/components/groups/NetworkEntry/NetworkEntry.test.tsx
index e0c09b5af..2d971ac4b 100644
--- a/clients/web/src/components/groups/NetworkEntry/NetworkEntry.test.tsx
+++ b/clients/web/src/components/groups/NetworkEntry/NetworkEntry.test.tsx
@@ -289,4 +289,24 @@ describe("NetworkEntry", () => {
screen.queryByRole("button", { name: "Reveal secrets in body" }),
).not.toBeInTheDocument();
});
+
+ it("labels an auth-category request with its OAuth flow phase", () => {
+ const tokenRequest: FetchRequestEntry = {
+ ...baseEntry,
+ url: "https://as.example.com/oauth/token",
+ category: "auth",
+ };
+ renderWithMantine(
+ ,
+ );
+ expect(screen.getByText("Token")).toBeInTheDocument();
+ });
+
+ it("does not label transport requests or unrecognized auth URLs", () => {
+ renderWithMantine(
+ ,
+ );
+ expect(screen.queryByText("Token")).not.toBeInTheDocument();
+ expect(screen.queryByText("Discovery")).not.toBeInTheDocument();
+ });
});
diff --git a/clients/web/src/components/groups/NetworkEntry/NetworkEntry.tsx b/clients/web/src/components/groups/NetworkEntry/NetworkEntry.tsx
index f2c7f33fc..939655445 100644
--- a/clients/web/src/components/groups/NetworkEntry/NetworkEntry.tsx
+++ b/clients/web/src/components/groups/NetworkEntry/NetworkEntry.tsx
@@ -19,6 +19,10 @@ import { ExpandToggle } from "../../elements/ExpandToggle/ExpandToggle";
import { MethodBadge } from "../../elements/MethodBadge/MethodBadge";
import { CategoryBadge } from "../../elements/CategoryBadge/CategoryBadge";
import { maskSecretsInBody } from "../../../utils/maskSecrets";
+import {
+ oauthNetworkPhase,
+ oauthNetworkPhaseLabel,
+} from "../../../utils/oauthNetworkPhase";
export interface NetworkEntryProps {
entry: FetchRequestEntry;
@@ -247,6 +251,16 @@ export function NetworkEntry({
setIsExpanded(isListExpanded);
}, [isListExpanded]);
+ // OAuth flow phase for `auth`-category requests (discovery / registration /
+ // authorize / token), so the Network tab labels the auth conversation.
+ const oauthPhase =
+ entry.category === "auth" ? oauthNetworkPhase(entry.url) : undefined;
+ const phaseBadge = oauthPhase ? (
+
+ {oauthNetworkPhaseLabel(oauthPhase)}
+
+ ) : null;
+
const metaBadges = (
<>
{entry.duration != null && (
@@ -278,6 +292,7 @@ export function NetworkEntry({
+ {phaseBadge}
{metaBadges}
@@ -306,6 +321,7 @@ export function NetworkEntry({
+ {phaseBadge}
{entry.url}
diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx
index 7e514bbfa..68b8c8650 100644
--- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx
+++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx
@@ -496,6 +496,26 @@ describe("ServerSettingsForm", () => {
});
});
+ it("invokes onOAuthChange with the chosen insufficient-scope policy (SEP-2350)", async () => {
+ const user = userEvent.setup();
+ const onOAuthChange = vi.fn();
+ renderWithMantine(
+ ,
+ );
+ await user.click(
+ screen.getByRole("textbox", { name: /Insufficient-scope/i }),
+ );
+ await user.click(screen.getByText("Throw (surface the error)"));
+ expect(onOAuthChange).toHaveBeenCalledWith(
+ expect.objectContaining({ onInsufficientScope: "throw" }),
+ );
+ });
+
it("invokes onOAuthChange when enterprise-managed is toggled", async () => {
const user = userEvent.setup();
const onOAuthChange = vi.fn();
diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx
index a3fef83e6..f1869431b 100644
--- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx
+++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx
@@ -6,6 +6,7 @@ import {
Flex,
Group,
NumberInput,
+ Select,
Stack,
Text,
TextInput,
@@ -255,6 +256,7 @@ export function ServerSettingsForm({
clientSecret: settings.oauthClientSecret ?? "",
scopes: settings.oauthScopes ?? "",
enterpriseManaged: settings.enterpriseManaged ?? false,
+ onInsufficientScope: settings.oauthOnInsufficientScope,
};
}
@@ -511,6 +513,26 @@ export function ServerSettingsForm({
) : null
}
/>
+