Skip to content

Commit a817ecf

Browse files
committed
test: add corner-case tests for AudioContent, tool version, and output schema
Cover gaps found during cross-implementation testing: - AudioContent serialization and handler round-trip (content.cpp, server_handler.cpp) - Tool version metadata appears in tools/list wire format (handler.cpp) - Output schema null vs empty-object distinction (handler.cpp) - Client-side AudioContent parsing via parse_content_block (test_audio_content.cpp) - Add AudioContent to client ContentBlock variant (types.hpp)
1 parent 76b23bd commit a817ecf

6 files changed

Lines changed: 162 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@ if(FASTMCPP_BUILD_TESTS)
434434
target_link_libraries(fastmcpp_client_sampling_handlers PRIVATE fastmcpp_core)
435435
add_test(NAME fastmcpp_client_sampling_handlers COMMAND fastmcpp_client_sampling_handlers)
436436

437+
add_executable(fastmcpp_client_audio_content tests/client/test_audio_content.cpp)
438+
target_link_libraries(fastmcpp_client_audio_content PRIVATE fastmcpp_core)
439+
add_test(NAME fastmcpp_client_audio_content COMMAND fastmcpp_client_audio_content)
440+
437441
add_executable(fastmcpp_server_middleware tests/server/middleware.cpp)
438442
target_link_libraries(fastmcpp_server_middleware PRIVATE fastmcpp_core)
439443
add_test(NAME fastmcpp_server_middleware COMMAND fastmcpp_server_middleware)

include/fastmcpp/client/types.hpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ struct ImageContent
3434
std::string mimeType; ///< e.g., "image/png"
3535
};
3636

37+
/// Audio content block
38+
struct AudioContent
39+
{
40+
std::string type{"audio"};
41+
std::string data; ///< Base64-encoded audio bytes
42+
std::string mimeType; ///< e.g., "audio/wav", "audio/mpeg"
43+
};
44+
3745
/// Embedded resource content
3846
struct EmbeddedResourceContent
3947
{
@@ -45,7 +53,7 @@ struct EmbeddedResourceContent
4553
};
4654

4755
/// Content block variant (matches mcp.types.ContentBlock)
48-
using ContentBlock = std::variant<TextContent, ImageContent, EmbeddedResourceContent>;
56+
using ContentBlock = std::variant<TextContent, ImageContent, AudioContent, EmbeddedResourceContent>;
4957

5058
// ============================================================================
5159
// Tool Types
@@ -331,6 +339,18 @@ inline void from_json(const fastmcpp::Json& j, ImageContent& c)
331339
c.mimeType = j.at("mimeType").get<std::string>();
332340
}
333341

342+
inline void to_json(fastmcpp::Json& j, const AudioContent& c)
343+
{
344+
j = fastmcpp::Json{{"type", c.type}, {"data", c.data}, {"mimeType", c.mimeType}};
345+
}
346+
347+
inline void from_json(const fastmcpp::Json& j, AudioContent& c)
348+
{
349+
c.type = j.value("type", "audio");
350+
c.data = j.at("data").get<std::string>();
351+
c.mimeType = j.at("mimeType").get<std::string>();
352+
}
353+
334354
inline void to_json(fastmcpp::Json& j, const ToolInfo& t)
335355
{
336356
j = fastmcpp::Json{{"name", t.name}, {"inputSchema", t.inputSchema}};
@@ -551,6 +571,10 @@ inline ContentBlock parse_content_block(const fastmcpp::Json& j)
551571
{
552572
return j.get<ImageContent>();
553573
}
574+
else if (type == "audio")
575+
{
576+
return j.get<AudioContent>();
577+
}
554578
else if (type == "resource")
555579
{
556580
EmbeddedResourceContent c;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#include "fastmcpp/client/types.hpp"
2+
3+
#include <cassert>
4+
5+
int main()
6+
{
7+
using namespace fastmcpp;
8+
9+
// Parse audio content block
10+
Json audio_json = {{"type", "audio"}, {"data", "aGVsbG8="}, {"mimeType", "audio/wav"}};
11+
auto block = client::parse_content_block(audio_json);
12+
auto* ac = std::get_if<client::AudioContent>(&block);
13+
assert(ac != nullptr);
14+
assert(ac->data == "aGVsbG8=");
15+
assert(ac->mimeType == "audio/wav");
16+
17+
// Parse text still works
18+
Json text_json = {{"type", "text"}, {"text", "hello"}};
19+
auto block2 = client::parse_content_block(text_json);
20+
assert(std::get_if<client::TextContent>(&block2) != nullptr);
21+
22+
// Parse image still works
23+
Json img_json = {{"type", "image"}, {"data", "x"}, {"mimeType", "image/png"}};
24+
auto block3 = client::parse_content_block(img_json);
25+
assert(std::get_if<client::ImageContent>(&block3) != nullptr);
26+
27+
return 0;
28+
}

tests/content.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,13 @@ int main()
1717
assert(ji.at("type") == "image");
1818
assert(ji.at("data") == "BASE64DATA");
1919
assert(ji.at("mimeType") == "image/png");
20+
21+
AudioContent audio;
22+
audio.data = "AQID";
23+
audio.mimeType = "audio/wav";
24+
Json ja = audio;
25+
assert(ja.at("type") == "audio");
26+
assert(ja.at("data") == "AQID");
27+
assert(ja.at("mimeType") == "audio/wav");
2028
return 0;
2129
}

tests/mcp/handler.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,74 @@ int main()
6969
{"params", Json{{"name", "any"}}}};
7070
auto prompt_get_resp = handler(prompt_get);
7171
assert(prompt_get_resp["result"]["messages"].is_array());
72+
73+
// ---- Tool version metadata in tools/list ----
74+
{
75+
tools::ToolManager tm2;
76+
Json schema = {{"type", "object"}, {"properties", Json::object()}};
77+
tools::Tool versioned{"versioned", schema, Json(),
78+
[](const Json&) { return 42; }};
79+
versioned.set_version("2.0.0");
80+
tm2.register_tool(versioned);
81+
tools::Tool plain{"plain", schema, Json(),
82+
[](const Json&) { return 1; }};
83+
tm2.register_tool(plain);
84+
85+
auto handler2 = mcp::make_mcp_handler("ver_test", "1.0.0", tm2);
86+
auto list2 =
87+
handler2(Json{{"jsonrpc", "2.0"}, {"id", 10}, {"method", "tools/list"}});
88+
bool checked_versioned = false, checked_plain = false;
89+
for (const auto& t : list2["result"]["tools"])
90+
{
91+
if (t["name"] == "versioned")
92+
{
93+
assert(t.contains("version"));
94+
assert(t["version"] == "2.0.0");
95+
checked_versioned = true;
96+
}
97+
if (t["name"] == "plain")
98+
{
99+
assert(!t.contains("version"));
100+
checked_plain = true;
101+
}
102+
}
103+
assert(checked_versioned);
104+
assert(checked_plain);
105+
}
106+
107+
// ---- Output schema: null vs non-null distinction ----
108+
{
109+
tools::ToolManager tm3;
110+
Json schema = {{"type", "object"}, {"properties", Json::object()}};
111+
// Json() = null → no outputSchema emitted
112+
tools::Tool no_schema{"no_schema", schema, Json(),
113+
[](const Json&) { return 1; }};
114+
// Json{{"type","object"}} → outputSchema present
115+
tools::Tool with_schema{"with_schema", schema, Json{{"type", "object"}},
116+
[](const Json&) { return 1; }};
117+
tm3.register_tool(no_schema);
118+
tm3.register_tool(with_schema);
119+
120+
auto handler3 = mcp::make_mcp_handler("schema_test", "1.0.0", tm3);
121+
auto list3 =
122+
handler3(Json{{"jsonrpc", "2.0"}, {"id", 20}, {"method", "tools/list"}});
123+
bool checked_no = false, checked_with = false;
124+
for (const auto& t : list3["result"]["tools"])
125+
{
126+
if (t["name"] == "no_schema")
127+
{
128+
assert(!t.contains("outputSchema"));
129+
checked_no = true;
130+
}
131+
if (t["name"] == "with_schema")
132+
{
133+
assert(t.contains("outputSchema"));
134+
checked_with = true;
135+
}
136+
}
137+
assert(checked_no);
138+
assert(checked_with);
139+
}
140+
72141
return 0;
73142
}

tests/mcp/server_handler.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,25 @@ int main()
2121
return Json{{"content", content}};
2222
});
2323

24+
// audio_tool returns mixed text + audio content
25+
s.route("audio_tool",
26+
[](const Json&)
27+
{
28+
AudioContent audio;
29+
audio.data = "aGVsbG8="; // base64("hello")
30+
audio.mimeType = "audio/wav";
31+
Json content =
32+
Json::array({TextContent{"text", "Audio attached"}, audio});
33+
return Json{{"content", content}};
34+
});
35+
2436
std::vector<std::tuple<std::string, std::string, Json>> meta;
2537
meta.emplace_back("generate_chart", "Generates a chart",
2638
Json{{"type", "object"},
2739
{"properties", Json::object({{"title", Json{{"type", "string"}}}})},
2840
{"required", Json::array({"title"})}});
41+
meta.emplace_back("audio_tool", "Returns audio content",
42+
Json{{"type", "object"}, {"properties", Json::object()}});
2943

3044
auto handler = mcp::make_mcp_handler("viz", "1.0.0", s, meta);
3145

@@ -47,6 +61,20 @@ int main()
4761
assert(content[1]["type"] == "image");
4862
assert(content[1]["mimeType"] == "image/png");
4963

64+
// call audio_tool — verify audio block preserved through handler
65+
Json audio_call = {
66+
{"jsonrpc", "2.0"},
67+
{"id", 10},
68+
{"method", "tools/call"},
69+
{"params", Json{{"name", "audio_tool"}, {"arguments", Json::object()}}}};
70+
auto audio_resp = handler(audio_call);
71+
auto audio_content = audio_resp["result"]["content"];
72+
assert(audio_content.size() == 2);
73+
assert(audio_content[0]["type"] == "text");
74+
assert(audio_content[1]["type"] == "audio");
75+
assert(audio_content[1]["data"] == "aGVsbG8=");
76+
assert(audio_content[1]["mimeType"] == "audio/wav");
77+
5078
// resources/list route
5179
s.route("resources/list",
5280
[](const Json&)

0 commit comments

Comments
 (0)