@@ -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
389396static 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
33383501char * 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