From 74a2b2a3abcf7a75efdda0073b71193e9c7dee9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Mon, 27 Oct 2025 11:36:23 +0100 Subject: [PATCH] feat(editor): Update MCP Access docs and telemetry (no-changelog) (#21157) --- .../get-workflow-details.tool.test.ts | 5 ++ .../__tests__/search-workflows.tool.test.ts | 11 +++- .../cli/src/modules/mcp/mcp.controller.ts | 31 +++------- packages/cli/src/modules/mcp/mcp.service.ts | 9 ++- packages/cli/src/modules/mcp/mcp.types.ts | 5 ++ .../mcp/tools/get-workflow-details.tool.ts | 62 +++++++++++++++---- .../mcp/tools/search-workflows.tool.ts | 62 ++++++++++++++----- .../features/ai/mcpAccess/mcp.constants.ts | 3 +- 8 files changed, 131 insertions(+), 57 deletions(-) diff --git a/packages/cli/src/modules/mcp/__tests__/get-workflow-details.tool.test.ts b/packages/cli/src/modules/mcp/__tests__/get-workflow-details.tool.test.ts index 3c6ebfbadb5..291d4db0a2a 100644 --- a/packages/cli/src/modules/mcp/__tests__/get-workflow-details.tool.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/get-workflow-details.tool.test.ts @@ -5,6 +5,7 @@ import { createWorkflow } from './mock.utils'; import { getWorkflowDetails, createWorkflowDetailsTool } from '../tools/get-workflow-details.tool'; import { CredentialsService } from '@/credentials/credentials.service'; +import { Telemetry } from '@/telemetry'; import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; jest.mock('../tools/webhook-utils', () => ({ @@ -21,6 +22,9 @@ describe('get-workflow-details MCP tool', () => { findWorkflowForUser: jest.fn(), }); const credentialsService = mockInstance(CredentialsService, {}); + const telemetry = mockInstance(Telemetry, { + track: jest.fn(), + }); const endpoints = { webhook: 'webhook', webhookTest: 'webhook-test' }; const tool = createWorkflowDetailsTool( @@ -29,6 +33,7 @@ describe('get-workflow-details MCP tool', () => { workflowFinderService, credentialsService, endpoints, + telemetry, ); expect(tool.name).toBe('get_workflow_details'); diff --git a/packages/cli/src/modules/mcp/__tests__/search-workflows.tool.test.ts b/packages/cli/src/modules/mcp/__tests__/search-workflows.tool.test.ts index 75aaa2cdf54..8632b6c0de1 100644 --- a/packages/cli/src/modules/mcp/__tests__/search-workflows.tool.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/search-workflows.tool.test.ts @@ -5,6 +5,7 @@ import type { INode } from 'n8n-workflow'; import { createWorkflow } from './mock.utils'; import { searchWorkflows, createSearchWorkflowsTool } from '../tools/search-workflows.tool'; +import { Telemetry } from '@/telemetry'; import { WorkflowService } from '@/workflows/workflow.service'; import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from 'n8n-workflow'; @@ -25,7 +26,15 @@ describe('search-workflows MCP tool', () => { getMany: jest.fn().mockResolvedValue({ workflows, count: 1 }), }); - const tool = createSearchWorkflowsTool(user, workflowService as unknown as WorkflowService); + const telemetry = mockInstance(Telemetry, { + track: jest.fn(), + }); + + const tool = createSearchWorkflowsTool( + user, + workflowService as unknown as WorkflowService, + telemetry, + ); expect(tool.name).toBe('search_workflows'); expect(tool.config).toBeDefined(); diff --git a/packages/cli/src/modules/mcp/mcp.controller.ts b/packages/cli/src/modules/mcp/mcp.controller.ts index 6594f0b7d9a..f881fa300f7 100644 --- a/packages/cli/src/modules/mcp/mcp.controller.ts +++ b/packages/cli/src/modules/mcp/mcp.controller.ts @@ -8,15 +8,14 @@ import { ErrorReporter } from 'n8n-core'; import { McpServerApiKeyService } from './mcp-api-key.service'; import { USER_CONNECTED_TO_MCP_EVENT, - USER_CALLED_MCP_TOOL_EVENT, MCP_ACCESS_DISABLED_ERROR_MESSAGE, INTERNAL_SERVER_ERROR_MESSAGE, } from './mcp.constants'; import { McpService } from './mcp.service'; import { McpSettingsService } from './mcp.settings.service'; import { isJSONRPCRequest } from './mcp.typeguards'; -import type { UserConnectedToMCPEventPayload, UserCalledMCPToolEventPayload } from './mcp.types'; -import { getClientInfo, getToolName, getToolArguments } from './mcp.utils'; +import type { UserConnectedToMCPEventPayload } from './mcp.types'; +import { getClientInfo } from './mcp.utils'; import { Telemetry } from '@/telemetry'; @@ -42,7 +41,6 @@ export class McpController { async build(req: AuthenticatedRequest, res: FlushableResponse) { const body = req.body; const isInitializationRequest = isJSONRPCRequest(body) ? body.method === 'initialize' : false; - const isToolCallRequest = isJSONRPCRequest(body) ? body.method === 'tools/call' : false; const clientInfo = getClientInfo(req); const telemetryPayload: Partial = { @@ -55,7 +53,7 @@ export class McpController { const enabled = await this.mcpSettingsService.getEnabled(); if (!enabled) { if (isInitializationRequest) { - this.trackMCPEvent('connected', { + this.trackConnectionEvent({ ...telemetryPayload, mcp_connection_status: 'error', error: MCP_ACCESS_DISABLED_ERROR_MESSAGE, @@ -80,23 +78,15 @@ export class McpController { await server.connect(transport); await transport.handleRequest(req, res, req.body); if (isInitializationRequest) { - this.trackMCPEvent('connected', { + this.trackConnectionEvent({ ...telemetryPayload, mcp_connection_status: 'success', }); - } else if (isToolCallRequest) { - const toolName = getToolName(body); - const parameters = getToolArguments(body); - this.trackMCPEvent('tool_call', { - user_id: req.user.id, - tool_name: toolName, - parameters, - }); } } catch (error) { this.errorReporter.error(error); if (isInitializationRequest) { - this.trackMCPEvent('connected', { + this.trackConnectionEvent({ ...telemetryPayload, mcp_connection_status: 'error', error: error instanceof Error ? error.message : String(error), @@ -116,14 +106,7 @@ export class McpController { } } - private trackMCPEvent( - type: 'connected' | 'tool_call', - payload: UserConnectedToMCPEventPayload | UserCalledMCPToolEventPayload, - ) { - if (type === 'connected') { - this.telemetry.track(USER_CONNECTED_TO_MCP_EVENT, payload); - } else if (type === 'tool_call') { - this.telemetry.track(USER_CALLED_MCP_TOOL_EVENT, payload); - } + private trackConnectionEvent(payload: UserConnectedToMCPEventPayload) { + this.telemetry.track(USER_CONNECTED_TO_MCP_EVENT, payload); } } diff --git a/packages/cli/src/modules/mcp/mcp.service.ts b/packages/cli/src/modules/mcp/mcp.service.ts index 953375d412e..126715fd846 100644 --- a/packages/cli/src/modules/mcp/mcp.service.ts +++ b/packages/cli/src/modules/mcp/mcp.service.ts @@ -8,6 +8,7 @@ import { createSearchWorkflowsTool } from './tools/search-workflows.tool'; import { CredentialsService } from '@/credentials/credentials.service'; import { UrlService } from '@/services/url.service'; +import { Telemetry } from '@/telemetry'; import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import { WorkflowService } from '@/workflows/workflow.service'; @@ -19,6 +20,7 @@ export class McpService { private readonly urlService: UrlService, private readonly credentialsService: CredentialsService, private readonly globalConfig: GlobalConfig, + private readonly telemetry: Telemetry, ) {} getServer(user: User) { @@ -27,7 +29,11 @@ export class McpService { version: '1.0.0', }); - const workflowSearchTool = createSearchWorkflowsTool(user, this.workflowService); + const workflowSearchTool = createSearchWorkflowsTool( + user, + this.workflowService, + this.telemetry, + ); server.registerTool( workflowSearchTool.name, workflowSearchTool.config, @@ -43,6 +49,7 @@ export class McpService { webhook: this.globalConfig.endpoints.webhook, webhookTest: this.globalConfig.endpoints.webhookTest, }, + this.telemetry, ); server.registerTool( workflowDetailsTool.name, diff --git a/packages/cli/src/modules/mcp/mcp.types.ts b/packages/cli/src/modules/mcp/mcp.types.ts index 190d8eab03b..281ef4f7535 100644 --- a/packages/cli/src/modules/mcp/mcp.types.ts +++ b/packages/cli/src/modules/mcp/mcp.types.ts @@ -67,4 +67,9 @@ export type UserCalledMCPToolEventPayload = { user_id?: string; tool_name: string; parameters?: Record; + results?: { + success: boolean; + data?: unknown; + error?: string; + }; }; diff --git a/packages/cli/src/modules/mcp/tools/get-workflow-details.tool.ts b/packages/cli/src/modules/mcp/tools/get-workflow-details.tool.ts index bdc9fdeeaca..a058ce6e1c2 100644 --- a/packages/cli/src/modules/mcp/tools/get-workflow-details.tool.ts +++ b/packages/cli/src/modules/mcp/tools/get-workflow-details.tool.ts @@ -2,11 +2,17 @@ import type { User } from '@n8n/db'; import { UserError, WEBHOOK_NODE_TYPE } from 'n8n-workflow'; import z from 'zod'; -import type { ToolDefinition, WorkflowDetailsResult } from '../mcp.types'; +import { USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants'; +import type { + ToolDefinition, + WorkflowDetailsResult, + UserCalledMCPToolEventPayload, +} from '../mcp.types'; import { workflowDetailsOutputSchema } from './schemas'; import { getWebhookDetails, type WebhookEndpoints } from './webhook-utils'; import type { CredentialsService } from '@/credentials/credentials.service'; +import type { Telemetry } from '@/telemetry'; import type { WorkflowFinderService } from '@/workflows/workflow-finder.service'; const inputSchema = { @@ -26,6 +32,7 @@ export const createWorkflowDetailsTool = ( workflowFinderService: WorkflowFinderService, credentialsService: CredentialsService, endpoints: WebhookEndpoints, + telemetry: Telemetry, ): ToolDefinition => { return { name: 'get_workflow_details', @@ -35,19 +42,48 @@ export const createWorkflowDetailsTool = ( outputSchema, }, handler: async ({ workflowId }) => { - const payload = await getWorkflowDetails( - user, - baseWebhookUrl, - workflowFinderService, - credentialsService, - endpoints, - { workflowId }, - ); - - return { - content: [{ type: 'text', text: JSON.stringify(payload) }], - structuredContent: payload, + const parameters = { workflowId }; + const telemetryPayload: UserCalledMCPToolEventPayload = { + user_id: user.id, + tool_name: 'get_workflow_details', + parameters, }; + + try { + const payload = await getWorkflowDetails( + user, + baseWebhookUrl, + workflowFinderService, + credentialsService, + endpoints, + { workflowId }, + ); + + // Track successful execution + telemetryPayload.results = { + success: true, + data: { + workflow_id: workflowId, + workflow_name: payload.workflow.name, + trigger_count: payload.workflow.triggerCount, + node_count: payload.workflow.nodes.length, + }, + }; + telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); + + return { + content: [{ type: 'text', text: JSON.stringify(payload) }], + structuredContent: payload, + }; + } catch (error) { + // Track failed execution + telemetryPayload.results = { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); + throw error; + } }, }; }; diff --git a/packages/cli/src/modules/mcp/tools/search-workflows.tool.ts b/packages/cli/src/modules/mcp/tools/search-workflows.tool.ts index 95bcbb447f4..de9bca88abc 100644 --- a/packages/cli/src/modules/mcp/tools/search-workflows.tool.ts +++ b/packages/cli/src/modules/mcp/tools/search-workflows.tool.ts @@ -2,15 +2,18 @@ import { type User, type WorkflowEntity } from '@n8n/db'; import type { INode } from 'n8n-workflow'; import z from 'zod'; +import { USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants'; import type { ToolDefinition, SearchWorkflowsParams, SearchWorkflowsResult, SearchWorkflowsItem, + UserCalledMCPToolEventPayload, } from '../mcp.types'; import { nodeSchema } from './schemas'; import type { ListQuery } from '@/requests'; +import type { Telemetry } from '@/telemetry'; import type { WorkflowService } from '@/workflows/workflow.service'; const MAX_RESULTS = 200; @@ -52,6 +55,7 @@ const outputSchema = { export const createSearchWorkflowsTool = ( user: User, workflowService: WorkflowService, + telemetry: Telemetry, ): ToolDefinition => { return { name: 'search_workflows', @@ -62,23 +66,49 @@ export const createSearchWorkflowsTool = ( outputSchema, }, handler: async ({ limit = MAX_RESULTS, active, name, projectId }) => { - const payload: SearchWorkflowsResult = await searchWorkflows(user, workflowService, { - limit, - active, - name, - projectId, - }); - - return { - structuredContent: payload, - // Keeping text content for compatibility with mcp clients that don's support structuredContent - content: [ - { - type: 'text', - text: JSON.stringify(payload), - }, - ], + const parameters = { limit, active, name, projectId }; + const telemetryPayload: UserCalledMCPToolEventPayload = { + user_id: user.id, + tool_name: 'search_workflows', + parameters, }; + + try { + const payload: SearchWorkflowsResult = await searchWorkflows(user, workflowService, { + limit, + active, + name, + projectId, + }); + + // Track successful execution + telemetryPayload.results = { + success: true, + data: { + count: payload.count, + }, + }; + telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); + + return { + structuredContent: payload, + // Keeping text content for compatibility with mcp clients that don's support structuredContent + content: [ + { + type: 'text', + text: JSON.stringify(payload), + }, + ], + }; + } catch (error) { + // Track failed execution + telemetryPayload.results = { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); + throw error; + } }, }; }; diff --git a/packages/frontend/editor-ui/src/features/ai/mcpAccess/mcp.constants.ts b/packages/frontend/editor-ui/src/features/ai/mcpAccess/mcp.constants.ts index 8b9735d1f82..b09d38de6a6 100644 --- a/packages/frontend/editor-ui/src/features/ai/mcpAccess/mcp.constants.ts +++ b/packages/frontend/editor-ui/src/features/ai/mcpAccess/mcp.constants.ts @@ -4,5 +4,4 @@ export const MCP_STORE = 'mcp'; export const LOADING_INDICATOR_TIMEOUT = 200; -// TODO: Update when we have the final docs page URL -export const MCP_DOCS_PAGE_URL = 'https://docs.n8n.io'; +export const MCP_DOCS_PAGE_URL = 'https://docs.n8n.io/advanced-ai/accessing-n8n-mcp-server';