From cabece18f10a11d87f810ba74c49e82dc1c5d46d Mon Sep 17 00:00:00 2001 From: wargloom Date: Sun, 12 Jul 2026 13:55:08 +0300 Subject: [PATCH] fix(mcp): return structured compact results Tools advertise an object outputSchema, so strict MCP clients reject compact TOON responses that omit structuredContent. Signed-off-by: wargloom --- src/mcp/mcp.c | 17 +++++++++++++++-- tests/test_mcp.c | 8 +++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 0d7aa14d6..2cf81d468 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -258,17 +258,30 @@ char *cbm_mcp_text_result(const char *text, bool is_error) { yyjson_mut_arr_add_val(content, item); yyjson_mut_obj_add_val(doc, root, "content", content); - if (!is_error && text) { + bool has_structured_content = false; + if (text) { yyjson_doc *structured_doc = yyjson_read(text, strlen(text), 0); if (structured_doc) { yyjson_val *structured_root = yyjson_doc_get_root(structured_doc); if (yyjson_is_obj(structured_root)) { yyjson_mut_val *structured = yyjson_val_mut_copy(doc, structured_root); - yyjson_mut_obj_add_val(doc, root, "structuredContent", structured); + if (structured) { + yyjson_mut_obj_add_val(doc, root, "structuredContent", structured); + has_structured_content = true; + } } yyjson_doc_free(structured_doc); } } + if (!has_structured_content) { + /* Every advertised MCP tool has an object outputSchema, so even compact + * TOON/plain-text and error results must carry a conforming structured + * object. Keep the text Content block above for model visibility and + * backwards compatibility. */ + yyjson_mut_val *structured = yyjson_mut_obj(doc); + yyjson_mut_obj_add_str(doc, structured, is_error ? "error" : "text", text ? text : ""); + yyjson_mut_obj_add_val(doc, root, "structuredContent", structured); + } yyjson_mut_obj_add_bool(doc, root, "isError", is_error); char *out = yy_doc_to_str(doc); diff --git a/tests/test_mcp.c b/tests/test_mcp.c index e24ec0d73..ccbb93ad4 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -448,10 +448,10 @@ TEST(mcp_text_result) { PASS(); } -TEST(mcp_text_result_skips_structured_content_for_plain_text) { +TEST(mcp_text_result_wraps_plain_text_as_structured_content) { char *json = cbm_mcp_text_result("plain text", false); ASSERT_NOT_NULL(json); - ASSERT_NULL(strstr(json, "\"structuredContent\"")); + ASSERT_NOT_NULL(strstr(json, "\"structuredContent\":{\"text\":\"plain text\"}")); ASSERT_NOT_NULL(strstr(json, "\"isError\":false")); free(json); PASS(); @@ -470,6 +470,7 @@ TEST(mcp_cancel_matches_request_id) { TEST(mcp_text_result_error) { char *json = cbm_mcp_text_result("something failed", true); ASSERT_NOT_NULL(json); + ASSERT_NOT_NULL(strstr(json, "\"structuredContent\":{\"error\":\"something failed\"}")); ASSERT_NOT_NULL(strstr(json, "\"isError\":true")); ASSERT_NOT_NULL(strstr(json, "something failed")); free(json); @@ -756,6 +757,7 @@ TEST(tool_search_graph_includes_node_properties) { "\"arguments\":{\"project\":\"test-project\",\"label\":\"Function\"," "\"name_pattern\":\"HandleRequest\",\"limit\":5}}}"); ASSERT_NOT_NULL(resp); + ASSERT_NOT_NULL(strstr(resp, "\"structuredContent\":{\"text\":")); char *inner = extract_text_content(resp); ASSERT_NOT_NULL(inner); ASSERT_NOT_NULL(strstr(inner, "results[")); /* TOON table header */ @@ -5611,7 +5613,7 @@ SUITE(mcp) { RUN_TEST(mcp_ingest_traces_items_disallow_additional_properties_issue731); RUN_TEST(mcp_get_architecture_aspects_schema_enum_pr560); RUN_TEST(mcp_text_result); - RUN_TEST(mcp_text_result_skips_structured_content_for_plain_text); + RUN_TEST(mcp_text_result_wraps_plain_text_as_structured_content); RUN_TEST(mcp_cancel_matches_request_id); RUN_TEST(mcp_text_result_error);