Skip to content

Commit caf14a6

Browse files
committed
feat: sampling handlers, tasks CLI UX, and server ergonomics
- Add OpenAI-compatible + Anthropic sampling HTTP handlers (libcurl-backed) - Implement tasks CLI subcommands (list/get/cancel/result) + UX tests - Add FastMCP convenience registration helpers (tool/prompt/resource/resource_template) - Include template parameters in resources/templates/list
1 parent aad196d commit caf14a6

11 files changed

Lines changed: 1840 additions & 29 deletions

File tree

CMakeLists.txt

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ option(FASTMCPP_BUILD_TESTS "Build tests" ON)
99
option(FASTMCPP_BUILD_EXAMPLES "Build examples" ON)
1010
option(FASTMCPP_ENABLE_POST_STREAMING "Enable POST streaming via libcurl (optional)" OFF)
1111
option(FASTMCPP_FETCH_CURL "Fetch and build libcurl statically for POST streaming" ON)
12+
option(FASTMCPP_ENABLE_SAMPLING_HTTP_HANDLERS "Enable built-in OpenAI/Anthropic sampling handlers (requires libcurl)" OFF)
1213
option(FASTMCPP_ENABLE_WS_STREAMING_TESTS "Enable WebSocket streaming tests (requires external server)" OFF)
1314
option(FASTMCPP_ENABLE_LOCAL_WS_TEST "Enable local WebSocket server test (depends on httplib ws server support)" OFF)
1415

@@ -37,6 +38,7 @@ add_library(fastmcpp_core STATIC
3738
src/server/sse_server.cpp
3839
src/server/streamable_http_server.cpp
3940
src/client/client.cpp
41+
src/client/sampling_handlers.cpp
4042
src/client/transports.cpp
4143
src/util/json_schema.cpp
4244
src/util/json_schema_type.cpp
@@ -94,8 +96,8 @@ endif()
9496
target_include_directories(fastmcpp_core PUBLIC ${easywsclient_SOURCE_DIR})
9597
target_sources(fastmcpp_core PRIVATE ${easywsclient_SOURCE_DIR}/easywsclient.cpp)
9698

97-
# Optional: libcurl for POST streaming receive support (modular)
98-
if(FASTMCPP_ENABLE_POST_STREAMING)
99+
# Optional: libcurl for POST streaming and sampling handlers (modular)
100+
if(FASTMCPP_ENABLE_POST_STREAMING OR FASTMCPP_ENABLE_SAMPLING_HTTP_HANDLERS)
99101
if(FASTMCPP_FETCH_CURL)
100102
message(STATUS "FASTMCPP_FETCH_CURL=ON: fetching curl via FetchContent (static-only)")
101103
include(FetchContent)
@@ -151,10 +153,18 @@ if(FASTMCPP_ENABLE_POST_STREAMING)
151153
endif()
152154

153155
if(TARGET CURL::libcurl)
154-
target_compile_definitions(fastmcpp_core PRIVATE FASTMCPP_POST_STREAMING)
155156
target_link_libraries(fastmcpp_core PRIVATE CURL::libcurl)
157+
target_compile_definitions(fastmcpp_core PRIVATE FASTMCPP_HAS_CURL)
158+
if(FASTMCPP_ENABLE_POST_STREAMING)
159+
target_compile_definitions(fastmcpp_core PRIVATE FASTMCPP_POST_STREAMING)
160+
endif()
156161
else()
157-
message(STATUS "libcurl not found; POST streaming will be disabled at runtime (request_stream_post throws)")
162+
if(FASTMCPP_ENABLE_POST_STREAMING)
163+
message(STATUS "libcurl not found; POST streaming will be disabled at runtime (request_stream_post throws)")
164+
endif()
165+
if(FASTMCPP_ENABLE_SAMPLING_HTTP_HANDLERS)
166+
message(STATUS "libcurl not found; built-in sampling handlers will be unavailable")
167+
endif()
158168
endif()
159169
endif()
160170

@@ -222,6 +232,9 @@ if(FASTMCPP_BUILD_TESTS)
222232

223233
add_test(NAME fastmcpp_cli_sum COMMAND fastmcpp client sum 2 3)
224234
add_test(NAME fastmcpp_cli_tasks_help COMMAND fastmcpp tasks --help)
235+
add_test(NAME fastmcpp_cli_tasks_demo COMMAND fastmcpp tasks demo)
236+
add_executable(fastmcpp_cli_tasks_ux tests/cli/tasks_cli.cpp)
237+
add_test(NAME fastmcpp_cli_tasks_ux COMMAND fastmcpp_cli_tasks_ux)
225238

226239
add_executable(fastmcpp_http_integration tests/server/http_integration.cpp)
227240
target_link_libraries(fastmcpp_http_integration PRIVATE fastmcpp_core)
@@ -414,8 +427,12 @@ if(FASTMCPP_BUILD_TESTS)
414427
add_test(NAME fastmcpp_client_api_icons COMMAND fastmcpp_client_api_icons)
415428

416429
add_executable(fastmcpp_client_tasks tests/client/tasks.cpp)
417-
target_link_libraries(fastmcpp_client_tasks PRIVATE fastmcpp_core)
418-
add_test(NAME fastmcpp_client_tasks COMMAND fastmcpp_client_tasks)
430+
target_link_libraries(fastmcpp_client_tasks PRIVATE fastmcpp_core)
431+
add_test(NAME fastmcpp_client_tasks COMMAND fastmcpp_client_tasks)
432+
433+
add_executable(fastmcpp_client_sampling_handlers tests/client/sampling_handlers.cpp)
434+
target_link_libraries(fastmcpp_client_sampling_handlers PRIVATE fastmcpp_core)
435+
add_test(NAME fastmcpp_client_sampling_handlers COMMAND fastmcpp_client_sampling_handlers)
419436

420437
add_executable(fastmcpp_server_middleware tests/server/middleware.cpp)
421438
target_link_libraries(fastmcpp_server_middleware PRIVATE fastmcpp_core)
@@ -438,6 +455,11 @@ if(FASTMCPP_BUILD_TESTS)
438455
target_link_libraries(fastmcpp_app_mounting PRIVATE fastmcpp_core)
439456
add_test(NAME fastmcpp_app_mounting COMMAND fastmcpp_app_mounting)
440457

458+
# App ergonomics tests
459+
add_executable(fastmcpp_app_ergonomics tests/app/ergonomics.cpp)
460+
target_link_libraries(fastmcpp_app_ergonomics PRIVATE fastmcpp_core)
461+
add_test(NAME fastmcpp_app_ergonomics COMMAND fastmcpp_app_ergonomics)
462+
441463
# Proxy tests
442464
add_executable(fastmcpp_proxy_basic tests/proxy/basic.cpp)
443465
target_link_libraries(fastmcpp_proxy_basic PRIVATE fastmcpp_core)

include/fastmcpp/app.hpp

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,42 @@ struct ProxyMountedApp
5757
class FastMCP
5858
{
5959
public:
60+
struct ToolOptions
61+
{
62+
std::optional<std::string> title;
63+
std::optional<std::string> description;
64+
std::optional<std::vector<Icon>> icons;
65+
std::vector<std::string> exclude_args;
66+
TaskSupport task_support{TaskSupport::Forbidden};
67+
Json output_schema{Json::object()};
68+
};
69+
70+
struct PromptOptions
71+
{
72+
std::optional<std::string> description;
73+
std::optional<Json> meta;
74+
std::vector<prompts::PromptArgument> arguments;
75+
TaskSupport task_support{TaskSupport::Forbidden};
76+
};
77+
78+
struct ResourceOptions
79+
{
80+
std::optional<std::string> description;
81+
std::optional<std::string> mime_type;
82+
TaskSupport task_support{TaskSupport::Forbidden};
83+
};
84+
85+
struct ResourceTemplateOptions
86+
{
87+
std::optional<std::string> description;
88+
std::optional<std::string> mime_type;
89+
TaskSupport task_support{TaskSupport::Forbidden};
90+
};
91+
6092
/// Construct app with metadata
6193
explicit FastMCP(std::string name = "fastmcpp_app", std::string version = "1.0.0",
62-
std::optional<std::string> website_url = std::nullopt,
63-
std::optional<std::vector<Icon>> icons = std::nullopt);
94+
std::optional<std::string> website_url = std::nullopt,
95+
std::optional<std::vector<Icon>> icons = std::nullopt);
6496

6597
// Metadata accessors
6698
const std::string& name() const
@@ -117,6 +149,40 @@ class FastMCP
117149
return server_;
118150
}
119151

152+
// =========================================================================
153+
// Ergonomic registration helpers (Python FastMCP decorator-style analogs)
154+
// =========================================================================
155+
156+
/// Register a tool using either a full JSON Schema or a "simple" param map
157+
/// (e.g., {"a":"number","b":"integer"}).
158+
FastMCP& tool(std::string name, const Json& input_schema_or_simple,
159+
tools::Tool::Fn fn, ToolOptions options = {});
160+
161+
/// Register a zero-argument tool (input schema defaults to {}).
162+
FastMCP& tool(std::string name, tools::Tool::Fn fn, ToolOptions options = {});
163+
164+
/// Register a prompt generator (equivalent to Python's @server.prompt).
165+
FastMCP& prompt(std::string name,
166+
std::function<std::vector<prompts::PromptMessage>(const Json&)> generator,
167+
PromptOptions options = {});
168+
169+
/// Register a template-backed prompt (legacy Prompt template string).
170+
FastMCP& prompt_template(std::string name, std::string template_string,
171+
PromptOptions options = {});
172+
173+
/// Register a concrete resource (equivalent to Python's @server.resource for fixed URIs).
174+
FastMCP& resource(std::string uri, std::string name,
175+
std::function<resources::ResourceContent(const Json&)> provider,
176+
ResourceOptions options = {});
177+
178+
/// Register a resource template (equivalent to Python's @server.resource for templated URIs).
179+
/// If parameters_schema_or_simple is empty, parameters are derived from the URI template.
180+
FastMCP& resource_template(
181+
std::string uri_template, std::string name,
182+
std::function<resources::ResourceContent(const Json& params)> provider,
183+
const Json& parameters_schema_or_simple = Json::object(),
184+
ResourceTemplateOptions options = {});
185+
120186
// =========================================================================
121187
// App Mounting
122188
// =========================================================================
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#pragma once
2+
3+
#include "fastmcpp/types.hpp"
4+
5+
#include <functional>
6+
#include <optional>
7+
#include <string>
8+
9+
namespace fastmcpp::client::sampling::handlers
10+
{
11+
12+
struct OpenAICompatibleOptions
13+
{
14+
std::string base_url = "https://api.openai.com";
15+
std::string endpoint_path = "/v1/chat/completions";
16+
17+
std::optional<std::string> api_key;
18+
std::string api_key_env = "OPENAI_API_KEY";
19+
20+
std::string default_model = "gpt-4o-mini";
21+
std::optional<std::string> organization;
22+
std::optional<std::string> project;
23+
24+
int timeout_ms = 60000;
25+
};
26+
27+
/// Create a sampling/createMessage callback that calls an OpenAI-compatible
28+
/// chat completions endpoint and returns MCP CreateMessageResult(+WithTools).
29+
std::function<fastmcpp::Json(const fastmcpp::Json&)>
30+
create_openai_compatible_sampling_callback(OpenAICompatibleOptions options);
31+
32+
struct AnthropicOptions
33+
{
34+
std::string base_url = "https://api.anthropic.com";
35+
std::string endpoint_path = "/v1/messages";
36+
37+
std::optional<std::string> api_key;
38+
std::string api_key_env = "ANTHROPIC_API_KEY";
39+
40+
std::string default_model = "claude-sonnet-4-5";
41+
std::string anthropic_version = "2023-06-01";
42+
43+
int timeout_ms = 60000;
44+
};
45+
46+
/// Create a sampling/createMessage callback that calls the Anthropic Messages
47+
/// API and returns MCP CreateMessageResult(+WithTools).
48+
std::function<fastmcpp::Json(const fastmcpp::Json&)>
49+
create_anthropic_sampling_callback(AnthropicOptions options);
50+
51+
} // namespace fastmcpp::client::sampling::handlers
52+

include/fastmcpp/client/types.hpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,10 @@ struct ResourceTemplate
141141
std::optional<std::string> title; ///< Human-readable title
142142
std::optional<std::string> description;
143143
std::optional<std::string> mimeType;
144+
std::optional<fastmcpp::Json> parameters; ///< JSON Schema for template parameters
144145
std::optional<fastmcpp::Json> annotations;
145-
std::optional<std::vector<fastmcpp::Icon>> icons; ///< Icons for UI display
146-
std::optional<fastmcpp::Json> _meta; ///< Protocol metadata
146+
std::optional<std::vector<fastmcpp::Icon>> icons; ///< Icons for UI display
147+
std::optional<fastmcpp::Json> _meta; ///< Protocol metadata
147148
};
148149

149150
/// Text resource content
@@ -391,13 +392,15 @@ inline void from_json(const fastmcpp::Json& j, ResourceInfo& r)
391392

392393
inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
393394
{
394-
j = fastmcpp::Json{{"uriTemplate", t.uriTemplate}, {"name", t.name}};
395+
j = fastmcpp::Json{{"uriTemplate", t.uriTemplate}, {"name", t.name}};
395396
if (t.title)
396397
j["title"] = *t.title;
397398
if (t.description)
398399
j["description"] = *t.description;
399400
if (t.mimeType)
400401
j["mimeType"] = *t.mimeType;
402+
if (t.parameters)
403+
j["parameters"] = *t.parameters;
401404
if (t.annotations)
402405
j["annotations"] = *t.annotations;
403406
if (t.icons)
@@ -416,6 +419,8 @@ inline void from_json(const fastmcpp::Json& j, ResourceTemplate& t)
416419
t.description = j["description"].get<std::string>();
417420
if (j.contains("mimeType"))
418421
t.mimeType = j["mimeType"].get<std::string>();
422+
if (j.contains("parameters"))
423+
t.parameters = j["parameters"];
419424
if (j.contains("annotations"))
420425
t.annotations = j["annotations"];
421426
if (j.contains("icons"))

src/app.cpp

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include "fastmcpp/client/types.hpp"
55
#include "fastmcpp/exceptions.hpp"
66
#include "fastmcpp/mcp/handler.hpp"
7+
#include "fastmcpp/resources/template.hpp"
8+
#include "fastmcpp/util/schema_build.hpp"
79

810
#include <unordered_set>
911

@@ -16,7 +18,125 @@ FastMCP::FastMCP(std::string name, std::string version, std::optional<std::strin
1618
{
1719
}
1820

19-
void FastMCP::mount(FastMCP& app, const std::string& prefix, bool as_proxy)
21+
namespace
22+
{
23+
fastmcpp::Json schema_from_schema_or_simple(const fastmcpp::Json& schema_or_simple)
24+
{
25+
return fastmcpp::util::schema_build::to_object_schema_from_simple(schema_or_simple);
26+
}
27+
28+
fastmcpp::Json build_resource_template_parameters_schema(const std::string& uri_template)
29+
{
30+
const auto path_params = fastmcpp::resources::extract_path_params(uri_template);
31+
const auto query_params = fastmcpp::resources::extract_query_params(uri_template);
32+
33+
fastmcpp::Json properties = fastmcpp::Json::object();
34+
fastmcpp::Json required = fastmcpp::Json::array();
35+
36+
for (const auto& p : path_params)
37+
{
38+
properties[p] = fastmcpp::Json{{"type", "string"}};
39+
required.push_back(p);
40+
}
41+
for (const auto& p : query_params)
42+
properties[p] = fastmcpp::Json{{"type", "string"}};
43+
44+
return fastmcpp::Json{
45+
{"type", "object"},
46+
{"properties", properties},
47+
{"required", required},
48+
};
49+
}
50+
} // namespace
51+
52+
FastMCP& FastMCP::tool(std::string name, const Json& input_schema_or_simple, tools::Tool::Fn fn,
53+
ToolOptions options)
54+
{
55+
auto input_schema = schema_from_schema_or_simple(input_schema_or_simple);
56+
57+
tools::Tool t{std::move(name),
58+
std::move(input_schema),
59+
std::move(options.output_schema),
60+
std::move(fn),
61+
std::move(options.title),
62+
std::move(options.description),
63+
std::move(options.icons),
64+
std::move(options.exclude_args),
65+
options.task_support};
66+
67+
tools_.register_tool(t);
68+
return *this;
69+
}
70+
71+
FastMCP& FastMCP::tool(std::string name, tools::Tool::Fn fn, ToolOptions options)
72+
{
73+
return tool(std::move(name), Json::object(), std::move(fn), std::move(options));
74+
}
75+
76+
FastMCP& FastMCP::prompt(std::string name,
77+
std::function<std::vector<prompts::PromptMessage>(const Json&)> generator,
78+
PromptOptions options)
79+
{
80+
prompts::Prompt p;
81+
p.name = std::move(name);
82+
p.description = std::move(options.description);
83+
p.meta = std::move(options.meta);
84+
p.arguments = std::move(options.arguments);
85+
p.generator = std::move(generator);
86+
p.task_support = options.task_support;
87+
prompts_.register_prompt(p);
88+
return *this;
89+
}
90+
91+
FastMCP& FastMCP::prompt_template(std::string name, std::string template_string, PromptOptions options)
92+
{
93+
prompts::Prompt p{std::move(template_string)};
94+
p.name = std::move(name);
95+
p.description = std::move(options.description);
96+
p.meta = std::move(options.meta);
97+
p.arguments = std::move(options.arguments);
98+
p.task_support = options.task_support;
99+
prompts_.register_prompt(p);
100+
return *this;
101+
}
102+
103+
FastMCP& FastMCP::resource(std::string uri, std::string name,
104+
std::function<resources::ResourceContent(const Json&)> provider,
105+
ResourceOptions options)
106+
{
107+
resources::Resource r;
108+
r.uri = std::move(uri);
109+
r.name = std::move(name);
110+
r.description = std::move(options.description);
111+
r.mime_type = std::move(options.mime_type);
112+
r.provider = std::move(provider);
113+
r.task_support = options.task_support;
114+
resources_.register_resource(r);
115+
return *this;
116+
}
117+
118+
FastMCP& FastMCP::resource_template(
119+
std::string uri_template, std::string name,
120+
std::function<resources::ResourceContent(const Json& params)> provider,
121+
const Json& parameters_schema_or_simple, ResourceTemplateOptions options)
122+
{
123+
resources::ResourceTemplate templ;
124+
templ.uri_template = std::move(uri_template);
125+
templ.name = std::move(name);
126+
templ.description = std::move(options.description);
127+
templ.mime_type = std::move(options.mime_type);
128+
templ.provider = std::move(provider);
129+
130+
if (parameters_schema_or_simple.is_object() && parameters_schema_or_simple.empty())
131+
templ.parameters = build_resource_template_parameters_schema(templ.uri_template);
132+
else
133+
templ.parameters = schema_from_schema_or_simple(parameters_schema_or_simple);
134+
135+
resources_.register_template(std::move(templ));
136+
return *this;
137+
}
138+
139+
void FastMCP::mount(FastMCP& app, const std::string& prefix, bool as_proxy)
20140
{
21141
mount(app, prefix, as_proxy, std::nullopt);
22142
}

0 commit comments

Comments
 (0)