diff --git a/clients/web/src/test/core/mcp/state/managedPromptsState.test.ts b/clients/web/src/test/core/mcp/state/managedPromptsState.test.ts index c7ef3e96e..21166356d 100644 --- a/clients/web/src/test/core/mcp/state/managedPromptsState.test.ts +++ b/clients/web/src/test/core/mcp/state/managedPromptsState.test.ts @@ -20,7 +20,10 @@ describe("ManagedPromptsState", () => { let state: ManagedPromptsState; beforeEach(() => { - client = new FakeInspectorClient(); + // Default to a server that advertises `prompts` so the existing flow tests + // exercise the live `listPrompts` path; capability-absent tests below + // override this. + client = new FakeInspectorClient({ capabilities: { prompts: {} } }); state = new ManagedPromptsState(client); }); @@ -40,6 +43,35 @@ describe("ManagedPromptsState", () => { expect(client.listPrompts).not.toHaveBeenCalled(); }); + it("refresh skips listPrompts when the server doesn't advertise prompts capability", async () => { + // Regression (#1350): a prompts-less server replied to prompts/list with + // -32601 "Method not found", surfacing in the console on every connect. + const promptless = new FakeInspectorClient({ + capabilities: { tools: {}, resources: {} }, + }); + promptless.setStatus("connected"); + const promptlessState = new ManagedPromptsState(promptless); + + const result = await promptlessState.refresh(); + expect(result).toEqual([]); + expect(promptless.listPrompts).not.toHaveBeenCalled(); + }); + + it("connect against a prompts-less server doesn't fire listPrompts", async () => { + // The connect event runs refresh; the capability gate must also catch it + // there, not only the publicly-callable refresh(). + const promptless = new FakeInspectorClient({ capabilities: { tools: {} } }); + promptless.setStatus("connected"); + const promptlessState = new ManagedPromptsState(promptless); + + promptless.dispatchTypedEvent("connect"); + // Yield so the async refresh chained off connect runs. + await Promise.resolve(); + await Promise.resolve(); + expect(promptless.listPrompts).not.toHaveBeenCalled(); + expect(promptlessState.getPrompts()).toEqual([]); + }); + it("refresh fetches a single page and dispatches promptsChange", async () => { client.setStatus("connected"); client.queuePromptPages({ prompts: [prompt("a"), prompt("b")] }); diff --git a/clients/web/src/test/core/mcp/state/managedResourceTemplatesState.test.ts b/clients/web/src/test/core/mcp/state/managedResourceTemplatesState.test.ts index 223d2be80..d7b6372d9 100644 --- a/clients/web/src/test/core/mcp/state/managedResourceTemplatesState.test.ts +++ b/clients/web/src/test/core/mcp/state/managedResourceTemplatesState.test.ts @@ -24,7 +24,11 @@ describe("ManagedResourceTemplatesState", () => { let state: ManagedResourceTemplatesState; beforeEach(() => { - client = new FakeInspectorClient(); + // Default to a server that advertises `resources` so the existing flow + // tests exercise the live `listResourceTemplates` path; capability-absent + // tests below override this. (Templates are gated on the `resources` + // capability — the spec defines no separate `resourceTemplates` one.) + client = new FakeInspectorClient({ capabilities: { resources: {} } }); state = new ManagedResourceTemplatesState(client); }); @@ -44,6 +48,38 @@ describe("ManagedResourceTemplatesState", () => { expect(client.listResourceTemplates).not.toHaveBeenCalled(); }); + it("refresh skips listResourceTemplates when the server doesn't advertise resources capability", async () => { + // Regression (#1350): templates are part of the resources surface, so a + // resources-less server replied to resources/templates/list with -32601 + // "Method not found", surfacing in the console on every connect. + const resourceless = new FakeInspectorClient({ + capabilities: { tools: {}, prompts: {} }, + }); + resourceless.setStatus("connected"); + const resourcelessState = new ManagedResourceTemplatesState(resourceless); + + const result = await resourcelessState.refresh(); + expect(result).toEqual([]); + expect(resourceless.listResourceTemplates).not.toHaveBeenCalled(); + }); + + it("connect against a resources-less server doesn't fire listResourceTemplates", async () => { + // The connect event runs refresh; the capability gate must also catch it + // there, not only the publicly-callable refresh(). + const resourceless = new FakeInspectorClient({ + capabilities: { tools: {} }, + }); + resourceless.setStatus("connected"); + const resourcelessState = new ManagedResourceTemplatesState(resourceless); + + resourceless.dispatchTypedEvent("connect"); + // Yield so the async refresh chained off connect runs. + await Promise.resolve(); + await Promise.resolve(); + expect(resourceless.listResourceTemplates).not.toHaveBeenCalled(); + expect(resourcelessState.getResourceTemplates()).toEqual([]); + }); + it("refresh fetches a single page and dispatches resourceTemplatesChange", async () => { client.setStatus("connected"); client.queueResourceTemplatePages({ diff --git a/clients/web/src/test/core/mcp/state/managedResourcesState.test.ts b/clients/web/src/test/core/mcp/state/managedResourcesState.test.ts index 6ce214653..cb22ca274 100644 --- a/clients/web/src/test/core/mcp/state/managedResourcesState.test.ts +++ b/clients/web/src/test/core/mcp/state/managedResourcesState.test.ts @@ -22,7 +22,10 @@ describe("ManagedResourcesState", () => { let state: ManagedResourcesState; beforeEach(() => { - client = new FakeInspectorClient(); + // Default to a server that advertises `resources` so the existing flow + // tests exercise the live `listResources` path; capability-absent tests + // below override this. + client = new FakeInspectorClient({ capabilities: { resources: {} } }); state = new ManagedResourcesState(client); }); @@ -42,6 +45,38 @@ describe("ManagedResourcesState", () => { expect(client.listResources).not.toHaveBeenCalled(); }); + it("refresh skips listResources when the server doesn't advertise resources capability", async () => { + // Regression (#1350): a resources-less server replied to resources/list + // with -32601 "Method not found", surfacing in the console on every + // connect. + const resourceless = new FakeInspectorClient({ + capabilities: { tools: {}, prompts: {} }, + }); + resourceless.setStatus("connected"); + const resourcelessState = new ManagedResourcesState(resourceless); + + const result = await resourcelessState.refresh(); + expect(result).toEqual([]); + expect(resourceless.listResources).not.toHaveBeenCalled(); + }); + + it("connect against a resources-less server doesn't fire listResources", async () => { + // The connect event runs refresh; the capability gate must also catch it + // there, not only the publicly-callable refresh(). + const resourceless = new FakeInspectorClient({ + capabilities: { tools: {} }, + }); + resourceless.setStatus("connected"); + const resourcelessState = new ManagedResourcesState(resourceless); + + resourceless.dispatchTypedEvent("connect"); + // Yield so the async refresh chained off connect runs. + await Promise.resolve(); + await Promise.resolve(); + expect(resourceless.listResources).not.toHaveBeenCalled(); + expect(resourcelessState.getResources()).toEqual([]); + }); + it("refresh fetches a single page and dispatches resourcesChange", async () => { client.setStatus("connected"); client.queueResourcePages({ diff --git a/clients/web/src/test/core/mcp/state/managedToolsState.test.ts b/clients/web/src/test/core/mcp/state/managedToolsState.test.ts index 51916d17f..edb04b927 100644 --- a/clients/web/src/test/core/mcp/state/managedToolsState.test.ts +++ b/clients/web/src/test/core/mcp/state/managedToolsState.test.ts @@ -20,7 +20,10 @@ describe("ManagedToolsState", () => { let state: ManagedToolsState; beforeEach(() => { - client = new FakeInspectorClient(); + // Default to a server that advertises `tools` so the existing flow tests + // exercise the live `listTools` path; capability-absent tests below + // override this. + client = new FakeInspectorClient({ capabilities: { tools: {} } }); state = new ManagedToolsState(client); }); @@ -40,6 +43,35 @@ describe("ManagedToolsState", () => { expect(client.listTools).not.toHaveBeenCalled(); }); + it("refresh skips listTools when the server doesn't advertise tools capability", async () => { + // Regression (#1350): a tools-less server replied to tools/list with + // -32601 "Method not found", surfacing in the console on every connect. + const toolless = new FakeInspectorClient({ + capabilities: { prompts: {}, resources: {} }, + }); + toolless.setStatus("connected"); + const toollessState = new ManagedToolsState(toolless); + + const result = await toollessState.refresh(); + expect(result).toEqual([]); + expect(toolless.listTools).not.toHaveBeenCalled(); + }); + + it("connect against a tools-less server doesn't fire listTools", async () => { + // The connect event runs refresh; the capability gate must also catch it + // there, not only the publicly-callable refresh(). + const toolless = new FakeInspectorClient({ capabilities: { prompts: {} } }); + toolless.setStatus("connected"); + const toollessState = new ManagedToolsState(toolless); + + toolless.dispatchTypedEvent("connect"); + // Yield so the async refresh chained off connect runs. + await Promise.resolve(); + await Promise.resolve(); + expect(toolless.listTools).not.toHaveBeenCalled(); + expect(toollessState.getTools()).toEqual([]); + }); + it("refresh fetches a single page and dispatches toolsChange", async () => { client.setStatus("connected"); client.queueToolPages({ tools: [tool("a"), tool("b")] }); diff --git a/clients/web/src/test/core/mcp/state/resourceSubscriptionsState.test.ts b/clients/web/src/test/core/mcp/state/resourceSubscriptionsState.test.ts index ac21c06d6..cc9c6ef63 100644 --- a/clients/web/src/test/core/mcp/state/resourceSubscriptionsState.test.ts +++ b/clients/web/src/test/core/mcp/state/resourceSubscriptionsState.test.ts @@ -25,7 +25,12 @@ describe("ResourceSubscriptionsState", () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-19T10:00:00Z")); - client = new FakeInspectorClient({ status: "connected" }); + // `resources` capability so the ManagedResourcesState refresh used by the + // reference-resolution test exercises the live `listResources` path. + client = new FakeInspectorClient({ + status: "connected", + capabilities: { resources: {} }, + }); }); afterEach(() => { diff --git a/clients/web/src/test/core/react/useManagedPrompts.test.tsx b/clients/web/src/test/core/react/useManagedPrompts.test.tsx index 146e71f75..43888fc63 100644 --- a/clients/web/src/test/core/react/useManagedPrompts.test.tsx +++ b/clients/web/src/test/core/react/useManagedPrompts.test.tsx @@ -14,7 +14,10 @@ describe("useManagedPrompts", () => { let state: ManagedPromptsState; beforeEach(() => { - client = new FakeInspectorClient({ status: "connected" }); + client = new FakeInspectorClient({ + status: "connected", + capabilities: { prompts: {} }, + }); state = new ManagedPromptsState(client); }); diff --git a/clients/web/src/test/core/react/useManagedResourceTemplates.test.tsx b/clients/web/src/test/core/react/useManagedResourceTemplates.test.tsx index 76d142708..ac097da80 100644 --- a/clients/web/src/test/core/react/useManagedResourceTemplates.test.tsx +++ b/clients/web/src/test/core/react/useManagedResourceTemplates.test.tsx @@ -14,7 +14,10 @@ describe("useManagedResourceTemplates", () => { let state: ManagedResourceTemplatesState; beforeEach(() => { - client = new FakeInspectorClient({ status: "connected" }); + client = new FakeInspectorClient({ + status: "connected", + capabilities: { resources: {} }, + }); state = new ManagedResourceTemplatesState(client); }); diff --git a/clients/web/src/test/core/react/useManagedResources.test.tsx b/clients/web/src/test/core/react/useManagedResources.test.tsx index 31769417a..7a6d385bc 100644 --- a/clients/web/src/test/core/react/useManagedResources.test.tsx +++ b/clients/web/src/test/core/react/useManagedResources.test.tsx @@ -14,7 +14,10 @@ describe("useManagedResources", () => { let state: ManagedResourcesState; beforeEach(() => { - client = new FakeInspectorClient({ status: "connected" }); + client = new FakeInspectorClient({ + status: "connected", + capabilities: { resources: {} }, + }); state = new ManagedResourcesState(client); }); diff --git a/clients/web/src/test/core/react/useManagedTools.test.tsx b/clients/web/src/test/core/react/useManagedTools.test.tsx index 5be746eda..4b1c0f244 100644 --- a/clients/web/src/test/core/react/useManagedTools.test.tsx +++ b/clients/web/src/test/core/react/useManagedTools.test.tsx @@ -14,7 +14,10 @@ describe("useManagedTools", () => { let state: ManagedToolsState; beforeEach(() => { - client = new FakeInspectorClient({ status: "connected" }); + client = new FakeInspectorClient({ + status: "connected", + capabilities: { tools: {} }, + }); state = new ManagedToolsState(client); }); diff --git a/core/mcp/state/managedPromptsState.ts b/core/mcp/state/managedPromptsState.ts index fdaf7f917..a53c3101d 100644 --- a/core/mcp/state/managedPromptsState.ts +++ b/core/mcp/state/managedPromptsState.ts @@ -70,6 +70,16 @@ export class ManagedPromptsState extends TypedEventTarget