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
34 changes: 33 additions & 1 deletion clients/web/src/test/core/mcp/state/managedPromptsState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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")] });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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({
Expand Down
34 changes: 33 additions & 1 deletion clients/web/src/test/core/mcp/state/managedToolsState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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")] });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
5 changes: 4 additions & 1 deletion clients/web/src/test/core/react/useManagedPrompts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
5 changes: 4 additions & 1 deletion clients/web/src/test/core/react/useManagedResources.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
5 changes: 4 additions & 1 deletion clients/web/src/test/core/react/useManagedTools.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
10 changes: 10 additions & 0 deletions core/mcp/state/managedPromptsState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ export class ManagedPromptsState extends TypedEventTarget<ManagedPromptsStateEve
if (!client || client.getStatus() !== "connected") {
return this.getPrompts();
}
// Gate on the server's `prompts` capability — calling prompts/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
// prompts-less server. Empty list is the right semantics for "this server
// doesn't support prompts."
if (!client.getCapabilities()?.prompts) {
this.prompts = [];
this.dispatchTypedEvent("promptsChange", this.prompts);
return this.getPrompts();
}
const effectiveMetadata = metadata ?? this._metadata;
this.prompts = [];
let cursor: string | undefined;
Expand Down
15 changes: 15 additions & 0 deletions core/mcp/state/managedResourceTemplatesState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ export class ManagedResourceTemplatesState extends TypedEventTarget<ManagedResou
if (!client || client.getStatus() !== "connected") {
return this.getResourceTemplates();
}
// Gate on the server's `resources` capability — the MCP spec doesn't define
// a separate `resourceTemplates` capability; resources/templates/list is
// part of the resources surface. Calling it against a server that doesn't
// advertise resources 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.resourceTemplates = [];
this.dispatchTypedEvent(
"resourceTemplatesChange",
this.resourceTemplates,
);
return this.getResourceTemplates();
}
const effectiveMetadata = metadata ?? this._metadata;
this.resourceTemplates = [];
let cursor: string | undefined;
Expand Down
10 changes: 10 additions & 0 deletions core/mcp/state/managedResourcesState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ export class ManagedResourcesState extends TypedEventTarget<ManagedResourcesStat
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;
Expand Down
10 changes: 10 additions & 0 deletions core/mcp/state/managedToolsState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ export class ManagedToolsState extends TypedEventTarget<ManagedToolsStateEventMa
if (!client || client.getStatus() !== "connected") {
return this.getTools();
}
// Gate on the server's `tools` capability — calling tools/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
// tools-less server. Empty list is the right semantics for "this server
// doesn't support tools."
if (!client.getCapabilities()?.tools) {
this.tools = [];
this.dispatchTypedEvent("toolsChange", this.tools);
return this.getTools();
}
const effectiveMetadata = metadata ?? this._metadata;
this.tools = [];
let cursor: string | undefined;
Expand Down
Loading