mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat(editor): Update MCP Access docs and telemetry (no-changelog) (#21157)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
Reference in New Issue
Block a user