test(insomnia-test): add mcp-client error-response, same-name selection and TLS specs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Bob QiuandClaude Sonnet 5.5 committed 2026-11-02 18:17:39 +08:00
1 parent 21d4d4146d
commit e124b2c8a8
20 files changed
+953

No files matched your search

@@ -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",
]),
);
});
@@ -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]);
}
});
@@ -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);
});
@@ -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);
});
@@ -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\\.$`,
);
@@ -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]');
});
@@ -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([]);
});
}
@@ -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']);
});
@@ -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);
});
}
@@ -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<string, { tools: unknown[]; skipped: string[] }> = {
'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']);
});
}
@@ -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'));
});
@@ -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);
});
@@ -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']);
});
@@ -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);
});
@@ -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']);
});
@@ -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<string, unknown> = {
'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([]);
});
@@ -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"]);
});
@@ -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]);
});
@@ -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"]);
});
@@ -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",
]),
);
});