Skip to content
Merged
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
12 changes: 8 additions & 4 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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];
Expand Down
57 changes: 57 additions & 0 deletions tests/test_integration.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 */
Expand Down
Loading