mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-26 06:43:15 +08:00
- Parallelize pass_similarity and pass_semantic_edges via worker pool with thread-local edge buffers; sequential final merge since gbuf is not thread-safe. Adds cbm_lsh_query_into() as a thread-safe variant with caller-provided candidate buffer. - Add activatable profiling subsystem (CBM_PROFILE=1 env or --profile flag) for step-level timing of extract, resolve, corpus build, vector phases, and sqlite dump. Zero overhead when disabled. - Fix cbm_index_mode_t enum mismatch between pipeline.h (FULL=0, MODERATE=1, FAST=2) and discover.h (FULL=0, FAST=1). mode=fast silently no-op'd fast-discovery filtering because discover.c compared against the wrong value. Linux kernel fast mode went 1:40 -> 3:11 as a result; now back to 1:40. Broaden the filter guard to mode != CBM_MODE_FULL so MODERATE and FAST both get aggressive discovery. - Clamp cbm_sem_combined_score output to [0, 1]. The proximity multiplier returns up to 1.10 as a same-file boost which could push the final cosine score above 1.0. - Short-circuit semantic scoring when MinHash jaccard >= 0.95. Exact near-clones are already emitted as SIMILAR_TO edges; returning 0 here avoids flooding SEMANTICALLY_RELATED with cross-service copy-paste boilerplate and frees the edge budget for genuine vocabulary-bridged relations. - Validate search_graph semantic_query as an array of strings and return a clear error for a single-string input. Update the tool description to spell out the requirement explicitly with an example. - JSON-escape user-controlled strings (callee names, call arguments, URL paths, import local_name) in call/argument properties. Introduces cbm_json_escape() in foundation/str_util. - Skip SQLite pending_byte_page (file offset 0x40000000) during raw page writes in sqlite_writer to avoid corrupting databases that cross the 1 GiB boundary. - Migrate pretrained vector blob from UniXcoder (51K tokens) to nomic-embed-code (40856 tokens x 768d int8). Includes the extraction script under scripts/extract_nomic_vectors.py.
376 lines
13 KiB
C
376 lines
13 KiB
C
/*
|
|
* main.c — Entry point for codebase-memory-mcp.
|
|
*
|
|
* Modes:
|
|
* (default) Run as MCP server on stdin/stdout (JSON-RPC 2.0)
|
|
* cli <tool> <json> Run a single tool call and print result
|
|
* --version Print version and exit
|
|
* --help Print usage and exit
|
|
* --ui=true/false Enable/disable HTTP UI server (persisted)
|
|
* --port=N Set HTTP UI port (persisted, default 9749)
|
|
*
|
|
* Signal handling: SIGTERM/SIGINT trigger graceful shutdown.
|
|
* Watcher runs in a background thread, polling for git changes.
|
|
* HTTP UI server (optional) runs in a background thread on localhost.
|
|
*/
|
|
#include "mcp/mcp.h"
|
|
#include "watcher/watcher.h"
|
|
#include "pipeline/pipeline.h"
|
|
#include "store/store.h"
|
|
#include "cli/cli.h"
|
|
#include "cli/progress_sink.h"
|
|
#include "foundation/constants.h"
|
|
|
|
enum {
|
|
MAIN_MIN_ARGC = 1,
|
|
MAIN_CLI_ARGC = 2,
|
|
MAIN_FLAG_OFF = 5, /* strlen("--ui=") */
|
|
MAIN_PORT_OFF = 7, /* strlen("--port=") */
|
|
MAIN_MAX_PORT = 65536,
|
|
};
|
|
#define MAIN_RAM_FRACTION 0.5
|
|
|
|
#define SLEN(s) (sizeof(s) - 1)
|
|
#include "foundation/log.h"
|
|
#include "foundation/diagnostics.h"
|
|
#include "foundation/platform.h"
|
|
#include "foundation/compat_thread.h"
|
|
#include "foundation/mem.h"
|
|
#include "foundation/profile.h"
|
|
#include "ui/config.h"
|
|
#include "ui/http_server.h"
|
|
#include "ui/embedded_assets.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <signal.h>
|
|
#include <stdatomic.h>
|
|
|
|
#ifndef CBM_VERSION
|
|
#define CBM_VERSION "dev"
|
|
#endif
|
|
|
|
/* ── Globals for signal handling ────────────────────────────────── */
|
|
|
|
static cbm_watcher_t *g_watcher = NULL;
|
|
static cbm_mcp_server_t *g_server = NULL;
|
|
static cbm_http_server_t *g_http_server = NULL;
|
|
static atomic_int g_shutdown = 0;
|
|
|
|
static void signal_handler(int sig) {
|
|
(void)sig;
|
|
atomic_store(&g_shutdown, 1);
|
|
if (g_watcher) {
|
|
cbm_watcher_stop(g_watcher);
|
|
}
|
|
if (g_http_server) {
|
|
cbm_http_server_stop(g_http_server);
|
|
}
|
|
/* Close stdin to unblock getline in the MCP server loop */
|
|
(void)fclose(stdin);
|
|
}
|
|
|
|
/* ── Watcher background thread ──────────────────────────────────── */
|
|
|
|
static void *watcher_thread(void *arg) {
|
|
cbm_watcher_t *w = arg;
|
|
#define WATCHER_BASE_INTERVAL_MS 5000
|
|
|
|
cbm_watcher_run(w, WATCHER_BASE_INTERVAL_MS);
|
|
return NULL;
|
|
}
|
|
|
|
/* ── HTTP UI background thread ──────────────────────────────────── */
|
|
|
|
static void *http_thread(void *arg) {
|
|
cbm_http_server_t *srv = arg;
|
|
cbm_http_server_run(srv);
|
|
return NULL;
|
|
}
|
|
|
|
/* ── Index callback for watcher ─────────────────────────────────── */
|
|
|
|
static int watcher_index_fn(const char *project_name, const char *root_path, void *user_data) {
|
|
(void)user_data;
|
|
|
|
/* Non-blocking: skip if another pipeline is already running.
|
|
* Watcher will retry on next poll cycle (5-60s). */
|
|
if (!cbm_pipeline_try_lock()) {
|
|
cbm_log_info("watcher.skip", "project", project_name, "reason", "pipeline_busy");
|
|
return 0;
|
|
}
|
|
|
|
cbm_log_info("watcher.reindex", "project", project_name, "path", root_path);
|
|
|
|
cbm_pipeline_t *p = cbm_pipeline_new(root_path, NULL, CBM_MODE_FULL);
|
|
if (!p) {
|
|
cbm_pipeline_unlock();
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
|
|
int rc = cbm_pipeline_run(p);
|
|
cbm_pipeline_free(p);
|
|
cbm_pipeline_unlock();
|
|
return rc;
|
|
}
|
|
|
|
/* ── CLI mode ───────────────────────────────────────────────────── */
|
|
|
|
static int run_cli(int argc, char **argv) {
|
|
if (argc < MAIN_MIN_ARGC) {
|
|
(void)fprintf(stderr,
|
|
"Usage: codebase-memory-mcp cli [--progress] <tool_name> [json_args]\n");
|
|
return SKIP_ONE;
|
|
}
|
|
|
|
/* Strip --progress flag from argv. */
|
|
bool progress = false;
|
|
for (int i = 0; i < argc; i++) {
|
|
if (strcmp(argv[i], "--progress") == 0) {
|
|
progress = true;
|
|
for (int j = i; j < argc - SKIP_ONE; j++) {
|
|
argv[j] = argv[j + SKIP_ONE];
|
|
}
|
|
argc--;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (argc < MAIN_MIN_ARGC) {
|
|
(void)fprintf(stderr,
|
|
"Usage: codebase-memory-mcp cli [--progress] <tool_name> [json_args]\n");
|
|
return SKIP_ONE;
|
|
}
|
|
|
|
const char *tool_name = argv[0];
|
|
const char *args_json = argc >= MAIN_CLI_ARGC ? argv[SKIP_ONE] : "{}";
|
|
|
|
if (progress) {
|
|
cbm_progress_sink_init(stderr);
|
|
}
|
|
|
|
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
|
if (!srv) {
|
|
(void)fprintf(stderr, "Failed to create server\n");
|
|
if (progress) {
|
|
cbm_progress_sink_fini();
|
|
}
|
|
return SKIP_ONE;
|
|
}
|
|
|
|
char *result = cbm_mcp_handle_tool(srv, tool_name, args_json);
|
|
if (result) {
|
|
printf("%s\n", result);
|
|
free(result);
|
|
}
|
|
|
|
cbm_mcp_server_free(srv);
|
|
if (progress) {
|
|
cbm_progress_sink_fini();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ── Help ───────────────────────────────────────────────────────── */
|
|
|
|
static void print_help(void) {
|
|
printf("codebase-memory-mcp %s\n\n", CBM_VERSION);
|
|
printf("Usage:\n");
|
|
printf(" codebase-memory-mcp Run MCP server on stdio\n");
|
|
printf(" codebase-memory-mcp cli <tool> [json] Run a single tool\n");
|
|
printf(" codebase-memory-mcp install [-y|-n] [--force] [--dry-run]\n");
|
|
printf(" codebase-memory-mcp uninstall [-y|-n] [--dry-run]\n");
|
|
printf(" codebase-memory-mcp update [-y|-n]\n");
|
|
printf(" codebase-memory-mcp config <list|get|set|reset>\n");
|
|
printf(" codebase-memory-mcp --version Print version\n");
|
|
printf(" codebase-memory-mcp --help Print this help\n");
|
|
printf("\nUI options:\n");
|
|
printf(" --ui=true Enable HTTP graph visualization (persisted)\n");
|
|
printf(" --ui=false Disable HTTP graph visualization (persisted)\n");
|
|
printf(" --port=N Set UI port (default 9749, persisted)\n");
|
|
printf("\nSupported agents (auto-detected):\n");
|
|
printf(" Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode\n");
|
|
printf("\nTools: index_repository, search_graph, query_graph, trace_path,\n");
|
|
printf(" get_code_snippet, get_graph_schema, get_architecture, search_code,\n");
|
|
printf(" list_projects, delete_project, index_status, detect_changes,\n");
|
|
printf(" manage_adr, ingest_traces\n");
|
|
}
|
|
|
|
/* ── Main ───────────────────────────────────────────────────────── */
|
|
|
|
/* Try to handle a subcommand (cli/install/uninstall/update/config/--version/--help).
|
|
* Returns -1 if no subcommand matched, otherwise the exit code. */
|
|
static int handle_subcommand(int argc, char **argv) {
|
|
/* First scan: global flags */
|
|
for (int i = SKIP_ONE; i < argc; i++) {
|
|
if (strcmp(argv[i], "--profile") == 0) {
|
|
cbm_profile_enable();
|
|
}
|
|
}
|
|
for (int i = SKIP_ONE; i < argc; i++) {
|
|
if (strcmp(argv[i], "--version") == 0) {
|
|
printf("codebase-memory-mcp %s\n", CBM_VERSION);
|
|
return 0;
|
|
}
|
|
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
|
print_help();
|
|
return 0;
|
|
}
|
|
if (strcmp(argv[i], "cli") == 0) {
|
|
cbm_mem_init(MAIN_RAM_FRACTION);
|
|
return run_cli(argc - i - SKIP_ONE, argv + i + SKIP_ONE);
|
|
}
|
|
if (strcmp(argv[i], "install") == 0) {
|
|
return cbm_cmd_install(argc - i - SKIP_ONE, argv + i + SKIP_ONE);
|
|
}
|
|
if (strcmp(argv[i], "uninstall") == 0) {
|
|
return cbm_cmd_uninstall(argc - i - SKIP_ONE, argv + i + SKIP_ONE);
|
|
}
|
|
if (strcmp(argv[i], "update") == 0) {
|
|
return cbm_cmd_update(argc - i - SKIP_ONE, argv + i + SKIP_ONE);
|
|
}
|
|
if (strcmp(argv[i], "config") == 0) {
|
|
return cbm_cmd_config(argc - i - SKIP_ONE, argv + i + SKIP_ONE);
|
|
}
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
|
|
/* Parse --ui= and --port= flags. Returns true if config was modified. */
|
|
static bool parse_ui_flags(int argc, char **argv, cbm_ui_config_t *cfg) {
|
|
bool changed = false;
|
|
for (int i = SKIP_ONE; i < argc; i++) {
|
|
if (strncmp(argv[i], "--ui=", SLEN("--ui=")) == 0) {
|
|
cfg->ui_enabled = (strcmp(argv[i] + MAIN_FLAG_OFF, "true") == 0);
|
|
changed = true;
|
|
}
|
|
if (strncmp(argv[i], "--port=", SLEN("--port=")) == 0) {
|
|
int p = (int)strtol(argv[i] + MAIN_PORT_OFF, NULL, CBM_DECIMAL_BASE);
|
|
if (p > 0 && p < MAIN_MAX_PORT) {
|
|
cfg->ui_port = p;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
/* Install platform-specific signal handlers. */
|
|
static void setup_signal_handlers(void) {
|
|
#ifdef _WIN32
|
|
signal(SIGTERM, signal_handler);
|
|
signal(SIGINT, signal_handler);
|
|
#else
|
|
struct sigaction sa = {0};
|
|
sa.sa_handler = signal_handler;
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = 0;
|
|
sigaction(SIGTERM, &sa, NULL);
|
|
sigaction(SIGINT, &sa, NULL);
|
|
#endif
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
cbm_profile_init(); /* reads CBM_PROFILE env var, gates all prof macros */
|
|
int subcmd = handle_subcommand(argc, argv);
|
|
if (subcmd >= 0) {
|
|
return subcmd;
|
|
}
|
|
|
|
/* Default: MCP server on stdio */
|
|
cbm_mem_init(MAIN_RAM_FRACTION); /* 50% of RAM — safe now because mimalloc tracks ALL
|
|
* memory (C + C++ allocations) via global override.
|
|
* No more untracked heap blind spots. */
|
|
/* Store binary path for subprocess spawning + hook log sink */
|
|
cbm_http_server_set_binary_path(argv[0]);
|
|
cbm_log_set_sink(cbm_ui_log_append);
|
|
cbm_log_info("server.start", "version", CBM_VERSION);
|
|
cbm_diag_start(); /* starts if CBM_DIAGNOSTICS=1 */
|
|
|
|
/* Parse --ui and --port flags (persisted config) */
|
|
cbm_ui_config_t ui_cfg;
|
|
cbm_ui_config_load(&ui_cfg);
|
|
if (parse_ui_flags(argc, argv, &ui_cfg)) {
|
|
cbm_ui_config_save(&ui_cfg);
|
|
}
|
|
|
|
setup_signal_handlers();
|
|
|
|
/* Open config store for runtime settings */
|
|
char config_dir[CBM_SZ_1K];
|
|
const char *cfg_home = cbm_get_home_dir();
|
|
cbm_config_t *runtime_config = NULL;
|
|
if (cfg_home) {
|
|
snprintf(config_dir, sizeof(config_dir), "%s", cbm_resolve_cache_dir());
|
|
runtime_config = cbm_config_open(config_dir);
|
|
}
|
|
|
|
/* Create MCP server */
|
|
g_server = cbm_mcp_server_new(NULL);
|
|
if (!g_server) {
|
|
cbm_log_error("server.err", "msg", "failed to create server");
|
|
cbm_config_close(runtime_config);
|
|
return SKIP_ONE;
|
|
}
|
|
|
|
/* Create and start watcher in background thread */
|
|
cbm_store_t *watch_store = cbm_store_open_memory();
|
|
g_watcher = cbm_watcher_new(watch_store, watcher_index_fn, NULL);
|
|
|
|
/* Wire watcher + config into MCP server for session auto-index */
|
|
cbm_mcp_server_set_watcher(g_server, g_watcher);
|
|
cbm_mcp_server_set_config(g_server, runtime_config);
|
|
cbm_thread_t watcher_tid;
|
|
bool watcher_started = false;
|
|
|
|
if (g_watcher) {
|
|
if (cbm_thread_create(&watcher_tid, 0, watcher_thread, g_watcher) == 0) {
|
|
watcher_started = true;
|
|
}
|
|
}
|
|
|
|
/* Optionally start HTTP UI server in background thread */
|
|
cbm_thread_t http_tid;
|
|
bool http_started = false;
|
|
|
|
if (ui_cfg.ui_enabled && CBM_EMBEDDED_FILE_COUNT > 0) {
|
|
g_http_server = cbm_http_server_new(ui_cfg.ui_port);
|
|
if (g_http_server) {
|
|
if (cbm_thread_create(&http_tid, 0, http_thread, g_http_server) == 0) {
|
|
http_started = true;
|
|
}
|
|
}
|
|
} else if (ui_cfg.ui_enabled && CBM_EMBEDDED_FILE_COUNT == 0) {
|
|
cbm_log_warn("ui.no_assets", "hint", "rebuild with: make -f Makefile.cbm cbm-with-ui");
|
|
}
|
|
|
|
/* Run MCP event loop (blocks until EOF or signal) */
|
|
int rc = cbm_mcp_server_run(g_server, stdin, stdout);
|
|
|
|
/* Shutdown */
|
|
cbm_log_info("server.shutdown");
|
|
|
|
if (http_started) {
|
|
cbm_http_server_stop(g_http_server);
|
|
cbm_thread_join(&http_tid);
|
|
cbm_http_server_free(g_http_server);
|
|
g_http_server = NULL;
|
|
}
|
|
|
|
if (watcher_started) {
|
|
cbm_watcher_stop(g_watcher);
|
|
cbm_thread_join(&watcher_tid);
|
|
}
|
|
cbm_watcher_free(g_watcher);
|
|
cbm_store_close(watch_store);
|
|
cbm_mcp_server_free(g_server);
|
|
cbm_config_close(runtime_config);
|
|
|
|
g_watcher = NULL;
|
|
g_server = NULL;
|
|
cbm_diag_stop();
|
|
|
|
return rc;
|
|
}
|