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:
@@ -24,18 +24,18 @@ else
|
|||||||
echo "❌ dist/mcp/index.js not found - run 'npm run build'"
|
echo "❌ dist/mcp/index.js not found - run 'npm run build'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "dist/mcp/server-update.js" ]; then
|
if [ -f "dist/mcp/server.js" ]; then
|
||||||
echo "✅ dist/mcp/server-update.js exists"
|
echo "✅ dist/mcp/server.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)"
|
echo " Last modified: $(stat -f "%Sm" dist/mcp/server.js 2>/dev/null || stat -c "%y" dist/mcp/server.js 2>/dev/null)"
|
||||||
else
|
else
|
||||||
echo "❌ dist/mcp/server-update.js not found"
|
echo "❌ dist/mcp/server.js not found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "dist/mcp/tools-update.js" ]; then
|
if [ -f "dist/mcp/tools.js" ]; then
|
||||||
echo "✅ dist/mcp/tools-update.js exists"
|
echo "✅ dist/mcp/tools.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)"
|
echo " Last modified: $(stat -f "%Sm" dist/mcp/tools.js 2>/dev/null || stat -c "%y" dist/mcp/tools.js 2>/dev/null)"
|
||||||
else
|
else
|
||||||
echo "❌ dist/mcp/tools-update.js not found"
|
echo "❌ dist/mcp/tools.js not found"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -52,12 +52,12 @@ echo ""
|
|||||||
|
|
||||||
# Check tools in compiled code
|
# Check tools in compiled code
|
||||||
echo "5. Compiled tools check:"
|
echo "5. Compiled tools check:"
|
||||||
if [ -f "dist/mcp/tools-update.js" ]; then
|
if [ -f "dist/mcp/tools.js" ]; then
|
||||||
TOOL_COUNT=$(grep "name: '" dist/mcp/tools-update.js | wc -l | tr -d ' ')
|
TOOL_COUNT=$(grep "name: '" dist/mcp/tools.js | wc -l | tr -d ' ')
|
||||||
echo " Total tools found: $TOOL_COUNT"
|
echo " Total tools found: $TOOL_COUNT"
|
||||||
echo " New tools present:"
|
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
|
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"
|
echo " ✅ $tool"
|
||||||
else
|
else
|
||||||
echo " ❌ $tool"
|
echo " ❌ $tool"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
"start": "node dist/mcp/index.js",
|
"start": "node dist/mcp/index.js",
|
||||||
"start:http": "MCP_MODE=http 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: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",
|
"http": "npm run build && npm run start:http:fixed",
|
||||||
"dev": "npm run build && npm run rebuild && npm run validate",
|
"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'",
|
"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",
|
"test:update-partial:debug": "node dist/scripts/test-update-partial-debug.js",
|
||||||
"db:rebuild": "node dist/scripts/rebuild-database.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')\"",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Debug the essentials implementation
|
* 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 { PropertyFilter } = require('../dist/services/property-filter');
|
||||||
const { ExampleGenerator } = require('../dist/services/example-generator');
|
const { ExampleGenerator } = require('../dist/services/example-generator');
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Debug script to check node data structure
|
* Debug script to check node data structure
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { N8NDocumentationMCPServer } = require('../dist/mcp/server-update');
|
const { N8NDocumentationMCPServer } = require('../dist/mcp/server');
|
||||||
|
|
||||||
async function debugNode() {
|
async function debugNode() {
|
||||||
console.log('🔍 Debugging node data\n');
|
console.log('🔍 Debugging node data\n');
|
||||||
|
|||||||
40
scripts/sync-runtime-version.js
Executable file
40
scripts/sync-runtime-version.js
Executable 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);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* Direct test of the server functionality without MCP protocol
|
* 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() {
|
async function testDirect() {
|
||||||
console.log('🧪 Direct server test\n');
|
console.log('🧪 Direct server test\n');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* 4. Tests the property search functionality
|
* 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 { readFileSync, writeFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Final validation test
|
* Final validation test
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { N8NDocumentationMCPServer } = require('../dist/mcp/server-update');
|
const { N8NDocumentationMCPServer } = require('../dist/mcp/server');
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
green: '\x1b[32m',
|
green: '\x1b[32m',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Test get_node_info to diagnose timeout issues
|
* Test get_node_info to diagnose timeout issues
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { N8NDocumentationMCPServer } = require('../dist/mcp/server-update');
|
const { N8NDocumentationMCPServer } = require('../dist/mcp/server');
|
||||||
|
|
||||||
async function testNodeInfo() {
|
async function testNodeInfo() {
|
||||||
console.log('🔍 Testing get_node_info...\n');
|
console.log('🔍 Testing get_node_info...\n');
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
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 { ConsoleManager } from './utils/console-manager';
|
||||||
import { logger } from './utils/logger';
|
import { logger } from './utils/logger';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Minimal HTTP server for n8n-MCP
|
* Fixed HTTP server for n8n-MCP that properly handles StreamableHTTPServerTransport initialization
|
||||||
* Single-user, stateless design for private deployments
|
* This implementation ensures the transport is properly initialized before handling requests
|
||||||
*/
|
*/
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { N8NDocumentationMCPServer } from './mcp/server-update';
|
import { n8nDocumentationToolsFinal } from './mcp/tools';
|
||||||
|
import { N8NDocumentationMCPServer } from './mcp/server';
|
||||||
import { logger } from './utils/logger';
|
import { logger } from './utils/logger';
|
||||||
|
import { PROJECT_VERSION } from './utils/version';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
let server: any;
|
let expressServer: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate required environment variables
|
* Validate required environment variables
|
||||||
@@ -27,7 +29,6 @@ function validateEnvironment() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate AUTH_TOKEN length
|
|
||||||
if (process.env.AUTH_TOKEN && process.env.AUTH_TOKEN.length < 32) {
|
if (process.env.AUTH_TOKEN && process.env.AUTH_TOKEN.length < 32) {
|
||||||
logger.warn('AUTH_TOKEN should be at least 32 characters for security');
|
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');
|
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...');
|
logger.info('Shutting down HTTP server...');
|
||||||
console.log('Shutting down HTTP server...');
|
console.log('Shutting down HTTP server...');
|
||||||
|
|
||||||
if (server) {
|
if (expressServer) {
|
||||||
server.close(() => {
|
expressServer.close(() => {
|
||||||
logger.info('HTTP server closed');
|
logger.info('HTTP server closed');
|
||||||
console.log('HTTP server closed');
|
console.log('HTTP server closed');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force shutdown after 10 seconds
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
logger.error('Forced shutdown after timeout');
|
logger.error('Forced shutdown after timeout');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -58,14 +58,12 @@ async function shutdown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startHTTPServer() {
|
export async function startFixedHTTPServer() {
|
||||||
// Validate environment
|
|
||||||
validateEnvironment();
|
validateEnvironment();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// DON'T parse JSON globally - StreamableHTTPServerTransport needs raw stream
|
// CRITICAL: Don't use any body parser - StreamableHTTPServerTransport needs raw stream
|
||||||
// Only parse for specific endpoints that need it
|
|
||||||
|
|
||||||
// Security headers
|
// Security headers
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
@@ -76,13 +74,13 @@ export async function startHTTPServer() {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// CORS configuration for mcp-remote compatibility
|
// CORS configuration
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const allowedOrigin = process.env.CORS_ORIGIN || '*';
|
const allowedOrigin = process.env.CORS_ORIGIN || '*';
|
||||||
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept');
|
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') {
|
if (req.method === 'OPTIONS') {
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
@@ -91,7 +89,7 @@ export async function startHTTPServer() {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request logging middleware
|
// Request logging
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
logger.info(`${req.method} ${req.path}`, {
|
logger.info(`${req.method} ${req.path}`, {
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
@@ -101,12 +99,16 @@ export async function startHTTPServer() {
|
|||||||
next();
|
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) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
mode: 'http',
|
mode: 'http-fixed',
|
||||||
version: '2.3.2',
|
version: PROJECT_VERSION,
|
||||||
uptime: Math.floor(process.uptime()),
|
uptime: Math.floor(process.uptime()),
|
||||||
memory: {
|
memory: {
|
||||||
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
||||||
@@ -116,8 +118,28 @@ export async function startHTTPServer() {
|
|||||||
timestamp: new Date().toISOString()
|
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<void> => {
|
app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
@@ -143,38 +165,119 @@ export async function startHTTPServer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new instances for each request (stateless)
|
|
||||||
const mcpServer = new N8NDocumentationMCPServer();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a stateless transport
|
// Instead of using StreamableHTTPServerTransport, we'll handle the request directly
|
||||||
const transport = new StreamableHTTPServerTransport({
|
// This avoids the initialization issues with the transport
|
||||||
sessionIdGenerator: undefined, // Stateless mode
|
|
||||||
|
// Collect the raw body
|
||||||
|
let body = '';
|
||||||
|
req.on('data', chunk => {
|
||||||
|
body += chunk.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect server to transport
|
req.on('end', async () => {
|
||||||
await mcpServer.connect(transport);
|
try {
|
||||||
|
const jsonRpcRequest = JSON.parse(body);
|
||||||
// Handle the request - Fixed: removed third parameter
|
logger.debug('Received JSON-RPC request:', { method: jsonRpcRequest.method });
|
||||||
await transport.handleRequest(req, res);
|
|
||||||
|
// Handle the request based on method
|
||||||
// Log request duration
|
let response;
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
logger.info('MCP request completed', {
|
switch (jsonRpcRequest.method) {
|
||||||
duration
|
case 'initialize':
|
||||||
});
|
response = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
// Clean up on close
|
result: {
|
||||||
res.on('close', () => {
|
protocolVersion: '1.0',
|
||||||
logger.debug('Request closed, cleaning up');
|
capabilities: {
|
||||||
transport.close().catch(err =>
|
tools: {},
|
||||||
logger.error('Error closing transport:', err)
|
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) {
|
} catch (error) {
|
||||||
logger.error('MCP request error:', error);
|
logger.error('MCP request error:', error);
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
logger.error('MCP request failed', { duration });
|
|
||||||
|
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -220,16 +323,16 @@ export async function startHTTPServer() {
|
|||||||
const port = parseInt(process.env.PORT || '3000');
|
const port = parseInt(process.env.PORT || '3000');
|
||||||
const host = process.env.HOST || '0.0.0.0';
|
const host = process.env.HOST || '0.0.0.0';
|
||||||
|
|
||||||
server = app.listen(port, host, () => {
|
expressServer = app.listen(port, host, () => {
|
||||||
logger.info(`n8n MCP HTTP Server started`, { port, host });
|
logger.info(`n8n MCP Fixed HTTP Server started`, { port, host });
|
||||||
console.log(`n8n MCP HTTP Server running on ${host}:${port}`);
|
console.log(`n8n MCP Fixed HTTP Server running on ${host}:${port}`);
|
||||||
console.log(`Health check: http://localhost:${port}/health`);
|
console.log(`Health check: http://localhost:${port}/health`);
|
||||||
console.log(`MCP endpoint: http://localhost:${port}/mcp`);
|
console.log(`MCP endpoint: http://localhost:${port}/mcp`);
|
||||||
console.log('\nPress Ctrl+C to stop the server');
|
console.log('\nPress Ctrl+C to stop the server');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
server.on('error', (error: any) => {
|
expressServer.on('error', (error: any) => {
|
||||||
if (error.code === 'EADDRINUSE') {
|
if (error.code === 'EADDRINUSE') {
|
||||||
logger.error(`Port ${port} is already in use`);
|
logger.error(`Port ${port} is already in use`);
|
||||||
console.error(`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
|
// Start if called directly
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
startHTTPServer().catch(error => {
|
startFixedHTTPServer().catch(error => {
|
||||||
logger.error('Failed to start HTTP server:', error);
|
logger.error('Failed to start Fixed HTTP server:', error);
|
||||||
console.error('Failed to start HTTP server:', error);
|
console.error('Failed to start Fixed HTTP server:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
export { N8NMCPEngine, EngineHealth, EngineOptions } from './mcp-engine';
|
export { N8NMCPEngine, EngineHealth, EngineOptions } from './mcp-engine';
|
||||||
export { SingleSessionHTTPServer } from './http-server-single-session';
|
export { SingleSessionHTTPServer } from './http-server-single-session';
|
||||||
export { ConsoleManager } from './utils/console-manager';
|
export { ConsoleManager } from './utils/console-manager';
|
||||||
export { N8NDocumentationMCPServer } from './mcp/server-update';
|
export { N8NDocumentationMCPServer } from './mcp/server';
|
||||||
|
|
||||||
// Default export for convenience
|
// Default export for convenience
|
||||||
import N8NMCPEngine from './mcp-engine';
|
import N8NMCPEngine from './mcp-engine';
|
||||||
|
|||||||
@@ -831,4 +831,108 @@ 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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { N8NDocumentationMCPServer } from './server-update';
|
import { N8NDocumentationMCPServer } from './server';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// Add error details to stderr for Claude Desktop debugging
|
// Add error details to stderr for Claude Desktop debugging
|
||||||
@@ -35,7 +35,7 @@ async function main() {
|
|||||||
// Check if we should use the fixed implementation
|
// Check if we should use the fixed implementation
|
||||||
if (process.env.USE_FIXED_HTTP === 'true') {
|
if (process.env.USE_FIXED_HTTP === 'true') {
|
||||||
// Use the fixed HTTP implementation that bypasses StreamableHTTPServerTransport issues
|
// Use the fixed HTTP implementation that bypasses StreamableHTTPServerTransport issues
|
||||||
const { startFixedHTTPServer } = await import('../http-server-fixed');
|
const { startFixedHTTPServer } = await import('../http-server');
|
||||||
await startFixedHTTPServer();
|
await startFixedHTTPServer();
|
||||||
} else {
|
} else {
|
||||||
// HTTP mode - for remote deployment with single-session architecture
|
// HTTP mode - for remote deployment with single-session architecture
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { n8nDocumentationToolsFinal } from './tools-update';
|
import { n8nDocumentationToolsFinal } from './tools';
|
||||||
import { n8nManagementTools } from './tools-n8n-manager';
|
import { n8nManagementTools } from './tools-n8n-manager';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { NodeRepository } from '../database/node-repository';
|
import { NodeRepository } from '../database/node-repository';
|
||||||
@@ -24,6 +24,7 @@ import { WorkflowValidator } from '../services/workflow-validator';
|
|||||||
import { isN8nApiConfigured } from '../config/n8n-api';
|
import { isN8nApiConfigured } from '../config/n8n-api';
|
||||||
import * as n8nHandlers from './handlers-n8n-manager';
|
import * as n8nHandlers from './handlers-n8n-manager';
|
||||||
import { handleUpdatePartialWorkflow } from './handlers-workflow-diff';
|
import { handleUpdatePartialWorkflow } from './handlers-workflow-diff';
|
||||||
|
import { PROJECT_VERSION } from '../utils/version';
|
||||||
|
|
||||||
interface NodeRow {
|
interface NodeRow {
|
||||||
node_type: string;
|
node_type: string;
|
||||||
@@ -121,7 +122,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
},
|
},
|
||||||
serverInfo: {
|
serverInfo: {
|
||||||
name: 'n8n-documentation-mcp',
|
name: 'n8n-documentation-mcp',
|
||||||
version: '2.4.1',
|
version: PROJECT_VERSION,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -261,6 +262,8 @@ export class N8NDocumentationMCPServer {
|
|||||||
return n8nHandlers.handleHealthCheck();
|
return n8nHandlers.handleHealthCheck();
|
||||||
case 'n8n_list_available_tools':
|
case 'n8n_list_available_tools':
|
||||||
return n8nHandlers.handleListAvailableTools();
|
return n8nHandlers.handleListAvailableTools();
|
||||||
|
case 'n8n_diagnostic':
|
||||||
|
return n8nHandlers.handleDiagnostic({ params: { arguments: args } });
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
@@ -40,7 +40,7 @@ console.count = () => {};
|
|||||||
console.countReset = () => {};
|
console.countReset = () => {};
|
||||||
|
|
||||||
// Import and run the server AFTER suppressing output
|
// Import and run the server AFTER suppressing output
|
||||||
import { N8NDocumentationMCPServer } from './server-update';
|
import { N8NDocumentationMCPServer } from './server';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -484,5 +484,18 @@ Validation example:
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {}
|
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)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { createDatabaseAdapter } from '../database/database-adapter';
|
import { createDatabaseAdapter } from '../database/database-adapter';
|
||||||
import { NodeRepository } from '../database/node-repository';
|
import { NodeRepository } from '../database/node-repository';
|
||||||
import { N8NDocumentationMCPServer } from '../mcp/server-update';
|
import { N8NDocumentationMCPServer } from '../mcp/server';
|
||||||
import { Logger } from '../utils/logger';
|
import { Logger } from '../utils/logger';
|
||||||
|
|
||||||
const logger = new Logger({ prefix: '[TestMCPTools]' });
|
const logger = new Logger({ prefix: '[TestMCPTools]' });
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ export interface MCPServerConfig {
|
|||||||
authToken?: string;
|
authToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface N8NConfig {
|
|
||||||
apiUrl: string;
|
|
||||||
apiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolDefinition {
|
export interface ToolDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
@@ -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
19
src/utils/version.ts
Normal 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();
|
||||||
Reference in New Issue
Block a user