Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

688 lines
25 KiB
TypeScript

import { nanoid } from 'nanoid';
import { test, expect } from '../../../fixtures/base';
import type { McpSession } from '../../../services/mcp-api-helper';
/**
* E2E tests for the MCP Server Trigger node.
*
* Tests cover:
* - SSE and Streamable HTTP transports
* - Authentication (none, bearer, header)
* - Tool listing and execution
* - Session management
* - Error handling
*/
test.describe('MCP Trigger Node', {
annotation: [
{ type: 'owner', description: 'AI' },
],
}, () => {
test.describe('Streamable HTTP Transport', () => {
test('should initialize session and return mcp-session-id', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
// Get the MCP path from the workflow
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.streamableHttpInitialize(mcpPath);
expect(session.sessionId).toBeTruthy();
expect(session.transport).toBe('streamableHttp');
});
test('should list tools via Streamable HTTP', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.streamableHttpInitialize(mcpPath);
const tools = await api.mcp.listTools(session, mcpPath);
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('echo');
expect(tools[0].description).toContain('Echoes');
});
test('should call tool via Streamable HTTP', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.streamableHttpInitialize(mcpPath);
const result = await api.mcp.callTool(session, mcpPath, 'echo', {
message: 'Hello from E2E test!',
});
expect(result.content).toBeDefined();
expect(result.content.length).toBeGreaterThan(0);
expect(result.content[0].text).toContain('Hello from E2E test!');
});
test('should close session via DELETE', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.streamableHttpInitialize(mcpPath);
const deleteResponse = await api.mcp.streamableHttpDelete(session, mcpPath);
// DELETE should return success (200 or 202)
expect(deleteResponse.status()).toBeLessThan(300);
});
});
test.describe('SSE Transport', () => {
test('should establish SSE connection and return session', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.sseSetup(mcpPath);
try {
expect(session.sessionId).toBeTruthy();
expect(session.transport).toBe('sse');
expect(session.postUrl).toBeTruthy();
} finally {
api.mcp.sseClose(session);
}
});
test('should list connected tools via SSE', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.sseSetup(mcpPath);
try {
const tools = await api.mcp.listTools(session, mcpPath);
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('echo');
} finally {
api.mcp.sseClose(session);
}
});
test('should call tool and receive response via SSE', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.sseSetup(mcpPath);
try {
const result = await api.mcp.callTool(session, mcpPath, 'echo', {
message: 'SSE test message',
});
expect(result.content).toBeDefined();
expect(result.content[0].text).toContain('SSE test message');
} finally {
api.mcp.sseClose(session);
}
});
});
test.describe('Authentication', () => {
test('should reject unauthenticated request with bearerAuth', async ({ api }) => {
const token = `secret-token-${nanoid()}`;
const credential = await api.credentials.createCredential({
type: 'httpBearerAuth',
name: `mcp-bearer-${nanoid()}`,
data: { token },
});
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-bearer-auth.json',
{
transform: (wf) => {
const mcpNode = wf.nodes?.find((n) => n.type.includes('mcpTrigger'));
if (mcpNode) {
mcpNode.credentials = {
httpBearerAuth: { id: credential.id, name: credential.name },
};
}
return wf;
},
},
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Try without auth - should fail
const noAuthResponse = await api.webhooks.trigger(mcpPath, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: api.mcp.createMessage('initialize', {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: { name: 'test', version: '1.0.0' },
}),
});
expect(noAuthResponse.status()).toBe(403);
});
test('should accept valid bearer token', async ({ api }) => {
const token = `secret-token-${nanoid()}`;
const credential = await api.credentials.createCredential({
type: 'httpBearerAuth',
name: `mcp-bearer-${nanoid()}`,
data: { token },
});
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-bearer-auth.json',
{
transform: (wf) => {
const mcpNode = wf.nodes?.find((n) => n.type.includes('mcpTrigger'));
if (mcpNode) {
mcpNode.credentials = {
httpBearerAuth: { id: credential.id, name: credential.name },
};
}
return wf;
},
},
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Try with valid auth - should succeed
const session = await api.mcp.streamableHttpInitialize(mcpPath, {
headers: { Authorization: `Bearer ${token}` },
});
expect(session.sessionId).toBeTruthy();
});
test('should accept valid header auth', async ({ api }) => {
const headerName = `X-Auth-${nanoid(8)}`;
const headerValue = `secret-value-${nanoid()}`;
const credential = await api.credentials.createCredential({
type: 'httpHeaderAuth',
name: `mcp-header-${nanoid()}`,
data: { name: headerName, value: headerValue },
});
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-header-auth.json',
{
transform: (wf) => {
const mcpNode = wf.nodes?.find((n) => n.type.includes('mcpTrigger'));
if (mcpNode) {
mcpNode.credentials = {
httpHeaderAuth: { id: credential.id, name: credential.name },
};
}
return wf;
},
},
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Try without auth - should fail
const noAuthResponse = await api.webhooks.trigger(mcpPath, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: api.mcp.createMessage('initialize', {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: { name: 'test', version: '1.0.0' },
}),
});
expect(noAuthResponse.status()).toBe(403);
// Try with valid auth - should succeed
const session = await api.mcp.streamableHttpInitialize(mcpPath, {
headers: { [headerName]: headerValue },
});
expect(session.sessionId).toBeTruthy();
});
});
test.describe('Tool Operations', () => {
test('should return all connected tools in tools/list', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-multi-tool.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.streamableHttpInitialize(mcpPath);
const tools = await api.mcp.listTools(session, mcpPath);
expect(tools).toHaveLength(3);
const toolNames = tools.map((t) => t.name).sort();
expect(toolNames).toEqual(['add', 'echo', 'multiply']);
});
test('should execute tool with arguments', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-multi-tool.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.streamableHttpInitialize(mcpPath);
// Test echo tool
const echoResult = await api.mcp.callTool(session, mcpPath, 'echo', {
message: 'Multi-tool test',
});
expect(echoResult.content[0].text).toContain('Multi-tool test');
// Test add tool
const addResult = await api.mcp.callTool(session, mcpPath, 'add', { a: 5, b: 3 });
expect(addResult.content[0].text).toContain('8');
// Test multiply tool
const multiplyResult = await api.mcp.callTool(session, mcpPath, 'multiply', { a: 4, b: 7 });
expect(multiplyResult.content[0].text).toContain('28');
});
test('should return error for unknown tool', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
const session = await api.mcp.streamableHttpInitialize(mcpPath);
// Try to call a non-existent tool
const message = api.mcp.createMessage('tools/call', {
name: 'nonexistent_tool',
arguments: {},
});
const response = await api.mcp.streamableHttpSendMessage(session, mcpPath, message);
const body = await response.text();
// Should get an error response
expect(body).toContain('error');
});
});
test.describe('Session Management', () => {
test('should reject requests with invalid session ID', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Create a fake session with an invalid session ID
const fakeSession: McpSession = {
sessionId: 'invalid-session-id-12345',
transport: 'streamableHttp',
};
const message = api.mcp.createMessage('tools/list');
const response = await api.mcp.streamableHttpSendMessage(fakeSession, mcpPath, message);
// Should return an error status (404 or 401)
expect(response.status()).toBeGreaterThanOrEqual(400);
});
test('should cleanup session on DELETE request', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Initialize and then delete session
const session = await api.mcp.streamableHttpInitialize(mcpPath);
await api.mcp.streamableHttpDelete(session, mcpPath);
// Try to use the deleted session - should fail
const message = api.mcp.createMessage('tools/list');
const response = await api.mcp.streamableHttpSendMessage(session, mcpPath, message);
expect(response.status()).toBeGreaterThanOrEqual(400);
});
});
test.describe('Error Handling', () => {
test('should return 404 for non-existent endpoint', async ({ api }) => {
const response = await api.webhooks.trigger('webhook/non-existent-mcp-endpoint-12345', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: api.mcp.createMessage('initialize'),
});
expect(response.status()).toBe(404);
});
test('should handle malformed JSON-RPC messages', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// First establish a valid session
const session = await api.mcp.streamableHttpInitialize(mcpPath);
// Send malformed message (missing required fields)
const malformedMessage = {
// Missing jsonrpc version
id: nanoid(),
method: 'tools/list',
};
const response = await api.mcp.streamableHttpSendMessage(session, mcpPath, malformedMessage);
// Server should handle gracefully (either error response or parse error)
// The exact behavior depends on implementation
const body = await response.text();
expect(body).toBeTruthy(); // Should return some response
});
});
});
// Queue mode tests - tagged with @mode:queue to run only in queue infrastructure
test.describe('MCP Trigger - Queue Mode', () => {
test('@mode:queue should return 202 Accepted for tool call in queue mode', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// In SSE mode with queue mode enabled, tool calls should return 202 Accepted
// because the execution is queued and response comes via Redis pub/sub
const session = await api.mcp.sseSetup(mcpPath);
try {
const message = api.mcp.createMessage('tools/call', {
name: 'echo',
arguments: { message: 'Queue mode test' },
});
const response = await api.mcp.sseSendMessage(session, message);
// In queue mode, SSE tool calls may return 202 Accepted
// The actual result would come back asynchronously
expect([200, 202]).toContain(response.status());
} finally {
api.mcp.sseClose(session);
}
});
});
// Multi-main tests - tagged with @mode:multi-main to run only in multi-main infrastructure
test.describe('MCP Trigger - Multi-Main', () => {
test.describe('Streamable HTTP Transport', () => {
test('@mode:multi-main should handle tool call on different main than session creator', async ({
api,
mainUrls,
createApiForMain,
}) => {
// This test verifies that MCP sessions work correctly in multi-main setups
// where the session might be created on one main but tool calls routed to another
// Skip if not in multi-main mode (need at least 2 mains)
test.skip(mainUrls.length < 2, 'Requires at least 2 mains for multi-main testing');
// Create workflow via load balancer (normal flow)
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Initialize session on main-1 (direct access, bypassing load balancer)
const main1Api = await createApiForMain(0);
const session = await main1Api.mcp.streamableHttpInitialize(mcpPath);
expect(session.sessionId).toBeTruthy();
// Send tool call to main-2 (different main than where session was created)
// This tests that session state is properly shared across mains via Redis
const main2Api = await createApiForMain(1);
const result = await main2Api.mcp.callTool(session, mcpPath, 'echo', {
message: 'Cross-main test',
});
expect(result.content).toBeDefined();
expect(result.content[0].text).toContain('Cross-main test');
});
test('@mode:multi-main should handle multiple tool calls across different mains', async ({
api,
mainUrls,
createApiForMain,
}) => {
// Test that multiple tool calls can be distributed across mains
test.skip(mainUrls.length < 2, 'Requires at least 2 mains for multi-main testing');
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-multi-tool.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Initialize session on main-1
const main1Api = await createApiForMain(0);
const session = await main1Api.mcp.streamableHttpInitialize(mcpPath);
// Alternate tool calls between mains to simulate load balancer behavior
const main2Api = await createApiForMain(1);
// Call 1: main-1
const echoResult = await main1Api.mcp.callTool(session, mcpPath, 'echo', {
message: 'From main 1',
});
expect(echoResult.content[0].text).toContain('From main 1');
// Call 2: main-2
const addResult = await main2Api.mcp.callTool(session, mcpPath, 'add', { a: 10, b: 20 });
expect(addResult.content[0].text).toContain('30');
// Call 3: main-1 again
const multiplyResult = await main1Api.mcp.callTool(session, mcpPath, 'multiply', {
a: 5,
b: 6,
});
expect(multiplyResult.content[0].text).toContain('30');
// Call 4: main-2 again
const echoResult2 = await main2Api.mcp.callTool(session, mcpPath, 'echo', {
message: 'From main 2',
});
expect(echoResult2.content[0].text).toContain('From main 2');
});
});
test.describe('SSE Transport', () => {
test('@mode:multi-main should handle SSE tool call on different main than session creator', async ({
api,
mainUrls,
createApiForMain,
}) => {
// This test verifies that SSE-based MCP sessions work correctly in multi-main setups
test.skip(mainUrls.length < 2, 'Requires at least 2 mains for multi-main testing');
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Initialize SSE session on main-1
const main1Api = await createApiForMain(0);
const session = await main1Api.mcp.sseSetup(mcpPath);
try {
expect(session.sessionId).toBeTruthy();
expect(session.transport).toBe('sse');
// Send tool call to main-2 (different main than where SSE session was created)
// This tests that SSE session state is properly shared across mains via Redis
// Use callToolCrossMain to POST to main2's URL while receiving on main1's SSE stream
const main2McpPath = `${mainUrls[1]}/${mcpPath}`;
const result = await main1Api.mcp.callToolCrossMain(session, main2McpPath, 'echo', {
message: 'SSE cross-main test',
});
expect(result.content).toBeDefined();
expect(result.content[0].text).toContain('SSE cross-main test');
} finally {
main1Api.mcp.sseClose(session);
}
});
test('@mode:multi-main should handle multiple SSE tool calls across different mains', async ({
api,
mainUrls,
createApiForMain,
}) => {
// Test that multiple SSE tool calls can be distributed across mains
test.skip(mainUrls.length < 2, 'Requires at least 2 mains for multi-main testing');
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-multi-tool.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Initialize SSE session on main-1
const main1Api = await createApiForMain(0);
const session = await main1Api.mcp.sseSetup(mcpPath);
// Construct full URL paths for cross-main calls
const main2McpPath = `${mainUrls[1]}/${mcpPath}`;
try {
// Call 1: main-1 (where SSE connection was established)
const echoResult = await main1Api.mcp.callTool(session, mcpPath, 'echo', {
message: 'SSE from main 1',
});
expect(echoResult.content[0].text).toContain('SSE from main 1');
// Call 2: main-2 (different main, tests Redis pub/sub for response routing)
// Use callToolCrossMain to POST to main2 but receive response on main1's SSE stream
const addResult = await main1Api.mcp.callToolCrossMain(session, main2McpPath, 'add', {
a: 15,
b: 25,
});
expect(addResult.content[0].text).toContain('40');
// Call 3: main-1 again
const multiplyResult = await main1Api.mcp.callTool(session, mcpPath, 'multiply', {
a: 7,
b: 8,
});
expect(multiplyResult.content[0].text).toContain('56');
// Call 4: main-2 again
const echoResult2 = await main1Api.mcp.callToolCrossMain(session, main2McpPath, 'echo', {
message: 'SSE from main 2',
});
expect(echoResult2.content[0].text).toContain('SSE from main 2');
} finally {
main1Api.mcp.sseClose(session);
}
});
test('@mode:multi-main should list tools via SSE from different main', async ({
api,
mainUrls,
createApiForMain,
}) => {
// Test that tools/list works across mains with SSE transport
test.skip(mainUrls.length < 2, 'Requires at least 2 mains for multi-main testing');
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-trigger/mcp-trigger-multi-tool.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const mcpNode = createdWorkflow.nodes?.find((n) => n.type.includes('mcpTrigger'));
const mcpPath = `webhook/${mcpNode?.parameters.path as string}`;
// Initialize SSE session on main-1
const main1Api = await createApiForMain(0);
const session = await main1Api.mcp.sseSetup(mcpPath);
try {
// List tools via main-2 (different main)
// Use listToolsCrossMain to POST to main2 but receive response on main1's SSE stream
const main2McpPath = `${mainUrls[1]}/${mcpPath}`;
const tools = await main1Api.mcp.listToolsCrossMain(session, main2McpPath);
expect(tools).toHaveLength(3);
const toolNames = tools.map((t) => t.name).sort();
expect(toolNames).toEqual(['add', 'echo', 'multiply']);
} finally {
main1Api.mcp.sseClose(session);
}
});
});
});