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:
czlonkowski
2025-07-18 18:51:24 +02:00
parent f76e2247f9
commit 6e52afd5af
7 changed files with 98 additions and 32 deletions

View File

@@ -69,6 +69,9 @@ ENV IS_DOCKER=true
# Expose HTTP port # Expose HTTP port
EXPOSE 3000 EXPOSE 3000
# Set stop signal to SIGTERM (default, but explicit is better)
STOPSIGNAL SIGTERM
# Health check # Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://127.0.0.1:3000/health || exit 1 CMD curl -f http://127.0.0.1:3000/health || exit 1

View File

@@ -2,7 +2,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/czlonkowski/n8n-mcp?style=social)](https://github.com/czlonkowski/n8n-mcp) [![GitHub stars](https://img.shields.io/github/stars/czlonkowski/n8n-mcp?style=social)](https://github.com/czlonkowski/n8n-mcp)
[![Version](https://img.shields.io/badge/version-2.7.19-blue.svg)](https://github.com/czlonkowski/n8n-mcp) [![Version](https://img.shields.io/badge/version-2.7.20-blue.svg)](https://github.com/czlonkowski/n8n-mcp)
[![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp) [![npm version](https://img.shields.io/npm/v/n8n-mcp.svg)](https://www.npmjs.com/package/n8n-mcp)
[![n8n version](https://img.shields.io/badge/n8n-v1.102.4-orange.svg)](https://github.com/n8n-io/n8n) [![n8n version](https://img.shields.io/badge/n8n-v1.102.4-orange.svg)](https://github.com/n8n-io/n8n)
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fczlonkowski%2Fn8n--mcp-green.svg)](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp) [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fczlonkowski%2Fn8n--mcp-green.svg)](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
@@ -161,6 +161,7 @@ Add to Claude Desktop config:
"run", "run",
"-i", "-i",
"--rm", "--rm",
"--init",
"-e", "MCP_MODE=stdio", "-e", "MCP_MODE=stdio",
"-e", "LOG_LEVEL=error", "-e", "LOG_LEVEL=error",
"-e", "DISABLE_CONSOLE_OUTPUT=true", "-e", "DISABLE_CONSOLE_OUTPUT=true",
@@ -181,6 +182,7 @@ Add to Claude Desktop config:
"run", "run",
"-i", "-i",
"--rm", "--rm",
"--init",
"-e", "MCP_MODE=stdio", "-e", "MCP_MODE=stdio",
"-e", "LOG_LEVEL=error", "-e", "LOG_LEVEL=error",
"-e", "DISABLE_CONSOLE_OUTPUT=true", "-e", "DISABLE_CONSOLE_OUTPUT=true",
@@ -667,23 +669,20 @@ See [CHANGELOG.md](./docs/CHANGELOG.md) for full version history and recent chan
## ⚠️ Known Issues ## ⚠️ Known Issues
### Claude Desktop Container Duplication ### Claude Desktop Container Management
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)).
**Symptoms:** #### Container Accumulation (Fixed in v2.7.20+)
- Two identical containers running for the same MCP server 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.
- Container name conflicts if using `--name` parameter
- Doubled resource usage
**Workarounds:** **For best container lifecycle management:**
1. **Avoid using --name parameter** - Let Docker assign random names: 1. **Use the --init flag** (recommended) - Docker's init system ensures proper signal handling:
```json ```json
{ {
"mcpServers": { "mcpServers": {
"n8n-mcp": { "n8n-mcp": {
"command": "docker", "command": "docker",
"args": [ "args": [
"run", "-i", "--rm", "run", "-i", "--rm", "--init",
"ghcr.io/czlonkowski/n8n-mcp:latest" "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 ```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 ## 📦 License

View File

@@ -70,22 +70,12 @@ if [ "$(id -u)" = "0" ]; then
if [ -d "/app/data" ]; then if [ -d "/app/data" ]; then
chown -R nodejs:nodejs /app/data chown -R nodejs:nodejs /app/data
fi fi
# Switch to nodejs user (using Alpine's native su) # Switch to nodejs user with proper exec chain for signal propagation
exec su nodejs -c "$*" exec su -s /bin/sh nodejs -c "exec $*"
fi fi
# Trap signals for graceful shutdown # Execute the main command directly with exec
# In stdio mode, don't output anything to stdout as it breaks JSON-RPC # This ensures our Node.js process becomes PID 1 and receives signals directly
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
if [ "$MCP_MODE" = "stdio" ]; then if [ "$MCP_MODE" = "stdio" ]; then
# Debug: Log to stderr to check if wrapper exists # Debug: Log to stderr to check if wrapper exists
if [ "$DEBUG_DOCKER" = "true" ]; then if [ "$DEBUG_DOCKER" = "true" ]; then
@@ -95,6 +85,7 @@ if [ "$MCP_MODE" = "stdio" ]; then
if [ -f "/app/dist/mcp/stdio-wrapper.js" ]; then if [ -f "/app/dist/mcp/stdio-wrapper.js" ]; then
# Use the stdio wrapper for clean JSON-RPC output # 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 exec node /app/dist/mcp/stdio-wrapper.js
else else
# Fallback: run with explicit environment # Fallback: run with explicit environment

View File

@@ -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/), 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). 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 ## [2.7.19] - 2025-07-18
### Fixed ### Fixed

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.7.19", "version": "2.7.20",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {

View File

@@ -2031,4 +2031,18 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
// Keep the process alive and listening // Keep the process alive and listening
process.stdin.resume(); 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);
}
}
}
} }

View File

@@ -42,9 +42,11 @@ console.countReset = () => {};
// Import and run the server AFTER suppressing output // Import and run the server AFTER suppressing output
import { N8NDocumentationMCPServer } from './server'; import { N8NDocumentationMCPServer } from './server';
let server: N8NDocumentationMCPServer | null = null;
async function main() { async function main() {
try { try {
const server = new N8NDocumentationMCPServer(); server = new N8NDocumentationMCPServer();
await server.run(); await server.run();
} catch (error) { } catch (error) {
// In case of fatal error, output to stderr only // In case of fatal error, output to stderr only
@@ -64,4 +66,47 @@ process.on('unhandledRejection', (reason) => {
process.exit(1); 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(); main();