fix(state): dispatch connect event after capabilities are populated (#1405)
* fix(state): dispatch connect event after capabilities are populated The list-state managers (tools/prompts/resources/templates) refresh on the "connect" event and, since #1395, gate their list RPC on getCapabilities(). But connect() dispatched "connect" before fetchServerInfo() populated this.capabilities, and refresh() runs synchronously up to its first await — so the capability gate executed during the synchronous dispatch, read undefined capabilities (reset to undefined on every disconnect), tripped, and wiped every list to empty on connect. Tools, prompts, resources, and resource templates all vanished against any server. Move the "connect" dispatch to after the awaited fetchServerInfo() so capabilities are in place before any manager refreshes. The unit tests missed this because their FakeInspectorClient has capabilities populated from construction; added an integration regression test wiring a real ManagedToolsState on connect and asserting the tool list is non-empty (fails without the fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(integration): harden connect-race regression test per review Address the two non-blocking suggestions from the PR review: - Replace the 100ms settle sleep with waitForEvent() on the managers' change events, so the test is deterministic against slow CI instead of racing a fixed timeout. - Assert a second affected primitive (ManagedResourcesState alongside ManagedToolsState). The fix is a single shared line, but covering two distinct capabilities guards against a future change gating one primitive differently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8b4375584a
commit
440b194f64
2 changed files with 44 additions and 2 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
PagedPromptsState,
|
||||
ManagedResourcesState,
|
||||
ManagedPromptsState,
|
||||
ManagedToolsState,
|
||||
} from "@inspector/core/mcp/state/index.js";
|
||||
import { createTransportNode } from "@inspector/core/mcp/node/transport.js";
|
||||
import { SamplingCreateMessage } from "@inspector/core/mcp/samplingCreateMessage.js";
|
||||
|
|
@ -687,6 +688,42 @@ describe("InspectorClient", () => {
|
|||
// Client no longer stores tools; listTools() still returns server tools when called
|
||||
expect((await client.listTools()).tools.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("managed list states populate on connect (regression: capability gate must see capabilities before the connect event)", async () => {
|
||||
// Regression for #1395 + connect-ordering: the managed list-state managers
|
||||
// refresh on the "connect" event and gate their list RPC on
|
||||
// getCapabilities(). If "connect" is dispatched before fetchServerInfo()
|
||||
// populates capabilities, the synchronous gate reads undefined and wipes
|
||||
// the list to empty — tools/prompts/resources all vanish on every connect.
|
||||
// The single fix is shared by all the managed states, so we cover tools
|
||||
// and resources (two distinct capabilities) to guard against a future
|
||||
// change gating only one primitive differently.
|
||||
client = new InspectorClient(
|
||||
{
|
||||
type: "stdio",
|
||||
command: serverCommand.command,
|
||||
args: serverCommand.args,
|
||||
},
|
||||
{ environment: { transport: createTransportNode } },
|
||||
);
|
||||
const toolsState = new ManagedToolsState(client);
|
||||
const resourcesState = new ManagedResourcesState(client);
|
||||
|
||||
// Await the change events the connect-triggered refresh() emits rather than
|
||||
// a fixed sleep — refresh() is async past its synchronous capability gate.
|
||||
const toolsChanged = waitForEvent(toolsState, "toolsChange");
|
||||
const resourcesChanged = waitForEvent(resourcesState, "resourcesChange");
|
||||
await client.connect();
|
||||
await Promise.all([toolsChanged, resourcesChanged]);
|
||||
|
||||
expect(client.getCapabilities()?.tools).toBeDefined();
|
||||
expect(toolsState.getTools().length).toBeGreaterThan(0);
|
||||
expect(client.getCapabilities()?.resources).toBeDefined();
|
||||
expect(resourcesState.getResources().length).toBeGreaterThan(0);
|
||||
|
||||
toolsState.destroy();
|
||||
resourcesState.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tool Methods", () => {
|
||||
|
|
|
|||
|
|
@ -656,11 +656,16 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
}
|
||||
this.status = "connected";
|
||||
this.dispatchTypedEvent("statusChange", this.status);
|
||||
this.dispatchTypedEvent("connect");
|
||||
|
||||
// Always fetch server info (capabilities, serverInfo, instructions) - this is just cached data from initialize
|
||||
// Always fetch server info (capabilities, serverInfo, instructions) - this is just cached data from initialize.
|
||||
// Must run BEFORE the "connect" event: the managed list-state managers
|
||||
// refresh on "connect" and gate their list RPC on getCapabilities() (see
|
||||
// #1395). If "connect" fired first, that gate would read undefined
|
||||
// capabilities and wipe tools/prompts/resources to empty on every connect.
|
||||
await this.fetchServerInfo();
|
||||
|
||||
this.dispatchTypedEvent("connect");
|
||||
|
||||
// Set initial logging level if configured and server supports it
|
||||
if (this.initialLoggingLevel && this.capabilities?.logging) {
|
||||
await this.client.setLoggingLevel(
|
||||
|
|
|
|||
Loading…
Reference in a new issue