Merge pull request #1319 from JJordan0C/fix/search-graph-semantic-only-results

fix(mcp): isolate semantic-only JSON search
This commit is contained in:
Martin Vogel
2026-08-18 00:04:36 +02:00
committed by GitHub
2 changed files with 76 additions and 38 deletions
+31 -38
View File
@@ -3170,22 +3170,6 @@ static bool run_semantic_query_core(const char *args, cbm_store_t *store, const
return type_error;
}
/* Append the semantic_query vector-search results onto the doc. Returns
* true if semantic_query was provided as a non-array (type error — caller
* should surface to the user). */
static bool run_semantic_query(yyjson_mut_doc *doc, yyjson_mut_val *root, const char *args,
cbm_store_t *store, const char *project, int limit) {
cbm_vector_result_t *vresults = NULL;
int vcount = 0;
bool type_error =
run_semantic_query_core(args, store, project, limit, &vresults, &vcount, NULL);
if (vcount > 0) {
emit_semantic_results(doc, root, vresults, vcount);
cbm_store_free_vector_results(vresults, vcount);
}
return type_error;
}
/* ── Tree output for search_graph ───────────────────────────────────
* Default response encoding: grouped tree rows (compact_out.h). The same
* model is available as structured JSON via format:"json"; include_connected
@@ -3620,6 +3604,9 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
.min_degree = min_degree,
.max_degree = max_degree,
};
bool has_structural_filters = label || name_pattern || qn_pattern || file_pattern ||
relationship || exclude_entry_points ||
min_degree != CBM_NOT_FOUND || max_degree != CBM_NOT_FOUND;
if (!legacy_json) {
const char *fields[SG_MAX_EXTRA_FIELDS];
@@ -3640,10 +3627,7 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
/* Semantic-only calls get semantic results only: the legacy
* behavior also ran the UNFILTERED regex search and prepended
* up to `limit` unrelated enriched nodes to the response. */
bool has_filters = label || name_pattern || qn_pattern || file_pattern ||
relationship || exclude_entry_points ||
min_degree != CBM_NOT_FOUND || max_degree != CBM_NOT_FOUND;
bool semantic_only = sq_present && !has_filters;
bool semantic_only = sq_present && !has_structural_filters;
cbm_sb_t sb;
cbm_sb_init(&sb);
@@ -3727,8 +3711,31 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
true);
}
cbm_vector_result_t *vresults = NULL;
int vcount = 0;
bool sq_present = false;
bool sq_type_error =
run_semantic_query_core(args, store, project, limit, &vresults, &vcount, &sq_present);
if (sq_type_error) {
free(project);
free(label);
free(name_pattern);
free(qn_pattern);
free(file_pattern);
free(relationship);
return cbm_mcp_text_result(
"semantic_query must be an array of keyword strings, e.g. "
"[\"send\",\"pubsub\",\"publish\"] — not a single string. Split your query "
"into individual keywords; each is scored independently via per-keyword "
"min-cosine.",
true);
}
bool semantic_only = sq_present && !has_structural_filters;
cbm_search_output_t out = {0};
cbm_store_search(store, &params, &out);
if (!semantic_only) {
cbm_store_search(store, &params, &out);
}
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
yyjson_mut_val *root = yyjson_mut_obj(doc);
@@ -3767,23 +3774,9 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
}
}
bool sq_type_error = run_semantic_query(doc, root, args, store, project, limit);
if (sq_type_error) {
yyjson_mut_doc_free(doc);
cbm_store_search_free(&out);
free(project);
free(label);
free(name_pattern);
free(qn_pattern);
free(file_pattern);
free(relationship);
return cbm_mcp_text_result(
"semantic_query must be an array of keyword strings, e.g. "
"[\"send\",\"pubsub\",\"publish\"] — not a single string. Split your query "
"into individual keywords; each is scored independently via per-keyword "
"min-cosine.",
true);
if (vcount > 0) {
emit_semantic_results(doc, root, vresults, vcount);
cbm_store_free_vector_results(vresults, vcount);
}
char *json = yy_doc_to_str(doc);
+45
View File
@@ -1637,6 +1637,50 @@ static cbm_mcp_server_t *setup_snippet_server(char *tmp_dir, size_t tmp_sz);
static void cleanup_snippet_dir(const char *tmp_dir);
static char *extract_text_content(const char *mcp_result);
TEST(tool_search_graph_semantic_only_skips_structural_results_issue1295) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
const char *proj = "semantic-only";
cbm_mcp_server_set_project(srv, proj);
cbm_store_upsert_project(st, proj, "/tmp/semantic-only");
cbm_node_t unrelated = {.project = proj,
.label = "Function",
.name = "unrelated_node",
.qualified_name = "semantic-only.unrelated_node",
.file_path = "unrelated.c",
.start_line = 1,
.end_line = 2};
ASSERT_GT(cbm_store_upsert_node(st, &unrelated), 0);
char *resp = cbm_mcp_handle_tool(
srv, "search_graph",
"{\"project\":\"semantic-only\",\"semantic_query\":[\"publish\"],\"limit\":5}");
ASSERT_NOT_NULL(resp);
char *inner = extract_text_content(resp);
free(resp);
ASSERT_NOT_NULL(inner);
ASSERT_NULL(strstr(inner, "unrelated_node"));
free(inner);
resp = cbm_mcp_handle_tool(srv, "search_graph",
"{\"project\":\"semantic-only\",\"semantic_query\":[\"publish\"],"
"\"format\":\"json\",\"limit\":5}");
ASSERT_NOT_NULL(resp);
inner = extract_text_content(resp);
free(resp);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"total\":0"));
ASSERT_NOT_NULL(strstr(inner, "\"count\":0"));
ASSERT_NOT_NULL(strstr(inner, "\"groups\":[]"));
ASSERT_NULL(strstr(inner, "unrelated_node"));
free(inner);
cbm_mcp_server_free(srv);
PASS();
}
/* callers_total/callees_total must count what the caller can enumerate: with
* include_tests=false (default) test-file rows are hidden from the table, so
* the totals must apply the same filter — a raw visited_count overstated the
@@ -10815,6 +10859,7 @@ SUITE(mcp) {
RUN_TEST(tool_get_graph_schema_empty);
RUN_TEST(tool_unknown_tool);
RUN_TEST(tool_search_graph_basic);
RUN_TEST(tool_search_graph_semantic_only_skips_structural_results_issue1295);
RUN_TEST(tool_trace_totals_respect_test_filter);
RUN_TEST(tool_trace_totals_respect_test_filter_tests_root_subtree_issue1294);
RUN_TEST(tool_get_architecture_cycles_detects_scc);