Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 31 additions & 38 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -3066,22 +3066,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
Expand Down Expand Up @@ -3516,6 +3500,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];
Expand All @@ -3536,10 +3523,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);
Expand Down Expand Up @@ -3623,8 +3607,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);
Expand Down Expand Up @@ -3663,23 +3670,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);
Expand Down
45 changes: 45 additions & 0 deletions tests/test_mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,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
Expand Down Expand Up @@ -9773,6 +9817,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_get_architecture_cycles_detects_scc);
RUN_TEST(tool_get_code_snippet_clips_whole_file_node);
Expand Down
Loading