Hot-Reload Configuration

Reload configuration without restarting the server. Add, remove, or modify MCP servers while preserving active connections for unchanged MCP servers.

Quick Start

# Start server, naming the file to watch
mcp-hangar serve --http --host 127.0.0.1 --port 8000 --config config.yaml

# Edit config.yaml in another terminal - changes apply automatically

# Or trigger manually
kill -HUP $(pgrep -f "mcp-hangar serve")

The file reloaded is the one the gateway booted from, found by the CLI's one rule. Before 2.25.0 a gateway that picked up ./config.yaml on its own had no path to reload: the watcher logged config_reload_worker_disabled with reason=no_config_path, and SIGHUP, the tool and the REST endpoint failed with No configuration path specified (mcp-hangar#1657). On those versions, name the file with --config or MCP_CONFIG.

Overview

TriggerLatencyUse Case
File pollingup to 5s (configurable)Default
File watcher (watchdog)~1sOnly when the watchdog package is installed
SIGHUP signalImmediateScripted deployments, CI/CD
MCP toolImmediateInteractive reload from AI assistant
POST /api/config/reloadImmediateHTTP mode, REST API

All reload operations are atomic: changes are validated before application. Invalid configuration is rejected; current config preserved. A file that changes tool_access.mode is refused the same way: the mode needs a restart. Since 2.25.0 so is a file with an invalid tools: or access: policy field, or a policy list that is not a list: the previous policy stays in force (Configuration).

Configuration

# Optional: customize hot-reload behavior
config_reload:
  enabled: true       # default: true
  use_watchdog: true  # default: true; needs the watchdog package, else polling
  interval_s: 5       # polling interval when watchdog is not used

Options

OptionTypeDefaultDescription
enabledbooltrueEnable automatic file watching
use_watchdogbooltrueUse watchdog library (inotify/fsevents) when it is installed
interval_sint5Polling interval in seconds

Triggering Reload

Automatic File Watching

Enabled by default. mcp-hangar does not install watchdog, so by default the file's modification time is polled every interval_s. With pip install watchdog and use_watchdog: true, file system events are used instead, with a 1s debounce.

SIGHUP Signal

Standard Unix reload pattern. Does not terminate the process.

# Find process
pgrep -f "mcp-hangar serve"

# Reload
kill -HUP <PID>

# One-liner
kill -HUP $(pgrep -f "mcp-hangar serve")

MCP Tool

Reload from Claude Desktop or any MCP client:

hangar_reload_config()                    # Graceful reload
hangar_reload_config(graceful=false)      # Immediate shutdown

Response:

{
  "status": "success",
  "message": "Configuration reloaded successfully",
  "mcp_servers_added": ["new-api"],
  "mcp_servers_removed": ["deprecated-service"],
  "mcp_servers_updated": ["modified-mcp-server"],
  "mcp_servers_unchanged": ["stable-mcp-server"],
  "duration_ms": 45.2
}

On failure the tool answers {"error": ..., "error_type": ..., "details": {}}.

REST API

In HTTP mode, POST /api/config/reload reloads the file the gateway was started with. The optional JSON body takes graceful; a body that sends config_path is rejected.

curl -X POST http://127.0.0.1:8000/api/config/reload

Reload Behavior

ScenarioBehaviorFinal State
AddedRegistered but not startedCOLD
RemovedStopped gracefully, then removed(deleted)
ModifiedOld stopped, new registeredCOLD
UnchangedNo action takenPreserved

Compared Fields

Changes to anything a server is built from restart it, among them:

FieldDescription
modeMCP Server mode (subprocess, docker, remote)
commandCommand and arguments
imageContainer image
endpointRemote endpoint URL
envEnvironment variables
volumesVolume mounts
networkNetwork mode
userUser/group ID
idle_ttl_sIdle timeout
health_check_interval_sHealth check interval
max_consecutive_failuresFailure threshold
args, resources, read_only, buildContainer settings
description, auth, tls, http, capabilities, max_response_bytesOther server settings

A server's tools access block, access, tool_access, tool_projection (pins) and header_exposure, and a group member's weight and priority, are applied without a restart.

Normalization:

  • Every default is applied before comparing: leaving a field out and writing its default value are the same
  • An explicit null, or an empty value where the default is not empty, is a change: env: null or resources: {} restarts the server (env: {} does not)

Examples

Add MCP Server

Before:

mcp_servers:
  math:
    mode: subprocess
    command: [python, -m, math_server]

After:

mcp_servers:
  math:
    mode: subprocess
    command: [python, -m, math_server]

  filesystem:
    mode: subprocess
    command: [npx, -y, "@modelcontextprotocol/server-filesystem", "/home/user/documents"]

Result: math unchanged, filesystem added in COLD state.

Update MCP Server

Before:

mcp_servers:
  database:
    mode: remote
    endpoint: https://db-mcp.internal/v1
    idle_ttl_s: 300

After:

mcp_servers:
  database:
    mode: remote
    endpoint: https://db-mcp.internal/v2
    idle_ttl_s: 600
    env:
      POOL_SIZE: "10"

Result: database stopped gracefully, new instance created.

Remove MCP Server

Before:

mcp_servers:
  legacy-api:
    mode: subprocess
    command: [python, legacy_server.py]

  modern-api:
    mode: remote
    endpoint: https://api.example.com/mcp

After:

mcp_servers:
  modern-api:
    mode: remote
    endpoint: https://api.example.com/mcp

Result: legacy-api stopped and removed, modern-api unchanged.

Change MCP Server Mode

Before:

mcp_servers:
  search:
    mode: subprocess
    command: [python, -m, search_v1]

After:

mcp_servers:
  search:
    mode: docker
    image: search-mcp:v2
    env:
      INDEX_PATH: /data/index
    volumes:
      - ./data:/data:ro

Result: Subprocess stopped; search is registered as a Docker server in COLD state, and its container starts on the next call.

Events

Hot-reload emits domain events for observability:

EventWhen
ConfigurationReloadRequestedBefore reload starts
ConfigurationReloadedAfter successful reload
ConfigurationReloadFailedOn validation/apply failure
ConfigurationReloaded(
    config_path="/app/config.yaml",
    mcp_servers_added=["new-api"],
    mcp_servers_removed=["old-api"],
    mcp_servers_updated=["modified"],
    mcp_servers_unchanged=["stable"],
    reload_duration_ms=42.5,
    requested_by="file_watcher"
)

Disabling Hot-Reload

config_reload:
  enabled: false

SIGHUP and MCP tool remain functional for manual reload.

Monitoring

Log Events

EventDescription
config_reload_worker_startedWorker initialized
config_file_modified_detectedFile change detected (polling mode)
reload_signal_receivedSIGHUP received
triggering_config_reloadReload initiated
configuration_reloadedReload successful
configuration_reload_failedReload failed

Structured Logs

{"event": "configuration_reloaded", "config_path": "config.yaml",
 "duration_ms": 42.5, "added": 1, "removed": 0, "updated": 2}

Limitations

LimitationWorkaround
tool_access.mode change refusedRestart the gateway
Hot-loaded MCP servers unaffectedUse hangar_unload to manage separately
Only mcp_servers (with groups and their policies), tool_access.required_catalogue, execution, headers.param_validation, resource_links, interceptors and ui_resources are appliedRestart the gateway for any other section (auth, event_store, logging, ...)
In-flight requestsCompleted before MCP server stops (graceful mode)

Troubleshooting

Reload Not Triggering

# Check watchdog installed
pip list | grep watchdog

# Check worker status (config_reload_worker_disabled reason=no_config_path,
# before 2.25.0: restart with --config or MCP_CONFIG)
grep "config_reload_worker" logs/mcp-hangar.log

# Manual reload
kill -HUP $(pgrep -f "mcp-hangar serve")

Invalid Configuration

# Validate YAML
python -c "import yaml; yaml.safe_load(open('config.yaml'))"

# Check mcp_servers section
grep -q "^mcp_servers:" config.yaml && echo "OK" || echo "Missing"

# Check logs
grep "configuration_reload_failed" logs/mcp-hangar.log

MCP Server Not Starting

# Check mcp_server added
grep "mcp_server_added" logs/mcp-hangar.log

# Check status
hangar_status()

# Start manually
hangar_start(mcp_server="my-mcp-server")

Security

ConsiderationImplementation
Validation before applyInvalid config rejected
Atomic operationsAll-or-nothing semantics
Graceful shutdownActive requests complete first
Audit trailAll reloads logged with source
File permissionsEnsure config not world-writable