* 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
|
|
@ -124,6 +124,7 @@ type StoredInspectorFields = Pick<
|
|||
| "taskTtl"
|
||||
| "autoRefreshOnListChanged"
|
||||
| "paginatedLists"
|
||||
| "advertisedExtensions"
|
||||
| "maxFetchRequests"
|
||||
| "oauth"
|
||||
| "roots"
|
||||
|
|
@ -191,6 +192,7 @@ export function storedFieldsToInspectorSettings(
|
|||
stored.taskTtl !== undefined ||
|
||||
stored.autoRefreshOnListChanged !== undefined ||
|
||||
stored.paginatedLists !== undefined ||
|
||||
stored.advertisedExtensions !== undefined ||
|
||||
stored.maxFetchRequests !== undefined ||
|
||||
stored.oauth !== undefined ||
|
||||
stored.roots !== undefined ||
|
||||
|
|
@ -237,6 +239,15 @@ export function storedFieldsToInspectorSettings(
|
|||
if (isModernLogLevel(stored.modernLogLevel)) {
|
||||
settings.modernLogLevel = stored.modernLogLevel;
|
||||
}
|
||||
// Optional map with no default; carried through only when the file has a
|
||||
// non-empty object. An absent/empty field reads back as unset, which the
|
||||
// write side then omits — keeping a byte-stable round-trip.
|
||||
if (
|
||||
stored.advertisedExtensions &&
|
||||
Object.keys(stored.advertisedExtensions).length > 0
|
||||
) {
|
||||
settings.advertisedExtensions = { ...stored.advertisedExtensions };
|
||||
}
|
||||
// Truthiness drops empty-string OAuth fields — mirrors the write-side
|
||||
// coercion in `validateSettings` (server.ts) so a round-trip can't
|
||||
// accidentally surface `oauthClientId: ""` to the form, where the
|
||||
|
|
@ -311,6 +322,16 @@ export function inspectorSettingsToStoredFields(
|
|||
out.paginatedLists = true;
|
||||
}
|
||||
|
||||
// Persist only when the user has toggled at least one extension override;
|
||||
// an empty map reads back as unset (above), keeping the diff minimal for the
|
||||
// common (no-override) case.
|
||||
if (
|
||||
settings.advertisedExtensions &&
|
||||
Object.keys(settings.advertisedExtensions).length > 0
|
||||
) {
|
||||
out.advertisedExtensions = { ...settings.advertisedExtensions };
|
||||
}
|
||||
|
||||
// Persist only when it differs from the default era. Absent reads back as
|
||||
// DEFAULT_PROTOCOL_ERA, so writing the default would inject the field into
|
||||
// hand-edited files that never had it and break byte-stable round-trips.
|
||||
|
|
@ -389,6 +410,7 @@ const INSPECTOR_FIELD_KEY_MAP = {
|
|||
taskTtl: true,
|
||||
autoRefreshOnListChanged: true,
|
||||
paginatedLists: true,
|
||||
advertisedExtensions: true,
|
||||
maxFetchRequests: true,
|
||||
oauth: true,
|
||||
roots: true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue