diff --git a/Dockerfile b/Dockerfile index bba18b5..ac01542 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,6 +76,9 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh # Switch to non-root user USER nodejs +# Set Docker environment flag +ENV IS_DOCKER=true + # Expose HTTP port EXPOSE 3000 diff --git a/README.md b/README.md index d702bb2..177c49b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,45 @@ When Claude, Anthropic's AI assistant, tested n8n-MCP, the results were transfor Get n8n-MCP running in 5 minutes: -### Option 1: Local Installation (Recommended) +### Option 1: Docker (Easiest) + +**Prerequisites:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed on your system + +```bash +# Pull the Docker image +docker pull ghcr.io/czlonkowski/n8n-mcp:latest +``` + +Add to Claude Desktop config: +```json +{ + "mcpServers": { + "n8n-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", "MCP_MODE=stdio", + "-e", "LOG_LEVEL=error", + "-e", "DISABLE_CONSOLE_OUTPUT=true", + "ghcr.io/czlonkowski/n8n-mcp:latest" + ] + } + } +} +``` + +**Important:** The `-i` flag is required for MCP stdio communication. + +**Configuration file locations:** +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` +- **Linux**: `~/.config/Claude/claude_desktop_config.json` + +**Restart Claude Desktop after updating configuration** - That's it! 🎉 + +### Option 2: Local Installation **Prerequisites:** [Node.js](https://nodejs.org/) installed on your system @@ -63,25 +101,6 @@ Add to Claude Desktop config: } ``` -### Option 2: Docker (Experimental) - -⚠️ **Known Issue**: Docker support has a timeout issue with MCP initialization. The server doesn't respond to Claude's initialize request within 60 seconds, causing connection failures. Use local installation until this is resolved. - -For brave souls who want to help debug: -```bash -docker pull ghcr.io/czlonkowski/n8n-mcp:latest -``` - -See [Issue #X](https://github.com/czlonkowski/n8n-mcp/issues) for updates. - -### Configuration File Locations - -- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` -- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` -- **Linux**: `~/.config/Claude/claude_desktop_config.json` - -**Remember to restart Claude Desktop after updating configuration!** 🎉 - ## Features diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index ab83b6c..fa62cd4 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -33,7 +33,12 @@ if [ "$(id -u)" = "0" ]; then fi # Trap signals for graceful shutdown -trap 'echo "Shutting down..."; kill -TERM $PID' TERM INT +# In stdio mode, don't output anything to stdout as it breaks JSON-RPC +if [ "$MCP_MODE" = "stdio" ]; then + trap 'kill -TERM $PID 2>/dev/null' TERM INT +else + trap 'echo "Shutting down..." >&2; kill -TERM $PID' TERM INT +fi # Execute the main command in background "$@" & diff --git a/docs/DOCKER_FIX_IMPLEMENTATION.md b/docs/DOCKER_FIX_IMPLEMENTATION.md new file mode 100644 index 0000000..b29f778 --- /dev/null +++ b/docs/DOCKER_FIX_IMPLEMENTATION.md @@ -0,0 +1,163 @@ +# Docker stdio Fix Implementation Plan for n8n-MCP + +Based on community research and successful MCP Docker deployments, here's a streamlined fix for the initialization timeout issue. + +## Root Cause + +Docker treats container stdout as a pipe (not TTY), causing block buffering. The MCP server's JSON-RPC responses sit in the buffer instead of being immediately sent to Claude Desktop, causing a 60-second timeout. + +## Implementation Steps + +### Step 1: Test Simple Interactive Mode + +First, verify if just using `-i` flag solves the issue: + +**Update README.md:** +```json +{ + "mcpServers": { + "n8n-mcp": { + "command": "docker", + "args": [ + "run", + "-i", // Interactive mode - keeps stdin open + "--rm", + "-e", "MCP_MODE=stdio", + "-e", "LOG_LEVEL=error", + "-e", "DISABLE_CONSOLE_OUTPUT=true", + "ghcr.io/czlonkowski/n8n-mcp:latest" + ] + } + } +} +``` + +**Test command:** +```bash +echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05"},"id":1}' | \ + docker run -i --rm \ + -e MCP_MODE=stdio \ + -e LOG_LEVEL=error \ + -e DISABLE_CONSOLE_OUTPUT=true \ + ghcr.io/czlonkowski/n8n-mcp:latest +``` + +Expected: Should receive a JSON response immediately. + +### Step 2: Add Explicit Stdout Flushing (If Needed) + +If Step 1 doesn't work, add minimal flushing to the Node.js server: + +**File: `src/mcp/server-update.ts`** + +Update the `run()` method: +```typescript +async run(): Promise { + await this.ensureInitialized(); + + const transport = new StdioServerTransport(); + await this.server.connect(transport); + + // Ensure stdout is not buffered in Docker + if (!process.stdout.isTTY && process.env.IS_DOCKER) { + // Force unbuffered stdout + process.stdout.write(''); + } + + logger.info('n8n Documentation MCP Server running on stdio transport'); + + // Keep process alive + process.stdin.resume(); +} +``` + +**File: `Dockerfile`** + +Add environment variable: +```dockerfile +ENV IS_DOCKER=true +``` + +### Step 3: System-Level Unbuffering (Last Resort) + +Only if Steps 1-2 fail, implement stdbuf wrapper: + +**File: `docker-entrypoint.sh`** +```bash +#!/bin/sh +# Force line buffering for stdio communication +exec stdbuf -oL -eL node /app/dist/mcp/index.js +``` + +**File: `Dockerfile`** +```dockerfile +# Add stdbuf utility +RUN apk add --no-cache coreutils + +# Copy and setup entrypoint +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] +``` + +## Testing Protocol + +### 1. Local Docker Test +```bash +# Build test image +docker build -t n8n-mcp:test . + +# Test with echo pipe +echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05"},"id":1}' | \ + docker run -i --rm n8n-mcp:test | head -1 + +# Should see immediate JSON response +``` + +### 2. Claude Desktop Test +1. Update `claude_desktop_config.json` with new configuration +2. Restart Claude Desktop +3. Check Developer tab for "running" status +4. Test a simple MCP command + +### 3. Debug if Needed +```bash +# Run with stderr output for debugging +docker run -i --rm \ + -e MCP_MODE=stdio \ + -e LOG_LEVEL=debug \ + ghcr.io/czlonkowski/n8n-mcp:latest 2>debug.log +``` + +## Success Criteria + +- [ ] No timeout errors in Claude Desktop logs +- [ ] MCP tools are accessible immediately +- [ ] No "Shutting down..." messages in stdout +- [ ] Simple `-i` flag configuration works + +## Rollout Plan + +1. **Test locally** with simple `-i` flag first +2. **Update Docker image** only if code changes needed +3. **Update README** with working configuration +4. **Community announcement** with simple Docker instructions + +## Key Insights from Research + +- Most MCP Docker deployments work with just `-i` flag +- Complex solutions often unnecessary +- Node.js typically doesn't need explicit unbuffering (unlike Python with `-u`) +- Claude Desktop only supports stdio for local servers (not HTTP) +- Proper testing can quickly identify if buffering is the actual issue + +## What NOT to Do + +- Don't add TTY flag (`-t`) - it's for terminal UI, not needed for MCP +- Don't implement complex multi-phase solutions +- Don't switch to HTTP transport (Claude Desktop doesn't support it locally) +- Don't modify MCP protocol handling +- Don't add unnecessary wrapper scripts unless proven necessary + +The solution should be as simple as possible - likely just the `-i` flag in the Docker command. \ No newline at end of file diff --git a/docs/DOCKER_MCP_FIX_PLAN.md b/docs/DOCKER_MCP_FIX_PLAN.md new file mode 100644 index 0000000..c06493d --- /dev/null +++ b/docs/DOCKER_MCP_FIX_PLAN.md @@ -0,0 +1,252 @@ +# Docker MCP Initialization Timeout Fix Plan + +## Problem Summary + +The n8n-MCP Docker container fails to work with Claude Desktop due to MCP initialization timeout: + +1. Claude sends `initialize` request +2. Server receives it (logs show "Message from client: {"method":"initialize"...}") +3. Server appears to connect successfully +4. **No response is sent back to Claude** +5. Claude times out after 60 seconds +6. Container outputs "Shutting down..." which breaks JSON-RPC protocol + +## Root Cause Analysis + +### 1. **Stdout Buffering in Docker** + +Docker containers often buffer stdout, especially when not running with TTY (`-t` flag). This is the most likely culprit: + +- Node.js/JavaScript may buffer stdout when not connected to a TTY +- Docker's stdout handling differs from direct execution +- The MCP response might be stuck in the buffer + +**Evidence:** +- Common Docker issue (moby/moby#1385, docker/compose#1549) +- Python requires `-u` flag for unbuffered output in Docker +- Different base images have different buffering behavior + +### 2. **MCP SDK Server Connection Issue** + +The server might not be properly completing the connection handshake: + +```typescript +const transport = new StdioServerTransport(); +await server.connect(transport); // This might not complete properly +``` + +### 3. **Missing Initialize Handler** + +While the MCP SDK should handle `initialize` automatically, there might be an issue with: +- Handler registration order +- Server capabilities configuration +- Transport initialization timing + +### 4. **Process Lifecycle Management** + +The container might be: +- Exiting too early +- Not keeping the event loop alive +- Missing proper signal handling + +## Fixing Plan + +### Phase 1: Immediate Fixes (High Priority) + +#### 1.1 Force Stdout Flushing + +**File:** `src/mcp/server-update.ts` + +Add explicit stdout flushing after server connection: + +```typescript +async run(): Promise { + await this.ensureInitialized(); + + const transport = new StdioServerTransport(); + await this.server.connect(transport); + + // Force flush stdout + if (process.stdout.isTTY === false) { + process.stdout.write('', () => {}); // Force flush + } + + logger.info('n8n Documentation MCP Server running on stdio transport'); + + // Keep process alive + process.stdin.resume(); +} +``` + +#### 1.2 Add TTY Support to Docker + +**File:** `Dockerfile` + +Add environment variable to detect Docker: + +```dockerfile +ENV IS_DOCKER=true +ENV NODE_OPTIONS="--max-old-space-size=2048" +``` + +**File:** Update Docker command in README + +```json +{ + "mcpServers": { + "n8n-mcp": { + "command": "docker", + "args": [ + "run", + "--rm", + "-i", + "-t", // Add TTY allocation + "--init", // Proper signal handling + "-e", "MCP_MODE=stdio", + "-e", "LOG_LEVEL=error", + "-e", "DISABLE_CONSOLE_OUTPUT=true", + "ghcr.io/czlonkowski/n8n-mcp:latest" + ] + } + } +} +``` + +### Phase 2: Robust Fixes (Medium Priority) + +#### 2.1 Implement Explicit Initialize Handler + +**File:** `src/mcp/server-update.ts` + +Add explicit initialize handler to ensure response: + +```typescript +import { + InitializeRequestSchema, + InitializeResult +} from '@modelcontextprotocol/sdk/types.js'; + +private setupHandlers(): void { + // Add explicit initialize handler + this.server.setRequestHandler(InitializeRequestSchema, async (request) => { + logger.debug('Handling initialize request', request); + + const result: InitializeResult = { + protocolVersion: "2024-11-05", + capabilities: { + tools: {} + }, + serverInfo: { + name: "n8n-documentation-mcp", + version: "1.0.0" + } + }; + + // Force immediate flush + if (process.stdout.isTTY === false) { + process.stdout.write('', () => {}); + } + + return result; + }); + + // ... existing handlers +} +``` + +#### 2.2 Add Docker-Specific Stdio Handling + +**File:** Create `src/utils/docker-stdio.ts` + +```typescript +export class DockerStdioTransport extends StdioServerTransport { + constructor() { + super(); + + // Disable buffering for Docker + if (process.env.IS_DOCKER === 'true') { + process.stdout.setDefaultEncoding('utf8'); + if (process.stdout._handle && process.stdout._handle.setBlocking) { + process.stdout._handle.setBlocking(true); + } + } + } + + protected async writeMessage(message: string): Promise { + await super.writeMessage(message); + + // Force flush in Docker + if (process.env.IS_DOCKER === 'true') { + process.stdout.write('', () => {}); + } + } +} +``` + +### Phase 3: Alternative Approaches (Low Priority) + +#### 3.1 Use Wrapper Script + +Create a Node.js wrapper that ensures proper buffering: + +**File:** `docker-entrypoint.js` + +```javascript +#!/usr/bin/env node + +// Disable all buffering +process.stdout._handle?.setBlocking?.(true); +process.stdin.setRawMode?.(false); + +// Import and run the actual server +require('./dist/mcp/index.js'); +``` + +#### 3.2 Switch to HTTP Transport for Docker + +Consider using HTTP transport instead of stdio for Docker deployments, as it doesn't have buffering issues. + +## Testing Plan + +1. **Local Testing:** + ```bash + # Test with Docker TTY + docker run -it --rm ghcr.io/czlonkowski/n8n-mcp:latest + + # Test initialize response + echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05"},"id":1}' | \ + docker run -i --rm ghcr.io/czlonkowski/n8n-mcp:latest + ``` + +2. **Claude Desktop Testing:** + - Apply fixes incrementally + - Test with each configuration change + - Monitor Claude Desktop logs + +3. **Debug Output:** + Add temporary debug logging to stderr: + ```typescript + console.error('DEBUG: Received initialize'); + console.error('DEBUG: Sending response'); + ``` + +## Implementation Priority + +1. **Immediate:** Add `-t` flag to Docker command (no code changes) +2. **High:** Force stdout flushing in server code +3. **Medium:** Add explicit initialize handler +4. **Low:** Create Docker-specific transport class + +## Success Criteria + +- Claude Desktop connects without timeout +- No "Shutting down..." message in JSON stream +- Tools are accessible after connection +- Connection remains stable + +## References + +- [Docker stdout buffering issue](https://github.com/moby/moby/issues/1385) +- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) +- [Python unbuffered mode in Docker](https://stackoverflow.com/questions/39486327/stdout-being-buffered-in-docker-container) +- [MCP initialization timeout issues](https://github.com/modelcontextprotocol/servers/issues/57) \ No newline at end of file diff --git a/src/mcp/server-update.ts b/src/mcp/server-update.ts index 341bbe1..9e03da6 100644 --- a/src/mcp/server-update.ts +++ b/src/mcp/server-update.ts @@ -912,6 +912,17 @@ Full documentation is being prepared. For now, use get_node_essentials for confi const transport = new StdioServerTransport(); await this.server.connect(transport); + + // Force flush stdout for Docker environments + // Docker uses block buffering which can delay MCP responses + if (!process.stdout.isTTY) { + // Write empty string to force flush + process.stdout.write('', () => {}); + } + logger.info('n8n Documentation MCP Server running on stdio transport'); + + // Keep the process alive and listening + process.stdin.resume(); } } \ No newline at end of file