From 6a2e66a9d12b68430dd99b101b7cc74f7697cfdf Mon Sep 17 00:00:00 2001 From: kirilklein Date: Sat, 20 Jun 2026 17:01:51 +0200 Subject: [PATCH] fix(mcp): trace_path cross_service follows CROSS_* edges (#522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit trace_path mode=cross_service resolved its default edge-type list to {HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, CALLS} — none of the CROSS_* edge types the cross-repo pass actually writes. Since cbm_store_bfs filters edges with an exact `WHERE e.type IN (...)` clause, cross-service traces silently skipped every cross hop even when the edge was present and visible via query_graph. Add the six CROSS_* types (CROSS_HTTP_CALLS, CROSS_ASYNC_CALLS, CROSS_CHANNEL, CROSS_GRPC_CALLS, CROSS_GRAPHQL_CALLS, CROSS_TRPC_CALLS) to the cross_service default list — exactly the set pass_cross_repo.c emits and cross_repo_delete_edges() cleans up. The count is now computed from the array size (sizeof) so it can't drift; the dead MCP_N_DEFAULTS_4 constant is removed. Tool description updated to match. Adds integ_mcp_trace_path_cross_service: seeds a CROSS_HTTP_CALLS edge between two indexed functions with no CALLS relationship and asserts cross_service surfaces the hop while calls mode does not. Co-Authored-By: Claude Opus 4.8 Signed-off-by: kirilklein --- src/mcp/mcp.c | 12 ++++++--- tests/test_integration.c | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 8102b1e77..a96ebb7ea 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -29,7 +29,6 @@ enum { MCP_DEFAULT_LIMIT = 10, MCP_BFS_LIMIT = 100, MCP_N_DEFAULTS_2 = 2, - MCP_N_DEFAULTS_4 = 4, MCP_URI_PREFIX = 7, /* strlen("file://") */ MCP_CONTENT_PREFIX = 15, /* strlen("Content-Length:") */ MCP_RETURN_2 = 2, @@ -362,7 +361,9 @@ static const tool_def_t TOOLS[] = { "\"type\":\"string\",\"enum\":[\"calls\",\"data_flow\",\"cross_service\"],\"default\":" "\"calls\",\"description\":\"calls: follow CALLS edges. data_flow: follow CALLS+DATA_FLOWS " "with arg expressions. cross_service: follow HTTP_CALLS+ASYNC_CALLS+DATA_FLOWS through " - "Routes.\"},\"parameter_name\":{\"type\":\"string\",\"description\":\"For data_flow mode: " + "Routes, plus CROSS_* cross-repo edges (CROSS_HTTP_CALLS/ASYNC_CALLS/CHANNEL/GRPC_CALLS/" + "GRAPHQL_CALLS/TRPC_CALLS) to hop into other services.\"},\"parameter_name\":{\"type\":" + "\"string\",\"description\":\"For data_flow mode: " "scope trace to a specific parameter name\"},\"edge_types\":{\"type\":\"array\",\"items\":{" "\"type\":\"string\"}},\"risk_labels\":{\"type\":\"boolean\",\"default\":false," "\"description\":\"Add risk classification (CRITICAL/HIGH/MEDIUM/LOW) based on hop distance" @@ -2164,7 +2165,10 @@ static yyjson_doc *resolve_trace_edge_types(const char *args, const char *mode, const char **out_types, int *out_count) { static const char *mode_calls[] = {"CALLS"}; static const char *mode_data_flow[] = {"CALLS", "DATA_FLOWS"}; - static const char *mode_cross_svc[] = {"HTTP_CALLS", "ASYNC_CALLS", "DATA_FLOWS", "CALLS"}; + static const char *mode_cross_svc[] = { + "HTTP_CALLS", "ASYNC_CALLS", "DATA_FLOWS", "CALLS", + "CROSS_HTTP_CALLS", "CROSS_ASYNC_CALLS", "CROSS_CHANNEL", "CROSS_GRPC_CALLS", + "CROSS_GRAPHQL_CALLS", "CROSS_TRPC_CALLS"}; *out_count = 0; @@ -2196,7 +2200,7 @@ static yyjson_doc *resolve_trace_edge_types(const char *args, const char *mode, n_defaults = MCP_N_DEFAULTS_2; } else if (mode && strcmp(mode, "cross_service") == 0) { defaults = mode_cross_svc; - n_defaults = MCP_N_DEFAULTS_4; + n_defaults = (int)(sizeof(mode_cross_svc) / sizeof(mode_cross_svc[0])); } for (int i = 0; i < n_defaults; i++) { out_types[i] = defaults[i]; diff --git a/tests/test_integration.c b/tests/test_integration.c index de0d22008..b81ac378b 100644 --- a/tests/test_integration.c +++ b/tests/test_integration.c @@ -383,6 +383,62 @@ TEST(integ_mcp_trace_path) { PASS(); } +/* #522: trace_path mode=cross_service must follow CROSS_* cross-repo edges. + * Seed a CROSS_HTTP_CALLS edge between two indexed functions that have no CALLS + * relationship, then confirm cross_service surfaces the hop while the default + * calls mode does not (proving the cross edge specifically is what's followed). + * + * The trace goes through a fresh server so it opens the db after the edge is + * committed — exactly what a new MCP session sees after a cross-repo pass writes + * CROSS_* edges (g_srv's cached connection predates this write). */ +TEST(integ_mcp_trace_path_cross_service) { + cbm_store_t *store = cbm_store_open_path(g_dbpath); + ASSERT_NOT_NULL(store); + + cbm_node_t *src = NULL; + cbm_node_t *dst = NULL; + int src_count = 0; + int dst_count = 0; + cbm_store_find_nodes_by_name(store, g_project, "greet", &src, &src_count); + cbm_store_find_nodes_by_name(store, g_project, "farewell", &dst, &dst_count); + ASSERT_TRUE(src_count > 0 && dst_count > 0); + + cbm_edge_t edge = {.project = g_project, + .source_id = src[0].id, + .target_id = dst[0].id, + .type = "CROSS_HTTP_CALLS"}; + ASSERT_TRUE(cbm_store_insert_edge(store, &edge) > 0); + + cbm_store_free_nodes(src, src_count); + cbm_store_free_nodes(dst, dst_count); + cbm_store_close(store); + + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + + char args[256]; + snprintf(args, sizeof(args), + "{\"function_name\":\"greet\",\"project\":\"%s\"," + "\"direction\":\"outbound\",\"mode\":\"cross_service\"}", + g_project); + char *resp = cbm_mcp_handle_tool(srv, "trace_path", args); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "farewell")); + free(resp); + + snprintf(args, sizeof(args), + "{\"function_name\":\"greet\",\"project\":\"%s\"," + "\"direction\":\"outbound\",\"mode\":\"calls\"}", + g_project); + resp = cbm_mcp_handle_tool(srv, "trace_path", args); + ASSERT_NOT_NULL(resp); + ASSERT_TRUE(strstr(resp, "farewell") == NULL); + free(resp); + + cbm_mcp_server_free(srv); + PASS(); +} + TEST(integ_mcp_index_status) { char args[128]; snprintf(args, sizeof(args), "{\"project\":\"%s\"}", g_project); @@ -604,6 +660,7 @@ SUITE(integration) { RUN_TEST(integ_mcp_get_graph_schema); RUN_TEST(integ_mcp_get_architecture); RUN_TEST(integ_mcp_trace_path); + RUN_TEST(integ_mcp_trace_path_cross_service); RUN_TEST(integ_mcp_index_status); /* Store query validation */