diff --git a/Dockerfile b/Dockerfile index ced41c8..4a42263 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 2536113..adaa61d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 0311468..9529516 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -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 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1ad01a4..7fe6ce7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index 42056ed..1a2b1c9 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 7d22734..f3de55c 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -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 { + 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); + } + } + } } \ No newline at end of file diff --git a/src/mcp/stdio-wrapper.ts b/src/mcp/stdio-wrapper.ts index 8811d9c..8dac848 100644 --- a/src/mcp/stdio-wrapper.ts +++ b/src/mcp/stdio-wrapper.ts @@ -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(); \ No newline at end of file