Skip to main content

Shutdown Behavior

This document describes how MCPProxy handles graceful shutdown of upstream servers, including subprocess termination timeouts and cleanup procedures.

Overview​

When MCPProxy shuts down (via Ctrl+C, SIGTERM, or tray quit), it follows a structured cleanup process:

  1. Cancel application context (signals all background services to stop)
  2. Stop OAuth refresh manager
  3. Stop Supervisor (reconciliation loop)
  4. Shutdown all upstream servers (graceful → force)
  5. Close caches, indexes, and storage

Subprocess Shutdown Flow​

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) │
└─────────────────────────────────────────────────────────────────┘

Timeout Constants​

ConstantValueDescription
mcpClientCloseTimeout10sMax time to wait for graceful MCP client close
processGracefulTimeout9sMax time after SIGTERM before SIGKILL
processTerminationPollInterval100msHow often to check if process exited
dockerCleanupTimeout30sMax time for Docker container cleanup

Why 9 seconds for SIGTERM?​

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 Container Shutdown​

Docker containers follow a similar pattern but use Docker's native stop mechanism:

  1. docker stop (sends SIGTERM, waits for graceful exit)
  2. If container doesn't stop: docker kill (sends SIGKILL)

Docker cleanup has a 30-second timeout to allow for container-specific cleanup procedures.

Process Groups​

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:

  • Child processes spawned by npm/npx are terminated
  • Orphaned processes don't accumulate
  • All related processes receive the shutdown signal

What Happens During Shutdown​

When call_tool is called during shutdown​

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

When retrieve_tools is called during shutdown​

  • If the search index is still open: Returns results (possibly stale)
  • After index is closed: Returns an error

When tools/list_changed notification arrives during shutdown​

The notification is safely ignored:

  • Callback context is cancelled
  • Discovery doesn't block shutdown
  • Logged as a warning, no user impact

Shutdown Order​

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

Debugging Shutdown Issues​

Check for orphaned processes​

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

Enable debug logging for shutdown​

mcpproxy serve --log-level=debug 2>&1 | grep -E "(Disconnect|shutdown|SIGTERM|SIGKILL|process group)"

View shutdown logs​

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

Troubleshooting​

Server processes not terminating​

Symptoms: npx or uvx processes remain running after MCPProxy stops.

Possible causes:

  1. Process ignoring SIGTERM (bad signal handling in MCP server)
  2. Process group not properly set up
  3. Zombie processes from previous crashes

Solutions:

  • Check server logs: mcpproxy upstream logs <server-name>
  • Manually kill orphaned processes
  • Report issue if consistently reproducible

Shutdown taking too long​

Symptoms: MCPProxy takes 20+ seconds to shut down.

Possible causes:

  1. Many servers running in parallel
  2. Servers not responding to graceful shutdown
  3. Docker containers with slow cleanup

Solutions:

  • Check which servers are slow: enable debug logging
  • Consider disabling problematic servers before shutdown
  • Report consistently slow servers as bugs

Docker containers not cleaning up​

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