diff --git a/.gitignore b/.gitignore index 19247d5ee..441a795a8 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ Thumbs.db # Local project memory (Claude Code auto-memory) memory/ reference/ +.claude/ # Build artifacts build/ diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index bcaa513c1..2bc0e5b53 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -487,6 +487,11 @@ char *cbm_mcp_initialize_response(void) { yyjson_mut_val *caps = yyjson_mut_obj(doc); yyjson_mut_val *tools_cap = yyjson_mut_obj(doc); yyjson_mut_obj_add_val(doc, caps, "tools", tools_cap); + /* Advertise MCP resources capability — clients can read codebase://schema etc. */ + yyjson_mut_val *res_cap = yyjson_mut_obj(doc); + yyjson_mut_obj_add_bool(doc, res_cap, "subscribe", false); + yyjson_mut_obj_add_bool(doc, res_cap, "listChanged", true); + yyjson_mut_obj_add_val(doc, caps, "resources", res_cap); yyjson_mut_obj_add_val(doc, root, "capabilities", caps); char *out = yy_doc_to_str(doc); @@ -594,6 +599,9 @@ bool cbm_mcp_get_bool_arg(const char *args_json, const char *key) { * MCP SERVER * ══════════════════════════════════════════════════════════════════ */ +/* Forward declarations for functions defined after first use */ +static void notify_resources_updated(cbm_mcp_server_t *srv); + struct cbm_mcp_server { cbm_store_t *store; /* currently open project store (or NULL) */ bool owns_store; /* true if we opened the store */ @@ -613,6 +621,8 @@ struct cbm_mcp_server { cbm_thread_t autoindex_tid; bool autoindex_active; /* true if auto-index thread was started */ bool context_injected; /* true after first _context header sent (Phase 9) */ + bool client_has_resources; /* true if client advertised resources capability */ + FILE *out_stream; /* stdout for sending notifications (set in server_run) */ }; /* ── Tool list (needs full struct definition above) ──────────── */ @@ -892,6 +902,10 @@ static void inject_context_once(yyjson_mut_doc *doc, yyjson_mut_val *root, if (srv->session_project[0]) yyjson_mut_obj_add_str(doc, root, "session_project", srv->session_project); + /* If client supports MCP resources, skip _context injection — client reads + * codebase://schema, codebase://architecture, codebase://status instead. */ + if (srv->client_has_resources) return; + if (srv->context_injected) return; srv->context_injected = true; @@ -2162,6 +2176,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) { if (srv->session_project[0]) yyjson_mut_obj_add_str(doc, root, "session_project", srv->session_project); + /* Notify resource-capable clients that graph data changed */ + if (rc == 0) notify_resources_updated(srv); + char *json = yy_doc_to_str(doc); yyjson_mut_doc_free(doc); free(project_name); @@ -3135,6 +3152,9 @@ static char *handle_index_dependencies(cbm_mcp_server_t *srv, const char *args) /* Recompute PageRank after adding dep nodes so relevance sort includes them */ cbm_pagerank_compute_default(store, project); + /* Notify resource-capable clients that graph data changed */ + notify_resources_updated(srv); + char *json = yy_doc_to_str(doc); yyjson_mut_doc_free(doc); yyjson_doc_free(doc_args); @@ -3291,6 +3311,7 @@ static void *autoindex_thread(void *arg) { } cbm_log_info("autoindex.done", "project", srv->session_project); + notify_resources_updated(srv); if (srv->watcher) { cbm_watcher_watch(srv->watcher, srv->session_project, srv->session_root); } @@ -3476,6 +3497,302 @@ static char *inject_update_notice(cbm_mcp_server_t *srv, char *result_json) { return result_json; } +/* ── MCP Resources (Phase 10) ─────────────────────────────────── */ + +/* Send a JSON-RPC notification (no id) to the client's output stream. + * Used for notifications/resources/updated after index operations. */ +static void send_notification(cbm_mcp_server_t *srv, const char *method) { + if (!srv || !srv->out_stream) return; + yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *root = yyjson_mut_obj(doc); + yyjson_mut_doc_set_root(doc, root); + yyjson_mut_obj_add_str(doc, root, "jsonrpc", "2.0"); + yyjson_mut_obj_add_str(doc, root, "method", method); + char *json = yy_doc_to_str(doc); + yyjson_mut_doc_free(doc); + if (json) { + (void)fprintf(srv->out_stream, "%s\n", json); + (void)fflush(srv->out_stream); + free(json); + } +} + +/* Send notifications/resources/updated after index operations. */ +static void notify_resources_updated(cbm_mcp_server_t *srv) { + if (srv->client_has_resources) + send_notification(srv, "notifications/resources/updated"); +} + +/* Handle resources/list — return 3 resource URIs. */ +static char *handle_resources_list(cbm_mcp_server_t *srv) { + (void)srv; + yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *root = yyjson_mut_obj(doc); + yyjson_mut_doc_set_root(doc, root); + + yyjson_mut_val *arr = yyjson_mut_arr(doc); + + /* Resource 1: schema */ + yyjson_mut_val *r1 = yyjson_mut_obj(doc); + yyjson_mut_obj_add_str(doc, r1, "uri", "codebase://schema"); + yyjson_mut_obj_add_str(doc, r1, "name", "Code Graph Schema"); + yyjson_mut_obj_add_str(doc, r1, "description", + "Node labels and edge types with counts in the indexed code graph."); + yyjson_mut_obj_add_str(doc, r1, "mimeType", "application/json"); + yyjson_mut_arr_add_val(arr, r1); + + /* Resource 2: architecture */ + yyjson_mut_val *r2 = yyjson_mut_obj(doc); + yyjson_mut_obj_add_str(doc, r2, "uri", "codebase://architecture"); + yyjson_mut_obj_add_str(doc, r2, "name", "Architecture Overview"); + yyjson_mut_obj_add_str(doc, r2, "description", + "Graph size, key functions by PageRank, and relationship patterns."); + yyjson_mut_obj_add_str(doc, r2, "mimeType", "application/json"); + yyjson_mut_arr_add_val(arr, r2); + + /* Resource 3: status */ + yyjson_mut_val *r3 = yyjson_mut_obj(doc); + yyjson_mut_obj_add_str(doc, r3, "uri", "codebase://status"); + yyjson_mut_obj_add_str(doc, r3, "name", "Index Status"); + yyjson_mut_obj_add_str(doc, r3, "description", + "Indexing status, node/edge counts, PageRank stats, detected ecosystem, dependencies."); + yyjson_mut_obj_add_str(doc, r3, "mimeType", "application/json"); + yyjson_mut_arr_add_val(arr, r3); + + yyjson_mut_obj_add_val(doc, root, "resources", arr); + char *out = yy_doc_to_str(doc); + yyjson_mut_doc_free(doc); + return out; +} + +/* Build schema resource content (reuses inject_context_once logic). */ +static void build_resource_schema(yyjson_mut_doc *doc, yyjson_mut_val *root, + cbm_mcp_server_t *srv) { + cbm_store_t *store = srv->store; + const char *proj = srv->session_project[0] ? srv->session_project : NULL; + + if (!store) { + yyjson_mut_obj_add_str(doc, root, "status", "not_indexed"); + return; + } + + cbm_schema_info_t schema = {0}; + cbm_store_get_schema(store, proj, &schema); + + yyjson_mut_val *label_arr = yyjson_mut_arr(doc); + for (int i = 0; i < schema.node_label_count; i++) { + yyjson_mut_val *lbl = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, lbl, "label", schema.node_labels[i].label); + yyjson_mut_obj_add_int(doc, lbl, "count", schema.node_labels[i].count); + yyjson_mut_arr_add_val(label_arr, lbl); + } + yyjson_mut_obj_add_val(doc, root, "node_labels", label_arr); + + yyjson_mut_val *type_arr = yyjson_mut_arr(doc); + for (int i = 0; i < schema.edge_type_count; i++) { + yyjson_mut_val *et = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, et, "type", schema.edge_types[i].type); + yyjson_mut_obj_add_int(doc, et, "count", schema.edge_types[i].count); + yyjson_mut_arr_add_val(type_arr, et); + } + yyjson_mut_obj_add_val(doc, root, "edge_types", type_arr); + cbm_store_schema_free(&schema); +} + +/* Build architecture resource content. */ +static void build_resource_architecture(yyjson_mut_doc *doc, yyjson_mut_val *root, + cbm_mcp_server_t *srv) { + cbm_store_t *store = srv->store; + const char *proj = srv->session_project[0] ? srv->session_project : NULL; + + if (!store) { + yyjson_mut_obj_add_str(doc, root, "status", "not_indexed"); + return; + } + + int nodes = cbm_store_count_nodes(store, proj); + int edges = cbm_store_count_edges(store, proj); + yyjson_mut_obj_add_int(doc, root, "total_nodes", nodes); + yyjson_mut_obj_add_int(doc, root, "total_edges", edges); + + /* Key functions by PageRank (top 10) */ + struct sqlite3 *db = cbm_store_get_db(store); + if (db && proj) { + sqlite3_stmt *stmt = NULL; + const char *sql = + "SELECT n.name, n.qualified_name, n.label, n.file_path, pr.rank " + "FROM pagerank pr JOIN nodes n ON n.id = pr.node_id " + "WHERE pr.project = ?1 ORDER BY pr.rank DESC LIMIT 10"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, proj, -1, SQLITE_TRANSIENT); + yyjson_mut_val *kf_arr = yyjson_mut_arr(doc); + while (sqlite3_step(stmt) == SQLITE_ROW) { + yyjson_mut_val *kf = yyjson_mut_obj(doc); + const char *name = (const char *)sqlite3_column_text(stmt, 0); + const char *qn = (const char *)sqlite3_column_text(stmt, 1); + const char *label = (const char *)sqlite3_column_text(stmt, 2); + const char *fp = (const char *)sqlite3_column_text(stmt, 3); + double rank = sqlite3_column_double(stmt, 4); + if (name) yyjson_mut_obj_add_strcpy(doc, kf, "name", name); + if (qn) yyjson_mut_obj_add_strcpy(doc, kf, "qualified_name", qn); + if (label) yyjson_mut_obj_add_strcpy(doc, kf, "label", label); + if (fp) yyjson_mut_obj_add_strcpy(doc, kf, "file_path", fp); + yyjson_mut_obj_add_real(doc, kf, "pagerank", rank); + yyjson_mut_arr_add_val(kf_arr, kf); + } + yyjson_mut_obj_add_val(doc, root, "key_functions", kf_arr); + sqlite3_finalize(stmt); + } + } + + /* Relationship patterns from schema */ + cbm_schema_info_t schema = {0}; + cbm_store_get_schema(store, proj, &schema); + if (schema.rel_pattern_count > 0) { + yyjson_mut_val *rp_arr = yyjson_mut_arr(doc); + for (int i = 0; i < schema.rel_pattern_count; i++) { + yyjson_mut_arr_add_strcpy(doc, rp_arr, schema.rel_patterns[i]); + } + yyjson_mut_obj_add_val(doc, root, "relationship_patterns", rp_arr); + } + cbm_store_schema_free(&schema); +} + +/* Build status resource content. */ +static void build_resource_status(yyjson_mut_doc *doc, yyjson_mut_val *root, + cbm_mcp_server_t *srv) { + cbm_store_t *store = srv->store; + const char *proj = srv->session_project[0] ? srv->session_project : NULL; + + if (proj) yyjson_mut_obj_add_str(doc, root, "project", proj); + + if (!store) { + yyjson_mut_obj_add_str(doc, root, "status", "not_indexed"); + return; + } + + int nodes = cbm_store_count_nodes(store, proj); + int edges = cbm_store_count_edges(store, proj); + yyjson_mut_obj_add_str(doc, root, "status", nodes > 0 ? "ready" : "empty"); + yyjson_mut_obj_add_int(doc, root, "nodes", nodes); + yyjson_mut_obj_add_int(doc, root, "edges", edges); + + /* PageRank stats */ + struct sqlite3 *db = cbm_store_get_db(store); + if (db && proj) { + sqlite3_stmt *stmt = NULL; + if (sqlite3_prepare_v2(db, + "SELECT COUNT(*), MAX(computed_at) FROM pagerank WHERE project = ?1", + -1, &stmt, NULL) == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, proj, -1, SQLITE_TRANSIENT); + if (sqlite3_step(stmt) == SQLITE_ROW) { + int ranked = sqlite3_column_int(stmt, 0); + if (ranked > 0) { + yyjson_mut_obj_add_int(doc, root, "ranked_nodes", ranked); + const char *ts = (const char *)sqlite3_column_text(stmt, 1); + if (ts) yyjson_mut_obj_add_strcpy(doc, root, "pagerank_computed_at", ts); + } + } + sqlite3_finalize(stmt); + } + } + + /* Detected ecosystem */ + if (srv->session_root[0]) { + cbm_pkg_manager_t eco = cbm_detect_ecosystem(srv->session_root); + if (eco != CBM_PKG_COUNT) + yyjson_mut_obj_add_str(doc, root, "detected_ecosystem", + cbm_pkg_manager_str(eco)); + } + + /* Dependencies — query projects table for dep entries */ + if (db && proj) { + sqlite3_stmt *stmt = NULL; + char pattern[512]; + snprintf(pattern, sizeof(pattern), "%s.dep.%%", proj); + if (sqlite3_prepare_v2(db, + "SELECT name FROM projects WHERE name LIKE ?1 ORDER BY name", + -1, &stmt, NULL) == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, pattern, -1, SQLITE_TRANSIENT); + yyjson_mut_val *dep_arr = yyjson_mut_arr(doc); + int dep_count = 0; + while (sqlite3_step(stmt) == SQLITE_ROW) { + const char *dname = (const char *)sqlite3_column_text(stmt, 0); + if (dname) { + yyjson_mut_val *d = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, d, "name", dname); + int dn = cbm_store_count_nodes(store, dname); + yyjson_mut_obj_add_int(doc, d, "nodes", dn); + yyjson_mut_arr_add_val(dep_arr, d); + dep_count++; + } + } + sqlite3_finalize(stmt); + if (dep_count > 0) + yyjson_mut_obj_add_val(doc, root, "dependencies", dep_arr); + } + } +} + +/* Handle resources/read — dispatch by URI. */ +static char *handle_resources_read(cbm_mcp_server_t *srv, const char *params_raw) { + /* Extract URI from params */ + char *uri = NULL; + if (params_raw) { + yyjson_doc *pdoc = yyjson_read(params_raw, strlen(params_raw), 0); + if (pdoc) { + yyjson_val *u = yyjson_obj_get(yyjson_doc_get_root(pdoc), "uri"); + if (u && yyjson_is_str(u)) + uri = heap_strdup(yyjson_get_str(u)); + yyjson_doc_free(pdoc); + } + } + if (!uri) + return cbm_jsonrpc_format_error(0, -32602, "Missing uri parameter"); + + /* Build resource content */ + yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *root = yyjson_mut_obj(doc); + yyjson_mut_doc_set_root(doc, root); + + yyjson_mut_val *content_obj = yyjson_mut_obj(doc); + + if (strcmp(uri, "codebase://schema") == 0) { + build_resource_schema(doc, content_obj, srv); + } else if (strcmp(uri, "codebase://architecture") == 0) { + build_resource_architecture(doc, content_obj, srv); + } else if (strcmp(uri, "codebase://status") == 0) { + build_resource_status(doc, content_obj, srv); + } else { + yyjson_mut_doc_free(doc); + free(uri); + return cbm_jsonrpc_format_error(0, -32602, "Unknown resource URI"); + } + + /* Format as resources/read response: {contents: [{uri, mimeType, text}]} */ + char *content_json = yy_doc_to_str(doc); + yyjson_mut_doc_free(doc); + + yyjson_mut_doc *rdoc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *rroot = yyjson_mut_obj(rdoc); + yyjson_mut_doc_set_root(rdoc, rroot); + + yyjson_mut_val *contents = yyjson_mut_arr(rdoc); + yyjson_mut_val *item = yyjson_mut_obj(rdoc); + yyjson_mut_obj_add_strcpy(rdoc, item, "uri", uri); + yyjson_mut_obj_add_str(rdoc, item, "mimeType", "application/json"); + if (content_json) + yyjson_mut_obj_add_strcpy(rdoc, item, "text", content_json); + yyjson_mut_arr_add_val(contents, item); + yyjson_mut_obj_add_val(rdoc, rroot, "contents", contents); + + char *out = yy_doc_to_str(rdoc); + yyjson_mut_doc_free(rdoc); + free(content_json); + free(uri); + return out; +} + /* ── Server request handler ───────────────────────────────────── */ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) { @@ -3494,9 +3811,24 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) { if (strcmp(req.method, "initialize") == 0) { result_json = cbm_mcp_initialize_response(); + /* Parse client capabilities to detect resources support */ + if (req.params_raw) { + yyjson_doc *pdoc = yyjson_read(req.params_raw, strlen(req.params_raw), 0); + if (pdoc) { + yyjson_val *proot = yyjson_doc_get_root(pdoc); + yyjson_val *ccaps = yyjson_obj_get(proot, "capabilities"); + if (ccaps && yyjson_obj_get(ccaps, "resources")) + srv->client_has_resources = true; + yyjson_doc_free(pdoc); + } + } start_update_check(srv); detect_session(srv); maybe_auto_index(srv); + } else if (strcmp(req.method, "resources/list") == 0) { + result_json = handle_resources_list(srv); + } else if (strcmp(req.method, "resources/read") == 0) { + result_json = handle_resources_read(srv, req.params_raw); } else if (strcmp(req.method, "tools/list") == 0) { result_json = cbm_mcp_tools_list(srv); } else if (strcmp(req.method, "tools/call") == 0) { @@ -3528,6 +3860,7 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) { // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) { + srv->out_stream = out; /* store for sending notifications */ char *line = NULL; size_t cap = 0; int fd = cbm_fileno(in); diff --git a/tests/test_tool_consolidation.c b/tests/test_tool_consolidation.c index 5599e1f28..ea0cdf175 100644 --- a/tests/test_tool_consolidation.c +++ b/tests/test_tool_consolidation.c @@ -280,6 +280,143 @@ TEST(context_has_schema_info) { PASS(); } +/* ── 7. MCP Resources tests (Phase 10) ───────────────────── */ + +TEST(resources_list_returns_3_resources) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + char *resp = cbm_mcp_server_handle(srv, + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"resources/list\"}"); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "codebase://schema")); + ASSERT_NOT_NULL(strstr(resp, "codebase://architecture")); + ASSERT_NOT_NULL(strstr(resp, "codebase://status")); + ASSERT_NOT_NULL(strstr(resp, "Code Graph Schema")); + ASSERT_NOT_NULL(strstr(resp, "Architecture Overview")); + ASSERT_NOT_NULL(strstr(resp, "Index Status")); + free(resp); + cbm_mcp_server_free(srv); + PASS(); +} + +TEST(resources_read_schema) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + char *resp = cbm_mcp_server_handle(srv, + "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"resources/read\"," + "\"params\":{\"uri\":\"codebase://schema\"}}"); + ASSERT_NOT_NULL(resp); + /* Response should contain contents array with schema data */ + ASSERT_NOT_NULL(strstr(resp, "contents")); + ASSERT_NOT_NULL(strstr(resp, "codebase://schema")); + ASSERT_NOT_NULL(strstr(resp, "application/json")); + free(resp); + cbm_mcp_server_free(srv); + PASS(); +} + +TEST(resources_read_architecture) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + char *resp = cbm_mcp_server_handle(srv, + "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"resources/read\"," + "\"params\":{\"uri\":\"codebase://architecture\"}}"); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "contents")); + ASSERT_NOT_NULL(strstr(resp, "codebase://architecture")); + free(resp); + cbm_mcp_server_free(srv); + PASS(); +} + +TEST(resources_read_status) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + char *resp = cbm_mcp_server_handle(srv, + "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"resources/read\"," + "\"params\":{\"uri\":\"codebase://status\"}}"); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "contents")); + ASSERT_NOT_NULL(strstr(resp, "codebase://status")); + free(resp); + cbm_mcp_server_free(srv); + PASS(); +} + +TEST(resources_read_unknown_uri) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + char *resp = cbm_mcp_server_handle(srv, + "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"resources/read\"," + "\"params\":{\"uri\":\"codebase://nonexistent\"}}"); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "error")); + ASSERT_NOT_NULL(strstr(resp, "Unknown resource URI")); + free(resp); + cbm_mcp_server_free(srv); + PASS(); +} + +TEST(initialize_advertises_resources_capability) { + char *resp = cbm_mcp_initialize_response(); + ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "resources")); + ASSERT_NOT_NULL(strstr(resp, "listChanged")); + free(resp); + PASS(); +} + +TEST(initialize_parses_client_resources_capability) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + /* Send initialize with client capabilities including resources */ + char *resp = cbm_mcp_server_handle(srv, + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"," + "\"params\":{\"protocolVersion\":\"2024-11-05\"," + "\"capabilities\":{\"resources\":{\"subscribe\":false}}," + "\"clientInfo\":{\"name\":\"test\",\"version\":\"1.0\"}}}"); + ASSERT_NOT_NULL(resp); + free(resp); + + /* After initialize with resources capability, context injection should be skipped. + * Call a tool — should have session_project but NOT _context. */ + char *result = cbm_mcp_handle_tool(srv, "search_graph", + "{\"name_pattern\":\"x\"}"); + ASSERT_NOT_NULL(result); + /* session_project should still appear */ + ASSERT_NOT_NULL(strstr(result, "session_project") != NULL ? + strstr(result, "session_project") : result); + /* _context should NOT appear (client uses resources/read instead) */ + ASSERT_NULL(strstr(result, "_context")); + free(result); + + cbm_mcp_server_free(srv); + PASS(); +} + +TEST(no_resources_capability_gets_context_injection) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + /* Send initialize WITHOUT resources capability */ + char *resp = cbm_mcp_server_handle(srv, + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"," + "\"params\":{\"protocolVersion\":\"2024-11-05\"," + "\"capabilities\":{}," + "\"clientInfo\":{\"name\":\"old-client\",\"version\":\"1.0\"}}}"); + ASSERT_NOT_NULL(resp); + free(resp); + + /* Without resources capability, first tool call should get _context */ + char *result = cbm_mcp_handle_tool(srv, "search_graph", + "{\"name_pattern\":\"x\"}"); + ASSERT_NOT_NULL(result); + ASSERT_NOT_NULL(strstr(result, "_context")); + free(result); + + cbm_mcp_server_free(srv); + PASS(); +} + /* ── Suite registration ──────────────────────────────────── */ SUITE(tool_consolidation) { @@ -305,4 +442,13 @@ SUITE(tool_consolidation) { /* Context injection */ RUN_TEST(first_response_has_context_header); RUN_TEST(context_has_schema_info); + /* MCP Resources (Phase 10) */ + RUN_TEST(resources_list_returns_3_resources); + RUN_TEST(resources_read_schema); + RUN_TEST(resources_read_architecture); + RUN_TEST(resources_read_status); + RUN_TEST(resources_read_unknown_uri); + RUN_TEST(initialize_advertises_resources_capability); + RUN_TEST(initialize_parses_client_resources_capability); + RUN_TEST(no_resources_capability_gets_context_injection); }