Skip to content
Open
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
1 change: 1 addition & 0 deletions clients/web/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ vi.mock("@inspector/core/mcp/index.js", () => {
getPendingElicitations = vi.fn().mockReturnValue([]);
getRoots = vi.fn().mockReturnValue([]);
setRoots = vi.fn().mockResolvedValue(undefined);
setServerSettings = vi.fn();
}
const instances: FakeInspectorClient[] = [];
return {
Expand Down
5 changes: 5 additions & 0 deletions clients/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,11 @@ function App() {
settingsModalTargetId === activeServerId &&
settingsDraft
) {
// Push the edited settings onto the live client so settings the managed
// state reads at notification time (auto-refresh-on-list-changed) take
// effect without a reconnect (#1444). Connection-time inputs (transport,
// OAuth, timeouts) still only apply on the next connect.
inspectorClient.setServerSettings(settingsDraft);
const nextRoots = cleanRoots(settingsDraft.roots);
const currentRoots = cleanRoots(inspectorClient.getRoots());
if (JSON.stringify(nextRoots) !== JSON.stringify(currentRoots)) {
Expand Down
42 changes: 23 additions & 19 deletions clients/web/src/test/core/mcp/state/managedPromptsState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ function waitForPromptsChange(state: ManagedPromptsState): Promise<Prompt[]> {
});
}

function waitForListChanged(state: ManagedPromptsState): Promise<boolean> {
return new Promise((resolve) => {
state.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}

describe("ManagedPromptsState", () => {
let client: FakeInspectorClient;
let state: ManagedPromptsState;
Expand All @@ -35,7 +43,7 @@ describe("ManagedPromptsState", () => {
// exercise the live `listPrompts` path; capability-absent tests below
// override this.
client = new FakeInspectorClient({ capabilities: { prompts: {} } });
state = new ManagedPromptsState(client);
state = new ManagedPromptsState(client, 0);
});

it("starts with empty prompts", () => {
Expand All @@ -61,7 +69,7 @@ describe("ManagedPromptsState", () => {
capabilities: { tools: {}, resources: {} },
});
promptless.setStatus("connected");
const promptlessState = new ManagedPromptsState(promptless);
const promptlessState = new ManagedPromptsState(promptless, 0);

const result = await promptlessState.refresh();
expect(result).toEqual([]);
Expand All @@ -73,7 +81,7 @@ describe("ManagedPromptsState", () => {
// there, not only the publicly-callable refresh().
const promptless = new FakeInspectorClient({ capabilities: { tools: {} } });
promptless.setStatus("connected");
const promptlessState = new ManagedPromptsState(promptless);
const promptlessState = new ManagedPromptsState(promptless, 0);

promptless.dispatchTypedEvent("connect");
// Yield so the async refresh chained off connect runs.
Expand Down Expand Up @@ -135,15 +143,15 @@ describe("ManagedPromptsState", () => {
expect(next.map((p) => p.name)).toEqual(["a"]);
});

it("promptsListChanged does NOT auto-refresh by default (the user pulls via Refresh)", async () => {
it("promptsListChanged lights the indicator without fetching by default (#1444)", async () => {
// Auto-refresh off: a list_changed lights the indicator with NO list call;
// the user pulls the new list via Refresh.
client.setStatus("connected");
client.queuePromptPages({ prompts: [prompt("a"), prompt("b")] });
const changed = waitForListChanged(state);
client.dispatchTypedEvent("promptsListChanged");
// Yield so a stray refresh would have landed.
await Promise.resolve();
await Promise.resolve();
expect(client.listPrompts).not.toHaveBeenCalled();
expect(state.getPrompts()).toEqual([]);
expect(await changed).toBe(true);
expect(client.listPrompts).not.toHaveBeenCalled(); // no automatic fetch
expect(state.getPrompts()).toEqual([]); // displayed list untouched
});

it("promptsListChanged auto-refreshes when the server opts in", async () => {
Expand All @@ -152,7 +160,7 @@ describe("ManagedPromptsState", () => {
serverSettings: AUTO_REFRESH_SETTINGS,
});
autoClient.setStatus("connected");
const autoState = new ManagedPromptsState(autoClient);
const autoState = new ManagedPromptsState(autoClient, 0);
autoClient.queuePromptPages({ prompts: [prompt("a")] });
const changed = waitForPromptsChange(autoState);
autoClient.dispatchTypedEvent("promptsListChanged");
Expand Down Expand Up @@ -206,14 +214,6 @@ describe("ManagedPromptsState", () => {
});

describe("listChanged (#1402)", () => {
function waitForListChanged(s: ManagedPromptsState): Promise<boolean> {
return new Promise((resolve) => {
s.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}

it("starts cleared", () => {
expect(state.getListChanged()).toBe(false);
});
Expand All @@ -230,7 +230,9 @@ describe("ManagedPromptsState", () => {
it("clearListChanged resets the flag and dispatches false", async () => {
client.setStatus("connected");
client.queuePromptPages({ prompts: [prompt("a")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("promptsListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);

const changed = waitForListChanged(state);
Expand All @@ -251,7 +253,9 @@ describe("ManagedPromptsState", () => {
it("disconnect clears the flag", async () => {
client.setStatus("connected");
client.queuePromptPages({ prompts: [prompt("a")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("promptsListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);

const changed = waitForListChanged(state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe("ManagedResourceTemplatesState", () => {
// 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);
state = new ManagedResourceTemplatesState(client, 0);
});

it("starts with empty resource templates", () => {
Expand All @@ -67,7 +67,10 @@ describe("ManagedResourceTemplatesState", () => {
capabilities: { tools: {}, prompts: {} },
});
resourceless.setStatus("connected");
const resourcelessState = new ManagedResourceTemplatesState(resourceless);
const resourcelessState = new ManagedResourceTemplatesState(
resourceless,
0,
);

const result = await resourcelessState.refresh();
expect(result).toEqual([]);
Expand All @@ -81,7 +84,10 @@ describe("ManagedResourceTemplatesState", () => {
capabilities: { tools: {} },
});
resourceless.setStatus("connected");
const resourcelessState = new ManagedResourceTemplatesState(resourceless);
const resourcelessState = new ManagedResourceTemplatesState(
resourceless,
0,
);

resourceless.dispatchTypedEvent("connect");
// Yield so the async refresh chained off connect runs.
Expand Down Expand Up @@ -166,7 +172,7 @@ describe("ManagedResourceTemplatesState", () => {
serverSettings: AUTO_REFRESH_SETTINGS,
});
autoClient.setStatus("connected");
const autoState = new ManagedResourceTemplatesState(autoClient);
const autoState = new ManagedResourceTemplatesState(autoClient, 0);
autoClient.queueResourceTemplatePages({
resourceTemplates: [template("a")],
});
Expand Down
44 changes: 23 additions & 21 deletions clients/web/src/test/core/mcp/state/managedResourcesState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ function waitForResourcesChange(
});
}

function waitForListChanged(state: ManagedResourcesState): Promise<boolean> {
return new Promise((resolve) => {
state.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}

describe("ManagedResourcesState", () => {
let client: FakeInspectorClient;
let state: ManagedResourcesState;
Expand All @@ -37,7 +45,7 @@ describe("ManagedResourcesState", () => {
// tests exercise the live `listResources` path; capability-absent tests
// below override this.
client = new FakeInspectorClient({ capabilities: { resources: {} } });
state = new ManagedResourcesState(client);
state = new ManagedResourcesState(client, 0);
});

it("starts with empty resources", () => {
Expand All @@ -64,7 +72,7 @@ describe("ManagedResourcesState", () => {
capabilities: { tools: {}, prompts: {} },
});
resourceless.setStatus("connected");
const resourcelessState = new ManagedResourcesState(resourceless);
const resourcelessState = new ManagedResourcesState(resourceless, 0);

const result = await resourcelessState.refresh();
expect(result).toEqual([]);
Expand All @@ -78,7 +86,7 @@ describe("ManagedResourcesState", () => {
capabilities: { tools: {} },
});
resourceless.setStatus("connected");
const resourcelessState = new ManagedResourcesState(resourceless);
const resourcelessState = new ManagedResourcesState(resourceless, 0);

resourceless.dispatchTypedEvent("connect");
// Yield so the async refresh chained off connect runs.
Expand Down Expand Up @@ -142,17 +150,15 @@ describe("ManagedResourcesState", () => {
expect(next.map((r) => r.uri)).toEqual(["a://1"]);
});

it("resourcesListChanged does NOT auto-refresh by default (the user pulls via Refresh)", async () => {
it("resourcesListChanged lights the indicator without fetching by default (#1444)", async () => {
// Auto-refresh off: a list_changed lights the indicator with NO list call;
// the user pulls the new list via Refresh.
client.setStatus("connected");
client.queueResourcePages({
resources: [resource("a://1"), resource("a://2")],
});
const changed = waitForListChanged(state);
client.dispatchTypedEvent("resourcesListChanged");
// Yield so a stray refresh would have landed.
await Promise.resolve();
await Promise.resolve();
expect(client.listResources).not.toHaveBeenCalled();
expect(state.getResources()).toEqual([]);
expect(await changed).toBe(true);
expect(client.listResources).not.toHaveBeenCalled(); // no automatic fetch
expect(state.getResources()).toEqual([]); // displayed list untouched
});

it("resourcesListChanged auto-refreshes when the server opts in", async () => {
Expand All @@ -161,7 +167,7 @@ describe("ManagedResourcesState", () => {
serverSettings: AUTO_REFRESH_SETTINGS,
});
autoClient.setStatus("connected");
const autoState = new ManagedResourcesState(autoClient);
const autoState = new ManagedResourcesState(autoClient, 0);
autoClient.queueResourcePages({ resources: [resource("a://1")] });
const changed = waitForResourcesChange(autoState);
autoClient.dispatchTypedEvent("resourcesListChanged");
Expand Down Expand Up @@ -215,14 +221,6 @@ describe("ManagedResourcesState", () => {
});

describe("listChanged (#1402)", () => {
function waitForListChanged(s: ManagedResourcesState): Promise<boolean> {
return new Promise((resolve) => {
s.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}

it("starts cleared", () => {
expect(state.getListChanged()).toBe(false);
});
Expand All @@ -239,7 +237,9 @@ describe("ManagedResourcesState", () => {
it("clearListChanged resets the flag and dispatches false", async () => {
client.setStatus("connected");
client.queueResourcePages({ resources: [resource("a://1")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("resourcesListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);

const changed = waitForListChanged(state);
Expand All @@ -260,7 +260,9 @@ describe("ManagedResourcesState", () => {
it("disconnect clears the flag", async () => {
client.setStatus("connected");
client.queueResourcePages({ resources: [resource("a://1")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("resourcesListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);

const changed = waitForListChanged(state);
Expand Down
Loading
Loading