mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-19 08:53:09 +00:00
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:
252
docs/DOCKER_MCP_FIX_PLAN.md
Normal file
252
docs/DOCKER_MCP_FIX_PLAN.md
Normal 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)
|
||||
Reference in New Issue
Block a user