Root cause
Authentication & access control introduced
fix: deprecate USE_FIXED_HTTP for SSE streaming support (Issue #524) (#525)
AI-assisted change: Claude
How AI contributed: Direct introductionPublished May 18, 2026 in
czlonkowski/n8n-mcpAll 11 findings in this repository
n8n-MCP: Multi-tenant MCP requests fall back to process-level n8n credentials when tenant headers are absent or incomplete. Sink: .env.example.
Root cause
fix: deprecate USE_FIXED_HTTP for SSE streaming support (Issue #524) (#525)
AI-assisted change: Claude
Fix
In multi-tenant mode, the fix rejects requests with no tenant headers, refuses environment credential fallback for missing or incomplete tenant context, and hides operator URL/key data from trigger and diagnostic responses.
Fix by Romuald Członkowski · no AI marker found
Case facts
@@ -1,7 +1,14 @@ #!/usr/bin/env node /**- * Fixed HTTP server for n8n-MCP that properly handles StreamableHTTPServerTransport initialization- * This implementation ensures the transport is properly initialized before handling requests+ * @deprecated This fixed HTTP server is deprecated as of v2.31.8.+ * Use SingleSessionHTTPServer from http-server-single-session.ts instead.+ *+ * This implementation does not support SSE streaming required by clients like OpenAI Codex.+ * See: https://github.com/czlonkowski/n8n-mcp/issues/524+ *+ * Original purpose: Fixed HTTP server for n8n-MCP that properly handles+ * StreamableHTTPServerTransport initialization by bypassing it entirely.+ * This implementation ensures the transport is properly initialized before handling requests. */ import express from 'express'; import { Server } from '@modelcontextprotocol/sdk/server/index.js';@@ -125,7 +132,18 @@ async function shutdown() { } } +/**+ * @deprecated Use SingleSessionHTTPServer from http-server-single-session.ts instead.+ * This function does not support SSE streaming required by clients like OpenAI Codex.+ */ export async function startFixedHTTPServer() {+ // Log deprecation warning+ logger.warn(+ 'DEPRECATION: startFixedHTTPServer() is deprecated as of v2.31.8. ' ++ 'Use SingleSessionHTTPServer which supports SSE streaming. ' ++ 'See: https://github.com/czlonkowski/n8n-mcp/issues/524'+ );+ validateEnvironment(); const app = express();@@ -71,10 +71,12 @@ const testCases: TestCase[] = [ } }, {- name: 'Fixed HTTP implementation',+ // DEPRECATED: This test case tests the deprecated fixed HTTP implementation+ // See: https://github.com/czlonkowski/n8n-mcp/issues/524+ name: 'Fixed HTTP implementation (DEPRECATED)', env: { MCP_MODE: 'http',- USE_FIXED_HTTP: 'true',+ USE_FIXED_HTTP: 'true', // DEPRECATED: Will be removed in future version AUTH_TOKEN: 'test-token-for-testing-only', PORT: '3005', BASE_URL: 'https://fixed.example.com'@@ -124,9 +124,23 @@ Learn more: https://github.com/czlonkowski/n8n-mcp/blob/main/PRIVACY.md checkpoints.push(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING); if (mode === 'http') {- // Check if we should use the fixed implementation+ // Check if we should use the fixed implementation (DEPRECATED) if (process.env.USE_FIXED_HTTP === 'true') {- // Use the fixed HTTP implementation that bypasses StreamableHTTPServerTransport issues+ // DEPRECATION WARNING: Fixed HTTP implementation is deprecated+ // It does not support SSE streaming required by clients like OpenAI Codex+ logger.warn(+ 'DEPRECATION WARNING: USE_FIXED_HTTP=true is deprecated as of v2.31.8. ' ++ 'The fixed HTTP implementation does not support SSE streaming required by clients like OpenAI Codex. ' ++ 'Please unset USE_FIXED_HTTP to use the modern SingleSessionHTTPServer which supports both JSON-RPC and SSE. ' ++ 'This option will be removed in a future version. See: https://github.com/czlonkowski/n8n-mcp/issues/524'+ );+ console.warn('\n⚠️ DEPRECATION WARNING ⚠️');+ console.warn('USE_FIXED_HTTP=true is deprecated as of v2.31.8.');+ console.warn('The fixed HTTP implementation does not support SSE streaming.');+ console.warn('Please unset USE_FIXED_HTTP to use SingleSessionHTTPServer.');+ console.warn('See: https://github.com/czlonkowski/n8n-mcp/issues/524\n');++ // Use the deprecated fixed HTTP implementation const { startFixedHTTPServer } = await import('../http-server'); await startFixedHTTPServer(); } else {@@ -78,6 +78,12 @@ export abstract class BaseTriggerHandler<T extends BaseTriggerInput = BaseTrigge if (this.context?.n8nApiUrl) { return this.context.n8nApiUrl.replace(/\/api\/v1\/?$/, ''); }+ // SECURITY (GHSA-jxx9-px88-pj69): in multi-tenant mode, refuse to fall+ // back to the operator's env URL. A handler running without a tenant+ // context must not surface the operator's instance URL.+ if (process.env.ENABLE_MULTI_TENANT === 'true') {+ return undefined;+ } // Fallback to environment config const config = getN8nApiConfig(); if (config?.baseUrl) {@@ -94,6 +100,11 @@ export abstract class BaseTriggerHandler<T extends BaseTriggerInput = BaseTrigge if (this.context?.n8nApiKey) { return this.context.n8nApiKey; }+ // SECURITY (GHSA-jxx9-px88-pj69): in multi-tenant mode, refuse to fall+ // back to the operator's env API key.+ if (process.env.ENABLE_MULTI_TENANT === 'true') {+ return undefined;+ } // Fallback to environment config const config = getN8nApiConfig(); return config?.apiKey;@@ -332,6 +332,14 @@ export function getN8nApiClient(context?: InstanceContext): N8nApiClient | null return null; } + // SECURITY (GHSA-jxx9-px88-pj69): never fall back to process-level credentials+ // when multi-tenant mode is enabled. A missing or incomplete tenant context+ // must result in no client, not the operator's N8N_API_KEY.+ if (process.env.ENABLE_MULTI_TENANT === 'true') {+ logger.warn('Refusing env-credential fallback in multi-tenant mode');+ return null;+ }+ // Fall back to default singleton from environment logger.info('Falling back to environment configuration for n8n API client'); const config = getN8nApiConfig();@@ -372,6 +380,27 @@ function ensureApiConfigured(context?: InstanceContext): N8nApiClient { return client; } +/**+ * Resolve the n8n API config to surface in a tool response (apiUrl,+ * baseUrl for workflow links, etc.). Prefers the per-request tenant+ * context; falls back to the process-env config only in single-tenant+ * mode.+ *+ * SECURITY (GHSA-jxx9-px88-pj69): in multi-tenant mode this never returns+ * the operator's env config, so handler responses cannot disclose the+ * operator's apiUrl to a tenant whose context was missing or incomplete.+ */+function resolveN8nApiConfigForResponse(context?: InstanceContext) {+ const fromContext = context ? getN8nApiConfigFromContext(context) : null;+ if (fromContext) {+ return fromContext;+ }+ if (process.env.ENABLE_MULTI_TENANT === 'true') {+ return null;+ }+ return getN8nApiConfig();+}+ // MCP transports may serialize JSON objects/arrays as strings. // Parse them back, but return the original value on failure so Zod reports a proper type error. export function tryParseJson(val: unknown): unknown {@@ -1719,7 +1748,7 @@ export async function handleHealthCheck(context?: InstanceContext): Promise<McpT instanceId: health.instanceId, n8nVersion: health.n8nVersion, features: health.features,- apiUrl: getN8nApiConfig()?.baseUrl,+ apiUrl: resolveN8nApiConfigForResponse(context)?.baseUrl, mcpVersion, supportedN8nVersion, versionCheck: {@@ -1779,7 +1808,7 @@ export async function handleHealthCheck(context?: InstanceContext): Promise<McpT error: getUserFriendlyErrorMessage(error), code: error.code, details: {- apiUrl: getN8nApiConfig()?.baseUrl,+ apiUrl: resolveN8nApiConfigForResponse(context)?.baseUrl, hint: 'Check if n8n is running and API is enabled', troubleshooting: [ '1. Verify n8n instance is running',@@ -1983,10 +2012,14 @@ export async function handleDiagnostic(request: any, context?: InstanceContext): const isDocker = process.env.IS_DOCKER === 'true'; const cloudPlatform = detectCloudPlatform(); - // Check environment variables+ // Check environment variables. SECURITY (GHSA-jxx9-px88-pj69): in+ // multi-tenant mode the operator's env credentials are not part of the+ // tenant's view of the system, so we mask them out of the diagnostic+ // payload rather than letting them leak through `environment.*`.+ const isMultiTenant = process.env.ENABLE_MULTI_TENANT === 'true'; const envVars = {- N8N_API_URL: process.env.N8N_API_URL || null,- N8N_API_KEY: process.env.N8N_API_KEY ? '***configured***' : null,+ N8N_API_URL: isMultiTenant ? null : (process.env.N8N_API_URL || null),+ N8N_API_KEY: isMultiTenant ? null : (process.env.N8N_API_KEY ? '***configured***' : null), NODE_ENV: process.env.NODE_ENV || 'production', MCP_MODE: mcpMode, isDocker,@@ -1996,7 +2029,7 @@ export async function handleDiagnostic(request: any, context?: InstanceContext): }; // Check API configuration- const apiConfig = getN8nApiConfig();+ const apiConfig = resolveN8nApiConfigForResponse(context); const apiConfigured = apiConfig !== null; const apiClient = getN8nApiClient(context); @@ -2567,7 +2600,7 @@ export async function handleDeployTemplate( });@@ -1324,6 +1324,25 @@ export class SingleSessionHTTPServer { const hasUrl = headers['x-n8n-url']; const hasKey = headers['x-n8n-key']; + // SECURITY (GHSA-jxx9-px88-pj69): in multi-tenant mode, both headers+ // must be present. Falling through with no context would silently use+ // the operator's process-level N8N_API_KEY for the tenant's request.+ if (process.env.ENABLE_MULTI_TENANT === 'true' && !hasUrl && !hasKey) {+ logger.warn('Multi-tenant request missing tenant headers', {+ hasUrl: false,+ hasKey: false+ });+ res.status(400).json({+ jsonrpc: '2.0',+ error: {+ code: -32602,+ message: 'Multi-tenant headers required'+ },+ id: req.body?.id ?? null+ });+ return;+ }+ if (hasUrl || hasKey) { // Create context with proper type handling const candidate: InstanceContext = {AI-assisted change 5dab509e34f41e5179b6d2520defe5aaaa957f2b6c8b9076a909f2506c04504b · Fix c2735c81a435125496a786e04178f5654889e7579b0daaf454d70a411f85ae6a