Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
This commit is contained in:
parent
5ee347571f
commit
5de4e911e3
2 changed files with 7585 additions and 595 deletions
|
|
@ -492,9 +492,47 @@ describe("ServerSettingsForm", () => {
|
|||
clientId: "a",
|
||||
clientSecret: "",
|
||||
scopes: "",
|
||||
enterpriseManaged: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("invokes onOAuthChange when enterprise-managed is toggled", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOAuthChange = vi.fn();
|
||||
renderWithMantine(
|
||||
<ServerSettingsForm
|
||||
{...baseHandlers}
|
||||
onOAuthChange={onOAuthChange}
|
||||
settings={emptySettings}
|
||||
expandedSections={["oauth"]}
|
||||
/>,
|
||||
);
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", {
|
||||
name: /Enterprise-managed authorization/i,
|
||||
}),
|
||||
);
|
||||
expect(onOAuthChange).toHaveBeenCalledWith({
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
scopes: "",
|
||||
enterpriseManaged: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("hides the OAuth Settings section for stdio servers", () => {
|
||||
renderWithMantine(
|
||||
<ServerSettingsForm
|
||||
{...baseHandlers}
|
||||
settings={emptySettings}
|
||||
serverType="stdio"
|
||||
expandedSections={[]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("OAuth Settings")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Client ID")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes onOAuthChange when typing in scopes (uses existing oauth values)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOAuthChange = vi.fn();
|
||||
|
|
@ -531,6 +569,7 @@ describe("ServerSettingsForm", () => {
|
|||
clientId: "",
|
||||
clientSecret: "z",
|
||||
scopes: "",
|
||||
enterpriseManaged: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import { ClearButton } from "../../elements/ClearButton/ClearButton";
|
|||
import type {
|
||||
InspectorServerSettings,
|
||||
OAuthSettings,
|
||||
ServerType,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { isOAuthCapableServerType } from "@inspector/core/mcp/config.js";
|
||||
import type { Root } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
export type ServerSettingsSection =
|
||||
|
|
@ -27,6 +29,8 @@ export type ServerSettingsSection =
|
|||
|
||||
export interface ServerSettingsFormProps {
|
||||
settings: InspectorServerSettings;
|
||||
/** Transport type — EMA checkbox is hidden for stdio. Defaults to streamable-http. */
|
||||
serverType?: ServerType;
|
||||
/**
|
||||
* Whether the target server uses the stdio transport. Gates the Working
|
||||
* Directory field and Environment Variables section in the Options area —
|
||||
|
|
@ -176,6 +180,7 @@ function RootRows({
|
|||
|
||||
export function ServerSettingsForm({
|
||||
settings,
|
||||
serverType = "streamable-http",
|
||||
isStdio,
|
||||
expandedSections,
|
||||
onExpandedSectionsChange,
|
||||
|
|
@ -197,6 +202,7 @@ export function ServerSettingsForm({
|
|||
onRemoveRoot,
|
||||
onRootChange,
|
||||
}: ServerSettingsFormProps) {
|
||||
const oauthCapable = isOAuthCapableServerType(serverType);
|
||||
const handleMaxFetchRequestsChange = (value: number | string) => {
|
||||
if (typeof value === "number") {
|
||||
onMaxFetchRequestsChange(value);
|
||||
|
|
@ -225,6 +231,7 @@ export function ServerSettingsForm({
|
|||
clientId: settings.oauthClientId ?? "",
|
||||
clientSecret: settings.oauthClientSecret ?? "",
|
||||
scopes: settings.oauthScopes ?? "",
|
||||
enterpriseManaged: settings.enterpriseManaged ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -400,78 +407,91 @@ export function ServerSettingsForm({
|
|||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item value="oauth">
|
||||
<Accordion.Control>OAuth Settings</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack gap="md">
|
||||
<HintText>
|
||||
Pre-configure OAuth credentials for servers requiring
|
||||
authentication
|
||||
</HintText>
|
||||
<TextInput
|
||||
label="Client ID"
|
||||
value={settings.oauthClientId ?? ""}
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
clientId: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
settings.oauthClientId ? (
|
||||
<ClearButton
|
||||
onClick={() =>
|
||||
onOAuthChange({ ...currentOAuth(), clientId: "" })
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Client Secret"
|
||||
value={settings.oauthClientSecret ?? ""}
|
||||
type="password"
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
clientSecret: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
settings.oauthClientSecret ? (
|
||||
<ClearButton
|
||||
onClick={() =>
|
||||
onOAuthChange({ ...currentOAuth(), clientSecret: "" })
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Scopes"
|
||||
value={settings.oauthScopes ?? ""}
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
scopes: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
settings.oauthScopes ? (
|
||||
<ClearButton
|
||||
onClick={() =>
|
||||
onOAuthChange({ ...currentOAuth(), scopes: "" })
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
{oauthCapable ? (
|
||||
<Accordion.Item value="oauth">
|
||||
<Accordion.Control>OAuth Settings</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack gap="md">
|
||||
<Checkbox
|
||||
label="Enterprise-managed authorization"
|
||||
description="Connect via the configured enterprise IdP instead of interactive OAuth to the MCP authorization server. OAuth fields below are resource authorization server credentials."
|
||||
checked={settings.enterpriseManaged ?? false}
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
enterpriseManaged: e.currentTarget.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<HintText>
|
||||
Pre-configure OAuth credentials for servers requiring
|
||||
authentication
|
||||
</HintText>
|
||||
<TextInput
|
||||
label="Client ID"
|
||||
value={settings.oauthClientId ?? ""}
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
clientId: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
settings.oauthClientId ? (
|
||||
<ClearButton
|
||||
onClick={() =>
|
||||
onOAuthChange({ ...currentOAuth(), clientId: "" })
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Client Secret"
|
||||
value={settings.oauthClientSecret ?? ""}
|
||||
type="password"
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
clientSecret: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
settings.oauthClientSecret ? (
|
||||
<ClearButton
|
||||
onClick={() =>
|
||||
onOAuthChange({ ...currentOAuth(), clientSecret: "" })
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Scopes"
|
||||
value={settings.oauthScopes ?? ""}
|
||||
onChange={(e) =>
|
||||
onOAuthChange({
|
||||
...currentOAuth(),
|
||||
scopes: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
settings.oauthScopes ? (
|
||||
<ClearButton
|
||||
onClick={() =>
|
||||
onOAuthChange({ ...currentOAuth(), scopes: "" })
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
) : null}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue