* 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
1 changed files with 585 additions and 0 deletions
|
|
@ -472,6 +472,19 @@ export interface ServerConfig {
|
|||
resourceTemplates?: number;
|
||||
prompts?: number;
|
||||
};
|
||||
/**
|
||||
* Gate a tool's visibility in `tools/list` on a client-declared extension
|
||||
* (SEP-2133 `capabilities.extensions`). Maps extension id → tool name (the
|
||||
* tool must be among the registered presets). The named tool is registered
|
||||
* but starts disabled; on `notifications/initialized`, if the connected
|
||||
* client declared that extension, it is enabled and appears in `tools/list`.
|
||||
*
|
||||
* Demonstrates that advertised client extensions change server tool
|
||||
* registration (#1739 / #1633). Legacy stateful leg only — the modern
|
||||
* per-request leg builds a fresh server per request with no persistent
|
||||
* `oninitialized`, so a gated tool stays disabled there.
|
||||
*/
|
||||
extensionGatedTools?: Record<string, string>;
|
||||
/**
|
||||
* Whether to advertise tasks capability
|
||||
* If enabled, server will advertise tasks capability with list and cancel support
|
||||
|
|
@ -1304,9 +1317,45 @@ export function createMcpServer(config: ServerConfig): McpServer {
|
|||
wireModernTaskHandlers(mcpServer, modernTaskRuntime);
|
||||
}
|
||||
|
||||
// Extension-gated tools (#1739): start each gated tool disabled, then enable
|
||||
// it on `initialized` iff the connected client declared its extension.
|
||||
if (config.extensionGatedTools) {
|
||||
wireExtensionGatedTools(mcpServer, state, config.extensionGatedTools);
|
||||
}
|
||||
|
||||
return mcpServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gate the named tools on a client-declared extension. Each gated tool is
|
||||
* disabled up front (so it is absent from `tools/list` by default), and the
|
||||
* server's `oninitialized` hook enables it only when the connected client
|
||||
* advertised the mapped extension id in `capabilities.extensions`. Because
|
||||
* `initialized` arrives before the client's first `tools/list`, the enable
|
||||
* lands in time for the list to reflect it. Chains any existing
|
||||
* `oninitialized` so this composes with other wiring.
|
||||
*/
|
||||
function wireExtensionGatedTools(
|
||||
mcpServer: McpServer,
|
||||
state: ServerState,
|
||||
gates: Record<string, string>,
|
||||
): void {
|
||||
const entries = Object.entries(gates);
|
||||
for (const [, toolName] of entries) {
|
||||
state.registeredTools.get(toolName)?.disable();
|
||||
}
|
||||
const previous = mcpServer.server.oninitialized;
|
||||
mcpServer.server.oninitialized = () => {
|
||||
previous?.();
|
||||
const declared = mcpServer.server.getClientCapabilities()?.extensions ?? {};
|
||||
for (const [extensionId, toolName] of entries) {
|
||||
if (declared[extensionId] !== undefined) {
|
||||
state.registeredTools.get(toolName)?.enable();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Structural view of the low-level `Server`'s request-handler registry. */
|
||||
interface RawHandlerHost {
|
||||
_requestHandlers: Map<
|
||||
|
|
|
|||
Loading…
Reference in a new issue