* Extensions: advertised-extensions toggle in Server Settings (Phase 2 of #1633) Adds the debugging knob from #1633: users choose which extensions the Inspector advertises to a server, and a server may register different tools in response. Builds on Phase 1's registry + advertisedExtensions option (#1738). Settings + persistence (core): - InspectorServerSettings.advertisedExtensions + StoredMCPServer field, wired through serverList's read/write converters and INSPECTOR_FIELD_KEY_MAP (omit-when-empty, byte-stable round-trip). - validateSettings (remote PUT route): isBooleanRecord guard + carry a non-empty map; unknown/malformed rejected. - App.tsx threads savedSettings.advertisedExtensions into the client options. UI: - ServerSettingsForm renders an "Advertised Extensions" group of checkboxes driven by ADVERTISABLE_EXTENSIONS; a per-server override wins over the registry default. Modal folds a toggle into settings.advertisedExtensions. Test server + proof: - composable-test-server extensionGatedTools: a tool registered disabled and enabled on `initialized` iff the client declared the mapped extension (getClientCapabilities). Legacy stateful leg. Threaded through load-config/resolve-config; advertised-extensions-http.json showcase + README. - Integration test connects legacy with/without the tasks extension advertised and asserts the gated tool appears/disappears in tools/list — the acceptance criterion, proven end to end, not asserted. Tests: serverList round-trip (non-empty/absent/empty), ServerSettingsForm toggle rows, ServerSettingsModal fold, validateSettings accept/reject, and the integration gating test. npm run ci green (per-file ≥90 gate held). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Extensions: add #1739 proof screenshots (advertised-extensions toggle) End-to-end smoke test in the web client against the legacy advertised-extensions-http.json test server (port 3220): connecting with the Tasks extension advertised (default) lists echo + get_weather, unchecking the Advertised Extensions → Tasks toggle and reconnecting lists only echo — the server's tool registration changing on the client-declared extension. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * Extensions: reconverge advertisedExtensions to no-override on default (review) Address Phase 2 review observation #1: toggling an extension back to its registry default now deletes the override key (and drops the whole map to undefined when it empties), so the on-disk `advertisedExtensions` reconverges to the byte-stable "no override" state instead of persisting a redundant `{ key: <default> }`. Matches the omit-when-default policy the other settings use. Only a value that differs from the default is persisted. Adds a modal test asserting the reconvergence (re-checking the sole disabling override drops advertisedExtensions to undefined). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f80958d3f
commit
fe295396af
2 changed files with 585 additions and 0 deletions
|
|
@ -162,6 +162,63 @@ describe("ServerSettingsModal", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("folds an advertised-extension toggle into settings.advertisedExtensions (#1739)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSettingsChange = vi.fn();
|
||||
renderWithMantine(
|
||||
<ServerSettingsModal
|
||||
opened
|
||||
settings={emptySettings}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
/>,
|
||||
);
|
||||
// The Options section is expanded by default; Tasks is checked (registry
|
||||
// default), so clicking it toggles the override off.
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", {
|
||||
name: /Tasks \(io\.modelcontextprotocol\/tasks\)/,
|
||||
}),
|
||||
);
|
||||
expect(onSettingsChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
advertisedExtensions: { "io.modelcontextprotocol/tasks": false },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reconverges to no override when an extension toggle returns to its default (#1739)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSettingsChange = vi.fn();
|
||||
renderWithMantine(
|
||||
<ServerSettingsModal
|
||||
opened
|
||||
// Start from a disabling override so re-checking Tasks returns it to the
|
||||
// registry default (advertised) and should drop the key entirely.
|
||||
settings={{
|
||||
...emptySettings,
|
||||
advertisedExtensions: { "io.modelcontextprotocol/tasks": false },
|
||||
}}
|
||||
serverType="streamable-http"
|
||||
isStdio={false}
|
||||
onClose={vi.fn()}
|
||||
onSettingsChange={onSettingsChange}
|
||||
/>,
|
||||
);
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", {
|
||||
name: /Tasks \(io\.modelcontextprotocol\/tasks\)/,
|
||||
}),
|
||||
);
|
||||
// The only override returned to its default → the whole map drops to
|
||||
// undefined (no-override), keeping the on-disk round-trip minimal.
|
||||
expect(onSettingsChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ advertisedExtensions: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("hides the modern log-level control when this server negotiated legacy under 'auto' (#1629)", () => {
|
||||
renderWithMantine(
|
||||
<ServerSettingsModal
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
ServerType,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { isOAuthCapableServerType } from "@inspector/core/mcp/config.js";
|
||||
import { ADVERTISABLE_EXTENSIONS } from "@inspector/core/mcp/extensions.js";
|
||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||
import {
|
||||
ServerSettingsForm,
|
||||
|
|
@ -176,6 +177,24 @@ export function ServerSettingsModal({
|
|||
onSettingsChange({ ...settings, paginatedLists: value });
|
||||
}
|
||||
|
||||
function handleAdvertisedExtensionChange(key: string, checked: boolean) {
|
||||
const next = { ...settings.advertisedExtensions };
|
||||
const ext = ADVERTISABLE_EXTENSIONS.find((e) => e.key === key);
|
||||
// Reconverge to "no override" when the toggle returns to the registry
|
||||
// default, so the on-disk map (and its byte-stable round-trip) stays minimal
|
||||
// — matching the omit-when-default policy used for the other settings. Only
|
||||
// a value that actually differs from the default is persisted.
|
||||
if (ext && checked === ext.defaultAdvertised) {
|
||||
delete next[key];
|
||||
} else {
|
||||
next[key] = checked;
|
||||
}
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
advertisedExtensions: Object.keys(next).length > 0 ? next : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function handleMaxFetchRequestsChange(value: number) {
|
||||
onSettingsChange({ ...settings, maxFetchRequests: value });
|
||||
}
|
||||
|
|
@ -251,6 +270,7 @@ export function ServerSettingsModal({
|
|||
onTimeoutChange={handleTimeoutChange}
|
||||
onAutoRefreshChange={handleAutoRefreshChange}
|
||||
onPaginatedListsChange={handlePaginatedListsChange}
|
||||
onAdvertisedExtensionChange={handleAdvertisedExtensionChange}
|
||||
onMaxFetchRequestsChange={handleMaxFetchRequestsChange}
|
||||
onProtocolEraChange={handleProtocolEraChange}
|
||||
onModernLogLevelChange={handleModernLogLevelChange}
|
||||
|
|
|
|||
Loading…
Reference in a new issue