Skip to content

Commit 926daf7

Browse files
committed
Add title and icons fields to client types
- Add `title` and `icons` fields to client types: ToolInfo, ResourceInfo, ResourceTemplate, PromptInfo - Add JSON serialization/deserialization for new fields - Add `to_json`/`from_json` for ResourceTemplate (was missing) - Add comprehensive unit tests for title/icons serialization - Add integration tests (api_icons.cpp) for round-trip verification: - Tools with icons from server to client - Resources with icons - Resource templates with icons - Prompts with icons
1 parent 35bc40b commit 926daf7

5 files changed

Lines changed: 443 additions & 49 deletions

File tree

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ if(FASTMCPP_BUILD_TESTS)
293293
target_link_libraries(fastmcpp_client_api_advanced PRIVATE fastmcpp_core)
294294
add_test(NAME fastmcpp_client_api_advanced COMMAND fastmcpp_client_api_advanced)
295295

296+
add_executable(fastmcpp_client_api_icons tests/client/api_icons.cpp)
297+
target_link_libraries(fastmcpp_client_api_icons PRIVATE fastmcpp_core)
298+
add_test(NAME fastmcpp_client_api_icons COMMAND fastmcpp_client_api_icons)
299+
296300
add_executable(fastmcpp_server_middleware tests/server/middleware.cpp)
297301
target_link_libraries(fastmcpp_server_middleware PRIVATE fastmcpp_core)
298302
add_test(NAME fastmcpp_server_middleware COMMAND fastmcpp_server_middleware)

include/fastmcpp/client/types.hpp

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ using ContentBlock = std::variant<TextContent, ImageContent, EmbeddedResourceCon
5656
struct ToolInfo
5757
{
5858
std::string name;
59+
std::optional<std::string> title; ///< Human-readable title
5960
std::optional<std::string> description;
60-
fastmcpp::Json inputSchema; ///< JSON Schema for tool input
61-
std::optional<fastmcpp::Json> outputSchema; ///< JSON Schema for structured output
61+
fastmcpp::Json inputSchema; ///< JSON Schema for tool input
62+
std::optional<fastmcpp::Json> outputSchema; ///< JSON Schema for structured output
63+
std::optional<std::vector<fastmcpp::Icon>> icons; ///< Icons for UI display
6264
};
6365

6466
/// Result of tools/list request
@@ -120,9 +122,11 @@ struct ResourceInfo
120122
{
121123
std::string uri;
122124
std::string name;
125+
std::optional<std::string> title; ///< Human-readable title
123126
std::optional<std::string> description;
124127
std::optional<std::string> mimeType;
125128
std::optional<fastmcpp::Json> annotations;
129+
std::optional<std::vector<fastmcpp::Icon>> icons; ///< Icons for UI display
126130
};
127131

128132
/// Resource template information
@@ -131,9 +135,11 @@ struct ResourceTemplate
131135
{
132136
std::string uriTemplate;
133137
std::string name;
138+
std::optional<std::string> title; ///< Human-readable title
134139
std::optional<std::string> description;
135140
std::optional<std::string> mimeType;
136141
std::optional<fastmcpp::Json> annotations;
142+
std::optional<std::vector<fastmcpp::Icon>> icons; ///< Icons for UI display
137143
};
138144

139145
/// Text resource content
@@ -195,8 +201,10 @@ struct PromptArgument
195201
struct PromptInfo
196202
{
197203
std::string name;
204+
std::optional<std::string> title; ///< Human-readable title
198205
std::optional<std::string> description;
199206
std::optional<std::vector<PromptArgument>> arguments;
207+
std::optional<std::vector<fastmcpp::Icon>> icons; ///< Icons for UI display
200208
};
201209

202210
/// Prompt message role
@@ -309,48 +317,97 @@ inline void from_json(const fastmcpp::Json& j, ImageContent& c)
309317
inline void to_json(fastmcpp::Json& j, const ToolInfo& t)
310318
{
311319
j = fastmcpp::Json{{"name", t.name}, {"inputSchema", t.inputSchema}};
320+
if (t.title)
321+
j["title"] = *t.title;
312322
if (t.description)
313323
j["description"] = *t.description;
314324
if (t.outputSchema)
315325
j["outputSchema"] = *t.outputSchema;
326+
if (t.icons)
327+
j["icons"] = *t.icons;
316328
}
317329

318330
inline void from_json(const fastmcpp::Json& j, ToolInfo& t)
319331
{
320332
t.name = j.at("name").get<std::string>();
333+
if (j.contains("title"))
334+
t.title = j["title"].get<std::string>();
321335
if (j.contains("description"))
322336
t.description = j["description"].get<std::string>();
323337
t.inputSchema = j.value("inputSchema", fastmcpp::Json::object());
324338
if (j.contains("outputSchema"))
325339
t.outputSchema = j["outputSchema"];
340+
if (j.contains("icons"))
341+
t.icons = j["icons"].get<std::vector<fastmcpp::Icon>>();
326342
}
327343

328344
inline void to_json(fastmcpp::Json& j, const ResourceInfo& r)
329345
{
330346
j = fastmcpp::Json{{"uri", r.uri}, {"name", r.name}};
347+
if (r.title)
348+
j["title"] = *r.title;
331349
if (r.description)
332350
j["description"] = *r.description;
333351
if (r.mimeType)
334352
j["mimeType"] = *r.mimeType;
335353
if (r.annotations)
336354
j["annotations"] = *r.annotations;
355+
if (r.icons)
356+
j["icons"] = *r.icons;
337357
}
338358

339359
inline void from_json(const fastmcpp::Json& j, ResourceInfo& r)
340360
{
341361
r.uri = j.at("uri").get<std::string>();
342362
r.name = j.at("name").get<std::string>();
363+
if (j.contains("title"))
364+
r.title = j["title"].get<std::string>();
343365
if (j.contains("description"))
344366
r.description = j["description"].get<std::string>();
345367
if (j.contains("mimeType"))
346368
r.mimeType = j["mimeType"].get<std::string>();
347369
if (j.contains("annotations"))
348370
r.annotations = j["annotations"];
371+
if (j.contains("icons"))
372+
r.icons = j["icons"].get<std::vector<fastmcpp::Icon>>();
373+
}
374+
375+
inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
376+
{
377+
j = fastmcpp::Json{{"uriTemplate", t.uriTemplate}, {"name", t.name}};
378+
if (t.title)
379+
j["title"] = *t.title;
380+
if (t.description)
381+
j["description"] = *t.description;
382+
if (t.mimeType)
383+
j["mimeType"] = *t.mimeType;
384+
if (t.annotations)
385+
j["annotations"] = *t.annotations;
386+
if (t.icons)
387+
j["icons"] = *t.icons;
388+
}
389+
390+
inline void from_json(const fastmcpp::Json& j, ResourceTemplate& t)
391+
{
392+
t.uriTemplate = j.at("uriTemplate").get<std::string>();
393+
t.name = j.at("name").get<std::string>();
394+
if (j.contains("title"))
395+
t.title = j["title"].get<std::string>();
396+
if (j.contains("description"))
397+
t.description = j["description"].get<std::string>();
398+
if (j.contains("mimeType"))
399+
t.mimeType = j["mimeType"].get<std::string>();
400+
if (j.contains("annotations"))
401+
t.annotations = j["annotations"];
402+
if (j.contains("icons"))
403+
t.icons = j["icons"].get<std::vector<fastmcpp::Icon>>();
349404
}
350405

351406
inline void to_json(fastmcpp::Json& j, const PromptInfo& p)
352407
{
353408
j = fastmcpp::Json{{"name", p.name}};
409+
if (p.title)
410+
j["title"] = *p.title;
354411
if (p.description)
355412
j["description"] = *p.description;
356413
if (p.arguments)
@@ -364,11 +421,15 @@ inline void to_json(fastmcpp::Json& j, const PromptInfo& p)
364421
j["arguments"].push_back(argJson);
365422
}
366423
}
424+
if (p.icons)
425+
j["icons"] = *p.icons;
367426
}
368427

369428
inline void from_json(const fastmcpp::Json& j, PromptInfo& p)
370429
{
371430
p.name = j.at("name").get<std::string>();
431+
if (j.contains("title"))
432+
p.title = j["title"].get<std::string>();
372433
if (j.contains("description"))
373434
p.description = j["description"].get<std::string>();
374435
if (j.contains("arguments"))
@@ -384,6 +445,8 @@ inline void from_json(const fastmcpp::Json& j, PromptInfo& p)
384445
p.arguments->push_back(arg);
385446
}
386447
}
448+
if (j.contains("icons"))
449+
p.icons = j["icons"].get<std::vector<fastmcpp::Icon>>();
387450
}
388451

389452
inline void from_json(const fastmcpp::Json& j, TextResourceContent& c)

tests/client/api_icons.cpp

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/// @file tests/client/api_icons.cpp
2+
/// @brief Integration tests for title and icons fields on client types
3+
4+
#include "test_helpers.hpp"
5+
6+
void test_tool_with_icons()
7+
{
8+
std::cout << "Test: tool with title and icons...\n";
9+
10+
auto srv = create_tool_server();
11+
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
12+
13+
auto tools = c.list_tools();
14+
15+
// Find the icon_tool
16+
auto it = std::find_if(tools.begin(), tools.end(),
17+
[](const auto& t) { return t.name == "icon_tool"; });
18+
19+
assert(it != tools.end());
20+
assert(it->title.has_value());
21+
assert(*it->title == "My Icon Tool");
22+
assert(it->icons.has_value());
23+
assert(it->icons->size() == 2);
24+
25+
// Check first icon (URL)
26+
assert((*it->icons)[0].src == "https://example.com/icon.png");
27+
assert((*it->icons)[0].mime_type.has_value());
28+
assert(*(*it->icons)[0].mime_type == "image/png");
29+
assert(!(*it->icons)[0].sizes.has_value());
30+
31+
// Check second icon (data URI with sizes)
32+
assert((*it->icons)[1].src == "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=");
33+
assert((*it->icons)[1].mime_type.has_value());
34+
assert(*(*it->icons)[1].mime_type == "image/svg+xml");
35+
assert((*it->icons)[1].sizes.has_value());
36+
assert((*it->icons)[1].sizes->size() == 2);
37+
assert((*(*it->icons)[1].sizes)[0] == "48x48");
38+
assert((*(*it->icons)[1].sizes)[1] == "any");
39+
40+
std::cout << " [PASS] Tool with title and icons round-trips correctly\n";
41+
}
42+
43+
void test_tool_without_icons()
44+
{
45+
std::cout << "Test: tool without icons...\n";
46+
47+
auto srv = create_tool_server();
48+
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
49+
50+
auto tools = c.list_tools();
51+
52+
// Find the add tool (has no icons)
53+
auto it =
54+
std::find_if(tools.begin(), tools.end(), [](const auto& t) { return t.name == "add"; });
55+
56+
assert(it != tools.end());
57+
assert(!it->title.has_value());
58+
assert(!it->icons.has_value());
59+
60+
std::cout << " [PASS] Tool without icons has nullopt fields\n";
61+
}
62+
63+
void test_resource_with_icons()
64+
{
65+
std::cout << "Test: resource with title and icons...\n";
66+
67+
auto srv = create_resource_server();
68+
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
69+
70+
auto resources = c.list_resources();
71+
72+
// Find the icon_resource
73+
auto it = std::find_if(resources.begin(), resources.end(),
74+
[](const auto& r) { return r.name == "icon_resource"; });
75+
76+
assert(it != resources.end());
77+
assert(it->title.has_value());
78+
assert(*it->title == "Resource With Icons");
79+
assert(it->icons.has_value());
80+
assert(it->icons->size() == 1);
81+
assert((*it->icons)[0].src == "https://example.com/res.png");
82+
83+
std::cout << " [PASS] Resource with title and icons round-trips correctly\n";
84+
}
85+
86+
void test_resource_template_with_icons()
87+
{
88+
std::cout << "Test: resource template with title and icons...\n";
89+
90+
auto srv = create_resource_server();
91+
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
92+
93+
auto templates = c.list_resource_templates();
94+
95+
// Find the icon_template
96+
auto it = std::find_if(templates.begin(), templates.end(),
97+
[](const auto& t) { return t.name == "icon_template"; });
98+
99+
assert(it != templates.end());
100+
assert(it->title.has_value());
101+
assert(*it->title == "Template With Icons");
102+
assert(it->icons.has_value());
103+
assert(it->icons->size() == 1);
104+
assert((*it->icons)[0].src == "https://example.com/tpl.svg");
105+
assert((*it->icons)[0].mime_type.has_value());
106+
assert(*(*it->icons)[0].mime_type == "image/svg+xml");
107+
108+
std::cout << " [PASS] Resource template with title and icons round-trips correctly\n";
109+
}
110+
111+
void test_prompt_with_icons()
112+
{
113+
std::cout << "Test: prompt with title and icons...\n";
114+
115+
auto srv = create_prompt_server();
116+
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
117+
118+
auto prompts = c.list_prompts();
119+
120+
// Find the icon_prompt
121+
auto it = std::find_if(prompts.begin(), prompts.end(),
122+
[](const auto& p) { return p.name == "icon_prompt"; });
123+
124+
assert(it != prompts.end());
125+
assert(it->title.has_value());
126+
assert(*it->title == "Prompt With Icons");
127+
assert(it->icons.has_value());
128+
assert(it->icons->size() == 1);
129+
assert((*it->icons)[0].src == "https://example.com/prompt.png");
130+
131+
std::cout << " [PASS] Prompt with title and icons round-trips correctly\n";
132+
}
133+
134+
int main()
135+
{
136+
std::cout << "\n=== Client API Icons Integration Tests ===\n\n";
137+
138+
test_tool_with_icons();
139+
test_tool_without_icons();
140+
test_resource_with_icons();
141+
test_resource_template_with_icons();
142+
test_prompt_with_icons();
143+
144+
std::cout << "\n=== All Icon Integration Tests Passed ===\n\n";
145+
return 0;
146+
}

0 commit comments

Comments
 (0)