From e124b2c8a88b05c646d86d83bb81158df2f2c0e7 Mon Sep 17 00:00:00 2001 From: Bob Qiu Date: Mon, 2 Nov 2026 14:30:06 +0800 Subject: [PATCH] test(insomnia-test): add mcp-client error-response, same-name selection and TLS specs Co-Authored-By: Claude Sonnet 5.5 --- .../custom-ca-root-certificate.spec.ts | 46 ++++++++++ .../duplicate-invalid-entries.spec.ts | 70 ++++++++++++++ .../entry-label-and-long-error.spec.ts | 49 ++++++++++ .../error-panel-expand-collapse.spec.ts | 52 +++++++++++ .../mcp-client/error-response/helpers.ts | 9 ++ .../jsonrpc-error-with-json-message.spec.ts | 24 +++++ .../error-response/jsonrpc-error.spec.ts | 30 ++++++ .../load-more-bad-entry-reports-error.spec.ts | 45 +++++++++ .../error-response/load-more-bad-page.spec.ts | 56 ++++++++++++ .../partial-response-invalid.spec.ts | 48 ++++++++++ .../refresh-clears-error.spec.ts | 34 +++++++ ...h-resources-clears-templates-error.spec.ts | 39 ++++++++ .../resources-and-prompts-invalid.spec.ts | 83 +++++++++++++++++ ...er-request-reusing-list-request-id.spec.ts | 28 ++++++ .../shared-resources-capability-error.spec.ts | 37 ++++++++ .../whole-response-invalid.spec.ts | 48 ++++++++++ ...ad-more-survives-switching-request.spec.ts | 61 +++++++++++++ .../same-name-primitives-selection.spec.ts | 91 +++++++++++++++++++ ...ame-selection-survives-list-change.spec.ts | 65 +++++++++++++ .../mcp-client/ssl-validation-toggle.spec.ts | 38 ++++++++ 20 files changed, 953 insertions(+) create mode 100644 packages/insomnia-test/tests/mcp-client/custom-ca-root-certificate.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/duplicate-invalid-entries.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/entry-label-and-long-error.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/error-panel-expand-collapse.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/helpers.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/jsonrpc-error-with-json-message.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/jsonrpc-error.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/load-more-bad-entry-reports-error.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/load-more-bad-page.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/partial-response-invalid.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/refresh-clears-error.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/refresh-resources-clears-templates-error.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/resources-and-prompts-invalid.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/server-request-reusing-list-request-id.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/shared-resources-capability-error.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/error-response/whole-response-invalid.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/load-more-survives-switching-request.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/same-name-primitives-selection.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/same-name-selection-survives-list-change.spec.ts create mode 100644 packages/insomnia-test/tests/mcp-client/ssl-validation-toggle.spec.ts diff --git a/packages/insomnia-test/tests/mcp-client/custom-ca-root-certificate.spec.ts b/packages/insomnia-test/tests/mcp-client/custom-ca-root-certificate.spec.ts new file mode 100644 index 0000000000..c4c29ce99e --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/custom-ca-root-certificate.spec.ts @@ -0,0 +1,46 @@ +import { faker } from "@faker-js/faker"; +import { expect } from "@playwright/test"; + +import { ProjectType } from "../../enums/project-types"; +import { certPath } from "../../misc/certs"; +import { MCP_SERVER_CUSTOM_CA_HTTPS, test } from "../../misc/fixtures"; +import { McpClient } from "../../models/mcp-client"; +import { Project } from "../../models/project"; + +test("Verify Custom CA Certificate Toggle Controls Trust For MCP Client Over TLS", async ({ + user, +}) => { + const { certificatesFlow, mcpClientFlow, workspaceFlow } = user.flowManager; + + const project = await workspaceFlow.createProject( + new Project(faker.string.alphanumeric(10), ProjectType.Local), + ); + + const mcpClient = await mcpClientFlow.create( + project, + new McpClient(faker.string.alphanumeric(10), MCP_SERVER_CUSTOM_CA_HTTPS), + ); + await mcpClientFlow.connect(mcpClient, { expectConnected: false }); + const toolsBeforeCa = mcpClientFlow.getTools(mcpClient); + await expect(toolsBeforeCa).rejects.toThrow(); + + await certificatesFlow.setCaCertificate(mcpClient, certPath("mtls-ca.pem")); + + await mcpClientFlow.connect(mcpClient); + const toolsAfterCa = await mcpClientFlow.getTools(mcpClient); + await mcpClientFlow.disconnect(mcpClient); + + await certificatesFlow.setCaCertificateEnabled(mcpClient, false); + + await mcpClientFlow.connect(mcpClient, { expectConnected: false }); + const toolsAfterDisable = mcpClientFlow.getTools(mcpClient); + await expect(toolsAfterDisable).rejects.toThrow(); + + expect(toolsAfterCa).toEqual( + expect.arrayContaining([ + "read_wiki_structure", + "read_wiki_contents", + "ask_question", + ]), + ); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/duplicate-invalid-entries.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/duplicate-invalid-entries.spec.ts new file mode 100644 index 0000000000..eb2d0c999d --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/duplicate-invalid-entries.spec.ts @@ -0,0 +1,70 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { skippedTitle, validTool } from './helpers'; + +test('Verify Invalid Entries Sharing One Name Are All Listed Per Response Alongside a Valid Entry of That Name', async ({ + user, +}) => { + test.fail(true, 'INS-4130'); + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + + const sharedName = faker.string.alphanumeric(8); + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'tools/list': { + result: { + tools: [ + { name: sharedName, inputSchema: { type: 'object' } }, + validTool, + { name: sharedName, inputSchema: { type: 'object' } }, + { name: sharedName }, + { name: sharedName }, + ], + }, + }, + 'resources/list': { + result: { resources: [{ uri: 'file:///valid.txt', name: sharedName }, { name: sharedName }, { name: sharedName }] }, + }, + 'resources/templates/list': { + result: { resourceTemplates: [{ uriTemplate: 'file:///{path}', name: sharedName }, { name: sharedName }] }, + }, + 'prompts/list': { + result: { + prompts: [{ name: sharedName }, { name: sharedName, arguments: 'x' }, { name: sharedName, arguments: 'y' }], + }, + }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const toolsError = await mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + const resourcesError = await mcpClientFlow.getListError(client, McpPrimitiveType.Resources); + const promptsError = await mcpClientFlow.getListError(client, McpPrimitiveType.Prompts); + + expect(toolsError.title).toMatch(skippedTitle(3)); + expect(toolsError.entries.map(entry => entry.label)).toEqual([sharedName, sharedName, sharedName]); + expect(resourcesError.entries.map(entry => entry.label)).toEqual([sharedName, sharedName, sharedName]); + expect(resourcesError.entries.map(entry => entry.reason)).toEqual([ + expect.stringMatching(/^uri:/), + expect.stringMatching(/^uri:/), + expect.stringMatching(/^uriTemplate:/), + ]); + expect(promptsError.title).toMatch(skippedTitle(2)); + expect(promptsError.entries.map(entry => entry.label)).toEqual([sharedName, sharedName]); + await expect + .poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }) + .toEqual([sharedName, validTool.name]); + for (const type of [McpPrimitiveType.Resources, McpPrimitiveType.Prompts, McpPrimitiveType.ResourceTemplates]) { + await expect + .poll(() => mcpClientPage.getPrimitives(type), { timeout: DEFAULT_TIMEOUT }) + .toEqual([sharedName]); + } +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/entry-label-and-long-error.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/entry-label-and-long-error.spec.ts new file mode 100644 index 0000000000..ef80fc16c5 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/entry-label-and-long-error.spec.ts @@ -0,0 +1,49 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { validTool } from './helpers'; + +test('Verify an Entry Without a Usable Name Is Labeled by Its Position and Shows Its First Issue', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'tools/list': { + result: { + tools: [validTool, 42, 'just-a-string', null, { name: 123, description: 456, inputSchema: 'not-an-object' }], + }, + }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + expect(error.entries.map(entry => entry.label)).toEqual(['index 1', 'index 2', 'index 3', 'index 4']); + expect(error.entries[3].reason).toMatch(/\(And \d+ more\)$/); +}); + +test('Verify a Long Error Panel Scrolls Inside Its Own Area Without Overlapping the Next Row', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const badTools = Array.from({ length: 12 }, (_, i) => ({ name: `bad-${i + 1}` })); + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'tools/list': { result: { tools: [validTool, ...badTools] } }, + 'prompts/list': { result: { prompts: [{ name: 'valid-prompt' }] } }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + expect(error.entries).toHaveLength(12); + expect(await mcpClientPage.isCapabilityErrorScrollable(McpPrimitiveType.Tools)).toBe(true); + + const panelBox = await mcpClientPage.getCapabilityErrorBounds(McpPrimitiveType.Tools); + const nextCapabilityBox = await mcpClientPage.getCapabilityBounds(McpPrimitiveType.Prompts); + expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(nextCapabilityBox.y + 1); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/error-panel-expand-collapse.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/error-panel-expand-collapse.spec.ts new file mode 100644 index 0000000000..2224eed977 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/error-panel-expand-collapse.spec.ts @@ -0,0 +1,52 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { validTool } from './helpers'; + +test('Verify the Error Panel of a Partially Invalid List Follows Expand and Collapse', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'tools/list': { + result: { tools: [validTool, { name: 'no-schema-tool' }] }, + }, + 'prompts/list': { result: { prompts: [{ name: 'valid-prompt' }] } }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + await mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + await expect.poll(() => mcpClientPage.hasCapability(McpPrimitiveType.Prompts)).toBe(true); + + const panelBox = await mcpClientPage.getCapabilityErrorBounds(McpPrimitiveType.Tools); + const nextCapabilityBox = await mcpClientPage.getCapabilityBounds(McpPrimitiveType.Prompts); + expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(nextCapabilityBox.y + 1); + + await mcpClientFlow.toggleCapability(client, McpPrimitiveType.Tools); + await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(0); + + await mcpClientFlow.toggleCapability(client, McpPrimitiveType.Tools); + await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(1); +}); + +test('Verify the Error Panel of an Empty List Stays Visible Without a Collapse Caret', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'tools/list': { result: { tools: 'not-an-array' } }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + await mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + + expect(await mcpClientPage.hasCaret(McpPrimitiveType.Tools)).toBe(false); + await mcpClientFlow.toggleCapability(client, McpPrimitiveType.Tools); + await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(1); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/helpers.ts b/packages/insomnia-test/tests/mcp-client/error-response/helpers.ts new file mode 100644 index 0000000000..e79fea3322 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/helpers.ts @@ -0,0 +1,9 @@ +export const validTool = { name: 'valid-tool', inputSchema: { type: 'object' } }; +export const wholeFailureTitle = (method: string) => + `The server returned a ${method} response that does not match the MCP schema.`; +// The wording of the skipped-entries title is still being settled (INS-3647 +// notes): accept either the primitive name or "entries" as the noun. +export const skippedTitle = (count: number) => + new RegExp( + `^${count} (entries|entry|tools?|resources?|prompts?) (was|were) skipped because they do not match the MCP schema\\.$`, + ); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/jsonrpc-error-with-json-message.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/jsonrpc-error-with-json-message.spec.ts new file mode 100644 index 0000000000..c7a170e938 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/jsonrpc-error-with-json-message.spec.ts @@ -0,0 +1,24 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; + +test('Verify a JSON-RPC Error Whose Message Is a JSON String Is Shown Readably', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'tools/list': { error: { code: -32_603, message: "{ detail: 'boom' }" } }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const error = await user.flowManager.mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + + expect(error.title).toContain('boom'); + expect(error.title).not.toContain('[object Object]'); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/jsonrpc-error.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/jsonrpc-error.spec.ts new file mode 100644 index 0000000000..f532540487 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/jsonrpc-error.spec.ts @@ -0,0 +1,30 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; + +const jsonRpcErrors = [ + { code: -32_601, message: 'Method not found' }, + { code: -32_603, message: 'Internal error' }, +]; + +for (const rpcError of jsonRpcErrors) { + test(`Verify a JSON-RPC Error on tools/list (${rpcError.message}) Is Shown`, async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'tools/list': { error: rpcError }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const error = await user.flowManager.mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + expect(error.title).toContain(rpcError.message); + expect(error.entries).toEqual([]); + }); +} diff --git a/packages/insomnia-test/tests/mcp-client/error-response/load-more-bad-entry-reports-error.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/load-more-bad-entry-reports-error.spec.ts new file mode 100644 index 0000000000..9bd3294db5 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/load-more-bad-entry-reports-error.spec.ts @@ -0,0 +1,45 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; + +test('Verify Load More Whose Next Page Has an Invalid Entry Lists the Skipped Entry in the Error Panel', async ({ + user, +}) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'resources/list': { + pages: { + '': { + result: { + resources: [{ name: 'item-1', uri: 'file:///item-1.txt' }], + nextCursor: 'page-2', + }, + }, + 'page-2': { + result: { + resources: [{ name: 'item-2', uri: 'file:///item-2.txt' }, { name: 'bad-resource' }], + }, + }, + }, + }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + await expect.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), { timeout: DEFAULT_TIMEOUT }).toEqual(['item-1']); + + await mcpClientFlow.loadMore(client, McpPrimitiveType.Resources); + await expect + .poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), { timeout: DEFAULT_TIMEOUT }) + .toEqual(['item-1', 'item-2']); + const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Resources); + + expect(error.entries.map(entry => entry.label)).toEqual(['bad-resource']); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/load-more-bad-page.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/load-more-bad-page.spec.ts new file mode 100644 index 0000000000..659299fc78 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/load-more-bad-page.spec.ts @@ -0,0 +1,56 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpListAction, McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; + +const loadMoreVariants = [ + { + type: McpPrimitiveType.Tools, + method: 'tools/list', + entry: (name: string) => ({ name, inputSchema: { type: 'object' } }), + badEntry: { name: 'bad-tool' }, + }, + { + type: McpPrimitiveType.Prompts, + method: 'prompts/list', + entry: (name: string) => ({ name }), + badEntry: { name: 'bad-prompt', arguments: 'x' }, + }, + { + type: McpPrimitiveType.Resources, + method: 'resources/list', + entry: (name: string) => ({ name, uri: `file:///${name}.txt` }), + badEntry: { name: 'bad-resource' }, + }, +] as const; + +for (const { type, method, entry, badEntry } of loadMoreVariants) { + test(`Verify Load More on ${type} with a Bad Page Keeps Loaded Entries and Clears the Cursor`, async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + [method]: { + pages: { + '': { result: { [type]: [entry('item-1')], nextCursor: 'page-2' } }, + 'page-2': { result: { [type]: [entry('item-2'), badEntry] } }, + }, + }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + await expect.poll(() => mcpClientPage.getPrimitives(type), { timeout: DEFAULT_TIMEOUT }).toEqual(['item-1']); + expect(await mcpClientFlow.getListAction(client, type)).toBe(McpListAction.LoadMore); + + await mcpClientFlow.loadMore(client, type); + await expect + .poll(() => mcpClientPage.getPrimitives(type), { timeout: DEFAULT_TIMEOUT }) + .toEqual(['item-1', 'item-2']); + expect(await mcpClientFlow.getListAction(client, type)).toBe(McpListAction.Reload); + }); +} diff --git a/packages/insomnia-test/tests/mcp-client/error-response/partial-response-invalid.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/partial-response-invalid.spec.ts new file mode 100644 index 0000000000..c8f88d60a9 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/partial-response-invalid.spec.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { skippedTitle, validTool } from './helpers'; + +const partialInvalidTools: Record = { + 'missing or invalid inputSchema': { + tools: [ + { ...validTool, description: 'ok', inputSchema: { type: 'object', properties: {} } }, + { name: 'no-schema-tool', description: 'missing inputSchema' }, + { name: 'bad-schema-tool', inputSchema: { type: 'string' } }, + ], + skipped: ['no-schema-tool', 'bad-schema-tool'], + }, + 'missing or non-string name': { + tools: [validTool, { inputSchema: { type: 'object' } }, { name: 123, inputSchema: { type: 'object' } }], + skipped: ['index 1', 'index 2'], + }, +}; + +for (const [variant, { tools, skipped }] of Object.entries(partialInvalidTools)) { + test(`Verify a Partially Invalid tools/list (${variant}) Skips the Bad Entries and Lists Them`, async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { 'tools/list': { result: { tools } } }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + expect(error.title).toMatch(skippedTitle(2)); + expect(error.entries.map(entry => entry.label)).toEqual(skipped); + for (const entry of error.entries) { + expect(entry.reason).not.toBe(''); + } + await expect + .poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Tools), { + timeout: DEFAULT_TIMEOUT, + }) + .toEqual(['valid-tool']); + }); +} diff --git a/packages/insomnia-test/tests/mcp-client/error-response/refresh-clears-error.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/refresh-clears-error.spec.ts new file mode 100644 index 0000000000..ccbb18a2ae --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/refresh-clears-error.spec.ts @@ -0,0 +1,34 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { type McpListConfig, scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { validTool, wholeFailureTitle } from './helpers'; + +test('Verify Refresh Clears the Error Once the Server Returns Valid Data', async ({ user }) => { + const { mcpClientFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const invalid: McpListConfig = { 'tools/list': { result: { tools: 'not-an-array' } } }; + const valid: McpListConfig = { 'tools/list': { result: { tools: [validTool] } } }; + const key = faker.string.alphanumeric(12); + const url = await scriptMcpListServer(key, invalid); + const project = await user.flowManager.workspaceFlow.createProject( + new Project(faker.string.alphanumeric(10), ProjectType.Local), + ); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + await mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + + await scriptMcpListServer(key, valid); + await mcpClientFlow.refresh(client, McpPrimitiveType.Tools); + await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(0); + await expect.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }).toEqual(['valid-tool']); + + await scriptMcpListServer(key, invalid); + await mcpClientFlow.refresh(client, McpPrimitiveType.Tools); + expect((await mcpClientFlow.getListError(client, McpPrimitiveType.Tools)).title).toBe(wholeFailureTitle('tools/list')); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/refresh-resources-clears-templates-error.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/refresh-resources-clears-templates-error.spec.ts new file mode 100644 index 0000000000..96306802a5 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/refresh-resources-clears-templates-error.spec.ts @@ -0,0 +1,39 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; + +test('Verify Refresh on Resources Clears a Resource Templates Error Once the Server Is Fixed', async ({ user }) => { + test.fail(true, "INS-4065"); + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const resources = { result: { resources: [{ name: 'valid-resource', uri: 'file:///valid.txt' }] } }; + const key = faker.string.alphanumeric(12); + const url = await scriptMcpListServer(key, { + 'resources/list': resources, + 'resources/templates/list': { + result: { + resourceTemplates: [{ uriTemplate: 'file:///{path}', name: 'valid-template' }, { name: 'no-template' }], + }, + }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + await mcpClientFlow.getListError(client, McpPrimitiveType.Resources); + + await scriptMcpListServer(key, { + 'resources/list': resources, + 'resources/templates/list': { + result: { resourceTemplates: [{ uriTemplate: 'file:///{path}', name: 'valid-template' }] }, + }, + }); + await mcpClientFlow.refresh(client, McpPrimitiveType.Resources); + + await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Resources), { timeout: DEFAULT_TIMEOUT }).toBe(0); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/resources-and-prompts-invalid.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/resources-and-prompts-invalid.spec.ts new file mode 100644 index 0000000000..89531b8dc7 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/resources-and-prompts-invalid.spec.ts @@ -0,0 +1,83 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { skippedTitle, wholeFailureTitle } from './helpers'; + +test('Verify Invalid resources/list and prompts/list Are Reported Like tools/list', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'resources/list': { + result: { + resources: [ + { uri: 'file:///valid.txt', name: 'valid-resource' }, + { name: 'no-uri-resource' }, + { uri: 42, name: 'numeric-uri-resource' }, + ], + }, + }, + 'prompts/list': { result: { prompts: 'none' } }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const resourcesError = await mcpClientFlow.getListError(client, McpPrimitiveType.Resources); + expect(resourcesError.title).toMatch(skippedTitle(2)); + expect(resourcesError.entries.map(entry => entry.label)).toEqual(['no-uri-resource', 'numeric-uri-resource']); + await expect + .poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), { + timeout: DEFAULT_TIMEOUT, + }) + .toEqual(['valid-resource']); + + expect(await mcpClientFlow.getListError(client, McpPrimitiveType.Prompts)).toEqual({ + title: wholeFailureTitle('prompts/list'), + entries: [], + }); + expect(await mcpClientPage.getPrimitives(McpPrimitiveType.Prompts)).toEqual([]); +}); + +test('Verify a prompts/list Whose prompts Is an Object Is a Whole-Response Error', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'prompts/list': { result: { prompts: {} } }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + expect(await user.flowManager.mcpClientFlow.getListError(client, McpPrimitiveType.Prompts)).toEqual({ + title: wholeFailureTitle('prompts/list'), + entries: [], + }); +}); + +test('Verify a Prompt with Bad arguments Is Skipped While Valid Prompts Are Listed', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'prompts/list': { + result: { + prompts: [{ name: 'valid-prompt' }, { name: 'bad-args-prompt', arguments: 'x' }], + }, + }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Prompts); + expect(error.title).toMatch(skippedTitle(1)); + expect(error.entries.map(entry => entry.label)).toEqual(['bad-args-prompt']); + await expect + .poll(() => user.pageManager.mcpClientPage.getPrimitives(McpPrimitiveType.Prompts), { + timeout: DEFAULT_TIMEOUT, + }) + .toEqual(['valid-prompt']); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/server-request-reusing-list-request-id.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/server-request-reusing-list-request-id.spec.ts new file mode 100644 index 0000000000..da802f92f4 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/server-request-reusing-list-request-id.spec.ts @@ -0,0 +1,28 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { validTool } from './helpers'; + +test('Verify a Server-Initiated Request Reusing the tools/list Request Id Does Not Produce a Tools Error', async ({ + user, +}) => { + test.fail(true, 'INS-4066'); + const { mcpClientPage } = user.pageManager; + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'tools/list': { result: { tools: [validTool] } }, + 'serverRequestReusingToolsListId': true, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + await expect.poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }).toEqual(['valid-tool']); + expect(await mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Tools)).toBe(0); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/shared-resources-capability-error.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/shared-resources-capability-error.spec.ts new file mode 100644 index 0000000000..5ff48edecf --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/shared-resources-capability-error.spec.ts @@ -0,0 +1,37 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { DEFAULT_TIMEOUT, test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { wholeFailureTitle } from './helpers'; + +test('Verify Resources and Resource Templates Errors Share One Capability Item Panel', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + 'resources/list': { result: { resources: null } }, + 'resources/templates/list': { + result: { + resourceTemplates: [{ uriTemplate: 'file:///{path}', name: 'valid-template' }, { name: 'no-uri-template' }], + }, + }, + }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + const error = await mcpClientFlow.getListError(client, McpPrimitiveType.Resources); + await expect.poll(() => mcpClientPage.getCapabilityErrorCount(McpPrimitiveType.Resources)).toBe(1); + expect(error.title).toContain(wholeFailureTitle('resources/list')); + expect(error.title).toContain('skipped because they do not match the MCP schema'); + expect(error.entries.map(entry => entry.label)).toEqual(['no-uri-template']); + await expect + .poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.ResourceTemplates), { + timeout: DEFAULT_TIMEOUT, + }) + .toEqual(['valid-template']); +}); diff --git a/packages/insomnia-test/tests/mcp-client/error-response/whole-response-invalid.spec.ts b/packages/insomnia-test/tests/mcp-client/error-response/whole-response-invalid.spec.ts new file mode 100644 index 0000000000..892624ff57 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/error-response/whole-response-invalid.spec.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker'; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from '../../../enums/mcp-capability'; +import { ProjectType } from '../../../enums/project-types'; +import { test } from "../../../misc/fixtures"; +import { scriptMcpListServer } from '../../../misc/mcp-list-server'; +import { McpClient } from '../../../models/mcp-client'; +import { Project } from '../../../models/project'; +import { wholeFailureTitle } from './helpers'; + +const wholeInvalidTools: Record = { + 'tools is not an array': { tools: 'not-an-array' }, + 'tools is null': { tools: null }, + 'nextCursor has the wrong type': { tools: [], nextCursor: 123 }, +}; + +for (const [variant, result] of Object.entries(wholeInvalidTools)) { + test(`Verify a Whole tools/list Response Invalid (${variant}) Shows a Title-Only Error`, async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { 'tools/list': { result } }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + expect(await mcpClientFlow.getListError(client, McpPrimitiveType.Tools)).toEqual({ + title: wholeFailureTitle('tools/list'), + entries: [], + }); + expect(await user.pageManager.mcpClientPage.getPrimitives(McpPrimitiveType.Tools)).toEqual([]); + }); +} + +test('Verify a tools/list Result That Is Not an Object Shows an Error and Lists No Tools', async ({ user }) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { 'tools/list': { result: 'oops' } }); + const project = await workspaceFlow.createProject(new Project(faker.string.alphanumeric(10), ProjectType.Local)); + const client = await mcpClientFlow.create(project, new McpClient(faker.string.alphanumeric(10), url)); + await mcpClientFlow.connect(client); + + // The MCP SDK rejects a non-object `result` before the pane's own + // validation runs, so the title is the SDK's ZodError, not the agreed + // "does not match the MCP schema" wording. + const error = await user.flowManager.mcpClientFlow.getListError(client, McpPrimitiveType.Tools); + expect(error.title).not.toBe(''); + expect(error.entries).toEqual([]); + expect(await user.pageManager.mcpClientPage.getPrimitives(McpPrimitiveType.Tools)).toEqual([]); +}); diff --git a/packages/insomnia-test/tests/mcp-client/load-more-survives-switching-request.spec.ts b/packages/insomnia-test/tests/mcp-client/load-more-survives-switching-request.spec.ts new file mode 100644 index 0000000000..2bac651ab5 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/load-more-survives-switching-request.spec.ts @@ -0,0 +1,61 @@ +import { faker } from "@faker-js/faker"; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from "../../enums/mcp-capability"; +import { ProjectType } from "../../enums/project-types"; +import { DEFAULT_TIMEOUT, MCP_SERVER, test } from "../../misc/fixtures"; +import { scriptMcpListServer } from "../../misc/mcp-list-server"; +import { McpClient } from "../../models/mcp-client"; +import { Project } from "../../models/project"; + +test("Verify Loaded Pages Are Kept After Switching to Another MCP Client and Back", async ({ + user, +}) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + "resources/list": { + pages: { + "": { + result: { + resources: [{ name: "item-1", uri: "file:///item-1.txt" }], + nextCursor: "page-2", + }, + }, + "page-2": { + result: { + resources: [{ name: "item-2", uri: "file:///item-2.txt" }], + }, + }, + }, + }, + }); + const project = await workspaceFlow.createProject( + new Project(faker.string.alphanumeric(10), ProjectType.Local), + ); + + const client = await mcpClientFlow.create( + project, + new McpClient(faker.string.alphanumeric(10), url), + ); + await mcpClientFlow.connect(client); + await mcpClientFlow.loadMore(client, McpPrimitiveType.Resources); + await expect + .poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), { + timeout: DEFAULT_TIMEOUT, + }) + .toEqual(["item-1", "item-2"]); + + await mcpClientFlow.create( + project, + new McpClient(faker.string.alphanumeric(10), MCP_SERVER), + ); + await mcpClientFlow.getListAction(client, McpPrimitiveType.Resources); + + await expect + .poll(() => mcpClientPage.getPrimitives(McpPrimitiveType.Resources), { + timeout: DEFAULT_TIMEOUT, + }) + .toEqual(["item-1", "item-2"]); +}); diff --git a/packages/insomnia-test/tests/mcp-client/same-name-primitives-selection.spec.ts b/packages/insomnia-test/tests/mcp-client/same-name-primitives-selection.spec.ts new file mode 100644 index 0000000000..ef5b3d1769 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/same-name-primitives-selection.spec.ts @@ -0,0 +1,91 @@ +import { faker } from "@faker-js/faker"; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from "../../enums/mcp-capability"; +import { ProjectType } from "../../enums/project-types"; +import { DEFAULT_TIMEOUT, test } from "../../misc/fixtures"; +import { scriptMcpListServer } from "../../misc/mcp-list-server"; +import { McpClient } from "../../models/mcp-client"; +import { Project } from "../../models/project"; + +test("Verify Same-Name Tools, Resources and Prompts Are Each Listed Once and Selecting One Selects Only That Row Through Filter, Collapse and Reload", async ({ + user, +}) => { + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + + const sharedName = faker.string.alphanumeric(8); + const url = await scriptMcpListServer(faker.string.alphanumeric(12), { + "tools/list": { + result: { + tools: [ + { name: sharedName, inputSchema: { type: "object", properties: { alpha: { type: "string" } } } }, + { name: sharedName, inputSchema: { type: "object", properties: { beta: { type: "string" } } } }, + ], + }, + }, + "resources/list": { + result: { + resources: [ + { uri: "file:///first.txt", name: sharedName }, + { uri: "file:///second.txt", name: sharedName }, + ], + }, + }, + "prompts/list": { + result: { prompts: [{ name: sharedName }, { name: sharedName }] }, + }, + }); + const project = await workspaceFlow.createProject( + new Project(faker.string.alphanumeric(10), ProjectType.Local), + ); + const client = await mcpClientFlow.create( + project, + new McpClient(faker.string.alphanumeric(10), url), + ); + await mcpClientFlow.connect(client); + + await mcpClientPage.clickPrimitiveAt(McpPrimitiveType.Tools, 1); + await expect + .poll(() => mcpClientPage.getPrimitiveSelection(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }) + .toEqual([false, true]); + await expect + .poll(() => mcpClientPage.getToolArgumentNames(), { timeout: DEFAULT_TIMEOUT }) + .toEqual(["beta"]); + await mcpClientPage.clickPrimitiveAt(McpPrimitiveType.Tools, 0); + await expect + .poll(() => mcpClientPage.getPrimitiveSelection(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }) + .toEqual([true, false]); + await expect + .poll(() => mcpClientPage.getToolArgumentNames(), { timeout: DEFAULT_TIMEOUT }) + .toEqual(["alpha"]); + + await mcpClientFlow.filterCapabilities(client, sharedName); + await mcpClientPage.clickPrimitiveAt(McpPrimitiveType.Tools, 1); + await expect + .poll(() => mcpClientPage.getPrimitiveSelection(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }) + .toEqual([false, true]); + + await mcpClientFlow.toggleCapability(client, McpPrimitiveType.Tools); + await mcpClientFlow.toggleCapability(client, McpPrimitiveType.Tools); + await mcpClientFlow.refresh(client, McpPrimitiveType.Tools); + await expect + .poll(() => mcpClientPage.getPrimitiveSelection(McpPrimitiveType.Tools), { timeout: DEFAULT_TIMEOUT }) + .toEqual([false, true]); + await expect + .poll(() => mcpClientPage.getToolArgumentNames(), { timeout: DEFAULT_TIMEOUT }) + .toEqual(["beta"]); + + await mcpClientPage.clickPrimitiveAt(McpPrimitiveType.Resources, 1); + await expect + .poll(() => mcpClientPage.getPrimitiveSelection(McpPrimitiveType.Resources), { timeout: DEFAULT_TIMEOUT }) + .toEqual([false, true]); + await mcpClientPage.clickPrimitiveAt(McpPrimitiveType.Prompts, 1); + + expect(await mcpClientPage.getPrimitives(McpPrimitiveType.Tools)).toEqual([sharedName, sharedName]); + expect(await mcpClientPage.getPrimitives(McpPrimitiveType.Resources)).toEqual([sharedName, sharedName]); + expect(await mcpClientPage.getPrimitives(McpPrimitiveType.Prompts)).toEqual([sharedName, sharedName]); + await expect + .poll(() => mcpClientPage.getPrimitiveSelection(McpPrimitiveType.Prompts), { timeout: DEFAULT_TIMEOUT }) + .toEqual([false, true]); +}); diff --git a/packages/insomnia-test/tests/mcp-client/same-name-selection-survives-list-change.spec.ts b/packages/insomnia-test/tests/mcp-client/same-name-selection-survives-list-change.spec.ts new file mode 100644 index 0000000000..f5b31cf5c7 --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/same-name-selection-survives-list-change.spec.ts @@ -0,0 +1,65 @@ +import { faker } from "@faker-js/faker"; +import { expect } from "@playwright/test"; + +import { McpPrimitiveType } from "../../enums/mcp-capability"; +import { ProjectType } from "../../enums/project-types"; +import { DEFAULT_TIMEOUT, test } from "../../misc/fixtures"; +import { scriptMcpListServer } from "../../misc/mcp-list-server"; +import { McpClient } from "../../models/mcp-client"; +import { Project } from "../../models/project"; + +test("Verify the Selected Same-Name Tool Stays Selected When a Reload Prepends Another Tool to the List", async ({ + user, +}) => { + test.fail(true, "INS-4147"); + const { mcpClientFlow, workspaceFlow } = user.flowManager; + const { mcpClientPage } = user.pageManager; + + const sharedName = faker.string.alphanumeric(8); + const alphaTool = { + name: sharedName, + inputSchema: { type: "object", properties: { alpha: { type: "string" } } }, + }; + const betaTool = { + name: sharedName, + inputSchema: { type: "object", properties: { beta: { type: "string" } } }, + }; + const key = faker.string.alphanumeric(12); + const url = await scriptMcpListServer(key, { + "tools/list": { result: { tools: [alphaTool, betaTool] } }, + }); + const project = await workspaceFlow.createProject( + new Project(faker.string.alphanumeric(10), ProjectType.Local), + ); + const client = await mcpClientFlow.create( + project, + new McpClient(faker.string.alphanumeric(10), url), + ); + await mcpClientFlow.connect(client); + await mcpClientPage.clickPrimitiveAt(McpPrimitiveType.Tools, 1); + await expect + .poll(() => mcpClientPage.getToolArgumentNames(), { timeout: DEFAULT_TIMEOUT }) + .toEqual(["beta"]); + + await scriptMcpListServer(key, { + "tools/list": { + result: { + tools: [ + { name: faker.string.alphanumeric(8), inputSchema: { type: "object" } }, + alphaTool, + betaTool, + ], + }, + }, + }); + await mcpClientFlow.refresh(client, McpPrimitiveType.Tools); + + await expect + .poll(() => mcpClientPage.getPrimitiveSelection(McpPrimitiveType.Tools), { + timeout: DEFAULT_TIMEOUT, + }) + .toEqual([false, false, true]); + await expect + .poll(() => mcpClientPage.getToolArgumentNames(), { timeout: DEFAULT_TIMEOUT }) + .toEqual(["beta"]); +}); diff --git a/packages/insomnia-test/tests/mcp-client/ssl-validation-toggle.spec.ts b/packages/insomnia-test/tests/mcp-client/ssl-validation-toggle.spec.ts new file mode 100644 index 0000000000..1fef497d2d --- /dev/null +++ b/packages/insomnia-test/tests/mcp-client/ssl-validation-toggle.spec.ts @@ -0,0 +1,38 @@ +import { faker } from "@faker-js/faker"; +import { expect } from "@playwright/test"; + +import { ProjectType } from "../../enums/project-types"; +import { MCP_SERVER_HTTPS, test } from "../../misc/fixtures"; +import { McpClient } from "../../models/mcp-client"; +import { Project } from "../../models/project"; + +test("Verify Disabling SSL Certificate Validation Lets MCP Client Connect To A Self-Signed Server", async ({ + user, +}) => { + const { certificatesFlow, mcpClientFlow, workspaceFlow } = user.flowManager; + + const project = await workspaceFlow.createProject( + new Project(faker.string.alphanumeric(10), ProjectType.Local), + ); + + const mcpClient = await mcpClientFlow.create( + project, + new McpClient(faker.string.alphanumeric(10), MCP_SERVER_HTTPS), + ); + await mcpClientFlow.connect(mcpClient, { expectConnected: false }); + const toolsWithValidation = mcpClientFlow.getTools(mcpClient); + await expect(toolsWithValidation).rejects.toThrow(); + + await certificatesFlow.setSslValidationEnabled(mcpClient, false); + + await mcpClientFlow.connect(mcpClient); + const toolsWithoutValidation = await mcpClientFlow.getTools(mcpClient); + + expect(toolsWithoutValidation).toEqual( + expect.arrayContaining([ + "read_wiki_structure", + "read_wiki_contents", + "ask_question", + ]), + ); +});