Skip to content

Commit aad196d

Browse files
authored
feat: static curl build + POST streaming transport test (#23)
* cmake: fetch libcurl when requested Implements FASTMCPP_FETCH_CURL via FetchContent when POST streaming is enabled and curl isn't available. Also aligns README version/options wording. * cmake: make fetched curl static + fix POST stream error - Prefer libcurl_static and avoid requiring a runtime DLL - Treat CURLE_PARTIAL_FILE as success for SSE-style POST streams * build(windows): avoid libcurl-d.dll loader errors Link fetched curl statically, and add a build-time helper to copy the curl DLL next to executables when it exists. * build: always avoid curl DLL dependency Remove the post-build DLL copy helper; the FetchContent curl build is kept static so examples/tests don't require libcurl-d.dll. * cmake: always fetch static curl when requested When FASTMCPP_ENABLE_POST_STREAMING=ON and FASTMCPP_FETCH_CURL=ON, skip find_package() and build curl via FetchContent as a static library. Also gates the POST streaming demo test on TARGET CURL::libcurl. * test(post-stream): add POST streaming transport test * style: apply clang-format
1 parent dd2468b commit aad196d

11 files changed

Lines changed: 211 additions & 45 deletions

File tree

CMakeLists.txt

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ set(CMAKE_CXX_EXTENSIONS OFF)
88
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)
11-
option(FASTMCPP_FETCH_CURL "Attempt to fetch libcurl if not found (experimental)" OFF)
11+
option(FASTMCPP_FETCH_CURL "Fetch and build libcurl statically for POST streaming" ON)
1212
option(FASTMCPP_ENABLE_WS_STREAMING_TESTS "Enable WebSocket streaming tests (requires external server)" OFF)
1313
option(FASTMCPP_ENABLE_LOCAL_WS_TEST "Enable local WebSocket server test (depends on httplib ws server support)" OFF)
1414

15-
add_library(fastmcpp_core
15+
add_library(fastmcpp_core STATIC
1616
src/types.cpp
1717
src/util/schema_build.cpp
1818
src/app.cpp
@@ -96,11 +96,61 @@ target_sources(fastmcpp_core PRIVATE ${easywsclient_SOURCE_DIR}/easywsclient.cpp
9696

9797
# Optional: libcurl for POST streaming receive support (modular)
9898
if(FASTMCPP_ENABLE_POST_STREAMING)
99-
find_package(CURL)
100-
if(NOT CURL_FOUND AND FASTMCPP_FETCH_CURL)
101-
message(STATUS "CURL not found; FASTMCPP_FETCH_CURL requested (skipping auto-fetch in this build). Please provide CURL via your toolchain or package manager.")
99+
if(FASTMCPP_FETCH_CURL)
100+
message(STATUS "FASTMCPP_FETCH_CURL=ON: fetching curl via FetchContent (static-only)")
101+
include(FetchContent)
102+
103+
# Configure curl for a minimal library build.
104+
set(BUILD_CURL_EXE OFF CACHE BOOL "Build curl executable" FORCE)
105+
set(CURL_DISABLE_TESTS ON CACHE BOOL "Disable curl tests" FORCE)
106+
set(CURL_DISABLE_INSTALL ON CACHE BOOL "Disable curl install targets" FORCE)
107+
set(CURL_DISABLE_LDAP ON CACHE BOOL "Disable LDAP support" FORCE)
108+
109+
# Prefer platform TLS backends to avoid OpenSSL as a dependency.
110+
if(WIN32)
111+
set(CURL_USE_SCHANNEL ON CACHE BOOL "Use Windows Schannel for TLS" FORCE)
112+
set(CURL_USE_OPENSSL OFF CACHE BOOL "Do not use OpenSSL" FORCE)
113+
endif()
114+
115+
# Always build curl statically to avoid runtime DLL dependencies.
116+
if(DEFINED BUILD_SHARED_LIBS)
117+
set(_fastmcpp_prev_build_shared_libs "${BUILD_SHARED_LIBS}")
118+
else()
119+
set(_fastmcpp_prev_build_shared_libs "__UNDEFINED__")
120+
endif()
121+
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries" FORCE)
122+
123+
FetchContent_Declare(
124+
curl
125+
GIT_REPOSITORY https://github.com/curl/curl.git
126+
GIT_TAG curl-8_9_1
127+
)
128+
FetchContent_MakeAvailable(curl)
129+
130+
if(TARGET libcurl_static AND NOT TARGET CURL::libcurl)
131+
add_library(CURL::libcurl ALIAS libcurl_static)
132+
endif()
133+
134+
if(_fastmcpp_prev_build_shared_libs STREQUAL "__UNDEFINED__")
135+
unset(BUILD_SHARED_LIBS CACHE)
136+
else()
137+
set(BUILD_SHARED_LIBS "${_fastmcpp_prev_build_shared_libs}" CACHE BOOL "Build shared libraries" FORCE)
138+
endif()
139+
else()
140+
# Best effort for users who provide a curl toolchain: request static linkage.
141+
set(CURL_USE_STATIC_LIBS ON)
142+
find_package(CURL QUIET)
143+
144+
if(CURL_FOUND AND NOT TARGET CURL::libcurl)
145+
add_library(CURL::libcurl UNKNOWN IMPORTED)
146+
set_target_properties(CURL::libcurl PROPERTIES
147+
IMPORTED_LOCATION "${CURL_LIBRARY}"
148+
INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIRS}"
149+
)
150+
endif()
102151
endif()
103-
if(CURL_FOUND)
152+
153+
if(TARGET CURL::libcurl)
104154
target_compile_definitions(fastmcpp_core PRIVATE FASTMCPP_POST_STREAMING)
105155
target_link_libraries(fastmcpp_core PRIVATE CURL::libcurl)
106156
else()
@@ -414,18 +464,26 @@ if(FASTMCPP_BUILD_TESTS)
414464
endif()
415465

416466
if(FASTMCPP_ENABLE_WS_STREAMING_TESTS)
417-
add_executable(fastmcpp_ws_streaming tests/transports/ws_streaming.cpp)
467+
add_executable(fastmcpp_ws_streaming tests/transports/ws_streaming.cpp)
418468
target_link_libraries(fastmcpp_ws_streaming PRIVATE fastmcpp_core)
419469
add_test(NAME fastmcpp_ws_streaming COMMAND fastmcpp_ws_streaming)
420470
# Test auto-skips if FASTMCPP_WS_URL is not set
421471

422472
if(FASTMCPP_ENABLE_LOCAL_WS_TEST)
423473
add_executable(fastmcpp_ws_streaming_local tests/transports/ws_streaming_local.cpp)
424-
target_link_libraries(fastmcpp_ws_streaming_local PRIVATE fastmcpp_core)
474+
target_link_libraries(fastmcpp_ws_streaming_local PRIVATE fastmcpp_core)
425475
add_test(NAME fastmcpp_ws_streaming_local COMMAND fastmcpp_ws_streaming_local)
426476
set_tests_properties(fastmcpp_ws_streaming_local PROPERTIES RUN_SERIAL TRUE)
427477
endif()
428478
endif()
479+
480+
# POST streaming transport test (requires libcurl)
481+
if(FASTMCPP_ENABLE_POST_STREAMING AND TARGET CURL::libcurl)
482+
add_executable(fastmcpp_post_streaming tests/transports/post_streaming.cpp)
483+
target_link_libraries(fastmcpp_post_streaming PRIVATE fastmcpp_core)
484+
add_test(NAME fastmcpp_post_streaming COMMAND fastmcpp_post_streaming)
485+
set_tests_properties(fastmcpp_post_streaming PROPERTIES RUN_SERIAL TRUE)
486+
endif()
429487
endif()
430488

431489
if(FASTMCPP_BUILD_EXAMPLES)
@@ -506,7 +564,7 @@ if(FASTMCPP_BUILD_EXAMPLES)
506564
if(FASTMCPP_ENABLE_POST_STREAMING)
507565
add_executable(fastmcpp_example_streaming_post_demo examples/streaming_post_demo.cpp)
508566
target_link_libraries(fastmcpp_example_streaming_post_demo PRIVATE fastmcpp_core)
509-
if(CURL_FOUND)
567+
if(TARGET CURL::libcurl)
510568
add_test(NAME fastmcpp_example_streaming_post_demo COMMAND fastmcpp_example_streaming_post_demo)
511569
else()
512570
message(STATUS "libcurl not found; skipping test fastmcpp_example_streaming_post_demo")

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fastmcpp is a C++ port of the Python [fastmcp](https://github.com/jlowin/fastmcp
1515

1616
**Status:** Beta – core MCP features track the Python `fastmcp` reference.
1717

18-
**Current version:** 2.14.0
18+
**Current version:** 2.14.1
1919

2020
## Features
2121

@@ -42,7 +42,7 @@ fastmcpp is a C++ port of the Python [fastmcp](https://github.com/jlowin/fastmcp
4242

4343
Optional:
4444

45-
- libcurl (for HTTP POST streaming).
45+
- libcurl (for HTTP POST streaming; can be fetched when `FASTMCPP_FETCH_CURL=ON`).
4646
- cpp‑httplib (HTTP server, fetched automatically).
4747
- easywsclient (WebSocket client, fetched automatically).
4848

@@ -75,7 +75,7 @@ Key options:
7575
|----------------------------------|---------|--------------------------------------------------|
7676
| `CMAKE_BUILD_TYPE` | Debug | Build configuration (Debug/Release/RelWithDebInfo) |
7777
| `FASTMCPP_ENABLE_POST_STREAMING` | OFF | Enable HTTP POST streaming (requires libcurl) |
78-
| `FASTMCPP_FETCH_CURL` | OFF | Fetch and build curl if not found |
78+
| `FASTMCPP_FETCH_CURL` | OFF | Fetch and build curl (via FetchContent) if not found |
7979
| `FASTMCPP_ENABLE_STREAMING_TESTS` | OFF | Enable SSE streaming tests |
8080
| `FASTMCPP_ENABLE_WS_STREAMING_TESTS` | OFF | Enable WebSocket streaming tests |
8181

include/fastmcpp/client/client.hpp

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ class IResettableTransport
5959
using ServerRequestHandler =
6060
std::function<fastmcpp::Json(const std::string& method, const fastmcpp::Json& params)>;
6161

62-
/// Optional transport interface: some transports can accept server-initiated requests and send responses.
62+
/// Optional transport interface: some transports can accept server-initiated requests and send
63+
/// responses.
6364
class IServerRequestTransport
6465
{
6566
public:
@@ -663,7 +664,7 @@ class Client
663664
}
664665

665666
/// Register roots/sampling/elicitation callbacks (placeholders for parity)
666-
void set_roots_callback(const std::function<fastmcpp::Json()>& cb)
667+
void set_roots_callback(const std::function<fastmcpp::Json()>& cb)
667668
{
668669
set_roots_callback_impl(cb);
669670
}
@@ -714,7 +715,7 @@ class Client
714715
};
715716

716717
std::shared_ptr<CallbackState> callbacks_;
717-
std::unordered_map<std::string, fastmcpp::Json> tool_output_schemas_;
718+
std::unordered_map<std::string, fastmcpp::Json> tool_output_schemas_;
718719

719720
std::function<fastmcpp::Json()> get_roots_callback() const
720721
{
@@ -745,16 +746,15 @@ class Client
745746
std::lock_guard<std::mutex> lock(callbacks_->mutex);
746747
callbacks_->roots_callback = cb;
747748
}
748-
void set_sampling_callback_impl(
749-
const std::function<fastmcpp::Json(const fastmcpp::Json&)>& cb)
749+
void set_sampling_callback_impl(const std::function<fastmcpp::Json(const fastmcpp::Json&)>& cb)
750750
{
751751
if (!callbacks_)
752752
callbacks_ = std::make_shared<CallbackState>();
753753
std::lock_guard<std::mutex> lock(callbacks_->mutex);
754754
callbacks_->sampling_callback = cb;
755755
}
756-
void set_elicitation_callback_impl(
757-
const std::function<fastmcpp::Json(const fastmcpp::Json&)>& cb)
756+
void
757+
set_elicitation_callback_impl(const std::function<fastmcpp::Json(const fastmcpp::Json&)>& cb)
758758
{
759759
if (!callbacks_)
760760
callbacks_ = std::make_shared<CallbackState>();
@@ -811,7 +811,8 @@ class Client
811811
}
812812

813813
// Internal constructor for cloning
814-
Client(std::shared_ptr<ITransport> t, std::shared_ptr<CallbackState> callbacks, bool /*internal*/)
814+
Client(std::shared_ptr<ITransport> t, std::shared_ptr<CallbackState> callbacks,
815+
bool /*internal*/)
815816
: transport_(std::move(t)), callbacks_(std::move(callbacks))
816817
{
817818
configure_transport_callbacks();

include/fastmcpp/client/sampling.hpp

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ using SamplingHandlerResult = std::variant<std::string, fastmcpp::Json>;
2222
using SamplingHandler = std::function<SamplingHandlerResult(const fastmcpp::Json& params)>;
2323

2424
/// Build a minimal MCP CreateMessageResult with a single text content block.
25-
inline fastmcpp::Json make_text_result(std::string text,
26-
std::string model = "fastmcpp-client",
27-
std::string role = "assistant")
25+
inline fastmcpp::Json make_text_result(std::string text, std::string model = "fastmcpp-client",
26+
std::string role = "assistant")
2827
{
2928
return fastmcpp::Json{
3029
{"role", std::move(role)},
3130
{"model", std::move(model)},
32-
{"content", fastmcpp::Json::array(
33-
{fastmcpp::Json{{"type", "text"}, {"text", std::move(text)}}})},
31+
{"content",
32+
fastmcpp::Json::array({fastmcpp::Json{{"type", "text"}, {"text", std::move(text)}}})},
3433
};
3534
}
3635

@@ -49,4 +48,3 @@ create_sampling_callback(SamplingHandler handler)
4948
}
5049

5150
} // namespace fastmcpp::client::sampling
52-

include/fastmcpp/client/transports.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ class StdioTransport : public ITransport
9696
/// 1. Client connects to /sse endpoint (GET) to establish event stream
9797
/// 2. Client sends JSON-RPC requests to /messages endpoint (POST)
9898
/// 3. Server sends JSON-RPC responses back via the SSE stream
99-
class SseClientTransport : public ITransport, public IServerRequestTransport, public IResettableTransport
99+
class SseClientTransport : public ITransport,
100+
public IServerRequestTransport,
101+
public IResettableTransport
100102
{
101103
public:
102104
/// Construct an SSE client transport
@@ -121,7 +123,7 @@ class SseClientTransport : public ITransport, public IServerRequestTransport, pu
121123
/// Check if a session ID has been set.
122124
bool has_session() const;
123125

124-
void set_server_request_handler(ServerRequestHandler handler) override;
126+
void set_server_request_handler(ServerRequestHandler handler) override;
125127

126128
void reset(bool full = false) override;
127129

include/fastmcpp/client/types.hpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ inline void from_json(const fastmcpp::Json& j, ToolInfo& t)
343343
t.title = j["title"].get<std::string>();
344344
if (j.contains("description"))
345345
t.description = j["description"].get<std::string>();
346-
t.inputSchema = j.value("inputSchema", fastmcpp::Json::object());
346+
t.inputSchema = j.value("inputSchema", fastmcpp::Json::object());
347347
if (j.contains("outputSchema"))
348348
t.outputSchema = j["outputSchema"];
349349
if (j.contains("execution"))
@@ -389,7 +389,7 @@ inline void from_json(const fastmcpp::Json& j, ResourceInfo& r)
389389
r._meta = j["_meta"];
390390
}
391391

392-
inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
392+
inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
393393
{
394394
j = fastmcpp::Json{{"uriTemplate", t.uriTemplate}, {"name", t.name}};
395395
if (t.title)
@@ -406,7 +406,7 @@ inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
406406
j["_meta"] = *t._meta;
407407
}
408408

409-
inline void from_json(const fastmcpp::Json& j, ResourceTemplate& t)
409+
inline void from_json(const fastmcpp::Json& j, ResourceTemplate& t)
410410
{
411411
t.uriTemplate = j.at("uriTemplate").get<std::string>();
412412
t.name = j.at("name").get<std::string>();

src/client/transports.cpp

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ void HttpTransport::request_stream_post(const std::string& route, const fastmcpp
295295
throw fastmcpp::TransportError("libcurl init failed");
296296

297297
std::string url = base_url_;
298+
if (url.find("://") == std::string::npos)
299+
url = "http://" + url;
298300
if (!url.empty() && url.back() != '/')
299301
url.push_back('/');
300302
url += route;
@@ -403,7 +405,7 @@ void HttpTransport::request_stream_post(const std::string& route, const fastmcpp
403405
// Parse whatever accumulated
404406
parse_and_emit(true);
405407

406-
if (code != CURLE_OK)
408+
if (code != CURLE_OK && code != CURLE_PARTIAL_FILE)
407409
{
408410
throw fastmcpp::TransportError(std::string("HTTP stream POST failed: ") +
409411
curl_easy_strerror(code));
@@ -706,7 +708,7 @@ void SseClientTransport::start_sse_listener()
706708

707709
if (!aggregated.empty())
708710
{
709-
// Handle endpoint event specially - it's not JSON
711+
// Handle endpoint event specially - it's not JSON
710712
if (event_type == "endpoint")
711713
{
712714
std::lock_guard<std::mutex> lock(endpoint_mutex_);
@@ -719,9 +721,9 @@ void SseClientTransport::start_sse_listener()
719721
{
720722
pos += std::string("session_id=").size();
721723
auto end = endpoint_path_.find_first_of("&#", pos);
722-
session_id_ = endpoint_path_.substr(
723-
pos, end == std::string::npos ? std::string::npos
724-
: (end - pos));
724+
session_id_ = endpoint_path_.substr(pos, end == std::string::npos
725+
? std::string::npos
726+
: (end - pos));
725727
}
726728
}
727729
else
@@ -917,7 +919,7 @@ fastmcpp::Json SseClientTransport::request(const std::string& route, const fastm
917919
cli.set_connection_timeout(5, 0);
918920
cli.set_read_timeout(30, 0);
919921

920-
// Use the endpoint path from SSE if available, otherwise use default
922+
// Use the endpoint path from SSE if available, otherwise use default
921923
std::string post_path;
922924
{
923925
std::lock_guard<std::mutex> lock(endpoint_mutex_);

tests/server/interactions_part2b.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,7 @@ void test_tool_meta_custom_fields()
904904
std::cout << "Test: tool list with meta fields...\n";
905905

906906
auto srv = create_meta_variations_server();
907-
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
907+
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
908908

909909
// Test that list_tools_mcp can access list-level _meta
910910
auto result = c.list_tools_mcp();
@@ -959,7 +959,7 @@ void test_resource_meta_fields()
959959
std::cout << "Test: resource with meta fields...\n";
960960

961961
auto srv = create_meta_variations_server();
962-
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
962+
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
963963

964964
auto resources = c.list_resources();
965965
bool found = false;

tests/server/sse_bidirectional_requests.cpp

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ Json make_result_response(const Json& request_id, Json result)
2828

2929
Json make_error_response(const Json& request_id, int code, const std::string& message)
3030
{
31-
return Json{{"jsonrpc", "2.0"}, {"id", request_id}, {"error", Json{{"code", code}, {"message", message}}}};
31+
return Json{{"jsonrpc", "2.0"},
32+
{"id", request_id},
33+
{"error", Json{{"code", code}, {"message", message}}}};
3234
}
3335
} // namespace
3436

@@ -101,8 +103,8 @@ int main()
101103

102104
std::this_thread::sleep_for(500ms);
103105

104-
auto transport = std::make_unique<fastmcpp::client::SseClientTransport>(
105-
"http://127.0.0.1:" + std::to_string(port));
106+
auto transport = std::make_unique<fastmcpp::client::SseClientTransport>("http://127.0.0.1:" +
107+
std::to_string(port));
106108
auto* sse_transport = transport.get();
107109
fastmcpp::client::Client client(std::move(transport));
108110

@@ -178,7 +180,8 @@ int main()
178180
Json result;
179181
try
180182
{
181-
result = session->send_request("sampling/createMessage", params, std::chrono::milliseconds(5000));
183+
result = session->send_request("sampling/createMessage", params,
184+
std::chrono::milliseconds(5000));
182185
}
183186
catch (const std::exception& e)
184187
{
@@ -201,7 +204,8 @@ int main()
201204
}
202205
if (result.value("model", std::string()) != "fastmcpp-client")
203206
{
204-
std::cerr << "Unexpected model in sampling response: " << result.value("model", std::string()) << "\n";
207+
std::cerr << "Unexpected model in sampling response: "
208+
<< result.value("model", std::string()) << "\n";
205209
sse_server->stop();
206210
return 1;
207211
}

tests/server/streamable_http_integration.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ void test_session_management()
238238
transport.request("initialize", init_params);
239239
assert(transport.has_session() && "Should have session after re-initialize");
240240
assert(transport.session_id() != session_id && "Session ID should change after reset");
241-
assert(server.session_count() == 2 && "Server should have 2 sessions after reset + initialize");
241+
assert(server.session_count() == 2 &&
242+
"Server should have 2 sessions after reset + initialize");
242243

243244
std::cout << "PASSED\n";
244245
}

0 commit comments

Comments
 (0)