mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
feat(mcp): warn on stale cypher surfaces
Add query_graph response warnings when Cypher queries reference route-derived or semantic-derived graph constructs whose derived view state is explicitly stale. The detector uses existing portable string matching and named derived-view constants, so legacy databases with missing freshness rows remain unchanged. Validated with: git diff --check; bash scripts/check-source-safety.sh; make -j8 -f Makefile.cbm build/c/test-runner; CBM_ONLY_SUITE=mcp ./build/c/test-runner. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
@@ -122,6 +122,51 @@ static void add_derived_freshness_warnings(yyjson_mut_doc *doc, yyjson_mut_val *
|
||||
}
|
||||
}
|
||||
|
||||
static bool query_mentions_any(const char *query, const char *const *terms, int term_count) {
|
||||
if (!query || !terms || term_count <= 0) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < term_count; i++) {
|
||||
if (terms[i] && cbm_strcasestr(query, terms[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool query_mentions_route_derived_graph(const char *query) {
|
||||
static const char *const terms[] = {"Route", "HANDLES", "HTTP_CALLS",
|
||||
"ASYNC_CALLS", "GRPC_CALLS", "GRAPHQL_CALLS",
|
||||
"TRPC_CALLS", "CROSS_HTTP_CALLS", "CROSS_ASYNC_CALLS",
|
||||
"CROSS_CHANNEL", "CROSS_GRPC_CALLS", "CROSS_GRAPHQL_CALLS",
|
||||
"CROSS_TRPC_CALLS"};
|
||||
return query_mentions_any(query, terms, (int)(sizeof(terms) / sizeof(terms[0])));
|
||||
}
|
||||
|
||||
static bool query_mentions_semantic_derived_graph(const char *query) {
|
||||
static const char *const terms[] = {"SEMANTICALLY_RELATED", "SIMILAR_TO"};
|
||||
return query_mentions_any(query, terms, (int)(sizeof(terms) / sizeof(terms[0])));
|
||||
}
|
||||
|
||||
static void add_query_graph_derived_warnings(yyjson_mut_doc *doc, yyjson_mut_val *root,
|
||||
cbm_store_t *store, const char *project,
|
||||
const char *query) {
|
||||
if (!doc || !root || !store || !project || !query) {
|
||||
return;
|
||||
}
|
||||
if (query_mentions_route_derived_graph(query) &&
|
||||
cbm_store_derived_view_is_stale(store, project, CBM_STORE_DERIVED_VIEW_ROUTES)) {
|
||||
add_response_warning(doc, root,
|
||||
"routes derived view is stale; query_graph route results may be stale.");
|
||||
}
|
||||
if (query_mentions_semantic_derived_graph(query) &&
|
||||
cbm_store_derived_view_is_stale(store, project, CBM_STORE_DERIVED_VIEW_SEMANTIC_EDGES)) {
|
||||
add_response_warning(
|
||||
doc, root,
|
||||
"semantic_edges derived view is stale; query_graph semantic edges may be stale.");
|
||||
}
|
||||
}
|
||||
|
||||
/* Default snippet fallback line count (when end_line unknown) */
|
||||
#define SNIPPET_DEFAULT_LINES 50
|
||||
|
||||
@@ -3461,6 +3506,7 @@ static char *handle_query_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *root = yyjson_mut_obj(doc);
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
add_query_graph_derived_warnings(doc, root, store, project, query);
|
||||
|
||||
/* columns */
|
||||
yyjson_mut_val *cols = yyjson_mut_arr(doc);
|
||||
|
||||
@@ -929,6 +929,69 @@ TEST(tool_query_graph_basic) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_query_graph_warns_on_stale_route_view) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
ASSERT_NOT_NULL(st);
|
||||
|
||||
const char *proj = "query-route-stale";
|
||||
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/query-route-stale"), CBM_STORE_OK);
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
ASSERT_EQ(cbm_store_set_derived_view_state(st, proj, CBM_STORE_DERIVED_VIEW_ROUTES,
|
||||
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
|
||||
CBM_STORE_DERIVED_STATUS_STALE),
|
||||
CBM_STORE_OK);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":114,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"query_graph\","
|
||||
"\"arguments\":{\"project\":\"query-route-stale\","
|
||||
"\"query\":\"MATCH (r:Route) RETURN r.name LIMIT 5\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"warnings\""));
|
||||
ASSERT_NOT_NULL(strstr(inner, "routes derived view is stale"));
|
||||
|
||||
free(inner);
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_query_graph_warns_on_stale_semantic_edges) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
cbm_store_t *st = cbm_mcp_server_store(srv);
|
||||
ASSERT_NOT_NULL(st);
|
||||
|
||||
const char *proj = "query-semantic-stale";
|
||||
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/query-semantic-stale"), CBM_STORE_OK);
|
||||
cbm_mcp_server_set_project(srv, proj);
|
||||
ASSERT_EQ(cbm_store_set_derived_view_state(st, proj, CBM_STORE_DERIVED_VIEW_SEMANTIC_EDGES,
|
||||
CBM_STORE_DERIVED_GENERATION_UNKNOWN,
|
||||
CBM_STORE_DERIVED_STATUS_STALE),
|
||||
CBM_STORE_OK);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":115,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"query_graph\","
|
||||
"\"arguments\":{\"project\":\"query-semantic-stale\","
|
||||
"\"query\":\"MATCH (a)-[:SEMANTICALLY_RELATED]->(b) "
|
||||
"RETURN a.name, b.name LIMIT 5\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"warnings\""));
|
||||
ASSERT_NOT_NULL(strstr(inner, "semantic_edges derived view is stale"));
|
||||
|
||||
free(inner);
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tool_index_status_no_project) {
|
||||
cbm_mcp_server_t *srv = setup_mcp_with_data();
|
||||
|
||||
@@ -3166,6 +3229,8 @@ SUITE(mcp) {
|
||||
RUN_TEST(tool_search_graph_query_rejects_bad_semantic_query);
|
||||
RUN_TEST(tool_search_graph_semantic_query_warns_on_stale_semantic_view);
|
||||
RUN_TEST(tool_query_graph_basic);
|
||||
RUN_TEST(tool_query_graph_warns_on_stale_route_view);
|
||||
RUN_TEST(tool_query_graph_warns_on_stale_semantic_edges);
|
||||
RUN_TEST(tool_index_status_no_project);
|
||||
RUN_TEST(tool_index_status_includes_git_metadata);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user