Skip to content

Commit d36f22a

Browse files
Shidfarclaude
andcommitted
feat: add cross_project_links MCP tool
New tool queries _crosslinks.db with optional protocol, project, and identifier filters. Returns formatted cross-project communication links grouped by protocol. Uses parameterized queries for filter safety. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 32ec2f2 commit d36f22a

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

src/mcp/mcp.c

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,13 @@ static const tool_def_t TOOLS[] = {
384384
"{\"type\":\"object\",\"properties\":{\"traces\":{\"type\":\"array\",\"items\":{\"type\":"
385385
"\"object\"}},\"project\":{\"type\":"
386386
"\"string\"}},\"required\":[\"traces\",\"project\"]}"},
387+
388+
{"cross_project_links", "Discover cross-project protocol communication links between indexed projects",
389+
"{\"type\":\"object\",\"properties\":{"
390+
"\"protocol\":{\"type\":\"string\",\"description\":\"Filter by protocol (graphql, grpc, kafka, etc.)\"},"
391+
"\"project\":{\"type\":\"string\",\"description\":\"Filter by project name (matches producer or consumer)\"},"
392+
"\"identifier\":{\"type\":\"string\",\"description\":\"Filter by identifier (topic name, operation, etc.)\"}"
393+
"}}"},
387394
};
388395

389396
static const int TOOL_COUNT = sizeof(TOOLS) / sizeof(TOOLS[0]);
@@ -3333,6 +3340,162 @@ static char *handle_ingest_traces(cbm_mcp_server_t *srv, const char *args) {
33333340
return result;
33343341
}
33353342

3343+
/* ── Cross-project links tool ────────────────────────────────── */
3344+
3345+
static char *handle_cross_project_links(cbm_mcp_server_t *srv, const char *args) {
3346+
(void)srv;
3347+
3348+
/* Parse optional filters */
3349+
char protocol[64] = {0};
3350+
char project[256] = {0};
3351+
char identifier[256] = {0};
3352+
3353+
if (args) {
3354+
yyjson_doc *doc = yyjson_read(args, strlen(args), 0);
3355+
if (doc) {
3356+
yyjson_val *root = yyjson_doc_get_root(doc);
3357+
yyjson_val *v;
3358+
v = yyjson_obj_get(root, "protocol");
3359+
if (v && yyjson_is_str(v))
3360+
snprintf(protocol, sizeof(protocol), "%s", yyjson_get_str(v));
3361+
v = yyjson_obj_get(root, "project");
3362+
if (v && yyjson_is_str(v))
3363+
snprintf(project, sizeof(project), "%s", yyjson_get_str(v));
3364+
v = yyjson_obj_get(root, "identifier");
3365+
if (v && yyjson_is_str(v))
3366+
snprintf(identifier, sizeof(identifier), "%s", yyjson_get_str(v));
3367+
yyjson_doc_free(doc);
3368+
}
3369+
}
3370+
3371+
/* Open _crosslinks.db */
3372+
const char *cache_dir = cbm_resolve_cache_dir();
3373+
if (!cache_dir) {
3374+
return cbm_mcp_text_result("Cache directory not found.", true);
3375+
}
3376+
3377+
char db_path[1024];
3378+
snprintf(db_path, sizeof(db_path), "%s/_crosslinks.db", cache_dir);
3379+
3380+
sqlite3 *db = NULL;
3381+
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
3382+
return cbm_mcp_text_result(
3383+
"No cross-project links found. Index at least 2 projects first.", false);
3384+
}
3385+
3386+
/* Build query with optional filters (using parameterized queries for safety) */
3387+
char sql[1024];
3388+
char where[512] = {0};
3389+
int wlen = 0;
3390+
3391+
if (protocol[0]) {
3392+
wlen += snprintf(where + wlen, sizeof(where) - (size_t)wlen,
3393+
"%sprotocol = ?", wlen ? " AND " : "");
3394+
}
3395+
if (project[0]) {
3396+
wlen += snprintf(where + wlen, sizeof(where) - (size_t)wlen,
3397+
"%s(producer_project = ? OR consumer_project = ?)",
3398+
wlen ? " AND " : "");
3399+
}
3400+
if (identifier[0]) {
3401+
wlen += snprintf(where + wlen, sizeof(where) - (size_t)wlen,
3402+
"%sidentifier = ?", wlen ? " AND " : "");
3403+
}
3404+
3405+
if (wlen > 0) {
3406+
snprintf(sql, sizeof(sql),
3407+
"SELECT protocol, identifier, producer_project, producer_qn, producer_file, "
3408+
"consumer_project, consumer_qn, consumer_file, confidence "
3409+
"FROM cross_links WHERE %s ORDER BY protocol, identifier, confidence DESC;", where);
3410+
} else {
3411+
snprintf(sql, sizeof(sql),
3412+
"SELECT protocol, identifier, producer_project, producer_qn, producer_file, "
3413+
"consumer_project, consumer_qn, consumer_file, confidence "
3414+
"FROM cross_links ORDER BY protocol, identifier, confidence DESC;");
3415+
}
3416+
3417+
sqlite3_stmt *stmt = NULL;
3418+
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
3419+
sqlite3_close(db);
3420+
return cbm_mcp_text_result("Failed to query cross-project links.", true);
3421+
}
3422+
3423+
/* Bind parameters */
3424+
int bind_idx = 1;
3425+
if (protocol[0]) {
3426+
sqlite3_bind_text(stmt, bind_idx++, protocol, -1, SQLITE_STATIC);
3427+
}
3428+
if (project[0]) {
3429+
sqlite3_bind_text(stmt, bind_idx++, project, -1, SQLITE_STATIC);
3430+
sqlite3_bind_text(stmt, bind_idx++, project, -1, SQLITE_STATIC);
3431+
}
3432+
if (identifier[0]) {
3433+
sqlite3_bind_text(stmt, bind_idx++, identifier, -1, SQLITE_STATIC);
3434+
}
3435+
3436+
/* Format output */
3437+
enum { XL_BUF_SIZE = 65536 };
3438+
char *buf = malloc(XL_BUF_SIZE);
3439+
if (!buf) { sqlite3_finalize(stmt); sqlite3_close(db); return NULL; }
3440+
int pos = 0;
3441+
int total = 0;
3442+
char cur_protocol[64] = {0};
3443+
int proto_count = 0;
3444+
3445+
while (sqlite3_step(stmt) == SQLITE_ROW) {
3446+
const char *proto = (const char *)sqlite3_column_text(stmt, 0);
3447+
const char *ident = (const char *)sqlite3_column_text(stmt, 1);
3448+
const char *pprod = (const char *)sqlite3_column_text(stmt, MCP_COL_2);
3449+
const char *qprod = (const char *)sqlite3_column_text(stmt, MCP_COL_3);
3450+
const char *fprod = (const char *)sqlite3_column_text(stmt, MCP_COL_4);
3451+
const char *pcons = (const char *)sqlite3_column_text(stmt, 5);
3452+
const char *qcons = (const char *)sqlite3_column_text(stmt, 6);
3453+
const char *fcons = (const char *)sqlite3_column_text(stmt, MCP_COL_7);
3454+
double conf = sqlite3_column_double(stmt, 8);
3455+
3456+
/* Protocol header */
3457+
if (strcmp(cur_protocol, proto ? proto : "") != 0) {
3458+
if (proto_count > 0) {
3459+
pos += snprintf(buf + pos, XL_BUF_SIZE - (size_t)pos, "\n");
3460+
}
3461+
snprintf(cur_protocol, sizeof(cur_protocol), "%s", proto ? proto : "");
3462+
pos += snprintf(buf + pos, XL_BUF_SIZE - (size_t)pos, "## %s\n\n", proto);
3463+
proto_count++;
3464+
}
3465+
3466+
pos += snprintf(buf + pos, XL_BUF_SIZE - (size_t)pos,
3467+
"%s (confidence: %.2f)\n"
3468+
" producer: %s :: %s (%s)\n"
3469+
" consumer: %s :: %s (%s)\n\n",
3470+
ident ? ident : "", conf,
3471+
pprod ? pprod : "", qprod ? qprod : "", fprod ? fprod : "",
3472+
pcons ? pcons : "", qcons ? qcons : "", fcons ? fcons : "");
3473+
total++;
3474+
3475+
if (pos > 60000) break; /* safety limit */
3476+
}
3477+
3478+
sqlite3_finalize(stmt);
3479+
sqlite3_close(db);
3480+
3481+
if (total == 0) {
3482+
free(buf);
3483+
return cbm_mcp_text_result(
3484+
"No cross-project links found. Index at least 2 projects first.", false);
3485+
}
3486+
3487+
/* Prepend summary */
3488+
char header[128];
3489+
snprintf(header, sizeof(header), "# Cross-Project Links (%d total)\n\n", total);
3490+
int hlen = (int)strlen(header);
3491+
memmove(buf + hlen, buf, (size_t)pos + 1);
3492+
memcpy(buf, header, (size_t)hlen);
3493+
3494+
char *result = cbm_mcp_text_result(buf, false);
3495+
free(buf);
3496+
return result;
3497+
}
3498+
33363499
/* ── Tool dispatch ────────────────────────────────────────────── */
33373500

33383501
char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const char *args_json) {
@@ -3384,6 +3547,9 @@ char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const ch
33843547
if (strcmp(tool_name, "ingest_traces") == 0) {
33853548
return handle_ingest_traces(srv, args_json);
33863549
}
3550+
if (strcmp(tool_name, "cross_project_links") == 0) {
3551+
return handle_cross_project_links(srv, args_json);
3552+
}
33873553
char msg[CBM_SZ_256];
33883554
snprintf(msg, sizeof(msg), "unknown tool: %s", tool_name);
33893555
return cbm_mcp_text_result(msg, true);

0 commit comments

Comments
 (0)