test: Add E2E tests for internal MCP service (#25380)

This commit is contained in:
Albert Alises
2026-02-06 13:38:56 +01:00
committed by GitHub
parent 0912de4a65
commit 6778c37ea4
6 changed files with 912 additions and 16 deletions
@@ -433,6 +433,65 @@ export class ApiHelpers {
}
}
// ===== MCP API KEY METHODS =====
/**
* Get or create MCP API key for the authenticated user.
* If the user already has an API key, returns the existing one (redacted).
* If not, creates a new one and returns the full key.
*
* @returns The MCP API key data including the key itself
*/
async getMcpApiKey(): Promise<{ id: string; apiKey: string; userId: string }> {
const response = await this.request.get('/rest/mcp/api-key');
if (!response.ok()) {
throw new TestError(
`Failed to get MCP API key: ${response.status()} ${await response.text()}`,
);
}
const result = await response.json();
return result.data ?? result;
}
/**
* Rotate the MCP API key for the authenticated user.
* Creates a new API key and invalidates the old one.
*
* @returns The new MCP API key data
*/
async rotateMcpApiKey(): Promise<{ id: string; apiKey: string; userId: string }> {
const response = await this.request.post('/rest/mcp/api-key/rotate');
if (!response.ok()) {
throw new TestError(
`Failed to rotate MCP API key: ${response.status()} ${await response.text()}`,
);
}
const result = await response.json();
return result.data ?? result;
}
/**
* Enable or disable MCP access for the instance.
* Uses the MCP settings endpoint to toggle access.
*
* @param enabled - Whether MCP access should be enabled
*/
async setMcpAccess(enabled: boolean): Promise<void> {
const response = await this.request.patch('/rest/mcp/settings', {
data: { mcpAccessEnabled: enabled },
});
if (!response.ok()) {
throw new TestError(
`Failed to set MCP access: ${response.status()} ${await response.text()}`,
);
}
}
// ===== PRIVATE METHODS =====
private async loginAndSetCookies(
@@ -65,6 +65,65 @@ interface McpJsonRpcResponse {
};
}
/** Internal MCP session for the /mcp-server/http endpoint */
export interface InternalMcpSession {
apiKey: string;
}
/** Response from the internal MCP tools/list */
export interface InternalMcpToolsListResult {
tools: McpToolDefinition[];
}
/** Response from search_workflows tool */
export interface SearchWorkflowsResult {
data: Array<{
id: string;
name: string | null;
description?: string | null;
active: boolean | null;
createdAt: string | null;
updatedAt: string | null;
triggerCount: number | null;
nodes: Array<{ name: string; type: string }>;
scopes: string[];
canExecute: boolean;
}>;
count: number;
}
/** Response from get_workflow_details tool */
export interface WorkflowDetailsResult {
workflow: {
id: string;
name: string;
active: boolean;
isArchived: boolean;
versionId: string;
triggerCount: number;
createdAt: string;
updatedAt: string;
settings: Record<string, unknown> | null;
connections: Record<string, unknown>;
nodes: Array<Record<string, unknown>>;
tags: Array<{ id: string; name: string }>;
meta: Record<string, unknown> | null;
parentFolderId: string | null;
description?: string;
scopes: string[];
canExecute: boolean;
};
triggerInfo: unknown;
}
/** Response from execute_workflow tool */
export interface ExecuteWorkflowResult {
success: boolean;
executionId: string | null;
result?: unknown;
error?: unknown;
}
/**
* Helper class for interacting with MCP Server endpoints.
* Supports both SSE and Streamable HTTP transports.
@@ -567,22 +626,6 @@ export class McpApiHelper {
return result.tools;
}
/**
* Sends a raw JSON-RPC message to the MCP server.
* Useful for testing malformed messages or custom methods.
*
* @param session - The MCP session
* @param path - The webhook path
* @param message - The raw message to send
* @returns The API response
*/
async sendRawMessage(session: McpSession, path: string, message: unknown): Promise<APIResponse> {
if (session.transport === 'sse') {
return await this.sseSendMessage(session, message);
}
return await this.streamableHttpSendMessage(session, path, message);
}
// ===== Helper Methods =====
/**
@@ -659,6 +702,34 @@ export class McpApiHelper {
return parsed.result as T;
}
/**
* Parses an SSE event stream for tool call responses.
* Extracts the McpToolCallResponse from the SSE body.
*/
private parseSSEToolResponse(body: string): McpToolCallResponse {
const lines = body.split('\n');
let jsonData = '';
for (const line of lines) {
if (line.startsWith('data:')) {
jsonData = line.slice(5).trim();
break;
}
}
if (!jsonData) {
throw new Error(`Could not extract data from SSE response: ${body}`);
}
const parsed = JSON.parse(jsonData) as McpJsonRpcResponse;
if (parsed.error) {
throw new Error(`MCP Error ${parsed.error.code}: ${parsed.error.message}`);
}
return parsed.result as McpToolCallResponse;
}
/**
* Triggers an HTTP request to the MCP endpoint with retry logic for 404s.
* Based on WebhookApiHelper.trigger().
@@ -686,4 +757,186 @@ export class McpApiHelper {
return lastResponse!;
}
// ===== Internal MCP Service Methods (/mcp-server/http) =====
/**
* Sends a JSON-RPC message to the internal MCP service endpoint.
* This endpoint uses Bearer token authentication (API key).
*
* @param apiKey - The MCP API key for authentication
* @param message - The JSON-RPC message to send
* @returns The API response
*/
async internalMcpSendMessage(apiKey: string, message: unknown): Promise<APIResponse> {
return await this.api.request.fetch('/mcp-server/http', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
Authorization: `Bearer ${apiKey}`,
},
data: message,
});
}
/**
* Sends a raw request to the internal MCP service without authentication.
* Useful for testing authentication rejection.
*
* @param message - The JSON-RPC message to send
* @param headers - Optional custom headers
* @returns The API response
*/
async internalMcpSendMessageNoAuth(
message: unknown,
headers?: Record<string, string>,
): Promise<APIResponse> {
return await this.api.request.fetch('/mcp-server/http', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
...headers,
},
data: message,
});
}
/**
* Lists all available tools from the internal MCP service.
*
* @param apiKey - The MCP API key for authentication
* @returns Array of tool definitions
*/
async internalMcpListTools(apiKey: string): Promise<McpToolDefinition[]> {
const message = this.createMessage('tools/list');
const response = await this.internalMcpSendMessage(apiKey, message);
const result = await this.parseResponse<InternalMcpToolsListResult>(response);
return result.tools;
}
/**
* Calls search_workflows tool on the internal MCP service.
*
* @param apiKey - The MCP API key for authentication
* @param args - Search arguments (limit, query, projectId)
* @returns Search results with workflow data
*/
async internalMcpSearchWorkflows(
apiKey: string,
args: { limit?: number; query?: string; projectId?: string } = {},
): Promise<SearchWorkflowsResult> {
const message = this.createMessage('tools/call', {
name: 'search_workflows',
arguments: args,
});
const response = await this.internalMcpSendMessage(apiKey, message);
const contentType = response.headers()['content-type'] ?? '';
const body = await response.text();
// Parse the response (handles both SSE and JSON)
let result: McpToolCallResponse;
if (contentType.includes('text/event-stream')) {
result = this.parseSSEToolResponse(body);
} else {
const parsed = JSON.parse(body) as { result?: McpToolCallResponse; error?: unknown };
if (parsed.error) {
throw new Error(`MCP Error: ${JSON.stringify(parsed.error)}`);
}
result = parsed.result as McpToolCallResponse;
}
// The tool returns structuredContent with the data, or text content with JSON
if (result?.content?.[0]?.text) {
return JSON.parse(result.content[0].text) as SearchWorkflowsResult;
}
throw new Error(
`Unexpected response format from search_workflows: ${JSON.stringify(result ?? body)}`,
);
}
/**
* Calls get_workflow_details tool on the internal MCP service.
*
* @param apiKey - The MCP API key for authentication
* @param workflowId - The workflow ID to get details for
* @returns Workflow details
*/
async internalMcpGetWorkflowDetails(
apiKey: string,
workflowId: string,
): Promise<WorkflowDetailsResult> {
const message = this.createMessage('tools/call', {
name: 'get_workflow_details',
arguments: { workflowId },
});
const response = await this.internalMcpSendMessage(apiKey, message);
const contentType = response.headers()['content-type'] ?? '';
const body = await response.text();
// Parse the response (handles both SSE and JSON)
let result: McpToolCallResponse;
if (contentType.includes('text/event-stream')) {
result = this.parseSSEToolResponse(body);
} else {
const parsed = JSON.parse(body) as { result?: McpToolCallResponse; error?: unknown };
if (parsed.error) {
throw new Error(`MCP Error: ${JSON.stringify(parsed.error)}`);
}
result = parsed.result as McpToolCallResponse;
}
if (result?.content?.[0]?.text) {
return JSON.parse(result.content[0].text) as WorkflowDetailsResult;
}
throw new Error(
`Unexpected response format from get_workflow_details: ${JSON.stringify(result ?? body)}`,
);
}
/**
* Calls execute_workflow tool on the internal MCP service.
*
* @param apiKey - The MCP API key for authentication
* @param workflowId - The workflow ID to execute
* @param inputs - Optional inputs for the workflow
* @returns Execution result
*/
async internalMcpExecuteWorkflow(
apiKey: string,
workflowId: string,
inputs?: Record<string, unknown>,
): Promise<ExecuteWorkflowResult> {
const args: Record<string, unknown> = { workflowId };
if (inputs) {
args.inputs = inputs;
}
const message = this.createMessage('tools/call', {
name: 'execute_workflow',
arguments: args,
});
const response = await this.internalMcpSendMessage(apiKey, message);
const contentType = response.headers()['content-type'] ?? '';
const body = await response.text();
// Parse the response (handles both SSE and JSON)
let result: McpToolCallResponse;
if (contentType.includes('text/event-stream')) {
result = this.parseSSEToolResponse(body);
} else {
const parsed = JSON.parse(body) as { result?: McpToolCallResponse; error?: unknown };
if (parsed.error) {
throw new Error(`MCP Error: ${JSON.stringify(parsed.error)}`);
}
result = parsed.result as McpToolCallResponse;
}
if (result?.content?.[0]?.text) {
return JSON.parse(result.content[0].text) as ExecuteWorkflowResult;
}
throw new Error(
`Unexpected response format from execute_workflow: ${JSON.stringify(result ?? body)}`,
);
}
}
@@ -0,0 +1,388 @@
import { nanoid } from 'nanoid';
import { test, expect } from '../../../fixtures/base';
/**
* E2E tests for the Internal MCP Service (/mcp-server/http).
*
* This tests the built-in MCP server that exposes n8n workflows to external
* MCP clients (like Claude AI). It provides 3 tools:
* - search_workflows: Search for workflows available in MCP
* - get_workflow_details: Get detailed information about a workflow
* - execute_workflow: Execute a workflow and get results
*
* Authentication is via Bearer token (MCP API key).
*
* NOTE: Tests run serially because n8n only supports ONE MCP API key at a time.
* Each test uses rotateMcpApiKey() to get a usable key (since getMcpApiKey()
* returns REDACTED after the first call), and rotation invalidates the previous
* key. Running in parallel would cause race conditions where tests invalidate
* each other's keys.
*/
test.describe('MCP Service', () => {
// Run tests serially - n8n only supports one MCP API key at a time,
// and rotation invalidates the previous key
test.describe.configure({ mode: 'serial' });
// Enable MCP access before each test
test.beforeEach(async ({ api }) => {
await api.setMcpAccess(true);
});
test.describe('Authentication', () => {
test('should reject requests without bearer token', async ({ api }) => {
const message = api.mcp.createMessage('tools/list');
const response = await api.mcp.internalMcpSendMessageNoAuth(message);
expect(response.status()).toBe(401);
});
test('should reject requests with invalid bearer token', async ({ api }) => {
const message = api.mcp.createMessage('tools/list');
const response = await api.mcp.internalMcpSendMessageNoAuth(message, {
Authorization: 'Bearer invalid-token-12345',
});
expect(response.status()).toBe(401);
});
test('should accept valid API key', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
const message = api.mcp.createMessage('tools/list');
const response = await api.mcp.internalMcpSendMessage(apiKey, message);
expect(response.status()).toBeLessThan(300);
});
test('should reject requests after key rotation with old key', async ({ api }) => {
const { apiKey: oldKey } = await api.rotateMcpApiKey();
const { apiKey: newKey } = await api.rotateMcpApiKey();
const message = api.mcp.createMessage('tools/list');
const responseWithOldKey = await api.mcp.internalMcpSendMessageNoAuth(message, {
Authorization: `Bearer ${oldKey}`,
});
expect(responseWithOldKey.status()).toBe(401);
const responseWithNewKey = await api.mcp.internalMcpSendMessage(newKey, message);
expect(responseWithNewKey.status()).toBeLessThan(300);
});
});
test.describe('MCP Settings', () => {
test('should reject when MCP access is disabled', async ({ api }) => {
await api.setMcpAccess(false);
try {
const { apiKey } = await api.rotateMcpApiKey();
const message = api.mcp.createMessage('tools/list');
const response = await api.mcp.internalMcpSendMessage(apiKey, message);
expect(response.status()).toBe(403);
const body = await response.json();
expect(body.message).toContain('MCP access is disabled');
} finally {
await api.setMcpAccess(true);
}
});
});
test.describe('tools/list', () => {
test('should return all 3 built-in tools', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
const tools = await api.mcp.internalMcpListTools(apiKey);
expect(tools).toHaveLength(3);
const toolNames = tools.map((t) => t.name).sort();
expect(toolNames).toEqual(['execute_workflow', 'get_workflow_details', 'search_workflows']);
});
test('should include proper tool descriptions and schemas', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
const tools = await api.mcp.internalMcpListTools(apiKey);
const searchTool = tools.find((t) => t.name === 'search_workflows');
expect(searchTool).toBeDefined();
expect(searchTool!.description).toContain('Search');
expect(searchTool!.inputSchema).toBeDefined();
const detailsTool = tools.find((t) => t.name === 'get_workflow_details');
expect(detailsTool).toBeDefined();
expect(detailsTool!.description).toContain('workflow');
expect(detailsTool!.inputSchema).toBeDefined();
const executeTool = tools.find((t) => t.name === 'execute_workflow');
expect(executeTool).toBeDefined();
expect(executeTool!.description).toContain('Execute');
expect(executeTool!.inputSchema).toBeDefined();
});
});
test.describe('search_workflows', () => {
test('should return workflows marked as available in MCP', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-available-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpSearchWorkflows(apiKey);
expect(result.count).toBeGreaterThanOrEqual(1);
expect(result.data.length).toBeGreaterThanOrEqual(1);
const foundWorkflow = result.data.find((w) => w.id === workflowId);
expect(foundWorkflow).toBeDefined();
expect(foundWorkflow!.active).toBe(true);
expect(foundWorkflow!.nodes).toBeDefined();
expect(foundWorkflow!.scopes).toBeDefined();
});
test('should NOT return workflows not marked as available in MCP', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-unavailable.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpSearchWorkflows(apiKey);
const foundWorkflow = result.data.find((w) => w.id === workflowId);
expect(foundWorkflow).toBeUndefined();
});
test('should support limit parameter', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpSearchWorkflows(apiKey, { limit: 1 });
expect(result.data.length).toBeLessThanOrEqual(1);
});
test('should support query filter for name search', async ({ api }) => {
const uniqueName = `Searchable-${nanoid(8)}`;
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-available-basic.json',
{
transform: (wf) => {
wf.name = uniqueName;
return wf;
},
},
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpSearchWorkflows(apiKey, { query: uniqueName });
expect(result.data.length).toBe(1);
expect(result.data[0].id).toBe(workflowId);
});
test('should return workflow metadata (id, name, nodes, scopes)', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-available-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpSearchWorkflows(apiKey);
const foundWorkflow = result.data.find((w) => w.id === workflowId);
expect(foundWorkflow).toBeDefined();
expect(foundWorkflow!.id).toBe(workflowId);
expect(foundWorkflow!.name).toBeTruthy();
expect(foundWorkflow!.nodes).toBeInstanceOf(Array);
expect(foundWorkflow!.scopes).toBeInstanceOf(Array);
expect(typeof foundWorkflow!.canExecute).toBe('boolean');
});
});
test.describe('get_workflow_details', () => {
test('should return detailed info for accessible workflow', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-available-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpGetWorkflowDetails(apiKey, workflowId);
expect(result.workflow).toBeDefined();
expect(result.workflow.id).toBe(workflowId);
expect(result.workflow.nodes).toBeDefined();
expect(result.workflow.connections).toBeDefined();
expect(result.workflow.settings).toBeDefined();
expect(result.workflow.scopes).toBeDefined();
expect(typeof result.workflow.canExecute).toBe('boolean');
});
test('should return error for non-existent workflow', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
const fakeWorkflowId = 'nonexistent-workflow-id-12345';
await expect(api.mcp.internalMcpGetWorkflowDetails(apiKey, fakeWorkflowId)).rejects.toThrow();
});
test('should return error for workflow not available in MCP', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-unavailable.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
await expect(api.mcp.internalMcpGetWorkflowDetails(apiKey, workflowId)).rejects.toThrow();
});
test('should include trigger info in response', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-available-webhook.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpGetWorkflowDetails(apiKey, workflowId);
expect(result.triggerInfo).toBeDefined();
});
});
test.describe('execute_workflow', () => {
test('should execute workflow successfully', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-available-basic.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpExecuteWorkflow(apiKey, workflowId);
expect(result.success).toBe(true);
expect(result.executionId).toBeTruthy();
expect(result.result).toBeDefined();
});
test('should return error for non-existent workflow', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
const fakeWorkflowId = 'nonexistent-workflow-id-12345';
const result = await api.mcp.internalMcpExecuteWorkflow(apiKey, fakeWorkflowId);
expect(result.success).toBe(false);
expect(result.error).toBeTruthy();
});
test('should return error for workflow not available in MCP', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-unavailable.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpExecuteWorkflow(apiKey, workflowId);
expect(result.success).toBe(false);
expect(result.error).toBeTruthy();
});
test('should execute webhook workflow with inputs', async ({ api }) => {
const { workflowId, createdWorkflow } = await api.workflows.importWorkflowFromFile(
'mcp-service/mcp-available-webhook.json',
);
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
const { apiKey } = await api.rotateMcpApiKey();
const result = await api.mcp.internalMcpExecuteWorkflow(apiKey, workflowId, {
type: 'webhook',
webhookData: {
method: 'POST',
body: { message: 'Hello from MCP test' },
},
});
expect(result.success).toBe(true);
expect(result.executionId).toBeTruthy();
});
});
test.describe('Error Handling', () => {
test('should handle malformed JSON-RPC messages', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
// Missing required 'jsonrpc: "2.0"' field
const malformedMessage = {
id: nanoid(),
method: 'tools/list',
};
const response = await api.mcp.internalMcpSendMessage(apiKey, malformedMessage);
// Server returns 400 Bad Request for malformed JSON-RPC
expect(response.status()).toBe(400);
const body = await response.json();
expect(body.error).toBeDefined();
expect(body.error.code).toBe(-32700); // Parse error
expect(body.error.message).toBeTruthy();
});
test('should handle unknown methods', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
const message = api.mcp.createMessage('unknown/method');
const response = await api.mcp.internalMcpSendMessage(apiKey, message);
// Server returns 200 OK with SSE response containing error
expect(response.status()).toBe(200);
expect(response.headers()['content-type']).toContain('text/event-stream');
// Parse SSE format: extract JSON from "data: {...}" line
const text = await response.text();
const dataLine = text.split('\n').find((line) => line.startsWith('data:'))!;
const body = JSON.parse(dataLine.slice(5).trim()) as {
error: { code: number; message: string };
};
expect(body.error).toBeDefined();
expect(body.error.code).toBe(-32601); // Method not found
expect(body.error.message).toBeTruthy();
});
test('should handle invalid tool parameters', async ({ api }) => {
const { apiKey } = await api.rotateMcpApiKey();
const message = api.mcp.createMessage('tools/call', {
name: 'search_workflows',
arguments: {
limit: 'not-a-number',
},
});
const response = await api.mcp.internalMcpSendMessage(apiKey, message);
// Server returns 200 OK with SSE response
expect(response.ok()).toBe(true);
expect(response.headers()['content-type']).toContain('text/event-stream');
// Parse SSE format: extract JSON from "data: {...}" line
const text = await response.text();
const dataLine = text.split('\n').find((line) => line.startsWith('data:'))!;
const body = JSON.parse(dataLine.slice(5).trim()) as {
error?: unknown;
result?: unknown;
jsonrpc: string;
};
expect(body.jsonrpc).toBe('2.0');
// Should return either a JSON-RPC error or a successful result
expect(body.error !== undefined || body.result !== undefined).toBe(true);
});
});
});
@@ -0,0 +1,64 @@
{
"name": "MCP Available Basic Test",
"active": false,
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"id": "schedule-trigger",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [300, 300]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "set-data-1",
"name": "message",
"value": "Hello from MCP workflow",
"type": "string"
}
]
}
},
"id": "set-node",
"name": "Set Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [500, 300]
}
],
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Set Data",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"availableInMCP": true
},
"staticData": null,
"meta": null,
"pinData": {},
"versionId": null,
"triggerCount": 1,
"tags": []
}
@@ -0,0 +1,68 @@
{
"name": "MCP Available Webhook Test",
"active": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "mcp-test-webhook",
"responseMode": "lastNode",
"options": {}
},
"id": "webhook-trigger",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [300, 300],
"webhookId": "mcp-test-webhook-id"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "set-data-1",
"name": "received",
"value": "={{ $json.body }}",
"type": "string"
},
{
"id": "set-data-2",
"name": "status",
"value": "processed",
"type": "string"
}
]
}
},
"id": "process-node",
"name": "Process Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [500, 300]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Process Data",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"availableInMCP": true
},
"staticData": null,
"meta": null,
"pinData": {},
"versionId": null,
"triggerCount": 1,
"tags": []
}
@@ -0,0 +1,64 @@
{
"name": "MCP Unavailable Test",
"active": false,
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"id": "schedule-trigger",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [300, 300]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "set-data-1",
"name": "message",
"value": "This workflow is NOT available in MCP",
"type": "string"
}
]
}
},
"id": "set-node",
"name": "Set Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [500, 300]
}
],
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Set Data",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"availableInMCP": false
},
"staticData": null,
"meta": null,
"pinData": {},
"versionId": null,
"triggerCount": 1,
"tags": []
}