Skip to content

Commit 3ce6213

Browse files
authored
feat: tasks CLI UX, sampling handlers, and server ergonomics (#24)
* 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 * ci: fix format check and POSIX build - Apply clang-format to touched files - Fix tasks_cli test on POSIX (sys/wait.h for WIFEXITED/WEXITSTATUS) * build: add missing standard headers * build: include <memory>/<utility> in cli * fix(posix): make ergonomics options overload-based * ci: limit unix build parallelism
1 parent aad196d commit 3ce6213

14 files changed

Lines changed: 1897 additions & 19 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
5050
- name: Build (Unix)
5151
if: runner.os != 'Windows'
52-
run: cmake --build build --config ${{ matrix.build_type }} --parallel
52+
run: cmake --build build --config ${{ matrix.build_type }} --parallel 2
5353

5454
- name: Build (Windows)
5555
if: runner.os == 'Windows'

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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,38 @@ 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",
6294
std::optional<std::string> website_url = std::nullopt,
@@ -117,6 +149,49 @@ 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, tools::Tool::Fn fn,
159+
ToolOptions options);
160+
FastMCP& tool(std::string name, const Json& input_schema_or_simple, tools::Tool::Fn fn);
161+
162+
/// Register a zero-argument tool (input schema defaults to {}).
163+
FastMCP& tool(std::string name, tools::Tool::Fn fn, ToolOptions options);
164+
FastMCP& tool(std::string name, tools::Tool::Fn fn);
165+
166+
/// Register a prompt generator (equivalent to Python's @server.prompt).
167+
FastMCP& prompt(std::string name,
168+
std::function<std::vector<prompts::PromptMessage>(const Json&)> generator,
169+
PromptOptions options);
170+
FastMCP& prompt(std::string name,
171+
std::function<std::vector<prompts::PromptMessage>(const Json&)> generator);
172+
173+
/// Register a template-backed prompt (legacy Prompt template string).
174+
FastMCP& prompt_template(std::string name, std::string template_string, PromptOptions options);
175+
FastMCP& prompt_template(std::string name, std::string template_string);
176+
177+
/// Register a concrete resource (equivalent to Python's @server.resource for fixed URIs).
178+
FastMCP& resource(std::string uri, std::string name,
179+
std::function<resources::ResourceContent(const Json&)> provider,
180+
ResourceOptions options);
181+
FastMCP& resource(std::string uri, std::string name,
182+
std::function<resources::ResourceContent(const Json&)> provider);
183+
184+
/// Register a resource template (equivalent to Python's @server.resource for templated URIs).
185+
/// If parameters_schema_or_simple is empty, parameters are derived from the URI template.
186+
FastMCP&
187+
resource_template(std::string uri_template, std::string name,
188+
std::function<resources::ResourceContent(const Json& params)> provider,
189+
const Json& parameters_schema_or_simple, ResourceTemplateOptions options);
190+
FastMCP&
191+
resource_template(std::string uri_template, std::string name,
192+
std::function<resources::ResourceContent(const Json& params)> provider,
193+
const Json& parameters_schema_or_simple = Json::object());
194+
120195
// =========================================================================
121196
// App Mounting
122197
// =========================================================================

include/fastmcpp/client/client.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ class Client
238238
/// @param options Call options (timeout, meta, progress handler)
239239
/// @return CallToolResult with content, error status, and metadata
240240
CallToolResult call_tool_mcp(const std::string& name, const fastmcpp::Json& arguments,
241-
const CallToolOptions& options = {})
241+
const CallToolOptions& options = CallToolOptions{})
242242
{
243243

244244
fastmcpp::Json payload = {{"name", name}, {"arguments", arguments}};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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

include/fastmcpp/client/types.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ 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;
145146
std::optional<std::vector<fastmcpp::Icon>> icons; ///< Icons for UI display
146147
std::optional<fastmcpp::Json> _meta; ///< Protocol metadata
@@ -398,6 +399,8 @@ inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
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"))

include/fastmcpp/server/context.hpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,22 +331,24 @@ class Context
331331
/// @param params Optional sampling parameters
332332
/// @return SamplingResult with text/image/audio content
333333
/// @throws std::runtime_error if sampling not available
334-
SamplingResult sample(const std::string& message, const SamplingParams& params = {}) const
334+
SamplingResult sample(const std::string& message,
335+
const SamplingParams& params = SamplingParams{}) const
335336
{
336337
std::vector<SamplingMessage> msgs = {{"user", message}};
337338
return sample(msgs, params);
338339
}
339340

340341
SamplingResult sample(const std::vector<SamplingMessage>& messages,
341-
const SamplingParams& params = {}) const
342+
const SamplingParams& params = SamplingParams{}) const
342343
{
343344
if (!sampling_callback_)
344345
throw std::runtime_error("Sampling not available: no sampling callback set");
345346
return sampling_callback_(messages, params);
346347
}
347348

348349
/// Convenience: sample and return just the text content
349-
std::string sample_text(const std::string& message, const SamplingParams& params = {}) const
350+
std::string sample_text(const std::string& message,
351+
const SamplingParams& params = SamplingParams{}) const
350352
{
351353
auto result = sample(message, params);
352354
return result.content;

0 commit comments

Comments
 (0)