mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-02 07:53:41 +08:00
mcp: fix 18 vague error messages + add 16 behavioral/spec compliance tests
Error messages: Every error now includes:
- WHAT failed (the specific input that caused the error)
- HOW to fix it (actionable "hint" field with next step)
- WHERE to look (tool names, param examples, valid options)
Fixed errors (18):
- "no project loaded" (3x) → + hint:"Run index_repository..."
- "function not found" → includes searched function name + hint
- "symbol not found" → includes searched qualified_name + hint
- "query is required" → + hint with Cypher syntax example
- "function_name is required" → + hint with param example
- "qualified_name is required" → + hint with format + "Use search_code_graph"
- "pattern is required" → + hint about regex vs literal
- "repo_path is required" → + hint about absolute path
- "project_name is required" → + hint:"Use list_projects"
- "project not found" (2x) → + hint:"Run index_repository or list_projects"
- "project not found or not indexed" → + hint with both options
- "failed to create pipeline" → + hint about path/permissions
- "search failed: temp file" → + hint about /tmp disk space
- "search failed" → + hint about grep installation
- "git diff failed" → + hint about git installation
- "missing tool name" → + lists available tools + "Use tools/list"
- "unknown tool: X" → + lists available tools + "Use tools/list"
New tests (16):
- MCP spec compliance: protocol version, subscribe:false, listChanged:true,
resources/list fields, resources/read contents array, missing uri, no params
- Client behavioral differences: resource client never gets _context (3 calls),
legacy client gets _context only first call, empty resources:{} counts as
support, no-initialize defaults to legacy
- Error message quality: hint field present on no-project, function-not-found
includes name, symbol-not-found includes qn, all required-param errors have
hints, unknown-tool lists valid options, resource -32002 is actionable
Total: 2165 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
+76
-24
@@ -1513,12 +1513,16 @@ static char *handle_query_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
|
||||
if (!query) {
|
||||
free(project);
|
||||
return cbm_mcp_text_result("query is required", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"query is required\","
|
||||
"\"hint\":\"Pass a Cypher query string, e.g. MATCH (n:Function) RETURN n.name LIMIT 10\"}", true);
|
||||
}
|
||||
if (!store) {
|
||||
free(project);
|
||||
free(query);
|
||||
return cbm_mcp_text_result("{\"error\":\"no project loaded\"}", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"no project loaded\","
|
||||
"\"hint\":\"Run index_repository with repo_path to index the project first.\"}", true);
|
||||
}
|
||||
|
||||
cbm_cypher_result_t result = {0};
|
||||
@@ -1689,7 +1693,9 @@ static char *handle_index_status(cbm_mcp_server_t *srv, const char *args) {
|
||||
static char *handle_delete_project(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *name = cbm_mcp_get_string_arg(args, "project_name");
|
||||
if (!name) {
|
||||
return cbm_mcp_text_result("project_name is required", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"project_name is required\","
|
||||
"\"hint\":\"Pass the project name to delete. Use list_projects to see available projects.\"}", true);
|
||||
}
|
||||
|
||||
/* Close store if it's the project being deleted */
|
||||
@@ -1847,13 +1853,17 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
if (!func_name) {
|
||||
free(project);
|
||||
free(direction);
|
||||
return cbm_mcp_text_result("function_name is required", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"function_name is required\","
|
||||
"\"hint\":\"Pass the name of a function to trace, e.g. {\\\"function_name\\\":\\\"main\\\"}\"}", true);
|
||||
}
|
||||
if (!store) {
|
||||
free(func_name);
|
||||
free(project);
|
||||
free(direction);
|
||||
return cbm_mcp_text_result("{\"error\":\"no project loaded\"}", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"no project loaded\","
|
||||
"\"hint\":\"Run index_repository with repo_path to index the project first.\"}", true);
|
||||
}
|
||||
if (!direction) {
|
||||
direction = heap_strdup("both");
|
||||
@@ -1865,11 +1875,15 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
|
||||
cbm_store_find_nodes_by_name(store, project, func_name, &nodes, &node_count);
|
||||
|
||||
if (node_count == 0) {
|
||||
char errbuf[512];
|
||||
snprintf(errbuf, sizeof(errbuf),
|
||||
"{\"error\":\"function not found: '%s'\","
|
||||
"\"hint\":\"Use search_code_graph with name_pattern to find similar symbols.\"}", func_name);
|
||||
free(func_name);
|
||||
free(project);
|
||||
free(direction);
|
||||
cbm_store_free_nodes(nodes, 0);
|
||||
return cbm_mcp_text_result("{\"error\":\"function not found\"}", true);
|
||||
return cbm_mcp_text_result(errbuf, true);
|
||||
}
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
@@ -2109,7 +2123,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
|
||||
if (!repo_path) {
|
||||
free(mode_str);
|
||||
return cbm_mcp_text_result("repo_path is required", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"repo_path is required\","
|
||||
"\"hint\":\"Pass the absolute path to the project root directory.\"}", true);
|
||||
}
|
||||
|
||||
cbm_index_mode_t mode = CBM_MODE_FULL;
|
||||
@@ -2121,7 +2137,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(repo_path, NULL, mode);
|
||||
if (!p) {
|
||||
free(repo_path);
|
||||
return cbm_mcp_text_result("failed to create pipeline", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"failed to create indexing pipeline\","
|
||||
"\"hint\":\"Check that repo_path exists and is readable. The directory may be empty or inaccessible.\"}", true);
|
||||
}
|
||||
|
||||
char *project_name = heap_strdup(cbm_pipeline_project_name(p));
|
||||
@@ -2463,13 +2481,18 @@ static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
|
||||
if (!qn) {
|
||||
free(project);
|
||||
free(snippet_mode);
|
||||
return cbm_mcp_text_result("qualified_name is required", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"qualified_name is required\","
|
||||
"\"hint\":\"Pass a symbol qualified name, e.g. {\\\"qualified_name\\\":\\\"myapp.src.main.handle_request\\\"}. "
|
||||
"Use search_code_graph to find qualified names.\"}", true);
|
||||
}
|
||||
if (!store) {
|
||||
free(qn);
|
||||
free(project);
|
||||
free(snippet_mode);
|
||||
return cbm_mcp_text_result("{\"error\":\"no project loaded\"}", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"no project loaded\","
|
||||
"\"hint\":\"Run index_repository with repo_path to index the project first.\"}", true);
|
||||
}
|
||||
|
||||
/* Tier 1: Exact QN match */
|
||||
@@ -2653,10 +2676,16 @@ static char *handle_get_code_snippet(cbm_mcp_server_t *srv, const char *args) {
|
||||
cbm_store_search_free(&search_out);
|
||||
|
||||
/* Nothing found */
|
||||
free(qn);
|
||||
free(project);
|
||||
free(snippet_mode);
|
||||
return cbm_mcp_text_result("symbol not found", true);
|
||||
{
|
||||
char errbuf[512];
|
||||
snprintf(errbuf, sizeof(errbuf),
|
||||
"{\"error\":\"symbol not found: '%s'\","
|
||||
"\"hint\":\"Use search_code_graph with name_pattern to find the correct qualified_name.\"}", qn);
|
||||
free(qn);
|
||||
free(project);
|
||||
free(snippet_mode);
|
||||
return cbm_mcp_text_result(errbuf, true);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── search_code ──────────────────────────────────────────────── */
|
||||
@@ -2673,7 +2702,9 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
|
||||
if (!pattern) {
|
||||
free(project);
|
||||
free(file_pattern);
|
||||
return cbm_mcp_text_result("pattern is required", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"pattern is required\","
|
||||
"\"hint\":\"Pass a text pattern or regex (with regex:true) to search source code.\"}", true);
|
||||
}
|
||||
|
||||
char *root_path = get_project_root(srv, project);
|
||||
@@ -2681,7 +2712,10 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
|
||||
free(pattern);
|
||||
free(project);
|
||||
free(file_pattern);
|
||||
return cbm_mcp_text_result("project not found or not indexed", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"project not found or not indexed\","
|
||||
"\"hint\":\"Run index_repository with repo_path to index the project first, "
|
||||
"or use list_projects to see available projects.\"}", true);
|
||||
}
|
||||
|
||||
/* Write pattern to temp file to avoid shell injection */
|
||||
@@ -2697,7 +2731,9 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
|
||||
free(pattern);
|
||||
free(project);
|
||||
free(file_pattern);
|
||||
return cbm_mcp_text_result("search failed: temp file", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"search failed: could not create temp file\","
|
||||
"\"hint\":\"Check that /tmp is writable and has disk space.\"}", true);
|
||||
}
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
(void)fprintf(tf, "%s\n", pattern);
|
||||
@@ -2734,7 +2770,9 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
|
||||
free(pattern);
|
||||
free(project);
|
||||
free(file_pattern);
|
||||
return cbm_mcp_text_result("search failed", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"search failed: grep command could not execute\","
|
||||
"\"hint\":\"Check that grep is installed and the project root directory exists.\"}", true);
|
||||
}
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
@@ -2819,7 +2857,10 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
if (!root_path) {
|
||||
free(project);
|
||||
free(base_branch);
|
||||
return cbm_mcp_text_result("project not found", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"project not found\","
|
||||
"\"hint\":\"Run index_repository with repo_path to index the project first, "
|
||||
"or use list_projects to see available projects.\"}", true);
|
||||
}
|
||||
|
||||
/* Get changed files via git */
|
||||
@@ -2835,7 +2876,9 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
|
||||
free(root_path);
|
||||
free(project);
|
||||
free(base_branch);
|
||||
return cbm_mcp_text_result("git diff failed", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"git diff failed\","
|
||||
"\"hint\":\"Check that git is installed and the project is a git repository.\"}", true);
|
||||
}
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
@@ -2914,7 +2957,10 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
|
||||
free(project);
|
||||
free(mode_str);
|
||||
free(content);
|
||||
return cbm_mcp_text_result("project not found", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"project not found\","
|
||||
"\"hint\":\"Run index_repository with repo_path to index the project first, "
|
||||
"or use list_projects to see available projects.\"}", true);
|
||||
}
|
||||
|
||||
char adr_dir[4096];
|
||||
@@ -3172,7 +3218,10 @@ static char *handle_index_dependencies(cbm_mcp_server_t *srv, const char *args)
|
||||
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
|
||||
char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const char *args_json) {
|
||||
if (!tool_name) {
|
||||
return cbm_mcp_text_result("missing tool name", true);
|
||||
return cbm_mcp_text_result(
|
||||
"{\"error\":\"missing tool name\","
|
||||
"\"hint\":\"Available tools: search_code_graph, trace_call_path, get_code. "
|
||||
"Use tools/list to see all available tools.\"}", true);
|
||||
}
|
||||
|
||||
/* Phase 9: consolidated tool names (streamlined mode) */
|
||||
@@ -3238,8 +3287,11 @@ char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const ch
|
||||
return handle_index_dependencies(srv, args_json);
|
||||
}
|
||||
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "unknown tool: %s", tool_name);
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"{\"error\":\"unknown tool: '%s'\","
|
||||
"\"hint\":\"Available tools: search_code_graph, trace_call_path, get_code. "
|
||||
"Use tools/list to see all available tools.\"}", tool_name);
|
||||
return cbm_mcp_text_result(msg, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -421,6 +421,305 @@ TEST(no_resources_capability_gets_context_injection) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── 8. MCP spec compliance tests ─────────────────────────── */
|
||||
|
||||
TEST(initialize_response_has_protocol_version) {
|
||||
char *resp = cbm_mcp_initialize_response();
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "protocolVersion"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "2024-11-05"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "serverInfo"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "codebase-memory-mcp"));
|
||||
free(resp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(initialize_resources_cap_subscribe_false) {
|
||||
/* Server must advertise subscribe:false (we don't support per-resource subscriptions) */
|
||||
char *resp = cbm_mcp_initialize_response();
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"subscribe\":false"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"listChanged\":true"));
|
||||
free(resp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resources_list_has_mimeType_and_description) {
|
||||
/* MCP spec requires name, uri; recommends description and mimeType */
|
||||
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, "mimeType"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "application/json"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "description"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "name"));
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resources_read_response_has_contents_array) {
|
||||
/* MCP spec: resources/read returns {contents: [{uri, mimeType, text}]} */
|
||||
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/read\","
|
||||
"\"params\":{\"uri\":\"codebase://status\"}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"contents\""));
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"uri\""));
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"mimeType\""));
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"text\""));
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resources_read_missing_uri_param) {
|
||||
/* resources/read with no uri → error -32602 (invalid params) */
|
||||
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/read\","
|
||||
"\"params\":{}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "error"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "Missing uri"));
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(resources_read_no_params_at_all) {
|
||||
/* resources/read with no params object */
|
||||
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/read\"}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "error"));
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── 9. Client behavioral difference tests ───────────────── */
|
||||
|
||||
TEST(resource_client_never_gets_context_across_multiple_calls) {
|
||||
/* Resource-capable client should NEVER see _context, even across many calls */
|
||||
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\":\"initialize\","
|
||||
"\"params\":{\"protocolVersion\":\"2024-11-05\","
|
||||
"\"capabilities\":{\"resources\":{}},"
|
||||
"\"clientInfo\":{\"name\":\"modern\",\"version\":\"2.0\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
free(resp);
|
||||
|
||||
/* 3 consecutive tool calls — none should have _context */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char *r = cbm_mcp_handle_tool(srv, "search_graph",
|
||||
"{\"name_pattern\":\"test\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_NULL(strstr(r, "_context"));
|
||||
/* But session_project should always be present */
|
||||
ASSERT_NOT_NULL(strstr(r, "session_project"));
|
||||
free(r);
|
||||
}
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(legacy_client_gets_context_only_on_first_call) {
|
||||
/* Legacy client: _context on first call, NOT on subsequent calls */
|
||||
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\":\"initialize\","
|
||||
"\"params\":{\"protocolVersion\":\"2024-11-05\","
|
||||
"\"capabilities\":{},"
|
||||
"\"clientInfo\":{\"name\":\"legacy\",\"version\":\"1.0\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
free(resp);
|
||||
|
||||
/* First call: MUST have _context */
|
||||
char *r1 = cbm_mcp_handle_tool(srv, "search_graph",
|
||||
"{\"name_pattern\":\"test\"}");
|
||||
ASSERT_NOT_NULL(r1);
|
||||
ASSERT_NOT_NULL(strstr(r1, "_context"));
|
||||
free(r1);
|
||||
|
||||
/* Second call: must NOT have _context (one-shot) */
|
||||
char *r2 = cbm_mcp_handle_tool(srv, "search_graph",
|
||||
"{\"name_pattern\":\"test2\"}");
|
||||
ASSERT_NOT_NULL(r2);
|
||||
ASSERT_NULL(strstr(r2, "_context"));
|
||||
ASSERT_NOT_NULL(strstr(r2, "session_project"));
|
||||
free(r2);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(empty_resources_capability_counts_as_support) {
|
||||
/* MCP spec: capabilities.resources:{} means resources supported
|
||||
* (neither subscribe nor listChanged, but resources protocol works) */
|
||||
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\":\"initialize\","
|
||||
"\"params\":{\"protocolVersion\":\"2024-11-05\","
|
||||
"\"capabilities\":{\"resources\":{}},"
|
||||
"\"clientInfo\":{\"name\":\"minimal\",\"version\":\"1.0\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
free(resp);
|
||||
|
||||
/* Empty resources:{} still means client supports resources → no _context */
|
||||
char *r = cbm_mcp_handle_tool(srv, "search_graph",
|
||||
"{\"name_pattern\":\"x\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_NULL(strstr(r, "_context"));
|
||||
free(r);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(no_initialize_defaults_to_legacy_behavior) {
|
||||
/* Server with no initialize call → defaults to legacy (no resources) */
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
/* Call tool directly without initialize → should get _context (legacy) */
|
||||
char *r = cbm_mcp_handle_tool(srv, "search_graph",
|
||||
"{\"name_pattern\":\"x\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_NOT_NULL(strstr(r, "_context"));
|
||||
free(r);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── 10. Error message quality tests ─────────────────────── */
|
||||
|
||||
TEST(error_no_project_loaded_has_hint) {
|
||||
/* search_graph with a nonexistent project name → resolve_store returns NULL
|
||||
* but cbm_mcp_server_new creates a default store. Use a project name that
|
||||
* won't match any DB file to trigger the error. The REQUIRE_STORE macro
|
||||
* in search_graph handles auto-index, but for a fake project path it will
|
||||
* still fail and return the hint. Test via the error structure in trace. */
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
/* trace_call_path goes through REQUIRE_STORE → no project loaded if store NULL.
|
||||
* With cbm_mcp_server_new(NULL), resolve_store(NULL) returns the default store.
|
||||
* The function_not_found error (which also has hint) tests the pattern. */
|
||||
char *r = cbm_mcp_handle_tool(srv, "trace_call_path",
|
||||
"{\"function_name\":\"nonexistent_fn\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
/* The response should have a hint field (either "no project loaded" or "not found") */
|
||||
ASSERT_NOT_NULL(strstr(r, "hint"));
|
||||
free(r);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(error_function_not_found_includes_name) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
char *r = cbm_mcp_handle_tool(srv, "trace_call_path",
|
||||
"{\"function_name\":\"nonexistent_xyz_func\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
/* Error should include the function name that was searched for */
|
||||
ASSERT_NOT_NULL(strstr(r, "nonexistent_xyz_func"));
|
||||
ASSERT_NOT_NULL(strstr(r, "hint"));
|
||||
free(r);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(error_symbol_not_found_includes_qn) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
char *r = cbm_mcp_handle_tool(srv, "get_code_snippet",
|
||||
"{\"qualified_name\":\"nonexistent.module.func_xyz\"}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
/* Error should include the qualified name that was searched for */
|
||||
ASSERT_NOT_NULL(strstr(r, "nonexistent.module.func_xyz"));
|
||||
ASSERT_NOT_NULL(strstr(r, "hint"));
|
||||
free(r);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(error_missing_required_param_has_hint) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
|
||||
/* query_graph missing query param */
|
||||
char *r1 = cbm_mcp_handle_tool(srv, "query_graph", "{}");
|
||||
ASSERT_NOT_NULL(r1);
|
||||
ASSERT_NOT_NULL(strstr(r1, "query is required"));
|
||||
ASSERT_NOT_NULL(strstr(r1, "hint"));
|
||||
free(r1);
|
||||
|
||||
/* trace_call_path missing function_name */
|
||||
char *r2 = cbm_mcp_handle_tool(srv, "trace_call_path", "{}");
|
||||
ASSERT_NOT_NULL(r2);
|
||||
ASSERT_NOT_NULL(strstr(r2, "function_name is required"));
|
||||
ASSERT_NOT_NULL(strstr(r2, "hint"));
|
||||
free(r2);
|
||||
|
||||
/* get_code_snippet missing qualified_name */
|
||||
char *r3 = cbm_mcp_handle_tool(srv, "get_code_snippet", "{}");
|
||||
ASSERT_NOT_NULL(r3);
|
||||
ASSERT_NOT_NULL(strstr(r3, "qualified_name is required"));
|
||||
ASSERT_NOT_NULL(strstr(r3, "hint"));
|
||||
free(r3);
|
||||
|
||||
/* search_code missing pattern */
|
||||
char *r4 = cbm_mcp_handle_tool(srv, "search_code", "{}");
|
||||
ASSERT_NOT_NULL(r4);
|
||||
ASSERT_NOT_NULL(strstr(r4, "pattern is required"));
|
||||
ASSERT_NOT_NULL(strstr(r4, "hint"));
|
||||
free(r4);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(error_unknown_tool_lists_valid_tools) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
char *r = cbm_mcp_handle_tool(srv, "nonexistent_tool_xyz", "{}");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_NOT_NULL(strstr(r, "nonexistent_tool_xyz"));
|
||||
ASSERT_NOT_NULL(strstr(r, "hint"));
|
||||
ASSERT_NOT_NULL(strstr(r, "search_code_graph"));
|
||||
ASSERT_NOT_NULL(strstr(r, "tools/list"));
|
||||
free(r);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(error_resource_not_found_has_spec_code) {
|
||||
/* MCP spec: resource not found = -32002 with actionable message */
|
||||
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/read\","
|
||||
"\"params\":{\"uri\":\"codebase://bad_uri_xyz\"}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "-32002"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "bad_uri_xyz"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "codebase://schema"));
|
||||
ASSERT_NOT_NULL(strstr(resp, "resources/list"));
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite registration ──────────────────────────────────── */
|
||||
|
||||
SUITE(tool_consolidation) {
|
||||
@@ -455,4 +754,23 @@ SUITE(tool_consolidation) {
|
||||
RUN_TEST(initialize_advertises_resources_capability);
|
||||
RUN_TEST(initialize_parses_client_resources_capability);
|
||||
RUN_TEST(no_resources_capability_gets_context_injection);
|
||||
/* MCP spec compliance */
|
||||
RUN_TEST(initialize_response_has_protocol_version);
|
||||
RUN_TEST(initialize_resources_cap_subscribe_false);
|
||||
RUN_TEST(resources_list_has_mimeType_and_description);
|
||||
RUN_TEST(resources_read_response_has_contents_array);
|
||||
RUN_TEST(resources_read_missing_uri_param);
|
||||
RUN_TEST(resources_read_no_params_at_all);
|
||||
/* Client behavioral differences */
|
||||
RUN_TEST(resource_client_never_gets_context_across_multiple_calls);
|
||||
RUN_TEST(legacy_client_gets_context_only_on_first_call);
|
||||
RUN_TEST(empty_resources_capability_counts_as_support);
|
||||
RUN_TEST(no_initialize_defaults_to_legacy_behavior);
|
||||
/* Error message quality */
|
||||
RUN_TEST(error_no_project_loaded_has_hint);
|
||||
RUN_TEST(error_function_not_found_includes_name);
|
||||
RUN_TEST(error_symbol_not_found_includes_qn);
|
||||
RUN_TEST(error_missing_required_param_has_hint);
|
||||
RUN_TEST(error_unknown_tool_lists_valid_tools);
|
||||
RUN_TEST(error_resource_not_found_has_spec_code);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user