From 470c76311763d4874d6fc6d1f7b38509241247b2 Mon Sep 17 00:00:00 2001 From: imi Date: Mon, 27 Jul 2026 16:32:28 +0000 Subject: [PATCH] fix(search): preserve compound requested fields Signed-off-by: imi --- src/mcp/mcp.c | 57 +++++++++++++++++++++++------------------------- tests/test_mcp.c | 26 +++++++++++++++++++--- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index d891e7dd9..e22b1500d 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -3082,6 +3082,27 @@ static int sg_parse_fields(const char *args, const char *out[], int max_out, yyj return n; } +/* Append a property as one compact-output cell. Compound values stay one + * column by using their compact JSON representation; the cell emitter quotes + * and escapes that representation as needed. */ +static void sg_toon_property_cell(cbm_sb_t *sb, yyjson_val *v) { + if (v && yyjson_is_str(v)) { + cbm_tree_cell_str(sb, yyjson_get_str(v), false); + } else if (v && yyjson_is_bool(v)) { + cbm_tree_cell_bool(sb, yyjson_get_bool(v), false); + } else if (v && yyjson_is_int(v)) { + cbm_tree_cell_int(sb, yyjson_get_int(v), false); + } else if (v && yyjson_is_real(v)) { + cbm_tree_cell_real(sb, yyjson_get_real(v), false); + } else if (v && !yyjson_is_null(v)) { + char *json = yyjson_val_write(v, 0, NULL); + cbm_tree_cell_str(sb, json ? json : "", false); + free(json); + } else { + cbm_tree_cell_str(sb, "", false); + } +} + /* Append one row's extra-field cells, pulled from the node's properties. */ static void sg_toon_extra_cells(cbm_sb_t *sb, const char *props_json, const char *const *fields, int nfields) { @@ -3090,17 +3111,7 @@ static void sg_toon_extra_cells(cbm_sb_t *sb, const char *props_json, const char yyjson_val *pr = pd ? yyjson_doc_get_root(pd) : NULL; for (int f = 0; f < nfields; f++) { yyjson_val *v = (pr && yyjson_is_obj(pr)) ? yyjson_obj_get(pr, fields[f]) : NULL; - if (v && yyjson_is_str(v)) { - cbm_tree_cell_str(sb, yyjson_get_str(v), false); - } else if (v && yyjson_is_bool(v)) { - cbm_tree_cell_bool(sb, yyjson_get_bool(v), false); - } else if (v && yyjson_is_int(v)) { - cbm_tree_cell_int(sb, yyjson_get_int(v), false); - } else if (v && yyjson_is_real(v)) { - cbm_tree_cell_real(sb, yyjson_get_real(v), false); - } else { - cbm_tree_cell_str(sb, "", false); - } + sg_toon_property_cell(sb, v); } if (pd) { yyjson_doc_free(pd); @@ -3232,17 +3243,7 @@ static void emit_search_results_tree(cbm_sb_t *sb, cbm_search_output_t *out, int yyjson_val *pr = pd ? yyjson_doc_get_root(pd) : NULL; for (int f = 0; f < nfields; f++) { yyjson_val *v = (pr && yyjson_is_obj(pr)) ? yyjson_obj_get(pr, fields[f]) : NULL; - if (v && yyjson_is_str(v)) { - cbm_tree_cell_str(sb, yyjson_get_str(v), false); - } else if (v && yyjson_is_int(v)) { - cbm_tree_cell_int(sb, yyjson_get_int(v), false); - } else if (v && yyjson_is_real(v)) { - cbm_tree_cell_real(sb, yyjson_get_real(v), false); - } else if (v && yyjson_is_bool(v)) { - cbm_tree_cell_bool(sb, yyjson_get_bool(v), false); - } else { - cbm_tree_cell_str(sb, "", false); /* emits "-" */ - } + sg_toon_property_cell(sb, v); } if (pd) { yyjson_doc_free(pd); @@ -3324,14 +3325,10 @@ static void emit_search_results_tree_json(yyjson_mut_doc *doc, yyjson_mut_val *r yyjson_val *pr = pd ? yyjson_doc_get_root(pd) : NULL; for (int f = 0; f < nfields; f++) { yyjson_val *v = (pr && yyjson_is_obj(pr)) ? yyjson_obj_get(pr, fields[f]) : NULL; - if (v && yyjson_is_str(v)) { - yyjson_mut_arr_add_strcpy(doc, row, yyjson_get_str(v)); - } else if (v && yyjson_is_int(v)) { - yyjson_mut_arr_add_int(doc, row, yyjson_get_int(v)); - } else if (v && yyjson_is_real(v)) { - yyjson_mut_arr_add_real(doc, row, yyjson_get_real(v)); - } else if (v && yyjson_is_bool(v)) { - yyjson_mut_arr_add_bool(doc, row, yyjson_get_bool(v)); + yyjson_mut_val *copy = + (v && !yyjson_is_null(v)) ? yyjson_val_mut_copy(doc, v) : NULL; + if (copy) { + yyjson_mut_arr_add_val(row, copy); } else { yyjson_mut_arr_add_null(doc, row); } diff --git a/tests/test_mcp.c b/tests/test_mcp.c index a56aebdf5..0b55529e9 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -1793,15 +1793,32 @@ TEST(tool_search_graph_includes_node_properties) { free(inner); free(resp); + /* List-valued fields are compact JSON in text output, rather than an + * empty placeholder. */ + resp = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":431,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"search_graph\"," + "\"arguments\":{\"project\":\"test-project\",\"label\":\"Function\"," + "\"name_pattern\":\"HandleRequest\",\"fields\":[\"base_classes\"]," + "\"limit\":5}}}"); + ASSERT_NOT_NULL(resp); + inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + ASSERT_NOT_NULL(strstr(inner, "base_classes")); + ASSERT_NOT_NULL(strstr(inner, "HandlerBase")); + ASSERT_NOT_NULL(strstr(inner, "Audited")); + free(inner); + free(resp); + /* format:"json" = json-stringified tree: same grouped model, column- * ordered row arrays — never per-row key envelopes or property blobs. - * fields adds columns there too. */ + * fields adds columns there too and preserves compound JSON types. */ resp = cbm_mcp_server_handle( srv, "{\"jsonrpc\":\"2.0\",\"id\":44,\"method\":\"tools/call\"," "\"params\":{\"name\":\"search_graph\"," "\"arguments\":{\"project\":\"test-project\",\"label\":\"Function\"," "\"name_pattern\":\"HandleRequest\",\"format\":\"json\"," - "\"fields\":[\"signature\"],\"limit\":5}}}"); + "\"fields\":[\"signature\",\"base_classes\"],\"limit\":5}}}"); ASSERT_NOT_NULL(resp); inner = extract_text_content(resp); ASSERT_NOT_NULL(inner); @@ -1810,6 +1827,8 @@ TEST(tool_search_graph_includes_node_properties) { ASSERT_NOT_NULL(strstr(inner, "\"rows\"")); ASSERT_NOT_NULL(strstr(inner, "\"signature\"")); /* requested column */ ASSERT_NOT_NULL(strstr(inner, "func HandleRequest")); /* its value */ + ASSERT_NOT_NULL(strstr(inner, "\"base_classes\"")); + ASSERT_NOT_NULL(strstr(inner, "[\"HandlerBase\",\"Audited\"]")); ASSERT_NULL(strstr(inner, "is_exported")); /* blob never spills */ free(inner); free(resp); @@ -6162,7 +6181,8 @@ static cbm_mcp_server_t *setup_snippet_server(char *tmp_dir, size_t tmp_sz) { n_hr.end_line = 5; n_hr.properties_json = "{\"signature\":\"func HandleRequest() error\"," "\"return_type\":\"error\"," - "\"is_exported\":true}"; + "\"is_exported\":true," + "\"base_classes\":[\"HandlerBase\",\"Audited\"]}"; int64_t id_hr = cbm_store_upsert_node(st, &n_hr); cbm_node_t n_po = {0};