diff --git a/diagnose-claude-desktop.sh b/diagnose-claude-desktop.sh index fd8ee72..5dc7826 100755 --- a/diagnose-claude-desktop.sh +++ b/diagnose-claude-desktop.sh @@ -24,18 +24,18 @@ else echo "❌ dist/mcp/index.js not found - run 'npm run build'" fi -if [ -f "dist/mcp/server-update.js" ]; then - echo "✅ dist/mcp/server-update.js exists" - echo " Last modified: $(stat -f "%Sm" dist/mcp/server-update.js 2>/dev/null || stat -c "%y" dist/mcp/server-update.js 2>/dev/null)" +if [ -f "dist/mcp/server.js" ]; then + echo "✅ dist/mcp/server.js exists" + echo " Last modified: $(stat -f "%Sm" dist/mcp/server.js 2>/dev/null || stat -c "%y" dist/mcp/server.js 2>/dev/null)" else - echo "❌ dist/mcp/server-update.js not found" + echo "❌ dist/mcp/server.js not found" fi -if [ -f "dist/mcp/tools-update.js" ]; then - echo "✅ dist/mcp/tools-update.js exists" - echo " Last modified: $(stat -f "%Sm" dist/mcp/tools-update.js 2>/dev/null || stat -c "%y" dist/mcp/tools-update.js 2>/dev/null)" +if [ -f "dist/mcp/tools.js" ]; then + echo "✅ dist/mcp/tools.js exists" + echo " Last modified: $(stat -f "%Sm" dist/mcp/tools.js 2>/dev/null || stat -c "%y" dist/mcp/tools.js 2>/dev/null)" else - echo "❌ dist/mcp/tools-update.js not found" + echo "❌ dist/mcp/tools.js not found" fi echo "" @@ -52,12 +52,12 @@ echo "" # Check tools in compiled code echo "5. Compiled tools check:" -if [ -f "dist/mcp/tools-update.js" ]; then - TOOL_COUNT=$(grep "name: '" dist/mcp/tools-update.js | wc -l | tr -d ' ') +if [ -f "dist/mcp/tools.js" ]; then + TOOL_COUNT=$(grep "name: '" dist/mcp/tools.js | wc -l | tr -d ' ') echo " Total tools found: $TOOL_COUNT" echo " New tools present:" for tool in "get_node_for_task" "validate_node_config" "get_property_dependencies" "list_tasks" "search_node_properties" "get_node_essentials"; do - if grep -q "name: '$tool'" dist/mcp/tools-update.js; then + if grep -q "name: '$tool'" dist/mcp/tools.js; then echo " ✅ $tool" else echo " ❌ $tool" diff --git a/package.json b/package.json index b6451d6..07372a2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "start": "node dist/mcp/index.js", "start:http": "MCP_MODE=http node dist/mcp/index.js", "start:http:fixed": "MCP_MODE=http USE_FIXED_HTTP=true node dist/mcp/index.js", - "start:http:legacy": "MCP_MODE=http node dist/http-server.js", "http": "npm run build && npm run start:http:fixed", "dev": "npm run build && npm run rebuild && npm run validate", "dev:http": "MCP_MODE=http nodemon --watch src --ext ts --exec 'npm run build && npm run start:http'", @@ -40,7 +39,8 @@ "test:update-partial:debug": "node dist/scripts/test-update-partial-debug.js", "db:rebuild": "node dist/scripts/rebuild-database.js", "db:init": "node -e \"new (require('./dist/services/sqlite-storage-service').SQLiteStorageService)(); console.log('Database initialized')\"", - "docs:rebuild": "ts-node src/scripts/rebuild-database.ts" + "docs:rebuild": "ts-node src/scripts/rebuild-database.ts", + "sync:runtime-version": "node scripts/sync-runtime-version.js" }, "repository": { "type": "git", diff --git a/scripts/debug-essentials.js b/scripts/debug-essentials.js index 9f24a0e..d1edc62 100644 --- a/scripts/debug-essentials.js +++ b/scripts/debug-essentials.js @@ -3,7 +3,7 @@ * Debug the essentials implementation */ -const { N8NDocumentationMCPServer } = require('../dist/mcp/server-update'); +const { N8NDocumentationMCPServer } = require('../dist/mcp/server'); const { PropertyFilter } = require('../dist/services/property-filter'); const { ExampleGenerator } = require('../dist/services/example-generator'); diff --git a/scripts/debug-node.js b/scripts/debug-node.js index 6c0285d..a9abb29 100644 --- a/scripts/debug-node.js +++ b/scripts/debug-node.js @@ -3,7 +3,7 @@ * Debug script to check node data structure */ -const { N8NDocumentationMCPServer } = require('../dist/mcp/server-update'); +const { N8NDocumentationMCPServer } = require('../dist/mcp/server'); async function debugNode() { console.log('🔍 Debugging node data\n'); diff --git a/scripts/sync-runtime-version.js b/scripts/sync-runtime-version.js new file mode 100755 index 0000000..8404c57 --- /dev/null +++ b/scripts/sync-runtime-version.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +/** + * Sync version from package.json to package.runtime.json + * This ensures both files always have the same version + */ + +const fs = require('fs'); +const path = require('path'); + +const packageJsonPath = path.join(__dirname, '..', 'package.json'); +const packageRuntimePath = path.join(__dirname, '..', 'package.runtime.json'); + +try { + // Read package.json + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + const version = packageJson.version; + + // Read package.runtime.json + const packageRuntime = JSON.parse(fs.readFileSync(packageRuntimePath, 'utf-8')); + + // Update version if different + if (packageRuntime.version !== version) { + packageRuntime.version = version; + + // Write back with proper formatting + fs.writeFileSync( + packageRuntimePath, + JSON.stringify(packageRuntime, null, 2) + '\n', + 'utf-8' + ); + + console.log(`✅ Updated package.runtime.json version to ${version}`); + } else { + console.log(`✓ package.runtime.json already at version ${version}`); + } +} catch (error) { + console.error('❌ Error syncing version:', error.message); + process.exit(1); +} \ No newline at end of file diff --git a/scripts/test-direct.js b/scripts/test-direct.js index 1a15c05..975da7f 100644 --- a/scripts/test-direct.js +++ b/scripts/test-direct.js @@ -3,7 +3,7 @@ * Direct test of the server functionality without MCP protocol */ -const { N8NDocumentationMCPServer } = require('../dist/mcp/server-update'); +const { N8NDocumentationMCPServer } = require('../dist/mcp/server'); async function testDirect() { console.log('🧪 Direct server test\n'); diff --git a/scripts/test-essentials.ts b/scripts/test-essentials.ts index 1324138..50bccff 100755 --- a/scripts/test-essentials.ts +++ b/scripts/test-essentials.ts @@ -9,7 +9,7 @@ * 4. Tests the property search functionality */ -import { N8NDocumentationMCPServer } from '../src/mcp/server-update'; +import { N8NDocumentationMCPServer } from '../src/mcp/server'; import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; diff --git a/scripts/test-final.js b/scripts/test-final.js index 8a17e97..f91dd24 100644 --- a/scripts/test-final.js +++ b/scripts/test-final.js @@ -3,7 +3,7 @@ * Final validation test */ -const { N8NDocumentationMCPServer } = require('../dist/mcp/server-update'); +const { N8NDocumentationMCPServer } = require('../dist/mcp/server'); const colors = { green: '\x1b[32m', diff --git a/scripts/test-node-info.js b/scripts/test-node-info.js index 902724f..ba6d286 100644 --- a/scripts/test-node-info.js +++ b/scripts/test-node-info.js @@ -3,7 +3,7 @@ * Test get_node_info to diagnose timeout issues */ -const { N8NDocumentationMCPServer } = require('../dist/mcp/server-update'); +const { N8NDocumentationMCPServer } = require('../dist/mcp/server'); async function testNodeInfo() { console.log('🔍 Testing get_node_info...\n'); diff --git a/src/http-server-fixed.ts b/src/http-server-fixed.ts deleted file mode 100644 index 6b43121..0000000 --- a/src/http-server-fixed.ts +++ /dev/null @@ -1,378 +0,0 @@ -#!/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 - */ -import express from 'express'; -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { n8nDocumentationToolsFinal } from './mcp/tools-update'; -import { N8NDocumentationMCPServer } from './mcp/server-update'; -import { logger } from './utils/logger'; -import dotenv from 'dotenv'; - -dotenv.config(); - -let expressServer: any; - -/** - * Validate required environment variables - */ -function validateEnvironment() { - const required = ['AUTH_TOKEN']; - const missing = required.filter(key => !process.env[key]); - - if (missing.length > 0) { - logger.error(`Missing required environment variables: ${missing.join(', ')}`); - console.error(`ERROR: Missing required environment variables: ${missing.join(', ')}`); - console.error('Generate AUTH_TOKEN with: openssl rand -base64 32'); - process.exit(1); - } - - if (process.env.AUTH_TOKEN && process.env.AUTH_TOKEN.length < 32) { - logger.warn('AUTH_TOKEN should be at least 32 characters for security'); - console.warn('WARNING: AUTH_TOKEN should be at least 32 characters for security'); - } -} - -/** - * Graceful shutdown handler - */ -async function shutdown() { - logger.info('Shutting down HTTP server...'); - console.log('Shutting down HTTP server...'); - - if (expressServer) { - expressServer.close(() => { - logger.info('HTTP server closed'); - console.log('HTTP server closed'); - process.exit(0); - }); - - setTimeout(() => { - logger.error('Forced shutdown after timeout'); - process.exit(1); - }, 10000); - } else { - process.exit(0); - } -} - -export async function startFixedHTTPServer() { - validateEnvironment(); - - const app = express(); - - // CRITICAL: Don't use any body parser - StreamableHTTPServerTransport needs raw stream - - // Security headers - app.use((req, res, next) => { - res.setHeader('X-Content-Type-Options', 'nosniff'); - res.setHeader('X-Frame-Options', 'DENY'); - res.setHeader('X-XSS-Protection', '1; mode=block'); - res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); - next(); - }); - - // CORS configuration - app.use((req, res, next) => { - const allowedOrigin = process.env.CORS_ORIGIN || '*'; - res.setHeader('Access-Control-Allow-Origin', allowedOrigin); - res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept'); - res.setHeader('Access-Control-Max-Age', '86400'); - - if (req.method === 'OPTIONS') { - res.sendStatus(204); - return; - } - next(); - }); - - // Request logging - app.use((req, res, next) => { - logger.info(`${req.method} ${req.path}`, { - ip: req.ip, - userAgent: req.get('user-agent'), - contentLength: req.get('content-length') - }); - next(); - }); - - // Create a single persistent MCP server instance - const mcpServer = new N8NDocumentationMCPServer(); - logger.info('Created persistent MCP server instance'); - - // Health check endpoint - app.get('/health', (req, res) => { - res.json({ - status: 'ok', - mode: 'http-fixed', - version: '2.4.1', - uptime: Math.floor(process.uptime()), - memory: { - used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), - total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), - unit: 'MB' - }, - timestamp: new Date().toISOString() - }); - }); - - // Version endpoint - app.get('/version', (req, res) => { - res.json({ - version: '2.4.1', - buildTime: new Date().toISOString(), - tools: n8nDocumentationToolsFinal.map(t => t.name), - commit: process.env.GIT_COMMIT || 'unknown' - }); - }); - - // Test tools endpoint - app.get('/test-tools', async (req, res) => { - try { - const result = await mcpServer.executeTool('get_node_essentials', { nodeType: 'nodes-base.httpRequest' }); - res.json({ status: 'ok', hasData: !!result, toolCount: n8nDocumentationToolsFinal.length }); - } catch (error) { - res.json({ status: 'error', message: error instanceof Error ? error.message : 'Unknown error' }); - } - }); - - // Main MCP endpoint - handle each request with custom transport handling - app.post('/mcp', async (req: express.Request, res: express.Response): Promise => { - const startTime = Date.now(); - - // Simple auth check - const authHeader = req.headers.authorization; - const token = authHeader?.startsWith('Bearer ') - ? authHeader.slice(7) - : authHeader; - - if (token !== process.env.AUTH_TOKEN) { - logger.warn('Authentication failed', { - ip: req.ip, - userAgent: req.get('user-agent') - }); - res.status(401).json({ - jsonrpc: '2.0', - error: { - code: -32001, - message: 'Unauthorized' - }, - id: null - }); - return; - } - - try { - // Instead of using StreamableHTTPServerTransport, we'll handle the request directly - // This avoids the initialization issues with the transport - - // Collect the raw body - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); - }); - - req.on('end', async () => { - try { - const jsonRpcRequest = JSON.parse(body); - logger.debug('Received JSON-RPC request:', { method: jsonRpcRequest.method }); - - // Handle the request based on method - let response; - - switch (jsonRpcRequest.method) { - case 'initialize': - response = { - jsonrpc: '2.0', - result: { - protocolVersion: '1.0', - capabilities: { - tools: {}, - resources: {} - }, - serverInfo: { - name: 'n8n-documentation-mcp', - version: '2.4.1' - } - }, - id: jsonRpcRequest.id - }; - break; - - case 'tools/list': - response = { - jsonrpc: '2.0', - result: { - tools: n8nDocumentationToolsFinal - }, - id: jsonRpcRequest.id - }; - break; - - case 'tools/call': - // Delegate to the MCP server - const toolName = jsonRpcRequest.params?.name; - const toolArgs = jsonRpcRequest.params?.arguments || {}; - - try { - const result = await mcpServer.executeTool(toolName, toolArgs); - response = { - jsonrpc: '2.0', - result: { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2) - } - ] - }, - id: jsonRpcRequest.id - }; - } catch (error) { - response = { - jsonrpc: '2.0', - error: { - code: -32603, - message: `Error executing tool ${toolName}: ${error instanceof Error ? error.message : 'Unknown error'}` - }, - id: jsonRpcRequest.id - }; - } - break; - - default: - response = { - jsonrpc: '2.0', - error: { - code: -32601, - message: `Method not found: ${jsonRpcRequest.method}` - }, - id: jsonRpcRequest.id - }; - } - - // Send response - res.setHeader('Content-Type', 'application/json'); - res.json(response); - - const duration = Date.now() - startTime; - logger.info('MCP request completed', { - duration, - method: jsonRpcRequest.method - }); - } catch (error) { - logger.error('Error processing request:', error); - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32700, - message: 'Parse error', - data: error instanceof Error ? error.message : 'Unknown error' - }, - id: null - }); - } - }); - } catch (error) { - logger.error('MCP request error:', error); - - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error', - data: process.env.NODE_ENV === 'development' - ? (error as Error).message - : undefined - }, - id: null - }); - } - } - }); - - // 404 handler - app.use((req, res) => { - res.status(404).json({ - error: 'Not found', - message: `Cannot ${req.method} ${req.path}` - }); - }); - - // Error handler - app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.error('Express error handler:', err); - - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error', - data: process.env.NODE_ENV === 'development' ? err.message : undefined - }, - id: null - }); - } - }); - - const port = parseInt(process.env.PORT || '3000'); - const host = process.env.HOST || '0.0.0.0'; - - expressServer = app.listen(port, host, () => { - logger.info(`n8n MCP Fixed HTTP Server started`, { port, host }); - console.log(`n8n MCP Fixed HTTP Server running on ${host}:${port}`); - console.log(`Health check: http://localhost:${port}/health`); - console.log(`MCP endpoint: http://localhost:${port}/mcp`); - console.log('\nPress Ctrl+C to stop the server'); - }); - - // Handle errors - expressServer.on('error', (error: any) => { - if (error.code === 'EADDRINUSE') { - logger.error(`Port ${port} is already in use`); - console.error(`ERROR: Port ${port} is already in use`); - process.exit(1); - } else { - logger.error('Server error:', error); - console.error('Server error:', error); - process.exit(1); - } - }); - - // Graceful shutdown handlers - process.on('SIGTERM', shutdown); - process.on('SIGINT', shutdown); - - // Handle uncaught errors - process.on('uncaughtException', (error) => { - logger.error('Uncaught exception:', error); - console.error('Uncaught exception:', error); - shutdown(); - }); - - process.on('unhandledRejection', (reason, promise) => { - logger.error('Unhandled rejection:', reason); - console.error('Unhandled rejection at:', promise, 'reason:', reason); - shutdown(); - }); -} - -// Make executeTool public on the server -declare module './mcp/server-update' { - interface N8NDocumentationMCPServer { - executeTool(name: string, args: any): Promise; - } -} - -// Start if called directly -if (require.main === module) { - startFixedHTTPServer().catch(error => { - logger.error('Failed to start Fixed HTTP server:', error); - console.error('Failed to start Fixed HTTP server:', error); - process.exit(1); - }); -} \ No newline at end of file diff --git a/src/http-server-single-session.ts b/src/http-server-single-session.ts index ca1d1ed..0fcb6a0 100644 --- a/src/http-server-single-session.ts +++ b/src/http-server-single-session.ts @@ -6,7 +6,7 @@ */ import express from 'express'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { N8NDocumentationMCPServer } from './mcp/server-update'; +import { N8NDocumentationMCPServer } from './mcp/server'; import { ConsoleManager } from './utils/console-manager'; import { logger } from './utils/logger'; import dotenv from 'dotenv'; diff --git a/src/http-server.ts b/src/http-server.ts index 002235e..99cf093 100644 --- a/src/http-server.ts +++ b/src/http-server.ts @@ -1,17 +1,19 @@ #!/usr/bin/env node /** - * Minimal HTTP server for n8n-MCP - * Single-user, stateless design for private deployments + * Fixed HTTP server for n8n-MCP that properly handles StreamableHTTPServerTransport initialization + * This implementation ensures the transport is properly initialized before handling requests */ import express from 'express'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { N8NDocumentationMCPServer } from './mcp/server-update'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { n8nDocumentationToolsFinal } from './mcp/tools'; +import { N8NDocumentationMCPServer } from './mcp/server'; import { logger } from './utils/logger'; +import { PROJECT_VERSION } from './utils/version'; import dotenv from 'dotenv'; dotenv.config(); -let server: any; +let expressServer: any; /** * Validate required environment variables @@ -27,7 +29,6 @@ function validateEnvironment() { process.exit(1); } - // Validate AUTH_TOKEN length if (process.env.AUTH_TOKEN && process.env.AUTH_TOKEN.length < 32) { logger.warn('AUTH_TOKEN should be at least 32 characters for security'); console.warn('WARNING: AUTH_TOKEN should be at least 32 characters for security'); @@ -41,14 +42,13 @@ async function shutdown() { logger.info('Shutting down HTTP server...'); console.log('Shutting down HTTP server...'); - if (server) { - server.close(() => { + if (expressServer) { + expressServer.close(() => { logger.info('HTTP server closed'); console.log('HTTP server closed'); process.exit(0); }); - // Force shutdown after 10 seconds setTimeout(() => { logger.error('Forced shutdown after timeout'); process.exit(1); @@ -58,14 +58,12 @@ async function shutdown() { } } -export async function startHTTPServer() { - // Validate environment +export async function startFixedHTTPServer() { validateEnvironment(); const app = express(); - // DON'T parse JSON globally - StreamableHTTPServerTransport needs raw stream - // Only parse for specific endpoints that need it + // CRITICAL: Don't use any body parser - StreamableHTTPServerTransport needs raw stream // Security headers app.use((req, res, next) => { @@ -76,13 +74,13 @@ export async function startHTTPServer() { next(); }); - // CORS configuration for mcp-remote compatibility + // CORS configuration app.use((req, res, next) => { const allowedOrigin = process.env.CORS_ORIGIN || '*'; res.setHeader('Access-Control-Allow-Origin', allowedOrigin); res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept'); - res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours + res.setHeader('Access-Control-Max-Age', '86400'); if (req.method === 'OPTIONS') { res.sendStatus(204); @@ -91,7 +89,7 @@ export async function startHTTPServer() { next(); }); - // Request logging middleware + // Request logging app.use((req, res, next) => { logger.info(`${req.method} ${req.path}`, { ip: req.ip, @@ -101,12 +99,16 @@ export async function startHTTPServer() { next(); }); - // Enhanced health check endpoint + // Create a single persistent MCP server instance + const mcpServer = new N8NDocumentationMCPServer(); + logger.info('Created persistent MCP server instance'); + + // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', - mode: 'http', - version: '2.3.2', + mode: 'http-fixed', + version: PROJECT_VERSION, uptime: Math.floor(process.uptime()), memory: { used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), @@ -116,8 +118,28 @@ export async function startHTTPServer() { timestamp: new Date().toISOString() }); }); + + // Version endpoint + app.get('/version', (req, res) => { + res.json({ + version: PROJECT_VERSION, + buildTime: new Date().toISOString(), + tools: n8nDocumentationToolsFinal.map(t => t.name), + commit: process.env.GIT_COMMIT || 'unknown' + }); + }); + + // Test tools endpoint + app.get('/test-tools', async (req, res) => { + try { + const result = await mcpServer.executeTool('get_node_essentials', { nodeType: 'nodes-base.httpRequest' }); + res.json({ status: 'ok', hasData: !!result, toolCount: n8nDocumentationToolsFinal.length }); + } catch (error) { + res.json({ status: 'error', message: error instanceof Error ? error.message : 'Unknown error' }); + } + }); - // Main MCP endpoint - Create a new server and transport for each request (stateless) + // Main MCP endpoint - handle each request with custom transport handling app.post('/mcp', async (req: express.Request, res: express.Response): Promise => { const startTime = Date.now(); @@ -143,38 +165,119 @@ export async function startHTTPServer() { return; } - // Create new instances for each request (stateless) - const mcpServer = new N8NDocumentationMCPServer(); - try { - // Create a stateless transport - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, // Stateless mode + // Instead of using StreamableHTTPServerTransport, we'll handle the request directly + // This avoids the initialization issues with the transport + + // Collect the raw body + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); }); - // Connect server to transport - await mcpServer.connect(transport); - - // Handle the request - Fixed: removed third parameter - await transport.handleRequest(req, res); - - // Log request duration - const duration = Date.now() - startTime; - logger.info('MCP request completed', { - duration - }); - - // Clean up on close - res.on('close', () => { - logger.debug('Request closed, cleaning up'); - transport.close().catch(err => - logger.error('Error closing transport:', err) - ); + req.on('end', async () => { + try { + const jsonRpcRequest = JSON.parse(body); + logger.debug('Received JSON-RPC request:', { method: jsonRpcRequest.method }); + + // Handle the request based on method + let response; + + switch (jsonRpcRequest.method) { + case 'initialize': + response = { + jsonrpc: '2.0', + result: { + protocolVersion: '1.0', + capabilities: { + tools: {}, + resources: {} + }, + serverInfo: { + name: 'n8n-documentation-mcp', + version: PROJECT_VERSION + } + }, + id: jsonRpcRequest.id + }; + break; + + case 'tools/list': + response = { + jsonrpc: '2.0', + result: { + tools: n8nDocumentationToolsFinal + }, + id: jsonRpcRequest.id + }; + break; + + case 'tools/call': + // Delegate to the MCP server + const toolName = jsonRpcRequest.params?.name; + const toolArgs = jsonRpcRequest.params?.arguments || {}; + + try { + const result = await mcpServer.executeTool(toolName, toolArgs); + response = { + jsonrpc: '2.0', + result: { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + }, + id: jsonRpcRequest.id + }; + } catch (error) { + response = { + jsonrpc: '2.0', + error: { + code: -32603, + message: `Error executing tool ${toolName}: ${error instanceof Error ? error.message : 'Unknown error'}` + }, + id: jsonRpcRequest.id + }; + } + break; + + default: + response = { + jsonrpc: '2.0', + error: { + code: -32601, + message: `Method not found: ${jsonRpcRequest.method}` + }, + id: jsonRpcRequest.id + }; + } + + // Send response + res.setHeader('Content-Type', 'application/json'); + res.json(response); + + const duration = Date.now() - startTime; + logger.info('MCP request completed', { + duration, + method: jsonRpcRequest.method + }); + } catch (error) { + logger.error('Error processing request:', error); + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32700, + message: 'Parse error', + data: error instanceof Error ? error.message : 'Unknown error' + }, + id: null + }); + } }); } catch (error) { logger.error('MCP request error:', error); - const duration = Date.now() - startTime; - logger.error('MCP request failed', { duration }); if (!res.headersSent) { res.status(500).json({ @@ -220,16 +323,16 @@ export async function startHTTPServer() { const port = parseInt(process.env.PORT || '3000'); const host = process.env.HOST || '0.0.0.0'; - server = app.listen(port, host, () => { - logger.info(`n8n MCP HTTP Server started`, { port, host }); - console.log(`n8n MCP HTTP Server running on ${host}:${port}`); + expressServer = app.listen(port, host, () => { + logger.info(`n8n MCP Fixed HTTP Server started`, { port, host }); + console.log(`n8n MCP Fixed HTTP Server running on ${host}:${port}`); console.log(`Health check: http://localhost:${port}/health`); console.log(`MCP endpoint: http://localhost:${port}/mcp`); console.log('\nPress Ctrl+C to stop the server'); }); // Handle errors - server.on('error', (error: any) => { + expressServer.on('error', (error: any) => { if (error.code === 'EADDRINUSE') { logger.error(`Port ${port} is already in use`); console.error(`ERROR: Port ${port} is already in use`); @@ -259,11 +362,18 @@ export async function startHTTPServer() { }); } +// Make executeTool public on the server +declare module './mcp/server' { + interface N8NDocumentationMCPServer { + executeTool(name: string, args: any): Promise; + } +} + // Start if called directly if (require.main === module) { - startHTTPServer().catch(error => { - logger.error('Failed to start HTTP server:', error); - console.error('Failed to start HTTP server:', error); + startFixedHTTPServer().catch(error => { + logger.error('Failed to start Fixed HTTP server:', error); + console.error('Failed to start Fixed HTTP server:', error); process.exit(1); }); } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index e55e711..572edc4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ export { N8NMCPEngine, EngineHealth, EngineOptions } from './mcp-engine'; export { SingleSessionHTTPServer } from './http-server-single-session'; export { ConsoleManager } from './utils/console-manager'; -export { N8NDocumentationMCPServer } from './mcp/server-update'; +export { N8NDocumentationMCPServer } from './mcp/server'; // Default export for convenience import N8NMCPEngine from './mcp-engine'; diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index 184c034..776541e 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -831,4 +831,108 @@ export async function handleListAvailableTools(): Promise { ] } }; +} + +// Handler: n8n_diagnostic +export async function handleDiagnostic(request: any): Promise { + const verbose = request.params?.arguments?.verbose || false; + + // Check environment variables + const envVars = { + N8N_API_URL: process.env.N8N_API_URL || null, + N8N_API_KEY: process.env.N8N_API_KEY ? '***configured***' : null, + NODE_ENV: process.env.NODE_ENV || 'production', + MCP_MODE: process.env.MCP_MODE || 'stdio' + }; + + // Check API configuration + const apiConfigured = n8nApiConfig !== null; + const apiClient = getN8nApiClient(); + + // Test API connectivity if configured + let apiStatus = { + configured: apiConfigured, + connected: false, + error: null as string | null, + version: null as string | null + }; + + if (apiClient) { + try { + const health = await apiClient.healthCheck(); + apiStatus.connected = true; + apiStatus.version = health.n8nVersion || 'unknown'; + } catch (error) { + apiStatus.error = error instanceof Error ? error.message : 'Unknown error'; + } + } + + // Check which tools are available + const documentationTools = 22; // Base documentation tools + const managementTools = apiConfigured ? 16 : 0; + const totalTools = documentationTools + managementTools; + + // Build diagnostic report + const diagnostic: any = { + timestamp: new Date().toISOString(), + environment: envVars, + apiConfiguration: { + configured: apiConfigured, + status: apiStatus, + config: apiConfigured && n8nApiConfig ? { + baseUrl: n8nApiConfig.baseUrl, + timeout: n8nApiConfig.timeout, + maxRetries: n8nApiConfig.maxRetries + } : null + }, + toolsAvailability: { + documentationTools: { + count: documentationTools, + enabled: true, + description: 'Always available - node info, search, validation, etc.' + }, + managementTools: { + count: managementTools, + enabled: apiConfigured, + description: apiConfigured ? + 'Management tools are ENABLED - create, update, execute workflows' : + 'Management tools are DISABLED - configure N8N_API_URL and N8N_API_KEY to enable' + }, + totalAvailable: totalTools + }, + troubleshooting: { + steps: apiConfigured ? [ + 'API is configured and should work', + 'If tools are not showing in Claude Desktop:', + '1. Restart Claude Desktop completely', + '2. Check if using latest Docker image', + '3. Verify environment variables are passed correctly', + '4. Try running n8n_health_check to test connectivity' + ] : [ + 'To enable management tools:', + '1. Set N8N_API_URL environment variable (e.g., https://your-n8n-instance.com)', + '2. Set N8N_API_KEY environment variable (get from n8n API settings)', + '3. Restart the MCP server', + '4. Management tools will automatically appear' + ], + documentation: 'For detailed setup instructions, see: https://github.com/czlonkowski/n8n-mcp#n8n-management-tools-new-v260---requires-api-configuration' + } + }; + + // Add verbose debug info if requested + if (verbose) { + diagnostic['debug'] = { + processEnv: Object.keys(process.env).filter(key => + key.startsWith('N8N_') || key.startsWith('MCP_') + ), + nodeVersion: process.version, + platform: process.platform, + workingDirectory: process.cwd() + }; + } + + return { + success: true, + data: diagnostic + }; } \ No newline at end of file diff --git a/src/mcp/index.ts b/src/mcp/index.ts index fdaf5a8..91d4468 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { N8NDocumentationMCPServer } from './server-update'; +import { N8NDocumentationMCPServer } from './server'; import { logger } from '../utils/logger'; // Add error details to stderr for Claude Desktop debugging @@ -35,7 +35,7 @@ async function main() { // Check if we should use the fixed implementation if (process.env.USE_FIXED_HTTP === 'true') { // Use the fixed HTTP implementation that bypasses StreamableHTTPServerTransport issues - const { startFixedHTTPServer } = await import('../http-server-fixed'); + const { startFixedHTTPServer } = await import('../http-server'); await startFixedHTTPServer(); } else { // HTTP mode - for remote deployment with single-session architecture diff --git a/src/mcp/server-update.ts b/src/mcp/server.ts similarity index 99% rename from src/mcp/server-update.ts rename to src/mcp/server.ts index d2872c5..0a4693a 100644 --- a/src/mcp/server-update.ts +++ b/src/mcp/server.ts @@ -7,7 +7,7 @@ import { } from '@modelcontextprotocol/sdk/types.js'; import { existsSync } from 'fs'; import path from 'path'; -import { n8nDocumentationToolsFinal } from './tools-update'; +import { n8nDocumentationToolsFinal } from './tools'; import { n8nManagementTools } from './tools-n8n-manager'; import { logger } from '../utils/logger'; import { NodeRepository } from '../database/node-repository'; @@ -24,6 +24,7 @@ import { WorkflowValidator } from '../services/workflow-validator'; import { isN8nApiConfigured } from '../config/n8n-api'; import * as n8nHandlers from './handlers-n8n-manager'; import { handleUpdatePartialWorkflow } from './handlers-workflow-diff'; +import { PROJECT_VERSION } from '../utils/version'; interface NodeRow { node_type: string; @@ -121,7 +122,7 @@ export class N8NDocumentationMCPServer { }, serverInfo: { name: 'n8n-documentation-mcp', - version: '2.4.1', + version: PROJECT_VERSION, }, }; @@ -261,6 +262,8 @@ export class N8NDocumentationMCPServer { return n8nHandlers.handleHealthCheck(); case 'n8n_list_available_tools': return n8nHandlers.handleListAvailableTools(); + case 'n8n_diagnostic': + return n8nHandlers.handleDiagnostic({ params: { arguments: args } }); default: throw new Error(`Unknown tool: ${name}`); diff --git a/src/mcp/stdio-wrapper.ts b/src/mcp/stdio-wrapper.ts index 171a07d..8811d9c 100644 --- a/src/mcp/stdio-wrapper.ts +++ b/src/mcp/stdio-wrapper.ts @@ -40,7 +40,7 @@ console.count = () => {}; console.countReset = () => {}; // Import and run the server AFTER suppressing output -import { N8NDocumentationMCPServer } from './server-update'; +import { N8NDocumentationMCPServer } from './server'; async function main() { try { diff --git a/src/mcp/tools-n8n-manager.ts b/src/mcp/tools-n8n-manager.ts index 479af34..aaaabd9 100644 --- a/src/mcp/tools-n8n-manager.ts +++ b/src/mcp/tools-n8n-manager.ts @@ -484,5 +484,18 @@ Validation example: type: 'object', properties: {} } + }, + { + name: 'n8n_diagnostic', + description: `Diagnose n8n API configuration and management tools availability. Shows current configuration status, which tools are enabled/disabled, and helps troubleshoot why management tools might not be appearing. Returns detailed diagnostic information including environment variables, API connectivity, and tool registration status.`, + inputSchema: { + type: 'object', + properties: { + verbose: { + type: 'boolean', + description: 'Include detailed debug information (default: false)' + } + } + } } ]; \ No newline at end of file diff --git a/src/mcp/tools-update.ts b/src/mcp/tools.ts similarity index 100% rename from src/mcp/tools-update.ts rename to src/mcp/tools.ts diff --git a/src/scripts/test-mcp-tools.ts b/src/scripts/test-mcp-tools.ts index 3cb7825..c2f61b9 100644 --- a/src/scripts/test-mcp-tools.ts +++ b/src/scripts/test-mcp-tools.ts @@ -4,7 +4,7 @@ */ import { createDatabaseAdapter } from '../database/database-adapter'; import { NodeRepository } from '../database/node-repository'; -import { N8NDocumentationMCPServer } from '../mcp/server-update'; +import { N8NDocumentationMCPServer } from '../mcp/server'; import { Logger } from '../utils/logger'; const logger = new Logger({ prefix: '[TestMCPTools]' }); diff --git a/src/types/index.ts b/src/types/index.ts index e3a6af7..02cb06c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,11 +4,6 @@ export interface MCPServerConfig { authToken?: string; } -export interface N8NConfig { - apiUrl: string; - apiKey: string; -} - export interface ToolDefinition { name: string; description: string; diff --git a/src/utils/n8n-client.ts b/src/utils/n8n-client.ts deleted file mode 100644 index 5dda7b1..0000000 --- a/src/utils/n8n-client.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { N8NConfig } from '../types'; - -export class N8NApiClient { - private config: N8NConfig; - private headers: Record; - - constructor(config: N8NConfig) { - this.config = config; - this.headers = { - 'Content-Type': 'application/json', - 'X-N8N-API-KEY': config.apiKey, - }; - } - - private async request(endpoint: string, options: RequestInit = {}): Promise { - const url = `${this.config.apiUrl}/api/v1${endpoint}`; - - try { - const response = await fetch(url, { - ...options, - headers: { - ...this.headers, - ...options.headers, - }, - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`n8n API error: ${response.status} - ${error}`); - } - - return await response.json(); - } catch (error) { - throw new Error(`Failed to connect to n8n: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - } - - // Workflow operations - async getWorkflows(filters?: { active?: boolean; tags?: string[] }): Promise { - const query = new URLSearchParams(); - if (filters?.active !== undefined) { - query.append('active', filters.active.toString()); - } - if (filters?.tags?.length) { - query.append('tags', filters.tags.join(',')); - } - - return this.request(`/workflows${query.toString() ? `?${query}` : ''}`); - } - - async getWorkflow(id: string): Promise { - return this.request(`/workflows/${id}`); - } - - async createWorkflow(workflowData: any): Promise { - return this.request('/workflows', { - method: 'POST', - body: JSON.stringify(workflowData), - }); - } - - async updateWorkflow(id: string, updates: any): Promise { - return this.request(`/workflows/${id}`, { - method: 'PATCH', - body: JSON.stringify(updates), - }); - } - - async deleteWorkflow(id: string): Promise { - return this.request(`/workflows/${id}`, { - method: 'DELETE', - }); - } - - async activateWorkflow(id: string): Promise { - return this.request(`/workflows/${id}/activate`, { - method: 'POST', - }); - } - - async deactivateWorkflow(id: string): Promise { - return this.request(`/workflows/${id}/deactivate`, { - method: 'POST', - }); - } - - // Execution operations - async executeWorkflow(id: string, data?: any): Promise { - return this.request(`/workflows/${id}/execute`, { - method: 'POST', - body: JSON.stringify({ data }), - }); - } - - async getExecutions(filters?: { - workflowId?: string; - status?: string; - limit?: number - }): Promise { - const query = new URLSearchParams(); - if (filters?.workflowId) { - query.append('workflowId', filters.workflowId); - } - if (filters?.status) { - query.append('status', filters.status); - } - if (filters?.limit) { - query.append('limit', filters.limit.toString()); - } - - return this.request(`/executions${query.toString() ? `?${query}` : ''}`); - } - - async getExecution(id: string): Promise { - return this.request(`/executions/${id}`); - } - - async deleteExecution(id: string): Promise { - return this.request(`/executions/${id}`, { - method: 'DELETE', - }); - } - - // Credential operations - async getCredentialTypes(): Promise { - return this.request('/credential-types'); - } - - async getCredentials(): Promise { - return this.request('/credentials'); - } - - // Node operations - async getNodeTypes(): Promise { - return this.request('/node-types'); - } - - async getNodeType(nodeType: string): Promise { - return this.request(`/node-types/${nodeType}`); - } - - // Extended methods for node source extraction - async getNodeSourceCode(nodeType: string): Promise { - // This is a special endpoint we'll need to handle differently - // as n8n doesn't expose source code directly through API - // We'll need to implement this through file system access - throw new Error('Node source code extraction requires special implementation'); - } - - async getNodeDescription(nodeType: string): Promise { - try { - const nodeTypeData = await this.getNodeType(nodeType); - return { - name: nodeTypeData.name, - displayName: nodeTypeData.displayName, - description: nodeTypeData.description, - version: nodeTypeData.version, - defaults: nodeTypeData.defaults, - inputs: nodeTypeData.inputs, - outputs: nodeTypeData.outputs, - properties: nodeTypeData.properties, - credentials: nodeTypeData.credentials, - }; - } catch (error) { - throw new Error(`Failed to get node description: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - } -} \ No newline at end of file diff --git a/src/utils/version.ts b/src/utils/version.ts new file mode 100644 index 0000000..5ce569c --- /dev/null +++ b/src/utils/version.ts @@ -0,0 +1,19 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +/** + * Get the project version from package.json + * This ensures we have a single source of truth for versioning + */ +function getProjectVersion(): string { + try { + const packageJsonPath = join(__dirname, '../../package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + return packageJson.version || '0.0.0'; + } catch (error) { + console.error('Failed to read version from package.json:', error); + return '0.0.0'; + } +} + +export const PROJECT_VERSION = getProjectVersion(); \ No newline at end of file