Skip to content

Commit 6fbc48c

Browse files
authored
Merge pull request #15 from 0xeb/feature/proxy-title-icons
Add title and icons fields to client types
2 parents 35bc40b + c5fb8e5 commit 6fbc48c

29 files changed

Lines changed: 1597 additions & 199 deletions

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)

examples/context_introspection.cpp

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,28 @@ int main()
2727
resources::ResourceManager resource_mgr;
2828

2929
// Register some sample resources
30-
resources::Resource doc1{Id{"file://docs/readme.txt"}, resources::Kind::File,
31-
Json{{"description", "Project README"}, {"size", 1024}}};
30+
resources::Resource doc1;
31+
doc1.uri = "file://docs/readme.txt";
32+
doc1.name = "readme.txt";
33+
doc1.id = Id{"file://docs/readme.txt"};
34+
doc1.kind = resources::Kind::File;
35+
doc1.metadata = Json{{"description", "Project README"}, {"size", 1024}};
3236
resource_mgr.register_resource(doc1);
3337

34-
resources::Resource doc2{Id{"file://docs/api.txt"}, resources::Kind::File,
35-
Json{{"description", "API Documentation"}, {"size", 2048}}};
38+
resources::Resource doc2;
39+
doc2.uri = "file://docs/api.txt";
40+
doc2.name = "api.txt";
41+
doc2.id = Id{"file://docs/api.txt"};
42+
doc2.kind = resources::Kind::File;
43+
doc2.metadata = Json{{"description", "API Documentation"}, {"size", 2048}};
3644
resource_mgr.register_resource(doc2);
3745

38-
resources::Resource config{Id{"config://app.json"}, resources::Kind::Json,
39-
Json{{"description", "Application config"}}};
46+
resources::Resource config;
47+
config.uri = "config://app.json";
48+
config.name = "app.json";
49+
config.id = Id{"config://app.json"};
50+
config.kind = resources::Kind::Json;
51+
config.metadata = Json{{"description", "Application config"}};
4052
resource_mgr.register_resource(config);
4153

4254
// ============================================================================
@@ -71,9 +83,9 @@ int main()
7183
std::cout << "\n2. Listing Prompts:\n";
7284
std::cout << " " << std::string(40, '-') << "\n";
7385
auto prompts = ctx.list_prompts();
74-
for (const auto& [name, prompt] : prompts)
86+
for (const auto& prompt : prompts)
7587
{
76-
std::cout << " - Name: " << name << "\n";
88+
std::cout << " - Name: " << prompt.name << "\n";
7789
std::cout << " Template: " << prompt.template_string() << "\n\n";
7890
}
7991

examples/tool_injection_middleware.cpp

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,21 @@ int main()
2828
prompts::PromptManager prompt_mgr;
2929

3030
// Register some resources
31-
resource_mgr.register_resource(resources::Resource{Id{"file://docs/readme.md"},
32-
resources::Kind::File,
33-
Json{{"description", "Project README"}}});
34-
35-
resource_mgr.register_resource(resources::Resource{Id{"file://docs/api.md"},
36-
resources::Kind::File,
37-
Json{{"description", "API Documentation"}}});
31+
resources::Resource res1;
32+
res1.uri = "file://docs/readme.md";
33+
res1.name = "readme.md";
34+
res1.id = Id{"file://docs/readme.md"};
35+
res1.kind = resources::Kind::File;
36+
res1.metadata = Json{{"description", "Project README"}};
37+
resource_mgr.register_resource(res1);
38+
39+
resources::Resource res2;
40+
res2.uri = "file://docs/api.md";
41+
res2.name = "api.md";
42+
res2.id = Id{"file://docs/api.md"};
43+
res2.kind = resources::Kind::File;
44+
res2.metadata = Json{{"description", "API Documentation"}};
45+
resource_mgr.register_resource(res2);
3846

3947
// Register some prompts
4048
prompt_mgr.add("greeting", prompts::Prompt("Hello {{name}}!"));

include/fastmcpp/client/client.hpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,28 @@ class Client
731731
rt.mimeType = r["mimeType"].get<std::string>();
732732
if (r.contains("annotations"))
733733
rt.annotations = r["annotations"];
734+
if (r.contains("title"))
735+
rt.title = r["title"].get<std::string>();
736+
if (r.contains("icons"))
737+
{
738+
std::vector<fastmcpp::Icon> icons;
739+
for (const auto& icon : r["icons"])
740+
{
741+
fastmcpp::Icon i;
742+
i.src = icon.at("src").get<std::string>();
743+
if (icon.contains("mimeType"))
744+
i.mime_type = icon["mimeType"].get<std::string>();
745+
if (icon.contains("sizes"))
746+
{
747+
std::vector<std::string> sizes;
748+
for (const auto& s : icon["sizes"])
749+
sizes.push_back(s.get<std::string>());
750+
i.sizes = sizes;
751+
}
752+
icons.push_back(i);
753+
}
754+
rt.icons = icons;
755+
}
734756
result.resourceTemplates.push_back(rt);
735757
}
736758
}
@@ -790,6 +812,11 @@ class Client
790812
tc.text = m["content"].get<std::string>();
791813
msg.content.push_back(tc);
792814
}
815+
else if (m["content"].is_object())
816+
{
817+
// Handle single content object (Python fastmcp format)
818+
msg.content.push_back(parse_content_block(m["content"]));
819+
}
793820
}
794821
result.messages.push_back(msg);
795822
}

include/fastmcpp/client/transports.hpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
#include "fastmcpp/client/client.hpp"
33
#include "fastmcpp/types.hpp"
44

5+
#include <atomic>
6+
#include <condition_variable>
57
#include <filesystem>
8+
#include <future>
9+
#include <memory>
10+
#include <mutex>
611
#include <optional>
712
#include <ostream>
813
#include <string>
14+
#include <thread>
15+
#include <unordered_map>
916
#include <vector>
1017

1118
namespace fastmcpp::client
@@ -82,4 +89,52 @@ class StdioTransport : public ITransport
8289
std::ostream* log_stream_ = nullptr;
8390
};
8491

92+
/// SSE client transport for connecting to MCP servers using Server-Sent Events protocol.
93+
/// This transport is compatible with Python fastmcp servers and other SSE-based MCP servers.
94+
///
95+
/// The SSE protocol works as follows:
96+
/// 1. Client connects to /sse endpoint (GET) to establish event stream
97+
/// 2. Client sends JSON-RPC requests to /messages endpoint (POST)
98+
/// 3. Server sends JSON-RPC responses back via the SSE stream
99+
class SseClientTransport : public ITransport
100+
{
101+
public:
102+
/// Construct an SSE client transport
103+
/// @param base_url The base URL of the MCP server (e.g., "http://127.0.0.1:8766")
104+
/// Will connect to {base_url}/sse and post to {base_url}/messages
105+
/// @param sse_path Path for SSE endpoint (default: "/sse")
106+
/// @param messages_path Path for message endpoint (default: "/messages")
107+
explicit SseClientTransport(std::string base_url, std::string sse_path = "/sse",
108+
std::string messages_path = "/messages");
109+
110+
~SseClientTransport();
111+
112+
/// Send a JSON-RPC request and wait for response
113+
fastmcpp::Json request(const std::string& route, const fastmcpp::Json& payload) override;
114+
115+
/// Check if connected to SSE stream
116+
bool is_connected() const;
117+
118+
private:
119+
void start_sse_listener();
120+
void stop_sse_listener();
121+
void process_sse_event(const fastmcpp::Json& event);
122+
123+
std::string base_url_;
124+
std::string sse_path_;
125+
std::string messages_path_;
126+
std::string endpoint_path_; // Endpoint path from SSE with session_id
127+
128+
// SSE listener thread and state
129+
std::unique_ptr<std::thread> sse_thread_;
130+
std::atomic<bool> running_{false};
131+
std::atomic<bool> connected_{false};
132+
133+
// Request/response matching
134+
std::atomic<int64_t> next_id_{1};
135+
std::mutex pending_mutex_;
136+
std::condition_variable pending_cv_;
137+
std::unordered_map<int64_t, std::promise<fastmcpp::Json>> pending_requests_;
138+
};
139+
85140
} // namespace fastmcpp::client

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)

include/fastmcpp/mcp/handler.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#pragma once
2+
#include "fastmcpp/prompts/manager.hpp"
3+
#include "fastmcpp/resources/manager.hpp"
24
#include "fastmcpp/server/server.hpp"
35
#include "fastmcpp/tools/manager.hpp"
46
#include "fastmcpp/types.hpp"
@@ -35,4 +37,11 @@ make_mcp_handler(const std::string& server_name, const std::string& version,
3537
const server::Server& server, const tools::ToolManager& tools,
3638
const std::unordered_map<std::string, std::string>& descriptions = {});
3739

40+
// Full MCP handler with tools, resources, and prompts support
41+
std::function<fastmcpp::Json(const fastmcpp::Json&)>
42+
make_mcp_handler(const std::string& server_name, const std::string& version,
43+
const server::Server& server, const tools::ToolManager& tools,
44+
const resources::ResourceManager& resources, const prompts::PromptManager& prompts,
45+
const std::unordered_map<std::string, std::string>& descriptions = {});
46+
3847
} // namespace fastmcpp::mcp

0 commit comments

Comments
 (0)