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
3 changed files with 7585 additions and 595 deletions
|
|
@ -53,6 +53,7 @@ const meta: Meta<typeof ServerSettingsModal> = {
|
|||
render: InteractiveRender,
|
||||
args: {
|
||||
opened: true,
|
||||
serverType: "streamable-http",
|
||||
isStdio: false,
|
||||
onClose: fn(),
|
||||
onSettingsChange: fn(),
|
||||
|
|
@ -87,6 +88,7 @@ export const EmptySettings: Story = {
|
|||
// Environment Variables section.
|
||||
export const StdioServer: Story = {
|
||||
args: {
|
||||
serverType: "stdio",
|
||||
isStdio: true,
|
||||
settings: {
|
||||
headers: [],
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened={false}
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={vi.fn()}
|
||||
|
|
@ -48,6 +49,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={vi.fn()}
|
||||
|
|
@ -57,6 +59,20 @@ describe("ServerSettingsModal", () => {
|
|||
expect(screen.getByText("Custom Headers")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the OAuth Settings section for stdio servers", () => {
|
||||
renderWithMantine(
|
||||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="stdio"
|
||||
isStdio
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("OAuth Settings")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes onClose when the CloseButton is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = vi.fn();
|
||||
|
|
@ -64,6 +80,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={onClose}
|
||||
onSettingsChange={vi.fn()}
|
||||
|
|
@ -85,6 +102,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -105,6 +123,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={initialSettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -126,6 +145,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={initialSettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -148,6 +168,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -168,6 +189,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={initialSettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -191,6 +213,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={initialSettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -212,6 +235,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -232,6 +256,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -245,6 +270,34 @@ describe("ServerSettingsModal", () => {
|
|||
expect(call.oauthClientId).toBe("a");
|
||||
});
|
||||
|
||||
it("calls onSettingsChange when toggling enterprise-managed authorization", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSettingsChange = vi.fn();
|
||||
renderWithMantine(
|
||||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "OAuth Settings" }));
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", {
|
||||
name: "Enterprise-managed authorization",
|
||||
}),
|
||||
);
|
||||
expect(onSettingsChange).toHaveBeenCalledWith({
|
||||
...emptySettings,
|
||||
oauthClientId: "",
|
||||
oauthClientSecret: "",
|
||||
oauthScopes: "",
|
||||
enterpriseManaged: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("calls onSettingsChange with a blank row when adding a root", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSettingsChange = vi.fn();
|
||||
|
|
@ -252,6 +305,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -272,6 +326,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={initialSettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -294,6 +349,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={initialSettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -315,6 +371,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={vi.fn()}
|
||||
|
|
@ -359,6 +416,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={vi.fn()}
|
||||
|
|
@ -376,6 +434,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="stdio"
|
||||
isStdio
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -401,6 +460,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={{ ...emptySettings, env: [{ key: "A", value: "1" }] }}
|
||||
serverType="stdio"
|
||||
isStdio
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -424,6 +484,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={{ ...emptySettings, env: [{ key: "A", value: "1" }] }}
|
||||
serverType="stdio"
|
||||
isStdio
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
@ -444,6 +505,7 @@ describe("ServerSettingsModal", () => {
|
|||
<ServerSettingsModal
|
||||
opened
|
||||
settings={{ ...emptySettings, cwd: "/srv" }}
|
||||
serverType="stdio"
|
||||
isStdio
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
|
|
|
|||
|
|
@ -3,24 +3,30 @@ import { CloseButton, Group, Modal, Stack, Title } from "@mantine/core";
|
|||
import type {
|
||||
InspectorServerSettings,
|
||||
OAuthSettings,
|
||||
ServerType,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { isOAuthCapableServerType } from "@inspector/core/mcp/config.js";
|
||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||
import {
|
||||
ServerSettingsForm,
|
||||
type ServerSettingsSection,
|
||||
} from "../ServerSettingsForm/ServerSettingsForm";
|
||||
|
||||
// The "environment" section only renders for stdio servers, so it joins the
|
||||
// expand/collapse-all set only then — otherwise `allExpanded` could never be
|
||||
// reached (the section isn't in the DOM to expand).
|
||||
function allSectionsFor(isStdio: boolean): ServerSettingsSection[] {
|
||||
// The "environment" section only renders for stdio servers and the "oauth"
|
||||
// section only for OAuth-capable transports, so each joins the
|
||||
// expand/collapse-all set only when present — otherwise `allExpanded` could
|
||||
// never be reached (a section not in the DOM can't be expanded).
|
||||
function allSectionsFor(
|
||||
serverType: ServerType,
|
||||
isStdio: boolean,
|
||||
): ServerSettingsSection[] {
|
||||
return [
|
||||
"options",
|
||||
...(isStdio ? (["environment"] as const) : []),
|
||||
"headers",
|
||||
"metadata",
|
||||
"timeouts",
|
||||
"oauth",
|
||||
...(isOAuthCapableServerType(serverType) ? (["oauth"] as const) : []),
|
||||
"roots",
|
||||
];
|
||||
}
|
||||
|
|
@ -28,6 +34,7 @@ function allSectionsFor(isStdio: boolean): ServerSettingsSection[] {
|
|||
export interface ServerSettingsModalProps {
|
||||
opened: boolean;
|
||||
settings: InspectorServerSettings;
|
||||
serverType: ServerType;
|
||||
/**
|
||||
* Whether the target server uses the stdio transport. Forwarded to the form
|
||||
* to gate the stdio-only Working Directory field and Environment Variables
|
||||
|
|
@ -41,10 +48,12 @@ export interface ServerSettingsModalProps {
|
|||
export function ServerSettingsModal({
|
||||
opened,
|
||||
settings,
|
||||
serverType,
|
||||
isStdio,
|
||||
onClose,
|
||||
onSettingsChange,
|
||||
}: ServerSettingsModalProps) {
|
||||
const sections = allSectionsFor(serverType, isStdio);
|
||||
// Initial expansion is the first ("options") section — where Network Log
|
||||
// Size lives, so a deep-link from the body-dropped toast lands on the
|
||||
// relevant control. The parent remounts this modal per open (via `key`), so
|
||||
|
|
@ -54,11 +63,10 @@ export function ServerSettingsModal({
|
|||
ServerSettingsSection[]
|
||||
>(["options"]);
|
||||
|
||||
const allSections = allSectionsFor(isStdio);
|
||||
const allExpanded = expandedSections.length === allSections.length;
|
||||
const allExpanded = expandedSections.length === sections.length;
|
||||
|
||||
function handleToggleAll() {
|
||||
setExpandedSections(allExpanded ? [] : allSections);
|
||||
setExpandedSections(allExpanded ? [] : sections);
|
||||
}
|
||||
|
||||
function handleAddHeader() {
|
||||
|
|
@ -139,6 +147,7 @@ export function ServerSettingsModal({
|
|||
oauthClientId: oauth.clientId,
|
||||
oauthClientSecret: oauth.clientSecret,
|
||||
oauthScopes: oauth.scopes,
|
||||
enterpriseManaged: oauth.enterpriseManaged ? true : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -195,6 +204,7 @@ export function ServerSettingsModal({
|
|||
</Group>
|
||||
<ServerSettingsForm
|
||||
settings={settings}
|
||||
serverType={serverType}
|
||||
isStdio={isStdio}
|
||||
expandedSections={expandedSections}
|
||||
onExpandedSectionsChange={setExpandedSections}
|
||||
|
|
|
|||
Loading…
Reference in a new issue