Root cause
Authentication & access control introduced
feat: implement n8n integration improvements and protocol version negotiation
How AI contributed: Direct introductionPublished Apr 25, 2026 in
czlonkowski/n8n-mcpAll 11 findings in this repository
The AI change in src/mcp/server.ts expanded initialize logging to record client identity, capabilities, and protocol negotiation, and in the same pass began serializing complete tool-call argument objects, including the raw parameters of each request, into its log output. On authenticated HTTP requests those lines carry caller-supplied secrets, so anyone who can read the logs can recover them. The fix replaces the values with metadata-only summaries produced by summarizeToolCallArgs in src/utils/redaction.ts.
Tool-call logging emitted whole argument objects as plaintext, so any credential a caller placed in an argument was persisted verbatim in the log stream. The redaction helper summarizeToolCallArgs now reports only type, key names, nesting, and size, never the values.
Root cause
feat: implement n8n integration improvements and protocol version negotiation
Fix
The fix replaces raw tool-call argument logging with metadata-only summaries of type, key names, nesting, and approximate size, and removes argument values from validation and coercion log fields.
Case facts
@@ -59,7 +59,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [ }, { name: 'get_node_info',- description: `Get FULL node schema (100KB+). TIP: Use get_node_essentials first! Returns all properties/operations/credentials. Prefix required: "nodes-base.httpRequest" not "httpRequest".`,+ description: `Get full node documentation. Pass nodeType as string with prefix. Example: nodeType="nodes-base.webhook"`, inputSchema: { type: 'object', properties: {@@ -73,7 +73,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [ }, { name: 'search_nodes',- description: `Search nodes by keywords. Modes: OR (any word), AND (all words), FUZZY (typos OK). Primary nodes ranked first. Examples: "webhook"→Webhook, "http call"→HTTP Request.`,+ description: `Search n8n nodes by keyword. Pass query as string. Example: query="webhook" or query="database". Returns max 20 results.`, inputSchema: { type: 'object', properties: {@@ -128,7 +128,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [ }, { name: 'get_node_essentials',- description: `Get 10-20 key properties only (<5KB vs 100KB+). USE THIS FIRST! Includes examples. Format: "nodes-base.httpRequest"`,+ description: `Get node essential info. Pass nodeType as string with prefix. Example: nodeType="nodes-base.slack"`, inputSchema: { type: 'object', properties: {@@ -192,44 +192,103 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [ }, { name: 'validate_node_operation',- description: `Validate node config. Checks required fields, types, operation rules. Returns errors with fixes. Essential for Slack/Sheets/DB nodes.`,+ description: `Validate n8n node configuration. Pass nodeType as string and config as object. Example: nodeType="nodes-base.slack", config={resource:"channel",operation:"create"}`, inputSchema: { type: 'object', properties: { nodeType: { type: 'string',- description: 'The node type to validate (e.g., "nodes-base.slack")',+ description: 'Node type as string. Example: "nodes-base.slack"', }, config: { type: 'object',- description: 'Your node configuration. Must include operation fields (resource/operation/action) if the node has multiple operations.',+ description: 'Configuration as object. For simple nodes use {}. For complex nodes include fields like {resource:"channel",operation:"create"}', }, profile: { type: 'string', enum: ['strict', 'runtime', 'ai-friendly', 'minimal'],- description: 'Validation profile: minimal (only required fields), runtime (critical errors only), ai-friendly (balanced - default), strict (all checks including best practices)',+ description: 'Profile string: "minimal", "runtime", "ai-friendly", or "strict". Default is "ai-friendly"', default: 'ai-friendly', }, }, required: ['nodeType', 'config'],+ additionalProperties: false,+ },+ outputSchema: {+ type: 'object',+ properties: {+ nodeType: { type: 'string' },+ workflowNodeType: { type: 'string' },+ displayName: { type: 'string' },+ valid: { type: 'boolean' },+ errors: {+ type: 'array',+ items: {+ type: 'object',+ properties: {+ type: { type: 'string' },+ property: { type: 'string' },+ message: { type: 'string' },+ fix: { type: 'string' }+ }+ }+ },+ warnings: {+ type: 'array',+ items: {+ type: 'object',+ properties: {+ type: { type: 'string' },+ property: { type: 'string' },+ message: { type: 'string' },+ suggestion: { type: 'string' }+ }+ }+ },+ suggestions: { type: 'array', items: { type: 'string' } },+ summary: {@@ -0,0 +1,175 @@+/**+ * n8n-friendly tool descriptions+ * These descriptions are optimized to reduce schema validation errors in n8n's AI Agent+ * + * Key principles:+ * 1. Use exact JSON examples in descriptions+ * 2. Be explicit about data types+ * 3. Keep descriptions short and directive+ * 4. Avoid ambiguity+ */++export const n8nFriendlyDescriptions: Record<string, {+ description: string;+ params: Record<string, string>;+}> = {+ // Validation tools - most prone to errors+ validate_node_operation: {+ description: 'Validate n8n node. ALWAYS pass two parameters: nodeType (string) and config (object). Example call: {"nodeType": "nodes-base.slack", "config": {"resource": "channel", "operation": "create"}}',+ params: {+ nodeType: 'String value like "nodes-base.slack"',+ config: 'Object value like {"resource": "channel", "operation": "create"} or empty object {}',+ profile: 'Optional string: "minimal" or "runtime" or "ai-friendly" or "strict"'+ }+ },+ + validate_node_minimal: {+ description: 'Check required fields. MUST pass: nodeType (string) and config (object). Example: {"nodeType": "nodes-base.webhook", "config": {}}',+ params: {+ nodeType: 'String like "nodes-base.webhook"',+ config: 'Object, use {} for empty'+ }+ },+ + // Search and info tools+ search_nodes: {+ description: 'Search nodes. Pass query (string). Example: {"query": "webhook"}',+ params: {+ query: 'String keyword like "webhook" or "database"',+ limit: 'Optional number, default 20'+ }+ },+ + get_node_info: {+ description: 'Get node details. Pass nodeType (string). Example: {"nodeType": "nodes-base.httpRequest"}',+ params: {+ nodeType: 'String with prefix like "nodes-base.httpRequest"'+ }+ },+ + get_node_essentials: {+ description: 'Get node basics. Pass nodeType (string). Example: {"nodeType": "nodes-base.slack"}',+ params: {+ nodeType: 'String with prefix like "nodes-base.slack"'+ }+ },+ + // Task tools+ get_node_for_task: {+ description: 'Find node for task. Pass task (string). Example: {"task": "send_http_request"}',+ params: {+ task: 'String task name like "send_http_request"'+ }+ },+ + list_tasks: {+ description: 'List tasks by category. Pass category (string). Example: {"category": "HTTP/API"}',+ params: {+ category: 'String: "HTTP/API" or "Webhooks" or "Database" or "AI/LangChain" or "Data Processing" or "Communication"'+ }+ },+ + // Workflow validation+ validate_workflow: {+ description: 'Validate workflow. Pass workflow object. MUST have: {"workflow": {"nodes": [array of node objects], "connections": {object with node connections}}}. Each node needs: name, type, typeVersion, position.',+ params: {+ workflow: 'Object with two required fields: nodes (array) and connections (object). Example: {"nodes": [{"name": "Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [250, 300], "parameters": {}}], "connections": {}}',+ options: 'Optional object. Example: {"validateNodes": true, "profile": "runtime"}'+ }+ },+ + validate_workflow_connections: {+ description: 'Validate workflow connections only. Pass workflow object. Example: {"workflow": {"nodes": [...], "connections": {}}}',+ params: {+ workflow: 'Object with nodes array and connections object. Minimal example: {"nodes": [{"name": "Webhook"}], "connections": {}}'+ }+ },+ + validate_workflow_expressions: {+ description: 'Validate n8n expressions in workflow. Pass workflow object. Example: {"workflow": {"nodes": [...], "connections": {}}}',@@ -9,6 +9,8 @@ import { existsSync, promises as fs } from 'fs'; import path from 'path'; import { n8nDocumentationToolsFinal } from './tools'; import { n8nManagementTools } from './tools-n8n-manager';+import { makeToolsN8nFriendly } from './tools-n8n-friendly';+import { getWorkflowExampleString } from './workflow-examples'; import { logger } from '../utils/logger'; import { NodeRepository } from '../database/node-repository'; import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';@@ -26,6 +28,11 @@ import { handleUpdatePartialWorkflow } from './handlers-workflow-diff'; import { getToolDocumentation, getToolsOverview } from './tools-documentation'; import { PROJECT_VERSION } from '../utils/version'; import { normalizeNodeType, getNodeTypeAlternatives, getWorkflowNodeType } from '../utils/node-utils';+import { + negotiateProtocolVersion, + logProtocolNegotiation,+ STANDARD_PROTOCOL_VERSION +} from '../utils/protocol-version'; interface NodeRow { node_type: string;@@ -52,6 +59,7 @@ export class N8NDocumentationMCPServer { private templateService: TemplateService | null = null; private initialized: Promise<void>; private cache = new SimpleCache();+ private clientInfo: any = null; constructor() { // Check for test environment first@@ -154,9 +162,39 @@ export class N8NDocumentationMCPServer { private setupHandlers(): void { // Handle initialization- this.server.setRequestHandler(InitializeRequestSchema, async () => {+ this.server.setRequestHandler(InitializeRequestSchema, async (request) => {+ const clientVersion = request.params.protocolVersion;+ const clientCapabilities = request.params.capabilities;+ const clientInfo = request.params.clientInfo;+ + logger.info('MCP Initialize request received', {+ clientVersion,+ clientCapabilities,+ clientInfo+ });+ + // Store client info for later use+ this.clientInfo = clientInfo;+ + // Negotiate protocol version based on client information+ const negotiationResult = negotiateProtocolVersion(+ clientVersion,+ clientInfo,+ undefined, // no user agent in MCP protocol+ undefined // no headers in MCP protocol+ );+ + logProtocolNegotiation(negotiationResult, logger, 'MCP_INITIALIZE');+ + // Warn if there's a version mismatch (for debugging)+ if (clientVersion && clientVersion !== negotiationResult.version) {+ logger.warn(`Protocol version negotiated: client requested ${clientVersion}, server will use ${negotiationResult.version}`, {+ reasoning: negotiationResult.reasoning+ });+ }+ const response = {- protocolVersion: '2024-11-05',+ protocolVersion: negotiationResult.version, capabilities: { tools: {}, },@@ -166,18 +204,14 @@ export class N8NDocumentationMCPServer { }, }; - // Debug logging- if (process.env.DEBUG_MCP === 'true') {- logger.debug('Initialize handler called', { response });- }- + logger.info('MCP Initialize response', { response }); return response; }); // Handle tool listing- this.server.setRequestHandler(ListToolsRequestSchema, async () => {+ this.server.setRequestHandler(ListToolsRequestSchema, async (request) => { // Combine documentation tools with management tools if API is configured- const tools = [...n8nDocumentationToolsFinal];+ let tools = [...n8nDocumentationToolsFinal];@@ -15,6 +15,7 @@ import { n8nManagementTools } from './tools-n8n-manager'; import { makeToolsN8nFriendly } from './tools-n8n-friendly'; import { getWorkflowExampleString } from './workflow-examples'; import { logger } from '../utils/logger';+import { summarizeToolCallArgs } from '../utils/redaction'; import { NodeRepository } from '../database/node-repository'; import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter'; import { getSharedDatabase, releaseSharedDatabase, SharedDatabaseState } from '../database/shared-database';@@ -686,16 +687,12 @@ export class N8NDocumentationMCPServer { this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; - // Enhanced logging for debugging tool calls- logger.info('Tool call received - DETAILED DEBUG', {+ // SECURITY (GHSA-wg4g-395p-mqv3): log metadata only, not raw arg values.+ logger.info('Tool call received', { toolName: name,- arguments: JSON.stringify(args, null, 2),- argumentsType: typeof args,- argumentsKeys: args ? Object.keys(args) : [],- hasNodeType: args && 'nodeType' in args,- hasConfig: args && 'config' in args,- configType: args && args.config ? typeof args.config : 'N/A',- rawRequest: JSON.stringify(request.params)+ ...summarizeToolCallArgs(args),+ hasNodeType: !!(args && typeof args === 'object' && 'nodeType' in args),+ hasConfig: !!(args && typeof args === 'object' && 'config' in args), }); // Check if tool is disabled via DISABLED_TOOLS environment variable@@ -738,19 +735,21 @@ export class N8NDocumentationMCPServer { if (typeof possibleNestedData === 'string' && possibleNestedData.trim().startsWith('{')) { const parsed = JSON.parse(possibleNestedData); if (parsed && typeof parsed === 'object') {+ // SECURITY (GHSA-wg4g-395p-mqv3): log key shape only, not values. logger.warn('Detected n8n nested output bug, attempting to extract actual arguments', {- originalArgs: args,- extractedArgs: parsed+ toolName: name,+ originalArgsKeys: Object.keys(args),+ extractedArgsKeys: Object.keys(parsed), });- + // Validate the extracted arguments match expected tool schema if (this.validateExtractedArgs(name, parsed)) { // Use the extracted data as args processedArgs = parsed; } else { logger.warn('Extracted arguments failed validation, using original args', { toolName: name,- extractedArgs: parsed+ extractedArgsKeys: Object.keys(parsed), }); } }@@ -776,7 +775,8 @@ export class N8NDocumentationMCPServer { } try {- logger.debug(`Executing tool: ${name}`, { args: processedArgs });+ // SECURITY (GHSA-wg4g-395p-mqv3): log metadata only, not raw arg values.+ logger.debug(`Executing tool: ${name}`, summarizeToolCallArgs(processedArgs)); const startTime = Date.now(); const result = await this.executeTool(name, processedArgs); const duration = Date.now() - startTime;@@ -1152,8 +1152,8 @@ export class N8NDocumentationMCPServer { if (!(requiredField in args)) { logger.debug(`Extracted args missing required field: ${requiredField}`, { toolName,- extractedArgs: args,- required+ extractedArgsKeys: Object.keys(args),+ required, }); return false; }@@ -1172,11 +1172,11 @@ export class N8NDocumentationMCPServer { continue; } + // SECURITY (GHSA-wg4g-395p-mqv3): log type mismatch shape only, not the value. logger.debug(`Extracted args field type mismatch: ${fieldName}`, { toolName, expectedType, actualType,- fieldValue }); return false; }@@ -50,3 +50,35 @@ export function summarizeMcpBody(body: unknown): Record<string, unknown> { hasParams: b.params !== undefined && b.params !== null, }; }++/**+ * Reduce MCP tool-call arguments to a safe metadata summary. Returns the type,+ * the top-level key list, a hasNestedOutput flag for the n8n nested-output+ * workaround, and an approximate serialized size. Never returns values.+ */+export function summarizeToolCallArgs(args: unknown): Record<string, unknown> {+ if (args === undefined || args === null) {+ return { argsType: args === null ? 'null' : 'undefined' };+ }+ if (typeof args !== 'object' || Array.isArray(args)) {+ let size: number | undefined;+ if (typeof args === 'string') size = args.length;+ return {+ argsType: Array.isArray(args) ? 'array' : typeof args,+ ...(size !== undefined ? { size } : {}),+ };+ }+ const keys = Object.keys(args as Record<string, unknown>);+ let size: number | undefined;+ try {+ size = JSON.stringify(args).length;+ } catch {+ size = undefined;+ }+ return {+ argsType: 'object',+ argsKeys: keys,+ hasNestedOutput: keys.includes('output'),+ ...(size !== undefined ? { size } : {}),+ };+}AI-assisted change 6b5f094acc4ba2a8b564f475e8682ac641484ade538706a79b5f6229a8342acb · Fix 9ea356ac26aea1e6f63ce924fe0680d24552cb6bfadb35c474f917be5ae21a3a