Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions clients/web/src/test/integration/mcp/inspectorClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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", () => {
Expand Down
9 changes: 7 additions & 2 deletions core/mcp/inspectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading