mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Discovery limits bound what indexing accepts, not what it then costs. A repository well inside those bounds can still exhaust the host through parser memory, or simply never finish, and a supervised worker that hangs leaves the parent waiting with nothing to report. Add index_max_rss_mb and index_max_duration_seconds, enforced by the parent against the worker process tree rather than the worker process alone, so a runaway child cannot hide behind a small parent. Resident memory is sampled through the platform interface on macOS, Linux and Windows. Crossing a limit terminates the tree and yields one trusted, structured terminal result that attributes the failure to the resource that caused it. Both limits default to off. A measurement that cannot be taken fails the attempt instead of passing it: a watchdog that quietly stops watching is worse than no watchdog at all. The shell fixture that stands in for the supervisor names the two new keys. The worker accepts only a policy that spells out every key it knows, which is what keeps a stale supervisor from starting a worker it cannot bound. Signed-off-by: 刘冲 <mail@liuchong.dev>
347 lines
18 KiB
C
347 lines
18 KiB
C
/*
|
|
* mcp.h — MCP (Model Context Protocol) server for codebase-memory-mcp.
|
|
*
|
|
* Implements JSON-RPC 2.0 over stdio with the MCP tool calling protocol.
|
|
* Provides 14 graph analysis tools (search, trace, query, index, etc.)
|
|
*/
|
|
#ifndef CBM_MCP_H
|
|
#define CBM_MCP_H
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
|
|
#include "mcp/index_supervisor.h"
|
|
|
|
/* ── Forward declarations ─────────────────────────────────────── */
|
|
|
|
typedef struct cbm_store cbm_store_t; /* from store/store.h */
|
|
struct cbm_watcher; /* from watcher/watcher.h */
|
|
struct cbm_config; /* from cli/cli.h */
|
|
|
|
/* ── JSON-RPC types ───────────────────────────────────────────── */
|
|
|
|
typedef struct {
|
|
const char *jsonrpc; /* "2.0" */
|
|
const char *method; /* e.g. "initialize", "tools/call" */
|
|
int64_t id; /* request ID (numeric form; -1 if notification) */
|
|
const char *id_str; /* non-NULL when id is a JSON string (issue #253) */
|
|
bool has_id; /* false for notifications */
|
|
const char *params_raw; /* raw JSON string of params */
|
|
} cbm_jsonrpc_request_t;
|
|
|
|
typedef struct {
|
|
int64_t id;
|
|
const char *id_str; /* non-NULL to echo a string id verbatim (issue #253) */
|
|
const char *result_json; /* JSON string for result (success) */
|
|
const char *error_json; /* JSON string for error (failure), NULL on success */
|
|
int error_code; /* JSON-RPC error code */
|
|
} cbm_jsonrpc_response_t;
|
|
|
|
/* ── JSON-RPC parsing / formatting ────────────────────────────── */
|
|
|
|
/* Parse a JSON-RPC request line. Returns 0 on success, -1 on error.
|
|
* Caller must call cbm_jsonrpc_request_free(). */
|
|
int cbm_jsonrpc_parse(const char *line, cbm_jsonrpc_request_t *out);
|
|
void cbm_jsonrpc_request_free(cbm_jsonrpc_request_t *r);
|
|
|
|
/* Format a JSON-RPC response. Returns heap-allocated JSON string. */
|
|
char *cbm_jsonrpc_format_response(const cbm_jsonrpc_response_t *resp);
|
|
|
|
/* Format a JSON-RPC error response. Returns heap-allocated JSON string. */
|
|
char *cbm_jsonrpc_format_error(int64_t id, int code, const char *message);
|
|
|
|
/* ── MCP protocol helpers ─────────────────────────────────────── */
|
|
|
|
/* Format an MCP tool result with text content. Returns heap-allocated JSON. */
|
|
char *cbm_mcp_text_result(const char *text, bool is_error);
|
|
|
|
/* Attach a notice to a complete MCP tool result: a JSON-object payload gains a
|
|
* "notice" key (content text and structuredContent stay in step), a text
|
|
* payload gains a trailing paragraph. Takes
|
|
* ownership of result and returns the rewritten result, or result unchanged
|
|
* when it is not a tool-result object (never NULL for a non-NULL result). */
|
|
char *cbm_mcp_tool_result_add_notice(char *result, const char *notice);
|
|
|
|
/* #2144: the advice every cut-short synchronous index_repository surfaces. A
|
|
* long index can outlive a client's per-call deadline; the async flag lets the
|
|
* daemon finish it while the client polls. */
|
|
#define CBM_MCP_INDEX_ASYNC_HINT \
|
|
"Long indexes can exceed an MCP client's per-call deadline: retry with " \
|
|
"index_repository(repo_path=..., async: true), then poll " \
|
|
"index_repository(repo_path=..., status: true) until state is succeeded, failed or " \
|
|
"cancelled."
|
|
|
|
/* Return true when notifications/cancelled params target the active request. */
|
|
bool cbm_mcp_cancel_request_matches(const char *params_json, int64_t active_id,
|
|
const char *active_id_str);
|
|
|
|
/* Format the tools/list response. Returns heap-allocated JSON. */
|
|
char *cbm_mcp_tools_list(void);
|
|
|
|
/* Return a tool's JSON input_schema string by name (static; do not free), or
|
|
* NULL if the tool is unknown. Backs the CLI flag parser + per-tool --help. */
|
|
const char *cbm_mcp_tool_input_schema(const char *tool_name);
|
|
|
|
/* Registry accessors: the number of tools tools/list advertises, and the name
|
|
* of tool `index` (static, do not free; NULL when out of range). */
|
|
int cbm_mcp_tool_count(void);
|
|
const char *cbm_mcp_tool_name(int index);
|
|
|
|
/* Tool metadata by name (static; do not free; NULL when unknown). Used by the
|
|
* generated client adapters so they can emit descriptions and parameter
|
|
* schemas without drifting from tools/list. */
|
|
const char *cbm_mcp_tool_description(const char *tool_name);
|
|
|
|
/* Render the top-level --help "Tools:" block from the registry so the help
|
|
* text cannot drift from tools/list (#1361). Heap-allocated; caller frees. */
|
|
char *cbm_mcp_tools_help_list(void);
|
|
|
|
/* Format the initialize response. params_json is the raw initialize params
|
|
* (used for protocol version negotiation). Returns heap-allocated JSON. */
|
|
char *cbm_mcp_initialize_response(const char *params_json);
|
|
|
|
/* ── Tool argument helpers ────────────────────────────────────── */
|
|
|
|
/* Extract a string argument from the tools/call params JSON.
|
|
* Returns heap-allocated copy, or NULL if not found. */
|
|
char *cbm_mcp_get_string_arg(const char *args_json, const char *key);
|
|
|
|
/* Extract an int argument. Returns default_val if not found. */
|
|
int cbm_mcp_get_int_arg(const char *args_json, const char *key, int default_val);
|
|
|
|
/* Extract a bool argument. Returns false if not found. */
|
|
bool cbm_mcp_get_bool_arg(const char *args_json, const char *key);
|
|
|
|
/* Extract the tool name from a tools/call params JSON. Heap-allocated. */
|
|
char *cbm_mcp_get_tool_name(const char *params_json);
|
|
|
|
/* Extract the arguments sub-object from tools/call params. Heap-allocated JSON string. */
|
|
char *cbm_mcp_get_arguments(const char *params_json);
|
|
|
|
/* ── MCP Server ───────────────────────────────────────────────── */
|
|
|
|
typedef struct cbm_mcp_server cbm_mcp_server_t;
|
|
|
|
typedef enum {
|
|
CBM_MCP_TOOL_PROFILE_ALL = 0,
|
|
/* Agent-facing restricted surfaces: only explicitly allowlisted inspection
|
|
* tools are advertised or callable. Internal cache maintenance may still
|
|
* write, so these are intentionally not named strictly read-only modes. */
|
|
CBM_MCP_TOOL_PROFILE_ANALYSIS = 1,
|
|
CBM_MCP_TOOL_PROFILE_SCOUT = 2,
|
|
} cbm_mcp_tool_profile_t;
|
|
|
|
/* Parse the process-level tool-profile flag. Explicit malformed or unknown
|
|
* values fail closed with -1; absence selects the full default surface. */
|
|
int cbm_mcp_parse_tool_profile_args(int argc, const char *const argv[const],
|
|
cbm_mcp_tool_profile_t *profile_out);
|
|
|
|
/* Restricted servers must not start a second unrestricted HTTP/RPC surface. */
|
|
bool cbm_mcp_tool_profile_allows_http(cbm_mcp_tool_profile_t profile);
|
|
|
|
/* Optional daemon-owned physical index executor. repo_path is canonical and
|
|
* already authorized against this session; args_json contains that canonical
|
|
* path plus all caller options. The callback returns a complete malloc-owned
|
|
* MCP tool result. A NULL result is reported as an error and never degrades to
|
|
* an uncoordinated in-process index. async (#2144) asks the executor to start
|
|
* or join the project's job and return at once; the job must then outlive the
|
|
* request and its session. */
|
|
typedef char *(*cbm_mcp_index_executor_fn)(void *context, const char *repo_path,
|
|
const char *args_json, bool async);
|
|
|
|
/* Optional daemon-owned index job status (#2144): the state of the running or
|
|
* most recent index job for one project key. Returns a malloc-owned tool
|
|
* result. Servers without one (in-process CLI workers, embedders) refuse
|
|
* index_repository's async and status modes. */
|
|
typedef char *(*cbm_mcp_index_status_fn)(void *context, const char *project);
|
|
|
|
/* Daemon-owned exclusive lease for operations that mutate a published project
|
|
* database. begin may wait, but must remain cancellable by its session owner;
|
|
* every successful begin is paired with exactly one end. Standalone/worker
|
|
* servers leave these callbacks unset. */
|
|
typedef bool (*cbm_mcp_project_mutation_begin_fn)(void *context, const char *project);
|
|
typedef void (*cbm_mcp_project_mutation_end_fn)(void *context, const char *project);
|
|
|
|
/* Nonblocking counterpart for opportunistic writes during a read request. A
|
|
* successful call is released through the primary mutation guard's end hook. */
|
|
typedef bool (*cbm_mcp_project_mutation_try_begin_fn)(void *context, const char *project);
|
|
|
|
/* Create an MCP server. store_path is the SQLite database directory. */
|
|
cbm_mcp_server_t *cbm_mcp_server_new(const char *store_path);
|
|
|
|
/* Select the tool surface exposed by tools/list and enforced by dispatch. */
|
|
void cbm_mcp_server_set_tool_profile(cbm_mcp_server_t *srv, cbm_mcp_tool_profile_t profile);
|
|
|
|
/* Free an MCP server. */
|
|
void cbm_mcp_server_free(cbm_mcp_server_t *srv);
|
|
|
|
/* Set external watcher reference (for auto-index registration). Not owned. */
|
|
void cbm_mcp_server_set_watcher(cbm_mcp_server_t *srv, struct cbm_watcher *w);
|
|
|
|
/* Watch visibility for index_status (#2167). The daemon, which owns the one
|
|
* background watcher, answers whether `project` is currently watched. All
|
|
* strings are static. A server without a provider runs no watcher at all and
|
|
* reports watched=false with reason "no_watcher". */
|
|
typedef struct {
|
|
bool watched;
|
|
const char *reason; /* why not watched (set when !watched) */
|
|
const char *strategy; /* "pending" | "git" | "tree" | "none" (set when watched) */
|
|
int poll_interval_ms; /* current adaptive cadence (when watched) */
|
|
int64_t last_scan_unix_s; /* last completed scan, wall clock; 0 = none yet */
|
|
} cbm_mcp_watch_status_t;
|
|
typedef void (*cbm_mcp_watch_status_fn)(void *context, const char *project,
|
|
cbm_mcp_watch_status_t *out);
|
|
void cbm_mcp_server_set_watch_status_provider(cbm_mcp_server_t *srv, cbm_mcp_watch_status_fn fn,
|
|
void *context);
|
|
|
|
/* Set external config store reference (for auto_index setting). Not owned. */
|
|
void cbm_mcp_server_set_config(cbm_mcp_server_t *srv, struct cbm_config *cfg);
|
|
|
|
/* Set an explicit session context for an embedded/daemon-backed server.
|
|
* session_root is copied and its project name is derived using the same naming
|
|
* rule as indexing. allowed_root is copied when non-NULL; an explicit NULL
|
|
* disables the workspace boundary for this server instead of falling back to
|
|
* CBM_ALLOWED_ROOT. Returns false when the context is invalid or cannot be
|
|
* copied. */
|
|
bool cbm_mcp_server_set_session_context(cbm_mcp_server_t *srv, const char *session_root,
|
|
const char *allowed_root);
|
|
|
|
/* Read-only session context accessors. Returned strings are owned by srv. */
|
|
const char *cbm_mcp_server_session_root(const cbm_mcp_server_t *srv);
|
|
const char *cbm_mcp_server_session_project(const cbm_mcp_server_t *srv);
|
|
const char *cbm_mcp_server_allowed_root(const cbm_mcp_server_t *srv);
|
|
|
|
/* Enable/disable per-server update checks and automatic indexing. Enabled by
|
|
* default for standalone servers; daemon sessions disable these so the shared
|
|
* coordinator owns background work. */
|
|
void cbm_mcp_server_set_background_tasks(cbm_mcp_server_t *srv, bool enabled);
|
|
|
|
void cbm_mcp_server_set_index_executor(cbm_mcp_server_t *srv, cbm_mcp_index_executor_fn executor,
|
|
void *context);
|
|
|
|
void cbm_mcp_server_set_index_status_provider(cbm_mcp_server_t *srv,
|
|
cbm_mcp_index_status_fn provider, void *context);
|
|
|
|
/* Relay supervised worker logs to one local request (for CLI progress). The
|
|
* callback/context are borrowed until the synchronous tool call returns. */
|
|
void cbm_mcp_server_set_index_log_callback(cbm_mcp_server_t *srv, cbm_proc_log_cb callback,
|
|
void *context);
|
|
|
|
void cbm_mcp_server_set_project_mutation_guard(cbm_mcp_server_t *srv,
|
|
cbm_mcp_project_mutation_begin_fn begin,
|
|
cbm_mcp_project_mutation_end_fn end, void *context);
|
|
|
|
/* Configure an optional nonblocking acquire for the configured mutation
|
|
* guard. If no try hook is set, a read that requires recovery fails with a
|
|
* configuration error rather than invoking the blocking begin callback. */
|
|
void cbm_mcp_server_set_project_mutation_try_guard(cbm_mcp_server_t *srv,
|
|
cbm_mcp_project_mutation_try_begin_fn try_begin);
|
|
|
|
/* Read one complete MCP message from in. Supports newline-delimited JSON and
|
|
* Content-Length framing, including additional headers. Returns 1 on success,
|
|
* 0 on clean EOF, and -1 on invalid input or an I/O/allocation error. On
|
|
* success, *message is heap-allocated and *content_length_framed identifies the
|
|
* transport framing used by the request. */
|
|
int cbm_mcp_read_message(FILE *in, char **message, bool *content_length_framed);
|
|
|
|
/* Run the MCP server event loop on the given streams (typically stdin/stdout).
|
|
* Blocks until EOF on input. Returns 0 on success, -1 on error. */
|
|
int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out);
|
|
|
|
/* Process a single JSON-RPC request line and return the response.
|
|
* Returns heap-allocated JSON response string, or NULL for notifications. */
|
|
char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line);
|
|
|
|
/* ── Tool handler dispatch (for testing) ──────────────────────── */
|
|
|
|
/* Handle a tools/call request. Returns MCP tool result JSON. */
|
|
char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const char *args_json);
|
|
|
|
/* ── Supervised background index (RSS isolation, #832) ────────── */
|
|
|
|
/* #1300: the explicit error response for a worker that exited cleanly without
|
|
* writing a response (worker_result->response_missing). Names the retained
|
|
* worker log and the last phase it reached; caller frees. */
|
|
char *cbm_mcp_index_worker_no_response_failure(const char *args,
|
|
const cbm_index_worker_result_t *worker_result);
|
|
|
|
/* One shared policy gate for supervised-index callers. Only an explicitly safe
|
|
* terminal can yield a result or enter crash/hang recovery; cancellation,
|
|
* failed tree containment, and unavailable worker startup are terminal. The
|
|
* FALLBACK disposition is an internal classification used to build an explicit
|
|
* error response, never permission for a marked host to index in-process.
|
|
* Exposed so the security boundary is directly regression-tested. */
|
|
typedef enum {
|
|
CBM_MCP_SUPERVISED_RESULT_FALLBACK = 0,
|
|
CBM_MCP_SUPERVISED_RESULT_SUCCESS,
|
|
CBM_MCP_SUPERVISED_RESULT_RESOURCE_FAILURE,
|
|
CBM_MCP_SUPERVISED_RESULT_CONTAINED_FAILURE,
|
|
CBM_MCP_SUPERVISED_RESULT_UNSAFE_TERMINAL,
|
|
} cbm_mcp_supervised_result_disposition_t;
|
|
|
|
cbm_mcp_supervised_result_disposition_t cbm_mcp_supervised_result_disposition(
|
|
int spawn_result, const cbm_index_worker_result_t *worker_result);
|
|
|
|
/* Run a full index of root_path in a supervised worker SUBPROCESS (the same
|
|
* crash/hang-isolating runner used by handle_index_repository), so the child
|
|
* returns 100% of its RSS to the OS on exit instead of ratcheting the long-lived
|
|
* parent. Builds {"repo_path": root_path} internally. Returns the worker's
|
|
* response or an explicit contained-error response (caller frees). NULL is
|
|
* reserved for invalid input/request construction failure; a marked host must
|
|
* stop rather than use it as permission to index in-process. This is the shared
|
|
* entry the session auto-index routes through. */
|
|
char *cbm_mcp_index_run_supervised_path(const char *root_path);
|
|
|
|
/* ── Idle store eviction ──────────────────────────────────────── */
|
|
|
|
/* Evict the cached project store if idle for more than timeout_s seconds.
|
|
* Protects initial in-memory stores (those never accessed via a named project).
|
|
* Called automatically by the event loop on poll() timeout. */
|
|
void cbm_mcp_server_evict_idle(cbm_mcp_server_t *srv, int timeout_s);
|
|
|
|
/* Check if the server currently has a cached store open. */
|
|
bool cbm_mcp_server_has_cached_store(cbm_mcp_server_t *srv);
|
|
|
|
/* ── Testing helpers ───────────────────────────────────────────── */
|
|
|
|
/* Get the store handle from a server (for test setup). */
|
|
cbm_store_t *cbm_mcp_server_store(cbm_mcp_server_t *srv);
|
|
|
|
/* Set the project name associated with the server's current store (for test setup).
|
|
* This prevents resolve_store() from trying to open a .db file when tools specify a project. */
|
|
void cbm_mcp_server_set_project(cbm_mcp_server_t *srv, const char *project);
|
|
|
|
/* ── Cancellation support ─────────────────────────────────────── */
|
|
|
|
struct cbm_pipeline; /* forward decl */
|
|
|
|
/* Get the currently active pipeline (for signal handler cancellation).
|
|
* Returns NULL if no pipeline is running. */
|
|
struct cbm_pipeline *cbm_mcp_server_active_pipeline(cbm_mcp_server_t *srv);
|
|
|
|
/* Thread-safe cancellation for daemon connection teardown. Returns false when
|
|
* no request scope is active. Long-running request operations share the atomic
|
|
* cancellation token and own any process-tree teardown before returning. */
|
|
bool cbm_mcp_server_cancel_active(cbm_mcp_server_t *srv);
|
|
|
|
/* Publish an outer request lifetime before crossing an asynchronous transport
|
|
* boundary. The daemon application uses this to close the short interval
|
|
* between accepting a request token and entering JSON-RPC/tool dispatch. Each
|
|
* successful begin must be paired with end. */
|
|
bool cbm_mcp_server_request_scope_begin(cbm_mcp_server_t *srv);
|
|
void cbm_mcp_server_request_scope_end(cbm_mcp_server_t *srv);
|
|
|
|
/* ── URI helpers ───────────────────────────────────────────────── */
|
|
|
|
/* Parse a file:// URI and extract the filesystem path.
|
|
* Writes to out_path (up to out_size bytes). Returns true on success.
|
|
* On Windows, strips leading / from /C:/path. */
|
|
bool cbm_parse_file_uri(const char *uri, char *out_path, int out_size);
|
|
|
|
/* How many restarts a failed index worker gets, as CBM_INDEX_MAX_RESTARTS sets
|
|
* it. Exposed so a test can check what the setting resolves to. */
|
|
int cbm_index_restart_cap_for_testing(void);
|
|
|
|
#endif /* CBM_MCP_H */
|