Skip to content

Commit c00723e

Browse files
committed
Require project param on all MCP tool calls
Make project a required parameter for all query tools (search_graph, query_graph, trace_call_path, get_code_snippet, get_graph_schema, get_architecture, search_code, index_status, detect_changes, manage_adr, ingest_traces). Removes implicit fallback to session project or last-opened store. When project is missing or not found, return error with list of available indexed projects so agents can self-correct. Rename delete_project param from project_name to project for consistency. Fix smoke test trace_call_path depth param name (max_depth -> depth).
1 parent 93cd5cf commit c00723e

4 files changed

Lines changed: 128 additions & 55 deletions

File tree

scripts/smoke-test.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ fi
138138
echo "OK: search_graph found $TOTAL result(s) for 'compute'"
139139

140140
# 3b: trace_call_path — verify compute has callers
141-
TRACE=$(cli trace_call_path "{\"project\":\"$PROJECT\",\"function_name\":\"compute\",\"direction\":\"inbound\",\"max_depth\":1}")
141+
TRACE=$(cli trace_call_path "{\"project\":\"$PROJECT\",\"function_name\":\"compute\",\"direction\":\"inbound\",\"depth\":1}")
142142
CALLERS=$(echo "$TRACE" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(len(d.get('callers',[])))" 2>/dev/null || echo "0")
143143
if [ "$CALLERS" -lt 1 ]; then
144144
echo "FAIL: trace_call_path found 0 callers for 'compute'"
@@ -165,7 +165,7 @@ fi
165165
echo "OK: $FOLDER_COUNT Folder nodes (init.py didn't clobber them)"
166166

167167
# 3e: delete_project cleanup
168-
cli delete_project "{\"project_name\":\"$PROJECT\"}" > /dev/null
168+
cli delete_project "{\"project\":\"$PROJECT\"}" > /dev/null
169169

170170
echo ""
171171
echo "=== Phase 4: security checks ==="

src/mcp/mcp.c

Lines changed: 108 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -241,15 +241,16 @@ static const tool_def_t TOOLS[] = {
241241
"{\"type\":\"integer\"},\"max_degree\":{\"type\":\"integer\"},\"exclude_entry_points\":{"
242242
"\"type\":\"boolean\"},\"include_connected\":{\"type\":\"boolean\"},\"limit\":{\"type\":"
243243
"\"integer\",\"description\":\"Max results. Default: "
244-
"unlimited\"},\"offset\":{\"type\":\"integer\",\"default\":0}}}"},
244+
"unlimited\"},\"offset\":{\"type\":\"integer\",\"default\":0}},\"required\":[\"project\"]}"},
245245

246246
{"query_graph",
247247
"Execute a Cypher query against the knowledge graph for complex multi-hop patterns, "
248248
"aggregations, and cross-service analysis.",
249249
"{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Cypher "
250250
"query\"},\"project\":{\"type\":\"string\"},\"max_rows\":{\"type\":\"integer\","
251251
"\"description\":"
252-
"\"Optional row limit. Default: unlimited (100k ceiling)\"}},\"required\":[\"query\"]}"},
252+
"\"Optional row limit. Default: unlimited (100k "
253+
"ceiling)\"}},\"required\":[\"query\",\"project\"]}"},
253254

254255
{"trace_call_path",
255256
"Trace function call paths — who calls a function and what it calls. Use INSTEAD OF grep when "
@@ -258,7 +259,7 @@ static const tool_def_t TOOLS[] = {
258259
"\"type\":\"string\"},\"direction\":{\"type\":\"string\",\"enum\":[\"inbound\",\"outbound\","
259260
"\"both\"],\"default\":\"both\"},\"depth\":{\"type\":\"integer\",\"default\":3},\"edge_"
260261
"types\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"function_"
261-
"name\"]}"},
262+
"name\",\"project\"]}"},
262263

263264
{"get_code_snippet",
264265
"Read source code for a function/class/symbol. IMPORTANT: First call search_graph to find the "
@@ -267,16 +268,17 @@ static const tool_def_t TOOLS[] = {
267268
"{\"type\":\"object\",\"properties\":{\"qualified_name\":{\"type\":\"string\",\"description\":"
268269
"\"Full qualified_name from search_graph, or short function name\"},\"project\":{"
269270
"\"type\":\"string\"},\"include_neighbors\":{"
270-
"\"type\":\"boolean\",\"default\":false}},\"required\":[\"qualified_name\"]}"},
271+
"\"type\":\"boolean\",\"default\":false}},\"required\":[\"qualified_name\",\"project\"]}"},
271272

272273
{"get_graph_schema", "Get the schema of the knowledge graph (node labels, edge types)",
273-
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"}}}"},
274+
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"}},\"required\":["
275+
"\"project\"]}"},
274276

275277
{"get_architecture",
276278
"Get high-level architecture overview — packages, services, dependencies, and project "
277279
"structure at a glance.",
278280
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"},\"aspects\":{\"type\":"
279-
"\"array\",\"items\":{\"type\":\"string\"}}}}"},
281+
"\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"project\"]}"},
280282

281283
{"search_code",
282284
"Graph-augmented code search. Finds text patterns via grep, then enriches results with "
@@ -294,31 +296,33 @@ static const tool_def_t TOOLS[] = {
294296
"(like grep -C). Only used in compact mode.\"},"
295297
"\"regex\":{\"type\":\"boolean\",\"default\":false},\"limit\":{\"type\":\"integer\","
296298
"\"description\":\"Max results (default 10)\",\"default\":10}},\"required\":["
297-
"\"pattern\"]}"},
299+
"\"pattern\",\"project\"]}"},
298300

299301
{"list_projects", "List all indexed projects", "{\"type\":\"object\",\"properties\":{}}"},
300302

301303
{"delete_project", "Delete a project from the index",
302-
"{\"type\":\"object\",\"properties\":{\"project_name\":{\"type\":\"string\"}},\"required\":["
303-
"\"project_name\"]}"},
304+
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"}},\"required\":["
305+
"\"project\"]}"},
304306

305307
{"index_status", "Get the indexing status of a project",
306-
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"}}}"},
308+
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"}},\"required\":["
309+
"\"project\"]}"},
307310

308311
{"detect_changes", "Detect code changes and their impact",
309312
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"},\"scope\":{\"type\":"
310313
"\"string\"},\"depth\":{\"type\":\"integer\",\"default\":2},\"base_branch\":{\"type\":"
311-
"\"string\",\"default\":\"main\"}}}"},
314+
"\"string\",\"default\":\"main\"}},\"required\":[\"project\"]}"},
312315

313316
{"manage_adr", "Create or update Architecture Decision Records",
314317
"{\"type\":\"object\",\"properties\":{\"project\":{\"type\":\"string\"},\"mode\":{\"type\":"
315318
"\"string\",\"enum\":[\"get\",\"update\",\"sections\"]},\"content\":{\"type\":\"string\"},"
316-
"\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}}}"},
319+
"\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"project\"]"
320+
"}"},
317321

318322
{"ingest_traces", "Ingest runtime traces to enhance the knowledge graph",
319323
"{\"type\":\"object\",\"properties\":{\"traces\":{\"type\":\"array\",\"items\":{\"type\":"
320324
"\"object\"}},\"project\":{\"type\":"
321-
"\"string\"}},\"required\":[\"traces\"]}"},
325+
"\"string\"}},\"required\":[\"traces\",\"project\"]}"},
322326
};
323327

324328
static const int TOOL_COUNT = sizeof(TOOLS) / sizeof(TOOLS[0]);
@@ -631,7 +635,7 @@ static const char *project_db_path(const char *project, char *buf, size_t bufsz)
631635
* Tracks last-access time so the event loop can evict idle stores. */
632636
static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
633637
if (!project) {
634-
return srv->store; /* no project specified → use whatever's open */
638+
return NULL; /* project is required — no implicit fallback */
635639
}
636640

637641
srv->store_last_used = time(NULL);
@@ -672,13 +676,66 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
672676
return srv->store;
673677
}
674678

675-
/* Bail with empty JSON result when no store is available. */
676-
#define REQUIRE_STORE(store, project) \
677-
do { \
678-
if (!(store)) { \
679-
free(project); \
680-
return cbm_mcp_text_result("{\"error\":\"no project loaded\"}", true); \
681-
} \
679+
/* Build a helpful error listing available projects. Caller must free() result. */
680+
static char *build_project_list_error(const char *reason) {
681+
char dir_path[1024];
682+
cache_dir(dir_path, sizeof(dir_path));
683+
684+
/* Collect project names from .db files */
685+
char projects[4096] = "";
686+
int count = 0;
687+
cbm_dir_t *d = cbm_opendir(dir_path);
688+
if (d) {
689+
int offset = 0;
690+
cbm_dirent_t *entry;
691+
while ((entry = cbm_readdir(d)) != NULL) {
692+
const char *n = entry->name;
693+
size_t len = strlen(n);
694+
if (len < 4 || strcmp(n + len - 3, ".db") != 0) {
695+
continue;
696+
}
697+
if (strncmp(n, "tmp-", 4) == 0 || strncmp(n, "_", 1) == 0) {
698+
continue;
699+
}
700+
if (count > 0 && offset < (int)sizeof(projects) - 2) {
701+
projects[offset++] = ',';
702+
}
703+
int wrote = snprintf(projects + offset, sizeof(projects) - (size_t)offset, "\"%.*s\"",
704+
(int)(len - 3), n);
705+
if (wrote > 0) {
706+
offset += wrote;
707+
}
708+
count++;
709+
}
710+
cbm_closedir(d);
711+
}
712+
713+
enum { ERR_BUF_SZ = 5120 };
714+
char buf[ERR_BUF_SZ];
715+
if (count > 0) {
716+
snprintf(buf, sizeof(buf),
717+
"{\"error\":\"%s\",\"hint\":\"Use list_projects to see all indexed projects, "
718+
"then pass the project name.\",\"available_projects\":[%s],\"count\":%d}",
719+
reason, projects, count);
720+
} else {
721+
snprintf(buf, sizeof(buf),
722+
"{\"error\":\"%s\",\"hint\":\"No projects indexed yet. "
723+
"Call index_repository first.\"}",
724+
reason);
725+
}
726+
return heap_strdup(buf);
727+
}
728+
729+
/* Bail with project list when no store is available. */
730+
#define REQUIRE_STORE(store, project) \
731+
do { \
732+
if (!(store)) { \
733+
char *_err = build_project_list_error("project not found or not indexed"); \
734+
char *_res = cbm_mcp_text_result(_err, true); \
735+
free(_err); \
736+
free(project); \
737+
return _res; \
738+
} \
682739
} while (0)
683740

684741
/* ── Tool handler implementations ─────────────────────────────── */
@@ -778,9 +835,6 @@ static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) {
778835
* Callers that receive a non-NULL return value must free(project) themselves
779836
* before returning the error string. */
780837
static char *verify_project_indexed(cbm_store_t *store, const char *project) {
781-
if (!project) {
782-
return NULL; /* default project — always exists */
783-
}
784838
cbm_project_t proj_check = {0};
785839
if (cbm_store_get_project(store, project, &proj_check) != CBM_STORE_OK) {
786840
return cbm_mcp_text_result(
@@ -935,9 +989,12 @@ static char *handle_query_graph(cbm_mcp_server_t *srv, const char *args) {
935989
return cbm_mcp_text_result("query is required", true);
936990
}
937991
if (!store) {
992+
char *_err = build_project_list_error("project not found or not indexed");
993+
char *_res = cbm_mcp_text_result(_err, true);
994+
free(_err);
938995
free(project);
939996
free(query);
940-
return cbm_mcp_text_result("{\"error\":\"no project loaded\"}", true);
997+
return _res;
941998
}
942999

9431000
char *not_indexed = verify_project_indexed(store, project);
@@ -1024,9 +1081,9 @@ static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) {
10241081

10251082
/* delete_project: just erase the .db file (and WAL/SHM). */
10261083
static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
1027-
char *name = cbm_mcp_get_string_arg(args, "project_name");
1084+
char *name = cbm_mcp_get_string_arg(args, "project");
10281085
if (!name) {
1029-
return cbm_mcp_text_result("project_name is required", true);
1086+
return cbm_mcp_text_result("project is required", true);
10301087
}
10311088

10321089
/* Close store if it's the project being deleted */
@@ -1151,10 +1208,13 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
11511208
return cbm_mcp_text_result("function_name is required", true);
11521209
}
11531210
if (!store) {
1211+
char *_err = build_project_list_error("project not found or not indexed");
1212+
char *_res = cbm_mcp_text_result(_err, true);
1213+
free(_err);
11541214
free(func_name);
11551215
free(project);
11561216
free(direction);
1157-
return cbm_mcp_text_result("{\"error\":\"no project loaded\"}", true);
1217+
return _res;
11581218
}
11591219

11601220
char *not_indexed = verify_project_indexed(store, project);
@@ -1650,9 +1710,12 @@ static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
16501710

16511711
cbm_store_t *store = resolve_store(srv, project);
16521712
if (!store) {
1713+
char *_err = build_project_list_error("project not found or not indexed");
1714+
char *_res = cbm_mcp_text_result(_err, true);
1715+
free(_err);
16531716
free(qn);
16541717
free(project);
1655-
return cbm_mcp_text_result("{\"error\":\"no project loaded\"}", true);
1718+
return _res;
16561719
}
16571720

16581721
char *not_indexed = verify_project_indexed(store, project);
@@ -2014,17 +2077,25 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
20142077
return cbm_mcp_text_result("pattern is required", true);
20152078
}
20162079

2017-
/* Resolve project */
2018-
if (!project && srv->session_project[0]) {
2019-
project = heap_strdup(srv->session_project);
2080+
/* Project is required */
2081+
if (!project) {
2082+
free(pattern);
2083+
free(file_pattern);
2084+
char *_err = build_project_list_error("project is required");
2085+
char *_res = cbm_mcp_text_result(_err, true);
2086+
free(_err);
2087+
return _res;
20202088
}
20212089

20222090
char *root_path = get_project_root(srv, project);
20232091
if (!root_path) {
20242092
free(pattern);
20252093
free(project);
20262094
free(file_pattern);
2027-
return cbm_mcp_text_result("project not found or not indexed", true);
2095+
char *_err = build_project_list_error("project not found or not indexed");
2096+
char *_res = cbm_mcp_text_result(_err, true);
2097+
free(_err);
2098+
return _res;
20282099
}
20292100

20302101
/* Reject shell metacharacters in user-supplied arguments */
@@ -2073,11 +2144,10 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
20732144

20742145
cbm_store_t *pre_store = resolve_store(srv, project);
20752146
if (pre_store) {
2076-
const char *pn = project ? project : srv->session_project;
20772147
char **indexed_files = NULL;
20782148
int indexed_count = 0;
2079-
if (pn &&
2080-
cbm_store_list_files(pre_store, pn, &indexed_files, &indexed_count) == CBM_STORE_OK &&
2149+
if (cbm_store_list_files(pre_store, project, &indexed_files, &indexed_count) ==
2150+
CBM_STORE_OK &&
20812151
indexed_count > 0) {
20822152
FILE *fl = fopen(filelist, "w");
20832153
if (fl) {
@@ -2173,7 +2243,6 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
21732243
* Then: one SQL query per unique file for nodes, one batch query for all degrees. */
21742244

21752245
cbm_store_t *store = resolve_store(srv, project);
2176-
const char *proj_name = project ? project : srv->session_project;
21772246

21782247
int sr_cap = 32;
21792248
int sr_count = 0;
@@ -2201,8 +2270,8 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
22012270
/* One SQL query: load all nodes in this file */
22022271
cbm_node_t *file_nodes = NULL;
22032272
int file_node_count = 0;
2204-
if (store && proj_name) {
2205-
cbm_store_find_nodes_by_file(store, proj_name, cur_file, &file_nodes, &file_node_count);
2273+
if (store) {
2274+
cbm_store_find_nodes_by_file(store, project, cur_file, &file_nodes, &file_node_count);
22062275
}
22072276

22082277
/* Match each grep hit to tightest containing node (in-memory) */

tests/test_integration.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,9 @@ TEST(integ_mcp_search_graph_by_name) {
314314
TEST(integ_mcp_query_graph_functions) {
315315
char args[512];
316316
snprintf(args, sizeof(args),
317-
"{\"query\":\"MATCH (f:Function) WHERE f.project = '%s' "
317+
"{\"project\":\"%s\",\"query\":\"MATCH (f:Function) WHERE f.project = '%s' "
318318
"RETURN f.name LIMIT 20\"}",
319-
g_project);
319+
g_project, g_project);
320320

321321
char *resp = call_tool("query_graph", args);
322322
ASSERT_NOT_NULL(resp);
@@ -331,9 +331,9 @@ TEST(integ_mcp_query_graph_functions) {
331331
TEST(integ_mcp_query_graph_calls) {
332332
char args[512];
333333
snprintf(args, sizeof(args),
334-
"{\"query\":\"MATCH (a)-[r:CALLS]->(b) WHERE a.project = '%s' "
334+
"{\"project\":\"%s\",\"query\":\"MATCH (a)-[r:CALLS]->(b) WHERE a.project = '%s' "
335335
"RETURN a.name, b.name LIMIT 20\"}",
336-
g_project);
336+
g_project, g_project);
337337

338338
char *resp = call_tool("query_graph", args);
339339
ASSERT_NOT_NULL(resp);
@@ -399,7 +399,7 @@ TEST(integ_mcp_index_status) {
399399
TEST(integ_mcp_delete_project) {
400400
/* Delete the project and verify it's gone */
401401
char args[256];
402-
snprintf(args, sizeof(args), "{\"project_name\":\"%s\"}", g_project);
402+
snprintf(args, sizeof(args), "{\"project\":\"%s\"}", g_project);
403403

404404
char *resp = call_tool("delete_project", args);
405405
ASSERT_NOT_NULL(resp);

0 commit comments

Comments
 (0)