feat(editor): Update MCP Access docs and telemetry (no-changelog) (#21157)

This commit is contained in:
Milorad FIlipović
2025-10-27 11:36:23 +01:00
committed by GitHub
parent c859f4e793
commit 74a2b2a3ab
8 changed files with 131 additions and 57 deletions
@@ -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');
@@ -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();
+7 -24
View File
@@ -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<UserConnectedToMCPEventPayload> = {
@@ -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);
}
}
+8 -1
View File
@@ -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,
@@ -67,4 +67,9 @@ export type UserCalledMCPToolEventPayload = {
user_id?: string;
tool_name: string;
parameters?: Record<string, unknown>;
results?: {
success: boolean;
data?: unknown;
error?: string;
};
};
@@ -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<typeof inputSchema> => {
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;
}
},
};
};
@@ -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<typeof inputSchema> => {
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;
}
},
};
};
@@ -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';