* feat(web): wire the listChanged indicator across all screens (#1402) The per-screen list-changed indicator was hardcoded false in InspectorView for Tools, Apps, Prompts, and Resources, so it could never light up. Source it from the managed-state layer instead. Each ManagedXState now tracks a listChanged flag: set true when its */list_changed notification arrives (the existing auto-refresh leaves it set), exposed via getListChanged()/a listChangedChange event, and cleared by clearListChanged() or on disconnect. The useManagedX hooks surface the flag and clear it on a user-initiated refresh (the indicator's Refresh button = acknowledge). App threads the three flags to InspectorView, which maps them to the screens — Apps shares the tools flag since apps are a filtered view of tools. Tests cover set/clear/disconnect at the state-manager layer, the hook subscription + refresh-clears behavior, and the InspectorView routing (including Apps sharing the tools flag and no cross-wiring between screens). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): close clear-after-await race in managed-list refresh (#1402 review) The useManagedX refresh() callbacks cleared the listChanged flag AFTER awaiting the fetch. If a */list_changed arrived mid-fetch, the state re-set the flag (and auto-refreshed), then the trailing clear wiped that genuinely-new signal. Clear BEFORE the await instead: the user's refresh acknowledges only the change in hand, and a mid-fetch change re-sets the flag and survives. Adds a race regression test on the tools hook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): drop duplicate Refresh on the Apps screen (#1402) AppControls uniquely had a standalone toolbar Refresh button AND the ListChangedIndicator on a separate row, so once the indicator could actually appear (this PR), the Apps screen showed two Refresh buttons. Match the other three controls (Tools/Prompts/Resources): the indicator is the single header-right refresh affordance. Updates AppControls and AppsScreen tests to assert exactly one Refresh (and none when the list hasn't changed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core): stop auto-invoking */list on list_changed; user pulls via Refresh (#1402) With auto-refresh on, the list was already current by the time the list-changed indicator appeared, making its Refresh button moot. Now the managed Tools/Prompts/Resources state managers only flag the change on */list_changed (they no longer auto-refresh); the list stays as-is until the user clicks Refresh, which fetches the new list and clears the indicator. The connect-time initial load is unchanged. Resource templates (no own indicator, part of the Resources screen) also stop auto-refreshing; onRefreshResources now refreshes both resources and templates, since a single resources/list_changed covers both. Tasks are left auto-refreshing — they have no list-changed indicator and need live status updates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): correct ManagedResourceTemplatesState header comments (#1402 review) The class no longer subscribes to resourceTemplatesListChanged; update the stale file/class docstrings that still claimed it syncs on that notification to reflect connect + statusChange only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): add per-server 'Auto Refresh on List Changed Notifications' option (#1402) New Options section in ServerSettingsForm with a single checkbox. When checked, the managed Tools/Prompts/Resources (and resource templates) state auto-refreshes on */list_changed notifications; when unchecked (default) the notification only lights the list-changed indicator and the user pulls via Refresh. Plumbing: new optional autoRefreshOnListChanged on InspectorServerSettings and StoredMCPServer, round-tripped through serverList (lifted with a false default, persisted only when true, added to the exhaustive field-key map); surfaced from InspectorClient via a new getServerSettings() on the protocol; the state managers read it live at notification time to decide auto-refresh vs flag-only. Resource templates re-subscribe to their list_changed but only auto-refresh when the option is on (they have no indicator of their own). Tests cover the round-trip (true persists, false omitted), the form section (render/checked/toggle), and each state manager's on/off branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): persist autoRefreshOnListChanged through validateSettings (#1402) The PUT /api/servers write path runs settings through validateSettings, which rebuilds the value from explicitly-named fields and silently drops unknown keys — so autoRefreshOnListChanged never reached disk (the serverList round-trip test passed because it calls inspectorSettingsToStoredFields directly, bypassing this path). Validate it as an optional boolean and include it in the built value; false is omitted on disk by inspectorSettingsToStoredFields as before. Adds integration tests over the real PUT path: true persists, false is omitted, and a non-boolean is rejected (400). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): default the Server Settings modal to the Options section (#1402) Open 'Options' instead of 'Custom Headers' when the Server Settings modal first opens, so the new Auto Refresh toggle is the first thing surfaced. Updates the modal default-expanded section, the modal test's initial-state assertions, the three header tests (now expand 'Custom Headers' first since it's no longer open by default), and the story defaults. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop accidentally-committed Inspector history dump; gitignore the class 83076441 swept an exported Inspector message-history JSON (manual-testing artifact at the repo root) into the tree via git add -A. Untrack it and extend the existing repo-root debug-artifact .gitignore block (alongside /*.png) to cover /inspector-history-*.json so these can't be re-added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
5 KiB
TypeScript
149 lines
5 KiB
TypeScript
/**
|
|
* ManagedResourcesState: holds full resource list, syncs on resourcesListChanged.
|
|
*
|
|
* Ported from v1.5/main. v2 substitutes `InspectorClientProtocol` for the
|
|
* concrete `InspectorClient` since the runtime class is not yet ported.
|
|
*/
|
|
|
|
import type { InspectorClientProtocol } from "../inspectorClientProtocol.js";
|
|
import type { Resource } from "@modelcontextprotocol/sdk/types.js";
|
|
import { TypedEventTarget } from "../typedEventTarget.js";
|
|
|
|
const MAX_PAGES = 100;
|
|
|
|
export interface ManagedResourcesStateEventMap {
|
|
resourcesChange: Resource[];
|
|
/**
|
|
* Fires when the "list changed since last refresh" flag flips. True when a
|
|
* `resources/list_changed` notification arrives, false once the user
|
|
* refreshes or the connection drops. Drives the list-changed indicator
|
|
* (#1402).
|
|
*/
|
|
listChangedChange: boolean;
|
|
}
|
|
|
|
/**
|
|
* State manager that keeps a full resource list in sync with the server.
|
|
* Subscribes to connect, resourcesListChanged, and statusChange; fetches all
|
|
* pages on refresh.
|
|
*/
|
|
export class ManagedResourcesState extends TypedEventTarget<ManagedResourcesStateEventMap> {
|
|
private resources: Resource[] = [];
|
|
private client: InspectorClientProtocol | null = null;
|
|
private unsubscribe: (() => void) | null = null;
|
|
private _metadata: Record<string, string> | undefined = undefined;
|
|
private listChanged = false;
|
|
|
|
constructor(client: InspectorClientProtocol) {
|
|
super();
|
|
this.client = client;
|
|
const onConnect = (): void => {
|
|
void this.refresh();
|
|
};
|
|
const onResourcesListChanged = (): void => {
|
|
// When the server opts into auto-refresh (per-server setting), pull the
|
|
// new list immediately. Otherwise just flag the change for the indicator
|
|
// and let the user pull via Refresh, which is what makes the indicator
|
|
// meaningful (#1402).
|
|
if (this.client?.getServerSettings()?.autoRefreshOnListChanged) {
|
|
void this.refresh();
|
|
} else {
|
|
this.setListChanged(true);
|
|
}
|
|
};
|
|
const onStatusChange = (): void => {
|
|
if (this.client?.getStatus() === "disconnected") {
|
|
this.resources = [];
|
|
this.dispatchTypedEvent("resourcesChange", []);
|
|
this.setListChanged(false);
|
|
}
|
|
};
|
|
this.client.addEventListener("connect", onConnect);
|
|
this.client.addEventListener(
|
|
"resourcesListChanged",
|
|
onResourcesListChanged,
|
|
);
|
|
this.client.addEventListener("statusChange", onStatusChange);
|
|
this.unsubscribe = () => {
|
|
if (this.client) {
|
|
this.client.removeEventListener("connect", onConnect);
|
|
this.client.removeEventListener(
|
|
"resourcesListChanged",
|
|
onResourcesListChanged,
|
|
);
|
|
this.client.removeEventListener("statusChange", onStatusChange);
|
|
}
|
|
this.client = null;
|
|
};
|
|
}
|
|
|
|
getResources(): Resource[] {
|
|
return [...this.resources];
|
|
}
|
|
|
|
/** Whether a `resources/list_changed` arrived since the last user refresh. */
|
|
getListChanged(): boolean {
|
|
return this.listChanged;
|
|
}
|
|
|
|
/**
|
|
* Clear the list-changed flag — called when the user refreshes the list
|
|
* (the auto-refresh on the notification leaves it set so the indicator
|
|
* stays visible until acknowledged).
|
|
*/
|
|
clearListChanged(): void {
|
|
this.setListChanged(false);
|
|
}
|
|
|
|
private setListChanged(value: boolean): void {
|
|
if (this.listChanged === value) return;
|
|
this.listChanged = value;
|
|
this.dispatchTypedEvent("listChangedChange", value);
|
|
}
|
|
|
|
setMetadata(metadata?: Record<string, string>): void {
|
|
this._metadata = metadata;
|
|
}
|
|
|
|
async refresh(metadata?: Record<string, string>): Promise<Resource[]> {
|
|
const client = this.client;
|
|
if (!client || client.getStatus() !== "connected") {
|
|
return this.getResources();
|
|
}
|
|
// Gate on the server's `resources` capability — calling resources/list
|
|
// against a server that doesn't advertise it returns -32601 "Method not
|
|
// found", which then surfaces in the console for every connect against a
|
|
// resources-less server. Empty list is the right semantics for "this
|
|
// server doesn't support resources."
|
|
if (!client.getCapabilities()?.resources) {
|
|
this.resources = [];
|
|
this.dispatchTypedEvent("resourcesChange", this.resources);
|
|
return this.getResources();
|
|
}
|
|
const effectiveMetadata = metadata ?? this._metadata;
|
|
this.resources = [];
|
|
let cursor: string | undefined;
|
|
let pageCount = 0;
|
|
do {
|
|
const result = await client.listResources(cursor, effectiveMetadata);
|
|
this.resources = cursor
|
|
? [...this.resources, ...result.resources]
|
|
: result.resources;
|
|
cursor = result.nextCursor;
|
|
pageCount++;
|
|
if (pageCount >= MAX_PAGES) {
|
|
throw new Error(
|
|
`Maximum pagination limit (${MAX_PAGES} pages) reached while listing resources`,
|
|
);
|
|
}
|
|
} while (cursor);
|
|
this.dispatchTypedEvent("resourcesChange", this.resources);
|
|
return this.getResources();
|
|
}
|
|
|
|
destroy(): void {
|
|
this.unsubscribe?.();
|
|
this.unsubscribe = null;
|
|
this.resources = [];
|
|
}
|
|
}
|