refactor: clean up file names and fix version management

- Renamed files to remove unnecessary suffixes:
  - tools-update.ts → tools.ts
  - server-update.ts → server.ts
  - http-server-fixed.ts → http-server.ts
- Created version utility to read from package.json as single source of truth
- Updated all imports across 21+ files
- Removed legacy files:
  - src/http-server.ts (legacy HTTP server with known issues)
  - src/utils/n8n-client.ts (unused legacy API client)
- Added n8n_diagnostic tool to help troubleshoot management tools visibility
- Added script to sync package.runtime.json version
- Fixed version mismatch issue (was hardcoded 2.4.1, now reads 2.7.0 from package.json)

This addresses GitHub issue #5 regarding version mismatch and provides better diagnostics for users.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-29 17:43:29 +02:00
parent a1e3f9cb39
commit 91386b2d02
23 changed files with 369 additions and 631 deletions

View File

@@ -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"

View File

@@ -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",

View File

@@ -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');

View File

@@ -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');

40
scripts/sync-runtime-version.js Executable file
View File

@@ -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);
}

View File

@@ -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');

View File

@@ -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';

View File

@@ -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',

View File

@@ -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');

View File

@@ -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<void> => {
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<any>;
}
}
// 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);
});
}

View File

@@ -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';

View File

@@ -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),
@@ -117,7 +119,27 @@ export async function startHTTPServer() {
});
});
// Main MCP endpoint - Create a new server and transport for each request (stateless)
// 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 - handle each request with custom transport handling
app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
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 {
// 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: 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 {
// Create a stateless transport
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless mode
});
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;
// Connect server to transport
await mcpServer.connect(transport);
default:
response = {
jsonrpc: '2.0',
error: {
code: -32601,
message: `Method not found: ${jsonRpcRequest.method}`
},
id: jsonRpcRequest.id
};
}
// Handle the request - Fixed: removed third parameter
await transport.handleRequest(req, res);
// Send response
res.setHeader('Content-Type', 'application/json');
res.json(response);
// Log request duration
const duration = Date.now() - startTime;
logger.info('MCP request completed', {
duration
duration,
method: jsonRpcRequest.method
});
// Clean up on close
res.on('close', () => {
logger.debug('Request closed, cleaning up');
transport.close().catch(err =>
logger.error('Error closing transport:', err)
);
} 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<any>;
}
}
// 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);
});
}

View File

@@ -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';

View File

@@ -832,3 +832,107 @@ export async function handleListAvailableTools(): Promise<McpToolResponse> {
}
};
}
// Handler: n8n_diagnostic
export async function handleDiagnostic(request: any): Promise<McpToolResponse> {
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
};
}

View File

@@ -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

View File

@@ -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}`);

View File

@@ -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 {

View File

@@ -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)'
}
}
}
}
];

View File

@@ -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]' });

View File

@@ -4,11 +4,6 @@ export interface MCPServerConfig {
authToken?: string;
}
export interface N8NConfig {
apiUrl: string;
apiKey: string;
}
export interface ToolDefinition {
name: string;
description: string;

View File

@@ -1,168 +0,0 @@
import { N8NConfig } from '../types';
export class N8NApiClient {
private config: N8NConfig;
private headers: Record<string, string>;
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<any> {
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<any> {
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<any> {
return this.request(`/workflows/${id}`);
}
async createWorkflow(workflowData: any): Promise<any> {
return this.request('/workflows', {
method: 'POST',
body: JSON.stringify(workflowData),
});
}
async updateWorkflow(id: string, updates: any): Promise<any> {
return this.request(`/workflows/${id}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
}
async deleteWorkflow(id: string): Promise<any> {
return this.request(`/workflows/${id}`, {
method: 'DELETE',
});
}
async activateWorkflow(id: string): Promise<any> {
return this.request(`/workflows/${id}/activate`, {
method: 'POST',
});
}
async deactivateWorkflow(id: string): Promise<any> {
return this.request(`/workflows/${id}/deactivate`, {
method: 'POST',
});
}
// Execution operations
async executeWorkflow(id: string, data?: any): Promise<any> {
return this.request(`/workflows/${id}/execute`, {
method: 'POST',
body: JSON.stringify({ data }),
});
}
async getExecutions(filters?: {
workflowId?: string;
status?: string;
limit?: number
}): Promise<any> {
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<any> {
return this.request(`/executions/${id}`);
}
async deleteExecution(id: string): Promise<any> {
return this.request(`/executions/${id}`, {
method: 'DELETE',
});
}
// Credential operations
async getCredentialTypes(): Promise<any> {
return this.request('/credential-types');
}
async getCredentials(): Promise<any> {
return this.request('/credentials');
}
// Node operations
async getNodeTypes(): Promise<any> {
return this.request('/node-types');
}
async getNodeType(nodeType: string): Promise<any> {
return this.request(`/node-types/${nodeType}`);
}
// Extended methods for node source extraction
async getNodeSourceCode(nodeType: string): Promise<any> {
// 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<any> {
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'}`);
}
}
}

19
src/utils/version.ts Normal file
View File

@@ -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();