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

View File

@@ -2,7 +2,7 @@
[![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)
[![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)
[![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)
@@ -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

View File

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

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/),
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

View File

@@ -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": {

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
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 { 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();