feat(mcp): report index publish metadata

Expose index_repository publish_kind and graph_changed in the JSON result so MCP clients can tell whether a run used a full, no-op, exact incremental, or containment route without relying on stderr logs.

Add a reusable pipeline publish-kind string helper plus focused pipeline and incremental response-contract tests. This is additive response metadata only; it does not change indexing behavior or enable incremental by default.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-01 15:11:16 -04:00
parent d1e4f50e0d
commit 5e12e9434a
5 changed files with 51 additions and 0 deletions
+3
View File
@@ -4995,6 +4995,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
yyjson_mut_obj_add_str(doc, root, "project", project_name);
yyjson_mut_obj_add_str(doc, root, "status", rc == 0 ? "indexed" : "error");
yyjson_mut_obj_add_str(doc, root, "publish_kind",
cbm_pipeline_publish_kind_name(publish_kind));
yyjson_mut_obj_add_bool(doc, root, "graph_changed", graph_changed);
if (rc == 0) {
cbm_store_t *store = resolve_store(srv, project_name);
+17
View File
@@ -403,6 +403,23 @@ cbm_pipeline_publish_kind_t cbm_pipeline_publish_kind(const cbm_pipeline_t *p) {
return p ? p->publish_kind : CBM_PIPELINE_PUBLISH_NONE;
}
const char *cbm_pipeline_publish_kind_name(cbm_pipeline_publish_kind_t kind) {
switch (kind) {
case CBM_PIPELINE_PUBLISH_NONE:
return "none";
case CBM_PIPELINE_PUBLISH_FULL:
return "full";
case CBM_PIPELINE_PUBLISH_INCREMENTAL_NOOP:
return "incremental_noop";
case CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT:
return "incremental_exact";
case CBM_PIPELINE_PUBLISH_INCREMENTAL_CONTAINMENT:
return "incremental_containment";
default:
return "unknown";
}
}
void cbm_pipeline_set_graph_changed(cbm_pipeline_t *p, bool changed) {
if (p) {
p->graph_changed = changed;
+1
View File
@@ -141,6 +141,7 @@ bool cbm_pipeline_graph_changed(const cbm_pipeline_t *p);
/* Last publish route for the most recent run. This is observability for callers
* that need derived-view policy decisions; it does not change graph contents. */
cbm_pipeline_publish_kind_t cbm_pipeline_publish_kind(const cbm_pipeline_t *p);
const char *cbm_pipeline_publish_kind_name(cbm_pipeline_publish_kind_t kind);
/* ── Index lock (prevents concurrent pipeline runs on same DB) ──── */
+16
View File
@@ -2174,6 +2174,21 @@ TEST(tool_index_mode_fast) {
PASS();
}
TEST(tool_index_publish_metadata) {
double ms;
char *r = call_tool_timed("index_repository", &ms, "{\"repo_path\":\"%s\",\"mode\":\"fast\"}",
g_repodir);
ASSERT(r != NULL);
ASSERT(strstr(r, "\\\"publish_kind\\\":\\\"") != NULL);
ASSERT(strstr(r, "\\\"graph_changed\\\":") != NULL);
ASSERT(strstr(r, "\\\"publish_kind\\\":\\\"full\\\"") != NULL ||
strstr(r, "\\\"publish_kind\\\":\\\"incremental_noop\\\"") != NULL ||
strstr(r, "\\\"publish_kind\\\":\\\"incremental_exact\\\"") != NULL ||
strstr(r, "\\\"publish_kind\\\":\\\"incremental_containment\\\"") != NULL);
free(r);
PASS();
}
TEST(tool_index_invalid_path) {
double ms;
char *r = call_tool_timed("index_repository", &ms, "{\"repo_path\":\"/nonexistent/path/xyz\"}");
@@ -3264,6 +3279,7 @@ SUITE(incremental) {
/* Phase 19: index_repository params */
RUN_TEST(tool_index_mode_fast);
RUN_TEST(tool_index_publish_metadata);
RUN_TEST(tool_index_invalid_path);
RUN_TEST(tool_index_missing_param);
+14
View File
@@ -10013,6 +10013,19 @@ TEST(pipeline_unit_threshold_setters_clamp_invalid_values) {
PASS();
}
TEST(pipeline_publish_kind_names_are_stable) {
ASSERT_STR_EQ(cbm_pipeline_publish_kind_name(CBM_PIPELINE_PUBLISH_NONE), "none");
ASSERT_STR_EQ(cbm_pipeline_publish_kind_name(CBM_PIPELINE_PUBLISH_FULL), "full");
ASSERT_STR_EQ(cbm_pipeline_publish_kind_name(CBM_PIPELINE_PUBLISH_INCREMENTAL_NOOP),
"incremental_noop");
ASSERT_STR_EQ(cbm_pipeline_publish_kind_name(CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT),
"incremental_exact");
ASSERT_STR_EQ(cbm_pipeline_publish_kind_name(CBM_PIPELINE_PUBLISH_INCREMENTAL_CONTAINMENT),
"incremental_containment");
ASSERT_STR_EQ(cbm_pipeline_publish_kind_name((cbm_pipeline_publish_kind_t)999), "unknown");
PASS();
}
TEST(pipeline_apply_config_sets_all_thresholds) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_pipeline_cfg_XXXXXX");
@@ -11175,6 +11188,7 @@ SUITE(pipeline) {
/* Resource management & internal helper tests */
RUN_TEST(pipeline_empty_path);
RUN_TEST(pipeline_project_name_content);
RUN_TEST(pipeline_publish_kind_names_are_stable);
RUN_TEST(pipeline_cancel_sets_flag);
RUN_TEST(pipeline_double_cancel);
RUN_TEST(pipeline_double_free_prevention);