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:
Andrew Hundt
2026-03-22 22:29:06 -04:00
parent 7adaf00851
commit d87d408447
2 changed files with 394 additions and 24 deletions
+76 -24
View File
@@ -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);
}
+318
View File
@@ -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);
}