How codebase-memory-mcp Handles Graceful Shutdown with POSIX Signals

The codebase-memory-mcp server registers POSIX signal handlers that capture termination signals in an atomic variable, triggering a five-step cleanup sequence—closing the listener socket, flushing SQLite transactions, stopping background workers, writing optional diagnostics, and classifying the exit status—to ensure the knowledge graph remains uncorrupted and no resources leak.

The codebase-memory-mcp repository from DeusData implements a long-running MCP server in C that must maintain knowledge-graph integrity across process restarts. Its graceful shutdown architecture separates asynchronous signal capture from synchronous cleanup logic, ensuring that user-initiated Ctrl-C or system kill commands safely commit in-flight transactions and release all allocated resources.

Signal Registration and Atomic Capture

In src/main.c (lines approximately 210–225), the main entry point installs handlers for standard termination signals during server initialization. The implementation uses a volatile sig_atomic_t variable named g_terminate_signal to ensure thread-safe signal capture without race conditions.

static volatile sig_atomic_t g_terminate_signal = 0;

static void handle_signal(int sig) {
    g_terminate_signal = sig;          // store the signal that arrived
}

void install_signal_handlers(void) {
    struct sigaction sa = { .sa_handler = handle_signal };
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT,  &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);
    sigaction(SIGALRM, &sa, NULL);
}

The main event loop (lines approximately 250–275) polls the atomic flag on each iteration. When g_terminate_signal becomes non-zero, the loop exits and proceeds to the cleanup phase.

while (!g_terminate_signal) {
    cbm_server_poll_once();   // process one RPC request if any
}

The Five-Step Graceful Shutdown Sequence

Once a signal is detected, the server executes a deterministic cleanup procedure defined in src/main.c (lines approximately 280–320). This sequence ensures that the embedded SQLite store and background threads terminate cleanly.

  1. Stop accepting new RPC requestscbm_server_stop() closes the listener socket immediately, preventing new connections from entering the system.
  2. Flush in-memory indexescbm_store_close() (implemented in src/store.c) commits the current SQLite transaction and checkpoints the Write-Ahead Log (WAL), guaranteeing that the knowledge graph persists to disk.
  3. Stop background workerscbm_watcher_stop() (implemented in src/watcher.c) signals the index-watcher, auto-watcher, and file-watcher threads to terminate, while cbm_workers_join() waits for all parallel indexing workers to finish.
  4. Write diagnostics – If the CBM_DIAGNOSTICS environment variable is enabled, cbm_diagnostics_write_final() writes a final snapshot to /tmp/cbm-diagnostics-<pid>.json.
  5. Exit with classification – The process returns a status determined by cbm_proc_classify(), mapping the received signal to a semantic exit category.
if (g_terminate_signal) {
    cbm_log_info("Shutdown signal %d received, cleaning up …", g_terminate_signal);
    cbm_server_stop();                // close listener socket
    cbm_store_close();                // commit & checkpoint SQLite DB
    cbm_watcher_stop();               // stop background file-watcher
    cbm_workers_join();               // wait for all worker threads
    cbm_diagnostics_write_final();    // optional diagnostics dump
}

Process Exit Classification

The helper function cbm_proc_classify() in src/proc.c (lines approximately 430–460) translates POSIX signal numbers into semantic process states. This classification allows external orchestrators to distinguish between intentional restarts and fatal errors.

enum cbm_proc_status cbm_proc_classify(bool is_child, int pid,
                                        int sig, bool is_hang) {
    switch (sig) {
        case SIGSEGV: case SIGABRT: case SIGBUS:
            return CBM_PROC_CRASH;
        case SIGTERM: case SIGINT: case SIGQUIT:
            return is_hang ? CBM_PROC_HANG : CBM_PROC_KILLED;
        default:
            return CBM_PROC_UNKNOWN;
    }
}

Signal-to-state mappings:

  • SIGTERM, SIGINT, SIGQUITCBM_PROC_KILLED (clean shutdown)
  • SIGSEGV, SIGABRT, SIGBUSCBM_PROC_CRASH (abnormal exit)
  • SIGKILL (or forced termination) → CBM_PROC_HANG (no cleanup possible)

Practical Shutdown Examples

To trigger a graceful shutdown manually, send SIGINT (equivalent to pressing Ctrl-C) or SIGTERM to the process:

$ codebase-memory-mcp --port=9749 &
[1] 12345

# … server is running …

$ kill -INT 12345          # equivalent to Ctrl-C

The server logs the signal, executes the five-step cleanup, and exits with status 0.

To capture a final diagnostics snapshot during shutdown, enable the diagnostics flag before starting:

export CBM_DIAGNOSTICS=1
codebase-memory-mcp &

# … send SIGTERM …

# After exit you’ll find /tmp/cbm-diagnostics-<pid>.json

Summary

  • Signal capture: The server uses sigaction() in src/main.c to register handlers for SIGINT, SIGTERM, SIGQUIT, and SIGALRM, storing the signal number in the atomic variable g_terminate_signal.
  • Cleanup sequence: The main loop exits when the flag is set, then closes sockets via cbm_server_stop(), flushes SQLite via cbm_store_close(), stops watchers via cbm_watcher_stop(), joins worker threads, and optionally writes diagnostics.
  • Data integrity: By separating signal reception from the shutdown logic, the server ensures the SQLite WAL is checkpointed and the knowledge graph is never left in a corrupted state.
  • Exit classification: The cbm_proc_classify() function in src/proc.c maps termination signals to CBM_PROC_KILLED, CBM_PROC_CRASH, or CBM_PROC_HANG for observability.

Frequently Asked Questions

What signals trigger a graceful shutdown in codebase-memory-mcp?

The server registers handlers for SIGINT, SIGTERM, SIGQUIT, and SIGALRM in src/main.c. When any of these signals are received, the atomic variable g_terminate_signal is set, causing the main event loop to exit and initiate the five-step cleanup sequence. Signals like SIGKILL bypass this mechanism and result in immediate termination without cleanup.

How does the server prevent SQLite corruption during shutdown?

The cbm_store_close() function in src/store.c commits any pending transactions and checkpoints the Write-Ahead Log (WAL) before the process exits. This ensures that all in-memory graph updates are persisted to disk atomically, preventing partial writes or database corruption even if the shutdown occurs during heavy indexing operations.

What is the difference between CBM_PROC_KILLED and CBM_PROC_CRASH?

According to the classification logic in src/proc.c, CBM_PROC_KILLED indicates that the process terminated via SIGTERM, SIGINT, or SIGQUIT with full cleanup performed. CBM_PROC_CRASH indicates an abnormal termination caused by SIGSEGV, SIGABRT, or SIGBUS, where the process may not have completed the graceful shutdown sequence.

Can I trigger a shutdown programmatically without sending OS signals?

Yes. The MCP server exposes a built-in shutdown tool that can be invoked by an authorized client. This tool internally sends SIGTERM to the server process, triggering the same atomic flag and cleanup sequence as an external signal, ensuring consistent behavior whether shutdown is initiated via RPC or system signals.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →