diff --git a/package.json b/package.json index d8d215d..69bce83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.7.0", + "version": "2.7.1", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "scripts": { diff --git a/src/config/n8n-api.ts b/src/config/n8n-api.ts index 9ae4c33..48684e6 100644 --- a/src/config/n8n-api.ts +++ b/src/config/n8n-api.ts @@ -2,9 +2,6 @@ import { z } from 'zod'; import dotenv from 'dotenv'; import { logger } from '../utils/logger'; -// Load environment variables -dotenv.config(); - // n8n API configuration schema const n8nApiConfigSchema = z.object({ N8N_API_URL: z.string().url().optional(), @@ -13,12 +10,20 @@ const n8nApiConfigSchema = z.object({ N8N_API_MAX_RETRIES: z.coerce.number().positive().default(3), }); +// Track if we've loaded env vars +let envLoaded = false; + // Parse and validate n8n API configuration -export function loadN8nApiConfig() { +export function getN8nApiConfig() { + // Load environment variables on first access + if (!envLoaded) { + dotenv.config(); + envLoaded = true; + } + const result = n8nApiConfigSchema.safeParse(process.env); if (!result.success) { - logger.warn('n8n API configuration validation failed:', result.error.format()); return null; } @@ -26,16 +31,9 @@ export function loadN8nApiConfig() { // Check if both URL and API key are provided if (!config.N8N_API_URL || !config.N8N_API_KEY) { - logger.info('n8n API not configured. Management tools will be disabled.'); return null; } - logger.info('n8n API configured successfully', { - url: config.N8N_API_URL, - timeout: config.N8N_API_TIMEOUT, - maxRetries: config.N8N_API_MAX_RETRIES, - }); - return { baseUrl: config.N8N_API_URL, apiKey: config.N8N_API_KEY, @@ -44,13 +42,11 @@ export function loadN8nApiConfig() { }; } -// Export the configuration (null if not configured) -export const n8nApiConfig = loadN8nApiConfig(); - -// Helper to check if n8n API is configured +// Helper to check if n8n API is configured (lazy check) export function isN8nApiConfigured(): boolean { - return n8nApiConfig !== null; + const config = getN8nApiConfig(); + return config !== null; } // Type export -export type N8nApiConfig = NonNullable>; \ No newline at end of file +export type N8nApiConfig = NonNullable>; \ No newline at end of file diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index 776541e..8a34343 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -1,5 +1,5 @@ import { N8nApiClient } from '../services/n8n-api-client'; -import { n8nApiConfig } from '../config/n8n-api'; +import { getN8nApiConfig } from '../config/n8n-api'; import { Workflow, WorkflowNode, @@ -26,15 +26,26 @@ import { NodeRepository } from '../database/node-repository'; // Singleton n8n API client instance let apiClient: N8nApiClient | null = null; +let lastConfigUrl: string | null = null; -// Get or create API client +// Get or create API client (with lazy config loading) export function getN8nApiClient(): N8nApiClient | null { - if (!n8nApiConfig) { + const config = getN8nApiConfig(); + + if (!config) { + if (apiClient) { + logger.info('n8n API configuration removed, clearing client'); + apiClient = null; + lastConfigUrl = null; + } return null; } - if (!apiClient) { - apiClient = new N8nApiClient(n8nApiConfig); + // Check if config has changed + if (!apiClient || lastConfigUrl !== config.baseUrl) { + logger.info('n8n API client initialized', { url: config.baseUrl }); + apiClient = new N8nApiClient(config); + lastConfigUrl = config.baseUrl; } return apiClient; @@ -754,7 +765,7 @@ export async function handleHealthCheck(): Promise { instanceId: health.instanceId, n8nVersion: health.n8nVersion, features: health.features, - apiUrl: n8nApiConfig?.baseUrl + apiUrl: getN8nApiConfig()?.baseUrl } }; } catch (error) { @@ -764,7 +775,7 @@ export async function handleHealthCheck(): Promise { error: getUserFriendlyErrorMessage(error), code: error.code, details: { - apiUrl: n8nApiConfig?.baseUrl, + apiUrl: getN8nApiConfig()?.baseUrl, hint: 'Check if n8n is running and API is enabled' } }; @@ -811,17 +822,18 @@ export async function handleListAvailableTools(): Promise { } ]; - const apiConfigured = n8nApiConfig !== null; + const config = getN8nApiConfig(); + const apiConfigured = config !== null; return { success: true, data: { tools, apiConfigured, - configuration: apiConfigured ? { - apiUrl: n8nApiConfig!.baseUrl, - timeout: n8nApiConfig!.timeout, - maxRetries: n8nApiConfig!.maxRetries + configuration: config ? { + apiUrl: config.baseUrl, + timeout: config.timeout, + maxRetries: config.maxRetries } : null, limitations: [ 'Cannot activate/deactivate workflows via API', @@ -846,7 +858,8 @@ export async function handleDiagnostic(request: any): Promise { }; // Check API configuration - const apiConfigured = n8nApiConfig !== null; + const apiConfig = getN8nApiConfig(); + const apiConfigured = apiConfig !== null; const apiClient = getN8nApiClient(); // Test API connectivity if configured @@ -879,10 +892,10 @@ export async function handleDiagnostic(request: any): Promise { apiConfiguration: { configured: apiConfigured, status: apiStatus, - config: apiConfigured && n8nApiConfig ? { - baseUrl: n8nApiConfig.baseUrl, - timeout: n8nApiConfig.timeout, - maxRetries: n8nApiConfig.maxRetries + config: apiConfig ? { + baseUrl: apiConfig.baseUrl, + timeout: apiConfig.timeout, + maxRetries: apiConfig.maxRetries } : null }, toolsAvailability: { diff --git a/src/mcp/handlers-workflow-diff.ts b/src/mcp/handlers-workflow-diff.ts index 6756f2b..b6f541e 100644 --- a/src/mcp/handlers-workflow-diff.ts +++ b/src/mcp/handlers-workflow-diff.ts @@ -40,11 +40,14 @@ const workflowDiffSchema = z.object({ export async function handleUpdatePartialWorkflow(args: unknown): Promise { try { - // Debug: Log what Claude Desktop sends - logger.info('[DEBUG] Full args from Claude Desktop:', JSON.stringify(args, null, 2)); - logger.info('[DEBUG] Args type:', typeof args); - if (args && typeof args === 'object') { - logger.info('[DEBUG] Args keys:', Object.keys(args as any)); + // Debug logging (only in debug mode) + if (process.env.DEBUG_MCP === 'true') { + logger.debug('Workflow diff request received', { + argsType: typeof args, + hasWorkflowId: args && typeof args === 'object' && 'workflowId' in args, + operationCount: args && typeof args === 'object' && 'operations' in args ? + (args as any).operations?.length : 0 + }); } // Validate input diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 5bf1895..46e97e0 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -78,6 +78,14 @@ export class N8NDocumentationMCPServer { logger.info('Initializing n8n Documentation MCP server'); + // Log n8n API configuration status at startup + const apiConfigured = isN8nApiConfigured(); + const totalTools = apiConfigured ? + n8nDocumentationToolsFinal.length + n8nManagementTools.length : + n8nDocumentationToolsFinal.length; + + logger.info(`MCP server initialized with ${totalTools} tools (n8n API: ${apiConfigured ? 'configured' : 'not configured'})`); + this.server = new Server( { name: 'n8n-documentation-mcp', @@ -126,9 +134,9 @@ export class N8NDocumentationMCPServer { }, }; - // Debug: Log to stderr to see if handler is called + // Debug logging if (process.env.DEBUG_MCP === 'true') { - console.error('Initialize handler called, returning:', JSON.stringify(response)); + logger.debug('Initialize handler called', { response }); } return response; @@ -138,12 +146,13 @@ export class N8NDocumentationMCPServer { this.server.setRequestHandler(ListToolsRequestSchema, async () => { // Combine documentation tools with management tools if API is configured const tools = [...n8nDocumentationToolsFinal]; + const isConfigured = isN8nApiConfigured(); - if (isN8nApiConfigured()) { + if (isConfigured) { tools.push(...n8nManagementTools); - logger.info('n8n management tools enabled'); + logger.debug(`Tool listing: ${tools.length} tools available (${n8nDocumentationToolsFinal.length} documentation + ${n8nManagementTools.length} management)`); } else { - logger.info('n8n management tools disabled (API not configured)'); + logger.debug(`Tool listing: ${tools.length} tools available (documentation only)`); } return { tools }; diff --git a/src/scripts/test-n8n-manager-integration.ts b/src/scripts/test-n8n-manager-integration.ts index 42b2acb..142ca85 100644 --- a/src/scripts/test-n8n-manager-integration.ts +++ b/src/scripts/test-n8n-manager-integration.ts @@ -2,7 +2,7 @@ import { config } from 'dotenv'; import { logger } from '../utils/logger'; -import { isN8nApiConfigured, n8nApiConfig } from '../config/n8n-api'; +import { isN8nApiConfigured, getN8nApiConfig } from '../config/n8n-api'; import { getN8nApiClient } from '../mcp/handlers-n8n-manager'; import { N8nApiClient } from '../services/n8n-api-client'; import { Workflow, ExecutionStatus } from '../types/n8n-api'; @@ -21,10 +21,11 @@ async function testN8nManagerIntegration() { return; } + const apiConfig = getN8nApiConfig(); logger.info('n8n API Configuration:', { - url: n8nApiConfig!.baseUrl, - timeout: n8nApiConfig!.timeout, - maxRetries: n8nApiConfig!.maxRetries + url: apiConfig!.baseUrl, + timeout: apiConfig!.timeout, + maxRetries: apiConfig!.maxRetries }); const client = getN8nApiClient(); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index fbdb254..2954711 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -16,6 +16,10 @@ export class Logger { private static instance: Logger; private useFileLogging = false; private fileStream: any = null; + // Cache environment variables for performance + private readonly isStdio = process.env.MCP_MODE === 'stdio'; + private readonly isDisabled = process.env.DISABLE_CONSOLE_OUTPUT === 'true'; + private readonly isHttp = process.env.MCP_MODE === 'http'; constructor(config?: Partial) { this.config = { @@ -53,10 +57,7 @@ export class Logger { private log(level: LogLevel, levelName: string, message: string, ...args: any[]): void { // Check environment variables FIRST, before level check // In stdio mode, suppress ALL console output to avoid corrupting JSON-RPC - const isStdio = process.env.MCP_MODE === 'stdio'; - const isDisabled = process.env.DISABLE_CONSOLE_OUTPUT === 'true'; - - if (isStdio || isDisabled) { + if (this.isStdio || this.isDisabled) { // Silently drop all logs in stdio mode return; } @@ -66,7 +67,7 @@ export class Logger { // In HTTP mode during request handling, suppress console output // The ConsoleManager will handle this, but we add a safety check - if (process.env.MCP_MODE === 'http' && process.env.MCP_REQUEST_ACTIVE === 'true') { + if (this.isHttp && process.env.MCP_REQUEST_ACTIVE === 'true') { // Silently drop the log during active MCP requests return; }