fix: Docker stdio communication for Claude Desktop compatibility

Fixed the initialization timeout issue with minimal changes:

1. Added stdout flush after server connection to combat Docker buffering
2. Fixed docker-entrypoint.sh to not output to stdout in stdio mode
3. Added process.stdin.resume() to keep server alive
4. Added IS_DOCKER environment variable for future use
5. Updated README to prioritize Docker with correct -i flag configuration

The core issue was Docker's block buffering preventing immediate JSON-RPC
responses. The -i flag maintains stdin connection, and explicit flushing
ensures responses reach Claude Desktop immediately.

Also fixed "Shutting down..." message that was breaking JSON-RPC protocol
by redirecting it to stderr in stdio mode.

Docker is now the recommended installation method as originally intended.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-17 00:30:54 +02:00
parent b769ff24ee
commit 75952f94ca
6 changed files with 474 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

252
docs/DOCKER_MCP_FIX_PLAN.md Normal file
View File

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

View File

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