Skip to content

Commit 70f6b39

Browse files
authored
Implement full MCP Client API with meta support (#5)
Implements full MCP Client API with meta parameter support (Issue #2) Features: - 20+ client methods matching Python fastmcp: list_tools, call_tool, list_resources, read_resource, list_prompts, get_prompt, complete - Meta parameter support for all tool calls (Issue #2) - Both convenience APIs and _mcp variants returning raw protocol types - New files: client/types.hpp, tests/client_api.cpp, examples/client_api.cpp - Full test coverage with 18 test cases Closes #2
1 parent 3d56623 commit 70f6b39

5 files changed

Lines changed: 2041 additions & 5 deletions

File tree

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ if(FASTMCPP_BUILD_TESTS)
220220
target_link_libraries(fastmcpp_client_transports PRIVATE fastmcpp_core)
221221
add_test(NAME fastmcpp_client_transports COMMAND fastmcpp_client_transports)
222222

223+
add_executable(fastmcpp_client_api tests/client_api.cpp)
224+
target_link_libraries(fastmcpp_client_api PRIVATE fastmcpp_core)
225+
add_test(NAME fastmcpp_client_api COMMAND fastmcpp_client_api)
226+
223227
add_executable(fastmcpp_server_middleware tests/server_middleware.cpp)
224228
target_link_libraries(fastmcpp_server_middleware PRIVATE fastmcpp_core)
225229
add_test(NAME fastmcpp_server_middleware COMMAND fastmcpp_server_middleware)
@@ -269,6 +273,9 @@ if(FASTMCPP_BUILD_EXAMPLES)
269273
add_executable(fastmcpp_example_client examples/client_quick_start.cpp)
270274
target_link_libraries(fastmcpp_example_client PRIVATE fastmcpp_core)
271275

276+
add_executable(fastmcpp_example_client_api examples/client_api.cpp)
277+
target_link_libraries(fastmcpp_example_client_api PRIVATE fastmcpp_core)
278+
272279
# Removed outdated server_toolmanager_handler example target (file no longer exists)
273280

274281
add_executable(fastmcpp_example_stdio examples/simple_echo.cpp)

examples/client_api.cpp

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/// @file examples/client_api.cpp
2+
/// @brief Example demonstrating the full MCP Client API
3+
/// @details Shows how to use list_tools, call_tool with meta, list_resources, etc.
4+
5+
#include <iostream>
6+
#include <iomanip>
7+
#include "fastmcpp/client/client.hpp"
8+
#include "fastmcpp/server/server.hpp"
9+
10+
using namespace fastmcpp;
11+
12+
/// Create a sample MCP server with tools, resources, and prompts
13+
std::shared_ptr<server::Server> create_sample_server() {
14+
auto srv = std::make_shared<server::Server>();
15+
16+
// ========================================================================
17+
// Tools
18+
// ========================================================================
19+
20+
srv->route("tools/list", [](const Json&) {
21+
return Json{{"tools", Json::array({
22+
{{"name", "calculate"}, {"description", "Perform arithmetic"}, {"inputSchema", {
23+
{"type", "object"},
24+
{"properties", {
25+
{"operation", {{"type", "string"}, {"enum", {"add", "subtract", "multiply", "divide"}}}},
26+
{"a", {{"type", "number"}}},
27+
{"b", {{"type", "number"}}}
28+
}},
29+
{"required", {"operation", "a", "b"}}
30+
}}},
31+
{{"name", "echo"}, {"description", "Echo back input with metadata"}, {"inputSchema", {
32+
{"type", "object"},
33+
{"properties", {{"message", {{"type", "string"}}}}}
34+
}}}
35+
})}};
36+
});
37+
38+
srv->route("tools/call", [](const Json& in) {
39+
std::string name = in.at("name").get<std::string>();
40+
Json args = in.value("arguments", Json::object());
41+
Json meta = in.value("_meta", Json(nullptr));
42+
43+
if (name == "calculate") {
44+
std::string op = args.at("operation").get<std::string>();
45+
double a = args.at("a").get<double>();
46+
double b = args.at("b").get<double>();
47+
double result = 0;
48+
if (op == "add") result = a + b;
49+
else if (op == "subtract") result = a - b;
50+
else if (op == "multiply") result = a * b;
51+
else if (op == "divide") result = (b != 0) ? a / b : 0;
52+
53+
return Json{
54+
{"content", Json::array({{{"type", "text"}, {"text", std::to_string(result)}}})},
55+
{"isError", false}
56+
};
57+
}
58+
else if (name == "echo") {
59+
std::string msg = args.value("message", "");
60+
Json response_content = {{"message", msg}};
61+
62+
// If meta was provided, include it in response
63+
if (!meta.is_null()) {
64+
response_content["received_meta"] = meta;
65+
}
66+
67+
return Json{
68+
{"content", Json::array({{{"type", "text"}, {"text", response_content.dump()}}})},
69+
{"isError", false},
70+
{"_meta", meta}
71+
};
72+
}
73+
74+
return Json{
75+
{"content", Json::array({{{"type", "text"}, {"text", "Unknown tool"}}})},
76+
{"isError", true}
77+
};
78+
});
79+
80+
// ========================================================================
81+
// Resources
82+
// ========================================================================
83+
84+
srv->route("resources/list", [](const Json&) {
85+
return Json{{"resources", Json::array({
86+
{{"uri", "config://app/settings"}, {"name", "App Settings"}, {"mimeType", "application/json"}},
87+
{{"uri", "file:///docs/readme.md"}, {"name", "README"}, {"mimeType", "text/markdown"}}
88+
})}};
89+
});
90+
91+
srv->route("resources/read", [](const Json& in) {
92+
std::string uri = in.at("uri").get<std::string>();
93+
if (uri == "config://app/settings") {
94+
return Json{{"contents", Json::array({
95+
{{"uri", uri}, {"mimeType", "application/json"},
96+
{"text", R"({"theme": "dark", "language": "en"})"}}
97+
})}};
98+
}
99+
return Json{{"contents", Json::array()}};
100+
});
101+
102+
// ========================================================================
103+
// Prompts
104+
// ========================================================================
105+
106+
srv->route("prompts/list", [](const Json&) {
107+
return Json{{"prompts", Json::array({
108+
{{"name", "code_review"}, {"description", "Review code for best practices"}},
109+
{{"name", "explain"}, {"description", "Explain a concept"}, {"arguments", Json::array({
110+
{{"name", "topic"}, {"description", "Topic to explain"}, {"required", true}}
111+
})}}
112+
})}};
113+
});
114+
115+
srv->route("prompts/get", [](const Json& in) {
116+
std::string name = in.at("name").get<std::string>();
117+
if (name == "code_review") {
118+
return Json{
119+
{"description", "Code review prompt"},
120+
{"messages", Json::array({
121+
{{"role", "user"}, {"content", "Please review the following code..."}}
122+
})}
123+
};
124+
}
125+
return Json{{"messages", Json::array()}};
126+
});
127+
128+
return srv;
129+
}
130+
131+
void print_separator(const std::string& title) {
132+
std::cout << "\n" << std::string(60, '=') << "\n";
133+
std::cout << " " << title << "\n";
134+
std::cout << std::string(60, '=') << "\n\n";
135+
}
136+
137+
int main() {
138+
std::cout << "fastmcpp Client API Example\n";
139+
std::cout << "(Demonstrates metadata support in tool calls)\n";
140+
141+
// Create server and client
142+
auto server = create_sample_server();
143+
client::Client c(std::make_unique<client::LoopbackTransport>(server));
144+
145+
// ========================================================================
146+
print_separator("1. List Tools");
147+
// ========================================================================
148+
149+
auto tools = c.list_tools();
150+
std::cout << "Available tools (" << tools.size() << "):\n";
151+
for (const auto& tool : tools) {
152+
std::cout << " - " << tool.name;
153+
if (tool.description) {
154+
std::cout << ": " << *tool.description;
155+
}
156+
std::cout << "\n";
157+
}
158+
159+
// ========================================================================
160+
print_separator("2. Call Tool (Basic)");
161+
// ========================================================================
162+
163+
auto calc_result = c.call_tool("calculate", {
164+
{"operation", "multiply"},
165+
{"a", 7},
166+
{"b", 6}
167+
});
168+
169+
std::cout << "7 * 6 = ";
170+
if (!calc_result.content.empty()) {
171+
if (auto* text = std::get_if<client::TextContent>(&calc_result.content[0])) {
172+
std::cout << text->text << "\n";
173+
}
174+
}
175+
176+
// ========================================================================
177+
print_separator("3. Call Tool with Metadata");
178+
// ========================================================================
179+
180+
std::cout << "Calling 'echo' tool with metadata:\n";
181+
std::cout << " meta: {user_id: 'user-123', trace_id: 'trace-abc', tenant: 'acme'}\n\n";
182+
183+
Json meta = {
184+
{"user_id", "user-123"},
185+
{"trace_id", "trace-abc"},
186+
{"tenant", "acme"}
187+
};
188+
189+
auto echo_result = c.call_tool("echo", {{"message", "Hello, World!"}}, meta);
190+
191+
std::cout << "Response:\n";
192+
if (!echo_result.content.empty()) {
193+
if (auto* text = std::get_if<client::TextContent>(&echo_result.content[0])) {
194+
std::cout << " Content: " << text->text << "\n";
195+
}
196+
}
197+
if (echo_result.meta) {
198+
std::cout << " Meta preserved: " << echo_result.meta->dump() << "\n";
199+
}
200+
201+
// ========================================================================
202+
print_separator("4. Call Tool with CallToolOptions");
203+
// ========================================================================
204+
205+
client::CallToolOptions opts;
206+
opts.meta = {{"request_id", "req-001"}, {"priority", "high"}};
207+
opts.timeout = std::chrono::milliseconds{5000};
208+
209+
auto opts_result = c.call_tool_mcp("calculate", {
210+
{"operation", "add"},
211+
{"a", 100},
212+
{"b", 200}
213+
}, opts);
214+
215+
std::cout << "100 + 200 = ";
216+
if (!opts_result.content.empty()) {
217+
if (auto* text = std::get_if<client::TextContent>(&opts_result.content[0])) {
218+
std::cout << text->text << "\n";
219+
}
220+
}
221+
std::cout << "Request metadata: " << opts.meta->dump() << "\n";
222+
223+
// ========================================================================
224+
print_separator("5. List Resources");
225+
// ========================================================================
226+
227+
auto resources = c.list_resources();
228+
std::cout << "Available resources (" << resources.size() << "):\n";
229+
for (const auto& res : resources) {
230+
std::cout << " - " << res.name << " (" << res.uri << ")";
231+
if (res.mimeType) {
232+
std::cout << " [" << *res.mimeType << "]";
233+
}
234+
std::cout << "\n";
235+
}
236+
237+
// ========================================================================
238+
print_separator("6. Read Resource");
239+
// ========================================================================
240+
241+
auto contents = c.read_resource("config://app/settings");
242+
std::cout << "Reading 'config://app/settings':\n";
243+
for (const auto& content : contents) {
244+
if (auto* text = std::get_if<client::TextResourceContent>(&content)) {
245+
std::cout << " Content: " << text->text << "\n";
246+
}
247+
}
248+
249+
// ========================================================================
250+
print_separator("7. List Prompts");
251+
// ========================================================================
252+
253+
auto prompts = c.list_prompts();
254+
std::cout << "Available prompts (" << prompts.size() << "):\n";
255+
for (const auto& prompt : prompts) {
256+
std::cout << " - " << prompt.name;
257+
if (prompt.description) {
258+
std::cout << ": " << *prompt.description;
259+
}
260+
if (prompt.arguments && !prompt.arguments->empty()) {
261+
std::cout << " (args: ";
262+
for (size_t i = 0; i < prompt.arguments->size(); ++i) {
263+
if (i > 0) std::cout << ", ";
264+
std::cout << (*prompt.arguments)[i].name;
265+
}
266+
std::cout << ")";
267+
}
268+
std::cout << "\n";
269+
}
270+
271+
// ========================================================================
272+
print_separator("8. Get Prompt");
273+
// ========================================================================
274+
275+
auto prompt_result = c.get_prompt("code_review");
276+
std::cout << "Prompt 'code_review':\n";
277+
if (prompt_result.description) {
278+
std::cout << " Description: " << *prompt_result.description << "\n";
279+
}
280+
std::cout << " Messages: " << prompt_result.messages.size() << "\n";
281+
for (const auto& msg : prompt_result.messages) {
282+
std::cout << " [" << (msg.role == client::Role::User ? "user" : "assistant") << "]: ";
283+
if (!msg.content.empty()) {
284+
if (auto* text = std::get_if<client::TextContent>(&msg.content[0])) {
285+
std::cout << text->text;
286+
}
287+
}
288+
std::cout << "\n";
289+
}
290+
291+
// ========================================================================
292+
print_separator("Summary");
293+
// ========================================================================
294+
295+
std::cout << "This example demonstrated:\n";
296+
std::cout << " - list_tools() / list_tools_mcp()\n";
297+
std::cout << " - call_tool() with optional meta parameter\n";
298+
std::cout << " - call_tool_mcp() with CallToolOptions\n";
299+
std::cout << " - list_resources() / read_resource()\n";
300+
std::cout << " - list_prompts() / get_prompt()\n";
301+
std::cout << "\nThe 'meta' parameter allows passing contextual information\n";
302+
std::cout << "(user IDs, trace IDs, tenant info) that servers can access\n";
303+
std::cout << "for logging, authorization, or request routing.\n";
304+
305+
return 0;
306+
}

0 commit comments

Comments
 (0)