fix: resolve Docker stdio initialization timeout issue
- Add InitializeRequestSchema handler to MCP server - Implement stdout flushing for Docker environments - Create stdio-wrapper for clean JSON-RPC communication - Update docker-entrypoint.sh to prevent stdout pollution - Fix logger to check MCP_MODE before level check These changes ensure the MCP server responds to initialization requests within Claude Desktop's 60-second timeout when running in Docker.
This commit is contained in:
@@ -35,12 +35,19 @@ fi
|
|||||||
# Trap signals for graceful shutdown
|
# Trap signals for graceful shutdown
|
||||||
# In stdio mode, don't output anything to stdout as it breaks JSON-RPC
|
# In stdio mode, don't output anything to stdout as it breaks JSON-RPC
|
||||||
if [ "$MCP_MODE" = "stdio" ]; then
|
if [ "$MCP_MODE" = "stdio" ]; then
|
||||||
trap 'kill -TERM $PID 2>/dev/null' TERM INT
|
# Silent trap - no output at all
|
||||||
|
trap 'kill -TERM $PID 2>/dev/null || true' TERM INT EXIT
|
||||||
else
|
else
|
||||||
trap 'echo "Shutting down..." >&2; kill -TERM $PID' TERM INT
|
# In HTTP mode, output to stderr
|
||||||
|
trap 'echo "Shutting down..." >&2; kill -TERM $PID 2>/dev/null' TERM INT EXIT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Execute the main command in background
|
# Execute the main command in background
|
||||||
"$@" &
|
# In stdio mode, use the wrapper for clean output
|
||||||
|
if [ "$MCP_MODE" = "stdio" ] && [ -f "/app/dist/mcp/stdio-wrapper.js" ]; then
|
||||||
|
node /app/dist/mcp/stdio-wrapper.js &
|
||||||
|
else
|
||||||
|
"$@" &
|
||||||
|
fi
|
||||||
PID=$!
|
PID=$!
|
||||||
wait $PID
|
wait $PID
|
||||||
79
docs/N8N_COMMUNITY_POST.md
Normal file
79
docs/N8N_COMMUNITY_POST.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# 🚀 Claude Desktop Can Now Build Perfect n8n Workflows - Thanks to MCP
|
||||||
|
|
||||||
|
Hey n8n community! 👋
|
||||||
|
|
||||||
|
**TL;DR:** I built an MCP server that gives Claude Desktop complete, up-to-date knowledge of all 525 n8n nodes. Now it can build workflows perfectly on the first try. What used to take 45 minutes with errors now takes 3 minutes with zero mistakes.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
Remember when Claude would guess node names wrong and mix up properties?
|
||||||
|
|
||||||
|
```
|
||||||
|
Me: "Create a Slack webhook workflow"
|
||||||
|
Claude: "Use slackNode with message property..."
|
||||||
|
Me: "It's 'slack' with 'text' property..."
|
||||||
|
Claude: "Oh, webhookTrigger then?"
|
||||||
|
Me: "Just 'webhook'..."
|
||||||
|
```
|
||||||
|
|
||||||
|
45 painful minutes later, maybe you'd have a working workflow.
|
||||||
|
|
||||||
|
## The Solution: n8n-MCP
|
||||||
|
|
||||||
|
Now Claude has direct access to:
|
||||||
|
- ✅ **All 525 n8n nodes** with complete documentation
|
||||||
|
- ✅ **Every property and operation** (no more guessing!)
|
||||||
|
- ✅ **Working examples** for common tasks
|
||||||
|
- ✅ **Real-time validation** before deployment
|
||||||
|
|
||||||
|
Result: **45 minutes → 3 minutes**, **6 errors → 0 errors**
|
||||||
|
|
||||||
|
## Quick Installation (5 Minutes)
|
||||||
|
|
||||||
|
Add this to your Claude Desktop config:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"n8n-mcp": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run", "--rm", "-i",
|
||||||
|
"--pull", "always",
|
||||||
|
"ghcr.io/czlonkowski/n8n-mcp:latest"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Config locations:**
|
||||||
|
- Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
Restart Claude Desktop and you're done! 🎉
|
||||||
|
|
||||||
|
## What You Can Do Now
|
||||||
|
|
||||||
|
Ask Claude to:
|
||||||
|
- "Build a workflow that monitors RSS feeds and posts to Discord"
|
||||||
|
- "Create an API endpoint that validates data and saves to Postgres"
|
||||||
|
- "Set up a daily report that pulls from multiple sources"
|
||||||
|
|
||||||
|
Claude will deliver working JSON you can paste directly into n8n.
|
||||||
|
|
||||||
|
## More Options & Details
|
||||||
|
|
||||||
|
🔗 **GitHub**: [github.com/czlonkowski/n8n-mcp](https://github.com/czlonkowski/n8n-mcp)
|
||||||
|
- Local installation options
|
||||||
|
- Full documentation
|
||||||
|
- MIT licensed (free forever)
|
||||||
|
|
||||||
|
## Feedback Welcome!
|
||||||
|
|
||||||
|
What workflow challenges do you face? What would make Claude even better at n8n? Drop a comment or open an issue on GitHub.
|
||||||
|
|
||||||
|
Let's make n8n + AI workflow creation delightful! ⭐
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**P.S.** - Claude even discovered features I didn't know about, like Google Sheets' built-in duplicate detection. Sometimes the AI teaches the human! 😄
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Claude Desktop Configuration Installer for n8n-MCP
|
|
||||||
|
|
||||||
echo "🔧 n8n-MCP Claude Desktop Configuration Installer"
|
|
||||||
echo "================================================"
|
|
||||||
|
|
||||||
# Get the current directory
|
|
||||||
CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
|
|
||||||
# Detect OS and set config path
|
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
||||||
CONFIG_PATH="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
|
|
||||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
|
||||||
CONFIG_PATH="$APPDATA/Claude/claude_desktop_config.json"
|
|
||||||
else
|
|
||||||
CONFIG_PATH="$HOME/.config/Claude/claude_desktop_config.json"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📍 Detected config location: $CONFIG_PATH"
|
|
||||||
echo "📂 n8n-MCP installation path: $CURRENT_DIR"
|
|
||||||
|
|
||||||
# Create directory if it doesn't exist
|
|
||||||
CONFIG_DIR=$(dirname "$CONFIG_PATH")
|
|
||||||
if [ ! -d "$CONFIG_DIR" ]; then
|
|
||||||
echo "📁 Creating config directory..."
|
|
||||||
mkdir -p "$CONFIG_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Backup existing config if it exists
|
|
||||||
if [ -f "$CONFIG_PATH" ]; then
|
|
||||||
echo "💾 Backing up existing config to ${CONFIG_PATH}.backup"
|
|
||||||
cp "$CONFIG_PATH" "${CONFIG_PATH}.backup"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the new config
|
|
||||||
cat > "$CONFIG_PATH" << EOF
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"n8n-documentation": {
|
|
||||||
"command": "node",
|
|
||||||
"args": [
|
|
||||||
"$CURRENT_DIR/dist/mcp/index.js"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Configuration installed!"
|
|
||||||
echo ""
|
|
||||||
echo "📋 Next steps:"
|
|
||||||
echo "1. Build the project: npm run build"
|
|
||||||
echo "2. Rebuild database: npm run rebuild"
|
|
||||||
echo "3. Restart Claude Desktop"
|
|
||||||
echo ""
|
|
||||||
echo "🚀 The n8n-documentation server will be available in Claude!"
|
|
||||||
@@ -46,33 +46,53 @@ export interface ColumnDefinition {
|
|||||||
*/
|
*/
|
||||||
export async function createDatabaseAdapter(dbPath: string): Promise<DatabaseAdapter> {
|
export async function createDatabaseAdapter(dbPath: string): Promise<DatabaseAdapter> {
|
||||||
// Log Node.js version information
|
// Log Node.js version information
|
||||||
|
// Only log in non-stdio mode
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.info(`Node.js version: ${process.version}`);
|
logger.info(`Node.js version: ${process.version}`);
|
||||||
|
}
|
||||||
|
// Only log in non-stdio mode
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.info(`Platform: ${process.platform} ${process.arch}`);
|
logger.info(`Platform: ${process.platform} ${process.arch}`);
|
||||||
|
}
|
||||||
|
|
||||||
// First, try to use better-sqlite3
|
// First, try to use better-sqlite3
|
||||||
try {
|
try {
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.info('Attempting to use better-sqlite3...');
|
logger.info('Attempting to use better-sqlite3...');
|
||||||
|
}
|
||||||
const adapter = await createBetterSQLiteAdapter(dbPath);
|
const adapter = await createBetterSQLiteAdapter(dbPath);
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.info('Successfully initialized better-sqlite3 adapter');
|
logger.info('Successfully initialized better-sqlite3 adapter');
|
||||||
|
}
|
||||||
return adapter;
|
return adapter;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
// Check if it's a version mismatch error
|
// Check if it's a version mismatch error
|
||||||
if (errorMessage.includes('NODE_MODULE_VERSION') || errorMessage.includes('was compiled against a different Node.js version')) {
|
if (errorMessage.includes('NODE_MODULE_VERSION') || errorMessage.includes('was compiled against a different Node.js version')) {
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.warn(`Node.js version mismatch detected. Better-sqlite3 was compiled for a different Node.js version.`);
|
logger.warn(`Node.js version mismatch detected. Better-sqlite3 was compiled for a different Node.js version.`);
|
||||||
|
}
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.warn(`Current Node.js version: ${process.version}`);
|
logger.warn(`Current Node.js version: ${process.version}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.warn('Failed to initialize better-sqlite3, falling back to sql.js', error);
|
logger.warn('Failed to initialize better-sqlite3, falling back to sql.js', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Fall back to sql.js
|
// Fall back to sql.js
|
||||||
try {
|
try {
|
||||||
const adapter = await createSQLJSAdapter(dbPath);
|
const adapter = await createSQLJSAdapter(dbPath);
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.info('Successfully initialized sql.js adapter (pure JavaScript, no native dependencies)');
|
logger.info('Successfully initialized sql.js adapter (pure JavaScript, no native dependencies)');
|
||||||
|
}
|
||||||
return adapter;
|
return adapter;
|
||||||
} catch (sqlJsError) {
|
} catch (sqlJsError) {
|
||||||
|
if (process.env.MCP_MODE !== 'stdio') {
|
||||||
logger.error('Failed to initialize sql.js adapter', sqlJsError);
|
logger.error('Failed to initialize sql.js adapter', sqlJsError);
|
||||||
|
}
|
||||||
throw new Error('Failed to initialize any database adapter');
|
throw new Error('Failed to initialize any database adapter');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|||||||
import {
|
import {
|
||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
|
InitializeRequestSchema,
|
||||||
} 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';
|
||||||
@@ -102,6 +103,27 @@ export class N8NDocumentationMCPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupHandlers(): void {
|
private setupHandlers(): void {
|
||||||
|
// Handle initialization
|
||||||
|
this.server.setRequestHandler(InitializeRequestSchema, async () => {
|
||||||
|
const response = {
|
||||||
|
protocolVersion: '2024-11-05',
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
serverInfo: {
|
||||||
|
name: 'n8n-documentation-mcp',
|
||||||
|
version: '2.4.1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug: Log to stderr to see if handler is called
|
||||||
|
if (process.env.DEBUG_MCP === 'true') {
|
||||||
|
console.error('Initialize handler called, returning:', JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
// Handle tool listing
|
// Handle tool listing
|
||||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
tools: n8nDocumentationToolsFinal,
|
tools: n8nDocumentationToolsFinal,
|
||||||
@@ -915,9 +937,15 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
|||||||
|
|
||||||
// Force flush stdout for Docker environments
|
// Force flush stdout for Docker environments
|
||||||
// Docker uses block buffering which can delay MCP responses
|
// Docker uses block buffering which can delay MCP responses
|
||||||
if (!process.stdout.isTTY) {
|
if (!process.stdout.isTTY || process.env.IS_DOCKER) {
|
||||||
// Write empty string to force flush
|
// Override write to auto-flush
|
||||||
process.stdout.write('', () => {});
|
const originalWrite = process.stdout.write.bind(process.stdout);
|
||||||
|
process.stdout.write = function(chunk: any, encoding?: any, callback?: any) {
|
||||||
|
const result = originalWrite(chunk, encoding, callback);
|
||||||
|
// Force immediate flush
|
||||||
|
process.stdout.emit('drain');
|
||||||
|
return result;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('n8n Documentation MCP Server running on stdio transport');
|
logger.info('n8n Documentation MCP Server running on stdio transport');
|
||||||
|
|||||||
52
src/mcp/stdio-wrapper.ts
Normal file
52
src/mcp/stdio-wrapper.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stdio wrapper for MCP server
|
||||||
|
* Ensures clean JSON-RPC communication by suppressing all non-JSON output
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Suppress all console output before anything else
|
||||||
|
const originalConsoleLog = console.log;
|
||||||
|
const originalConsoleError = console.error;
|
||||||
|
const originalConsoleWarn = console.warn;
|
||||||
|
const originalConsoleInfo = console.info;
|
||||||
|
const originalConsoleDebug = console.debug;
|
||||||
|
|
||||||
|
// Override all console methods
|
||||||
|
console.log = () => {};
|
||||||
|
console.error = () => {};
|
||||||
|
console.warn = () => {};
|
||||||
|
console.info = () => {};
|
||||||
|
console.debug = () => {};
|
||||||
|
|
||||||
|
// Set environment to ensure logger suppression
|
||||||
|
process.env.MCP_MODE = 'stdio';
|
||||||
|
process.env.DISABLE_CONSOLE_OUTPUT = 'true';
|
||||||
|
process.env.LOG_LEVEL = 'error';
|
||||||
|
|
||||||
|
// Import and run the server
|
||||||
|
import { N8NDocumentationMCPServer } from './server-update';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const server = new N8NDocumentationMCPServer();
|
||||||
|
await server.run();
|
||||||
|
} catch (error) {
|
||||||
|
// In case of fatal error, output to stderr only
|
||||||
|
originalConsoleError('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle uncaught errors silently
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
originalConsoleError('Uncaught exception:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
originalConsoleError('Unhandled rejection:', reason);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -51,15 +51,19 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private log(level: LogLevel, levelName: string, message: string, ...args: any[]): void {
|
private log(level: LogLevel, levelName: string, message: string, ...args: any[]): void {
|
||||||
if (level <= this.config.level) {
|
// Check environment variables FIRST, before level check
|
||||||
const formattedMessage = this.formatMessage(levelName, message);
|
|
||||||
|
|
||||||
// In stdio mode, suppress ALL console output to avoid corrupting JSON-RPC
|
// In stdio mode, suppress ALL console output to avoid corrupting JSON-RPC
|
||||||
if (process.env.MCP_MODE === 'stdio' || process.env.DISABLE_CONSOLE_OUTPUT === 'true') {
|
const isStdio = process.env.MCP_MODE === 'stdio';
|
||||||
|
const isDisabled = process.env.DISABLE_CONSOLE_OUTPUT === 'true';
|
||||||
|
|
||||||
|
if (isStdio || isDisabled) {
|
||||||
// Silently drop all logs in stdio mode
|
// Silently drop all logs in stdio mode
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (level <= this.config.level) {
|
||||||
|
const formattedMessage = this.formatMessage(levelName, message);
|
||||||
|
|
||||||
// In HTTP mode during request handling, suppress console output
|
// In HTTP mode during request handling, suppress console output
|
||||||
// The ConsoleManager will handle this, but we add a safety check
|
// The ConsoleManager will handle this, but we add a safety check
|
||||||
if (process.env.MCP_MODE === 'http' && process.env.MCP_REQUEST_ACTIVE === 'true') {
|
if (process.env.MCP_MODE === 'http' && process.env.MCP_REQUEST_ACTIVE === 'true') {
|
||||||
|
|||||||
Reference in New Issue
Block a user