fix: Docker container cleanup on session end (Issue #66)
- Added proper SIGTERM/SIGINT signal handlers to stdio-wrapper.ts - Removed problematic trap commands from docker-entrypoint.sh - Added STOPSIGNAL directive to Dockerfile for explicit signal handling - Implemented graceful shutdown in MCP server with database cleanup - Added stdin close detection for proper cleanup when Claude Desktop closes the pipe - Containers now properly exit with the --rm flag, preventing accumulation - Added --init flag to all Docker configuration examples - Updated documentation with container lifecycle management best practices - Bumped version to 2.7.20 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,9 @@ ENV IS_DOCKER=true
|
||||
# Expose HTTP port
|
||||
EXPOSE 3000
|
||||
|
||||
# Set stop signal to SIGTERM (default, but explicit is better)
|
||||
STOPSIGNAL SIGTERM
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://127.0.0.1:3000/health || exit 1
|
||||
|
||||
27
README.md
27
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/czlonkowski/n8n-mcp)
|
||||
[](https://github.com/czlonkowski/n8n-mcp)
|
||||
[](https://github.com/czlonkowski/n8n-mcp)
|
||||
[](https://www.npmjs.com/package/n8n-mcp)
|
||||
[](https://github.com/n8n-io/n8n)
|
||||
[](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
|
||||
@@ -161,6 +161,7 @@ Add to Claude Desktop config:
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--init",
|
||||
"-e", "MCP_MODE=stdio",
|
||||
"-e", "LOG_LEVEL=error",
|
||||
"-e", "DISABLE_CONSOLE_OUTPUT=true",
|
||||
@@ -181,6 +182,7 @@ Add to Claude Desktop config:
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--init",
|
||||
"-e", "MCP_MODE=stdio",
|
||||
"-e", "LOG_LEVEL=error",
|
||||
"-e", "DISABLE_CONSOLE_OUTPUT=true",
|
||||
@@ -667,23 +669,20 @@ See [CHANGELOG.md](./docs/CHANGELOG.md) for full version history and recent chan
|
||||
|
||||
## ⚠️ Known Issues
|
||||
|
||||
### Claude Desktop Container Duplication
|
||||
When using n8n-MCP with Claude Desktop in Docker mode, Claude Desktop may start the container twice during initialization. This is a known Claude Desktop bug ([modelcontextprotocol/servers#812](https://github.com/modelcontextprotocol/servers/issues/812)).
|
||||
### Claude Desktop Container Management
|
||||
|
||||
**Symptoms:**
|
||||
- Two identical containers running for the same MCP server
|
||||
- Container name conflicts if using `--name` parameter
|
||||
- Doubled resource usage
|
||||
#### Container Accumulation (Fixed in v2.7.20+)
|
||||
Previous versions had an issue where containers would not properly clean up when Claude Desktop sessions ended. This has been fixed in v2.7.20+ with proper signal handling.
|
||||
|
||||
**Workarounds:**
|
||||
1. **Avoid using --name parameter** - Let Docker assign random names:
|
||||
**For best container lifecycle management:**
|
||||
1. **Use the --init flag** (recommended) - Docker's init system ensures proper signal handling:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"n8n-mcp": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run", "-i", "--rm",
|
||||
"run", "-i", "--rm", "--init",
|
||||
"ghcr.io/czlonkowski/n8n-mcp:latest"
|
||||
]
|
||||
}
|
||||
@@ -691,15 +690,11 @@ When using n8n-MCP with Claude Desktop in Docker mode, Claude Desktop may start
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use HTTP mode instead** - Deploy n8n-mcp as a standalone HTTP server:
|
||||
2. **Ensure you're using v2.7.20 or later** - Check your version:
|
||||
```bash
|
||||
docker compose up -d # Start HTTP server
|
||||
docker run --rm ghcr.io/czlonkowski/n8n-mcp:latest --version
|
||||
```
|
||||
Then connect via mcp-remote (see [HTTP Deployment Guide](./docs/HTTP_DEPLOYMENT.md))
|
||||
|
||||
3. **Use Docker MCP Toolkit** - Better container management through Docker Desktop
|
||||
|
||||
This issue does not affect the functionality of n8n-MCP itself, only the container management in Claude Desktop.
|
||||
|
||||
## 📦 License
|
||||
|
||||
|
||||
@@ -70,22 +70,12 @@ if [ "$(id -u)" = "0" ]; then
|
||||
if [ -d "/app/data" ]; then
|
||||
chown -R nodejs:nodejs /app/data
|
||||
fi
|
||||
# Switch to nodejs user (using Alpine's native su)
|
||||
exec su nodejs -c "$*"
|
||||
# Switch to nodejs user with proper exec chain for signal propagation
|
||||
exec su -s /bin/sh nodejs -c "exec $*"
|
||||
fi
|
||||
|
||||
# Trap signals for graceful shutdown
|
||||
# In stdio mode, don't output anything to stdout as it breaks JSON-RPC
|
||||
if [ "$MCP_MODE" = "stdio" ]; then
|
||||
# Silent trap - no output at all
|
||||
trap 'kill -TERM $PID 2>/dev/null || true' TERM INT EXIT
|
||||
else
|
||||
# In HTTP mode, output to stderr
|
||||
trap 'echo "Shutting down..." >&2; kill -TERM $PID 2>/dev/null' TERM INT EXIT
|
||||
fi
|
||||
|
||||
# Execute the main command in background
|
||||
# In stdio mode, use the wrapper for clean output
|
||||
# Execute the main command directly with exec
|
||||
# This ensures our Node.js process becomes PID 1 and receives signals directly
|
||||
if [ "$MCP_MODE" = "stdio" ]; then
|
||||
# Debug: Log to stderr to check if wrapper exists
|
||||
if [ "$DEBUG_DOCKER" = "true" ]; then
|
||||
@@ -95,6 +85,7 @@ if [ "$MCP_MODE" = "stdio" ]; then
|
||||
|
||||
if [ -f "/app/dist/mcp/stdio-wrapper.js" ]; then
|
||||
# Use the stdio wrapper for clean JSON-RPC output
|
||||
# exec replaces the shell with node process as PID 1
|
||||
exec node /app/dist/mcp/stdio-wrapper.js
|
||||
else
|
||||
# Fallback: run with explicit environment
|
||||
|
||||
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.7.20] - 2025-07-18
|
||||
|
||||
### Fixed
|
||||
- **Docker container cleanup on session end** (Issue #66)
|
||||
- Fixed containers not responding to termination signals when Claude Desktop sessions end
|
||||
- Added proper SIGTERM/SIGINT signal handlers to stdio-wrapper.ts
|
||||
- Removed problematic trap commands from docker-entrypoint.sh
|
||||
- Added STOPSIGNAL directive to Dockerfile for explicit signal handling
|
||||
- Implemented graceful shutdown in MCP server with database cleanup
|
||||
- Added stdin close detection for proper cleanup when Claude Desktop closes the pipe
|
||||
- Containers now properly exit with the `--rm` flag, preventing accumulation
|
||||
- Recommended using `--init` flag in Docker run command for best signal handling
|
||||
|
||||
### Documentation
|
||||
- Updated README with container lifecycle management best practices
|
||||
- Added `--init` flag to all Docker configuration examples
|
||||
- Added troubleshooting section for container accumulation issues
|
||||
|
||||
## [2.7.19] - 2025-07-18
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.7.19",
|
||||
"version": "2.7.20",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
|
||||
@@ -2031,4 +2031,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
// Keep the process alive and listening
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
logger.info('Shutting down MCP server...');
|
||||
|
||||
// Close database connection if it exists
|
||||
if (this.db) {
|
||||
try {
|
||||
await this.db.close();
|
||||
logger.info('Database connection closed');
|
||||
} catch (error) {
|
||||
logger.error('Error closing database:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,11 @@ console.countReset = () => {};
|
||||
// Import and run the server AFTER suppressing output
|
||||
import { N8NDocumentationMCPServer } from './server';
|
||||
|
||||
let server: N8NDocumentationMCPServer | null = null;
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
server = new N8NDocumentationMCPServer();
|
||||
await server.run();
|
||||
} catch (error) {
|
||||
// In case of fatal error, output to stderr only
|
||||
@@ -64,4 +66,47 @@ process.on('unhandledRejection', (reason) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle termination signals for proper cleanup
|
||||
let isShuttingDown = false;
|
||||
|
||||
async function shutdown(signal: string) {
|
||||
if (isShuttingDown) return;
|
||||
isShuttingDown = true;
|
||||
|
||||
// Log to stderr only (not stdout which would corrupt JSON-RPC)
|
||||
originalConsoleError(`Received ${signal}, shutting down gracefully...`);
|
||||
|
||||
try {
|
||||
// Shutdown the server if it exists
|
||||
if (server) {
|
||||
await server.shutdown();
|
||||
}
|
||||
} catch (error) {
|
||||
originalConsoleError('Error during shutdown:', error);
|
||||
}
|
||||
|
||||
// Close stdin to signal we're done reading
|
||||
process.stdin.pause();
|
||||
process.stdin.destroy();
|
||||
|
||||
// Exit with timeout to ensure we don't hang
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 500).unref(); // unref() allows process to exit if this is the only thing keeping it alive
|
||||
|
||||
// But also exit immediately if nothing else is pending
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Register signal handlers
|
||||
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => void shutdown('SIGINT'));
|
||||
process.on('SIGHUP', () => void shutdown('SIGHUP'));
|
||||
|
||||
// Also handle stdin close (when Claude Desktop closes the pipe)
|
||||
process.stdin.on('end', () => {
|
||||
originalConsoleError('stdin closed, shutting down...');
|
||||
void shutdown('STDIN_CLOSE');
|
||||
});
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user