docs/operations/shutdown-behavior.md
This document describes how MCPProxy handles graceful shutdown of upstream servers, including subprocess termination timeouts and cleanup procedures.
When MCPProxy shuts down (via Ctrl+C, SIGTERM, or tray quit), it follows a structured cleanup process:
For stdio-based MCP servers (processes started via command), MCPProxy uses a two-phase shutdown approach:
┌─────────────────────────────────────────────────────────────────┐
│ Graceful Close Phase │
│ (10 seconds max) │
├─────────────────────────────────────────────────────────────────┤
│ 1. Close MCP client connection (stdin/stdout) │
│ 2. Subprocess receives EOF and should exit cleanly │
│ 3. Wait up to 10 seconds for graceful exit │
└─────────────────────────────────────────────────────────────────┘
│
▼ (if timeout)
┌─────────────────────────────────────────────────────────────────┐
│ Force Kill Phase │
│ (9 seconds max) │
├─────────────────────────────────────────────────────────────────┤
│ 1. Send SIGTERM to entire process group │
│ 2. Poll every 100ms to check if process exited │
│ 3. After 9 seconds: send SIGKILL (force kill) │
└─────────────────────────────────────────────────────────────────┘
| Constant | Value | Description |
|---|---|---|
mcpClientCloseTimeout | 10s | Max time to wait for graceful MCP client close |
processGracefulTimeout | 9s | Max time after SIGTERM before SIGKILL |
processTerminationPollInterval | 100ms | How often to check if process exited |
dockerCleanupTimeout | 30s | Max time for Docker container cleanup |
The SIGTERM timeout (9s) is intentionally less than the MCP client close timeout (10s). This ensures that if graceful close times out, the force kill phase can complete within a reasonable time window.
Total worst case for stdio servers: 10s (graceful) + 9s (force kill) = 19 seconds
Docker containers follow a similar pattern but use Docker's native stop mechanism:
docker stop (sends SIGTERM, waits for graceful exit)docker kill (sends SIGKILL)Docker cleanup has a 30-second timeout to allow for container-specific cleanup procedures.
MCPProxy uses Unix process groups to ensure all child processes are properly cleaned up:
// All child processes are placed in a new process group
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // Create new process group
Pgid: 0, // Make this process the group leader
}
When shutting down, MCPProxy sends signals to the entire process group (-pgid), ensuring that:
call_tool is called during shutdownIf an AI client tries to call a tool while MCPProxy is shutting down:
Error: "Server 'xxx' is not connected (state: Disconnected)"
Or if the server client was already removed:
Error: "No client found for server: xxx"
retrieve_tools is called during shutdowntools/list_changed notification arrives during shutdownThe notification is safely ignored:
Runtime.Close()
│
├─► Cancel app context
│
├─► Stop OAuth refresh manager
│ └─► Prevents token refresh during shutdown
│
├─► Stop Supervisor
│ ├─► Cancel reconciliation context
│ ├─► Wait for goroutines to exit
│ └─► Close upstream adapter
│
├─► ShutdownAll on upstream manager (45s total timeout)
│ └─► For each server (parallel):
│ ├─► Graceful close (10s)
│ └─► Force kill if needed (9s)
│
├─► Close cache manager
│
├─► Close index manager
│
├─► Close storage manager
│
└─► Close config service
# After stopping MCPProxy, check for orphaned MCP server processes
pgrep -f "npx.*mcp"
pgrep -f "uvx.*mcp"
pgrep -f "node.*server"
# If found, kill them manually
pkill -f "npx.*mcp"
mcpproxy serve --log-level=debug 2>&1 | grep -E "(Disconnect|shutdown|SIGTERM|SIGKILL|process group)"
Look for these log messages during shutdown:
INFO Disconnecting from upstream MCP server
DEBUG Attempting graceful MCP client close
DEBUG MCP client closed gracefully # Success!
# OR
WARN MCP client close timed out # Graceful failed
INFO Graceful close failed, force killing process group
DEBUG SIGTERM sent to process group
INFO Process group terminated gracefully # SIGTERM worked
# OR
WARN Process group still running after SIGTERM, sending SIGKILL
INFO SIGKILL sent to process group
Symptoms: npx or uvx processes remain running after MCPProxy stops.
Possible causes:
Solutions:
mcpproxy upstream logs <server-name>Symptoms: MCPProxy takes 20+ seconds to shut down.
Possible causes:
Solutions:
Symptoms: Docker containers remain running after MCPProxy stops.
Solutions:
# List MCPProxy containers
docker ps --filter "label=mcpproxy.managed=true"
# Force remove all MCPProxy containers
docker rm -f $(docker ps -q --filter "label=mcpproxy.managed=true")