Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ function InteractiveRender(args: ServerSettingsFormProps) {
settings: { ...args.settings, suppressNotificationStream: value },
});
}}
onCookieJarChange={(value) => {
args.onCookieJarChange(value);
updateArgs({ settings: { ...args.settings, cookieJar: value } });
}}
onAdvertisedExtensionChange={(key, checked) => {
args.onAdvertisedExtensionChange(key, checked);
updateArgs({
Expand Down Expand Up @@ -189,6 +193,7 @@ const meta: Meta<typeof ServerSettingsForm> = {
onAutoRefreshChange: fn(),
onPaginatedListsChange: fn(),
onSuppressNotificationStreamChange: fn(),
onCookieJarChange: fn(),
onAdvertisedExtensionChange: fn(),
onMaxFetchRequestsChange: fn(),
onSkillCatalogLimitChange: fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const baseHandlers = {
onAutoRefreshChange: vi.fn(),
onPaginatedListsChange: vi.fn(),
onSuppressNotificationStreamChange: vi.fn(),
onCookieJarChange: vi.fn(),
onAdvertisedExtensionChange: vi.fn(),
onMaxFetchRequestsChange: vi.fn(),
onSkillCatalogLimitChange: vi.fn(),
Expand Down Expand Up @@ -534,6 +535,69 @@ describe("ServerSettingsForm", () => {
);
});

describe("HTTP Cookie Jar (#2936)", () => {
const name = /Enable HTTP Cookie Jar/;

it("is unchecked by default and reflects an explicit true", () => {
const { rerender } = renderWithMantine(
<ServerSettingsForm
{...baseHandlers}
settings={emptySettings}
expandedSections={["options"]}
/>,
);
expect(screen.getByRole("checkbox", { name })).not.toBeChecked();
rerender(
<ServerSettingsForm
{...baseHandlers}
settings={{ ...emptySettings, cookieJar: true }}
expandedSections={["options"]}
/>,
);
expect(screen.getByRole("checkbox", { name })).toBeChecked();
});

it("invokes onCookieJarChange when toggled", async () => {
const user = userEvent.setup();
const onCookieJarChange = vi.fn();
renderWithMantine(
<ServerSettingsForm
{...baseHandlers}
onCookieJarChange={onCookieJarChange}
settings={emptySettings}
expandedSections={["options"]}
/>,
);
await user.click(screen.getByRole("checkbox", { name }));
expect(onCookieJarChange).toHaveBeenCalledWith(true);
});

it("is shown for an sse server", () => {
renderWithMantine(
<ServerSettingsForm
{...baseHandlers}
serverType="sse"
settings={emptySettings}
expandedSections={["options"]}
/>,
);
expect(screen.getByRole("checkbox", { name })).toBeInTheDocument();
});

it("is hidden for a stdio server, which has no HTTP cookies", () => {
renderWithMantine(
<ServerSettingsForm
{...baseHandlers}
serverType="stdio"
isStdio
settings={emptySettings}
expandedSections={["options"]}
/>,
);
expect(screen.queryByRole("checkbox", { name })).not.toBeInTheDocument();
});
});

it("renders the Advertised Extensions section with Tasks checked by default", () => {
renderWithMantine(
<ServerSettingsForm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export interface ServerSettingsFormProps {
onPaginatedListsChange: (value: boolean) => void;
/** Toggle the standalone `GET` notification stream suppression (#2317). */
onSuppressNotificationStreamChange: (value: boolean) => void;
/** Toggle the connection-scoped HTTP cookie jar (#2936). */
onCookieJarChange: (value: boolean) => void;
/**
* Toggle whether the Inspector advertises the extension `key` to this server.
* `checked` is the new advertise state; the modal folds it into
Expand Down Expand Up @@ -506,6 +508,7 @@ export function ServerSettingsForm({
onAutoRefreshChange,
onPaginatedListsChange,
onSuppressNotificationStreamChange,
onCookieJarChange,
onAdvertisedExtensionChange,
rendersApps = true,
onMaxFetchRequestsChange,
Expand Down Expand Up @@ -564,6 +567,9 @@ export function ServerSettingsForm({
// no-op there (#2317). `auto` keeps it, since it may resolve to legacy.
const showSuppressNotificationStream =
serverType === "streamable-http" && configuredEra !== "modern";
// Cookies only exist on the HTTP transports; stdio has no use for a jar.
const showCookieJar =
serverType === "streamable-http" || serverType === "sse";
const showModernLogLevel =
configuredEra === "modern" ||
(configuredEra === "auto" && negotiatedEra !== "legacy");
Expand Down Expand Up @@ -780,6 +786,14 @@ export function ServerSettingsForm({
}
/>
) : null}
{showCookieJar ? (
<Checkbox
label="Enable HTTP Cookie Jar (sticky sessions)"
description="When checked, cookies the server sets are stored for this connection and sent back on later requests to the same origin. Use it for a server running as several replicas behind a load balancer that routes by cookie affinity, where requests after initialize otherwise land on a replica without the session and fail with 404 Session not found. Cookies are kept in memory for one connection only, never saved, and never sent to another origin. Takes effect on the next connect."
checked={settings.cookieJar ?? false}
onChange={(e) => onCookieJarChange(e.currentTarget.checked)}
/>
) : null}
<LogSizeInput
label="Network Log Size"
description="Maximum number of HTTP requests kept in the Network log for this server. Older entries rotate out past this limit; a response body that arrives after its entry rotated out is dropped. Use 0 for unlimited (not recommended). Applies immediately to the active connection."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,42 @@ describe("ServerSettingsModal", () => {
);
});

// #2936 — omit-when-off, like the stream suppression box.
it("maps the cookie jar into settings, and back to unset", async () => {
const user = userEvent.setup();
const onSettingsChange = vi.fn();
const { rerender } = renderWithMantine(
<ServerSettingsModal
opened
settings={emptySettings}
serverType="streamable-http"
isStdio={false}
onClose={vi.fn()}
onSettingsChange={onSettingsChange}
/>,
);
const name = /Enable HTTP Cookie Jar/;
await user.click(screen.getByRole("checkbox", { name }));
expect(onSettingsChange).toHaveBeenLastCalledWith(
expect.objectContaining({ cookieJar: true }),
);

rerender(
<ServerSettingsModal
opened
settings={{ ...emptySettings, cookieJar: true }}
serverType="streamable-http"
isStdio={false}
onClose={vi.fn()}
onSettingsChange={onSettingsChange}
/>,
);
await user.click(screen.getByRole("checkbox", { name }));
expect(onSettingsChange).toHaveBeenLastCalledWith(
expect.objectContaining({ cookieJar: undefined }),
);
});

// #2144 — same omit-the-default shape as the refresh-token pair above.
it("maps the revoke-on-clear opt-out into settings, and back to unset", async () => {
const user = userEvent.setup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ export function ServerSettingsModal({
});
}

function handleCookieJarChange(value: boolean) {
// Omit-when-off, like the stream suppression box above (#2936).
onSettingsChange({ ...settings, cookieJar: value ? true : undefined });
}

function handleAdvertisedExtensionChange(key: string, checked: boolean) {
const next = { ...settings.advertisedExtensions };
const ext = ADVERTISABLE_EXTENSIONS.find((e) => e.key === key);
Expand Down Expand Up @@ -333,6 +338,7 @@ export function ServerSettingsModal({
onSuppressNotificationStreamChange={
handleSuppressNotificationStreamChange
}
onCookieJarChange={handleCookieJarChange}
onAdvertisedExtensionChange={handleAdvertisedExtensionChange}
rendersApps={rendersApps}
onMaxFetchRequestsChange={handleMaxFetchRequestsChange}
Expand Down
Loading
Loading