Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 67 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ set(CMAKE_CXX_EXTENSIONS OFF)
option(FASTMCPP_BUILD_TESTS "Build tests" ON)
option(FASTMCPP_BUILD_EXAMPLES "Build examples" ON)
option(FASTMCPP_ENABLE_POST_STREAMING "Enable POST streaming via libcurl (optional)" OFF)
option(FASTMCPP_FETCH_CURL "Attempt to fetch libcurl if not found (experimental)" OFF)
option(FASTMCPP_FETCH_CURL "Fetch and build libcurl statically for POST streaming" ON)
option(FASTMCPP_ENABLE_WS_STREAMING_TESTS "Enable WebSocket streaming tests (requires external server)" OFF)
option(FASTMCPP_ENABLE_LOCAL_WS_TEST "Enable local WebSocket server test (depends on httplib ws server support)" OFF)

add_library(fastmcpp_core
add_library(fastmcpp_core STATIC
src/types.cpp
src/util/schema_build.cpp
src/app.cpp
Expand Down Expand Up @@ -96,11 +96,61 @@ target_sources(fastmcpp_core PRIVATE ${easywsclient_SOURCE_DIR}/easywsclient.cpp

# Optional: libcurl for POST streaming receive support (modular)
if(FASTMCPP_ENABLE_POST_STREAMING)
find_package(CURL)
if(NOT CURL_FOUND AND FASTMCPP_FETCH_CURL)
message(STATUS "CURL not found; FASTMCPP_FETCH_CURL requested (skipping auto-fetch in this build). Please provide CURL via your toolchain or package manager.")
if(FASTMCPP_FETCH_CURL)
message(STATUS "FASTMCPP_FETCH_CURL=ON: fetching curl via FetchContent (static-only)")
include(FetchContent)

# Configure curl for a minimal library build.
set(BUILD_CURL_EXE OFF CACHE BOOL "Build curl executable" FORCE)
set(CURL_DISABLE_TESTS ON CACHE BOOL "Disable curl tests" FORCE)
set(CURL_DISABLE_INSTALL ON CACHE BOOL "Disable curl install targets" FORCE)
set(CURL_DISABLE_LDAP ON CACHE BOOL "Disable LDAP support" FORCE)

# Prefer platform TLS backends to avoid OpenSSL as a dependency.
if(WIN32)
set(CURL_USE_SCHANNEL ON CACHE BOOL "Use Windows Schannel for TLS" FORCE)
set(CURL_USE_OPENSSL OFF CACHE BOOL "Do not use OpenSSL" FORCE)
endif()

# Always build curl statically to avoid runtime DLL dependencies.
if(DEFINED BUILD_SHARED_LIBS)
set(_fastmcpp_prev_build_shared_libs "${BUILD_SHARED_LIBS}")
else()
set(_fastmcpp_prev_build_shared_libs "__UNDEFINED__")
endif()
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries" FORCE)

FetchContent_Declare(
curl
GIT_REPOSITORY https://github.com/curl/curl.git
GIT_TAG curl-8_9_1
)
FetchContent_MakeAvailable(curl)

if(TARGET libcurl_static AND NOT TARGET CURL::libcurl)
add_library(CURL::libcurl ALIAS libcurl_static)
endif()

if(_fastmcpp_prev_build_shared_libs STREQUAL "__UNDEFINED__")
unset(BUILD_SHARED_LIBS CACHE)
else()
set(BUILD_SHARED_LIBS "${_fastmcpp_prev_build_shared_libs}" CACHE BOOL "Build shared libraries" FORCE)
endif()
else()
# Best effort for users who provide a curl toolchain: request static linkage.
set(CURL_USE_STATIC_LIBS ON)
find_package(CURL QUIET)

if(CURL_FOUND AND NOT TARGET CURL::libcurl)
add_library(CURL::libcurl UNKNOWN IMPORTED)
set_target_properties(CURL::libcurl PROPERTIES
IMPORTED_LOCATION "${CURL_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIRS}"
)
endif()
endif()
if(CURL_FOUND)

if(TARGET CURL::libcurl)
target_compile_definitions(fastmcpp_core PRIVATE FASTMCPP_POST_STREAMING)
target_link_libraries(fastmcpp_core PRIVATE CURL::libcurl)
else()
Expand Down Expand Up @@ -414,18 +464,26 @@ if(FASTMCPP_BUILD_TESTS)
endif()

if(FASTMCPP_ENABLE_WS_STREAMING_TESTS)
add_executable(fastmcpp_ws_streaming tests/transports/ws_streaming.cpp)
add_executable(fastmcpp_ws_streaming tests/transports/ws_streaming.cpp)
target_link_libraries(fastmcpp_ws_streaming PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_ws_streaming COMMAND fastmcpp_ws_streaming)
# Test auto-skips if FASTMCPP_WS_URL is not set

if(FASTMCPP_ENABLE_LOCAL_WS_TEST)
add_executable(fastmcpp_ws_streaming_local tests/transports/ws_streaming_local.cpp)
target_link_libraries(fastmcpp_ws_streaming_local PRIVATE fastmcpp_core)
target_link_libraries(fastmcpp_ws_streaming_local PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_ws_streaming_local COMMAND fastmcpp_ws_streaming_local)
set_tests_properties(fastmcpp_ws_streaming_local PROPERTIES RUN_SERIAL TRUE)
endif()
endif()

# POST streaming transport test (requires libcurl)
if(FASTMCPP_ENABLE_POST_STREAMING AND TARGET CURL::libcurl)
add_executable(fastmcpp_post_streaming tests/transports/post_streaming.cpp)
target_link_libraries(fastmcpp_post_streaming PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_post_streaming COMMAND fastmcpp_post_streaming)
set_tests_properties(fastmcpp_post_streaming PROPERTIES RUN_SERIAL TRUE)
endif()
endif()

if(FASTMCPP_BUILD_EXAMPLES)
Expand Down Expand Up @@ -506,7 +564,7 @@ if(FASTMCPP_BUILD_EXAMPLES)
if(FASTMCPP_ENABLE_POST_STREAMING)
add_executable(fastmcpp_example_streaming_post_demo examples/streaming_post_demo.cpp)
target_link_libraries(fastmcpp_example_streaming_post_demo PRIVATE fastmcpp_core)
if(CURL_FOUND)
if(TARGET CURL::libcurl)
add_test(NAME fastmcpp_example_streaming_post_demo COMMAND fastmcpp_example_streaming_post_demo)
else()
message(STATUS "libcurl not found; skipping test fastmcpp_example_streaming_post_demo")
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fastmcpp is a C++ port of the Python [fastmcp](https://github.com/jlowin/fastmcp

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

**Current version:** 2.14.0
**Current version:** 2.14.1

## Features

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

Optional:

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

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

Expand Down
17 changes: 9 additions & 8 deletions include/fastmcpp/client/client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ class IResettableTransport
using ServerRequestHandler =
std::function<fastmcpp::Json(const std::string& method, const fastmcpp::Json& params)>;

/// Optional transport interface: some transports can accept server-initiated requests and send responses.
/// Optional transport interface: some transports can accept server-initiated requests and send
/// responses.
class IServerRequestTransport
{
public:
Expand Down Expand Up @@ -663,7 +664,7 @@ class Client
}

/// Register roots/sampling/elicitation callbacks (placeholders for parity)
void set_roots_callback(const std::function<fastmcpp::Json()>& cb)
void set_roots_callback(const std::function<fastmcpp::Json()>& cb)
{
set_roots_callback_impl(cb);
}
Expand Down Expand Up @@ -714,7 +715,7 @@ class Client
};

std::shared_ptr<CallbackState> callbacks_;
std::unordered_map<std::string, fastmcpp::Json> tool_output_schemas_;
std::unordered_map<std::string, fastmcpp::Json> tool_output_schemas_;

std::function<fastmcpp::Json()> get_roots_callback() const
{
Expand Down Expand Up @@ -745,16 +746,15 @@ class Client
std::lock_guard<std::mutex> lock(callbacks_->mutex);
callbacks_->roots_callback = cb;
}
void set_sampling_callback_impl(
const std::function<fastmcpp::Json(const fastmcpp::Json&)>& cb)
void set_sampling_callback_impl(const std::function<fastmcpp::Json(const fastmcpp::Json&)>& cb)
{
if (!callbacks_)
callbacks_ = std::make_shared<CallbackState>();
std::lock_guard<std::mutex> lock(callbacks_->mutex);
callbacks_->sampling_callback = cb;
}
void set_elicitation_callback_impl(
const std::function<fastmcpp::Json(const fastmcpp::Json&)>& cb)
void
set_elicitation_callback_impl(const std::function<fastmcpp::Json(const fastmcpp::Json&)>& cb)
{
if (!callbacks_)
callbacks_ = std::make_shared<CallbackState>();
Expand Down Expand Up @@ -811,7 +811,8 @@ class Client
}

// Internal constructor for cloning
Client(std::shared_ptr<ITransport> t, std::shared_ptr<CallbackState> callbacks, bool /*internal*/)
Client(std::shared_ptr<ITransport> t, std::shared_ptr<CallbackState> callbacks,
bool /*internal*/)
: transport_(std::move(t)), callbacks_(std::move(callbacks))
{
configure_transport_callbacks();
Expand Down
10 changes: 4 additions & 6 deletions include/fastmcpp/client/sampling.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ using SamplingHandlerResult = std::variant<std::string, fastmcpp::Json>;
using SamplingHandler = std::function<SamplingHandlerResult(const fastmcpp::Json& params)>;

/// Build a minimal MCP CreateMessageResult with a single text content block.
inline fastmcpp::Json make_text_result(std::string text,
std::string model = "fastmcpp-client",
std::string role = "assistant")
inline fastmcpp::Json make_text_result(std::string text, std::string model = "fastmcpp-client",
std::string role = "assistant")
{
return fastmcpp::Json{
{"role", std::move(role)},
{"model", std::move(model)},
{"content", fastmcpp::Json::array(
{fastmcpp::Json{{"type", "text"}, {"text", std::move(text)}}})},
{"content",
fastmcpp::Json::array({fastmcpp::Json{{"type", "text"}, {"text", std::move(text)}}})},
};
}

Expand All @@ -49,4 +48,3 @@ create_sampling_callback(SamplingHandler handler)
}

} // namespace fastmcpp::client::sampling

6 changes: 4 additions & 2 deletions include/fastmcpp/client/transports.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ class StdioTransport : public ITransport
/// 1. Client connects to /sse endpoint (GET) to establish event stream
/// 2. Client sends JSON-RPC requests to /messages endpoint (POST)
/// 3. Server sends JSON-RPC responses back via the SSE stream
class SseClientTransport : public ITransport, public IServerRequestTransport, public IResettableTransport
class SseClientTransport : public ITransport,
public IServerRequestTransport,
public IResettableTransport
{
public:
/// Construct an SSE client transport
Expand All @@ -121,7 +123,7 @@ class SseClientTransport : public ITransport, public IServerRequestTransport, pu
/// Check if a session ID has been set.
bool has_session() const;

void set_server_request_handler(ServerRequestHandler handler) override;
void set_server_request_handler(ServerRequestHandler handler) override;

void reset(bool full = false) override;

Expand Down
6 changes: 3 additions & 3 deletions include/fastmcpp/client/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ inline void from_json(const fastmcpp::Json& j, ToolInfo& t)
t.title = j["title"].get<std::string>();
if (j.contains("description"))
t.description = j["description"].get<std::string>();
t.inputSchema = j.value("inputSchema", fastmcpp::Json::object());
t.inputSchema = j.value("inputSchema", fastmcpp::Json::object());
if (j.contains("outputSchema"))
t.outputSchema = j["outputSchema"];
if (j.contains("execution"))
Expand Down Expand Up @@ -389,7 +389,7 @@ inline void from_json(const fastmcpp::Json& j, ResourceInfo& r)
r._meta = j["_meta"];
}

inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
{
j = fastmcpp::Json{{"uriTemplate", t.uriTemplate}, {"name", t.name}};
if (t.title)
Expand All @@ -406,7 +406,7 @@ inline void to_json(fastmcpp::Json& j, const ResourceTemplate& t)
j["_meta"] = *t._meta;
}

inline void from_json(const fastmcpp::Json& j, ResourceTemplate& t)
inline void from_json(const fastmcpp::Json& j, ResourceTemplate& t)
{
t.uriTemplate = j.at("uriTemplate").get<std::string>();
t.name = j.at("name").get<std::string>();
Expand Down
14 changes: 8 additions & 6 deletions src/client/transports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ void HttpTransport::request_stream_post(const std::string& route, const fastmcpp
throw fastmcpp::TransportError("libcurl init failed");

std::string url = base_url_;
if (url.find("://") == std::string::npos)
url = "http://" + url;
if (!url.empty() && url.back() != '/')
url.push_back('/');
url += route;
Expand Down Expand Up @@ -403,7 +405,7 @@ void HttpTransport::request_stream_post(const std::string& route, const fastmcpp
// Parse whatever accumulated
parse_and_emit(true);

if (code != CURLE_OK)
if (code != CURLE_OK && code != CURLE_PARTIAL_FILE)
{
throw fastmcpp::TransportError(std::string("HTTP stream POST failed: ") +
curl_easy_strerror(code));
Expand Down Expand Up @@ -706,7 +708,7 @@ void SseClientTransport::start_sse_listener()

if (!aggregated.empty())
{
// Handle endpoint event specially - it's not JSON
// Handle endpoint event specially - it's not JSON
if (event_type == "endpoint")
{
std::lock_guard<std::mutex> lock(endpoint_mutex_);
Expand All @@ -719,9 +721,9 @@ void SseClientTransport::start_sse_listener()
{
pos += std::string("session_id=").size();
auto end = endpoint_path_.find_first_of("&#", pos);
session_id_ = endpoint_path_.substr(
pos, end == std::string::npos ? std::string::npos
: (end - pos));
session_id_ = endpoint_path_.substr(pos, end == std::string::npos
? std::string::npos
: (end - pos));
}
}
else
Expand Down Expand Up @@ -917,7 +919,7 @@ fastmcpp::Json SseClientTransport::request(const std::string& route, const fastm
cli.set_connection_timeout(5, 0);
cli.set_read_timeout(30, 0);

// Use the endpoint path from SSE if available, otherwise use default
// Use the endpoint path from SSE if available, otherwise use default
std::string post_path;
{
std::lock_guard<std::mutex> lock(endpoint_mutex_);
Expand Down
4 changes: 2 additions & 2 deletions tests/server/interactions_part2b.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ void test_tool_meta_custom_fields()
std::cout << "Test: tool list with meta fields...\n";

auto srv = create_meta_variations_server();
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
client::Client c(std::make_unique<client::LoopbackTransport>(srv));

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

auto srv = create_meta_variations_server();
client::Client c(std::make_unique<client::LoopbackTransport>(srv));
client::Client c(std::make_unique<client::LoopbackTransport>(srv));

auto resources = c.list_resources();
bool found = false;
Expand Down
14 changes: 9 additions & 5 deletions tests/server/sse_bidirectional_requests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ Json make_result_response(const Json& request_id, Json result)

Json make_error_response(const Json& request_id, int code, const std::string& message)
{
return Json{{"jsonrpc", "2.0"}, {"id", request_id}, {"error", Json{{"code", code}, {"message", message}}}};
return Json{{"jsonrpc", "2.0"},
{"id", request_id},
{"error", Json{{"code", code}, {"message", message}}}};
}
} // namespace

Expand Down Expand Up @@ -101,8 +103,8 @@ int main()

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

auto transport = std::make_unique<fastmcpp::client::SseClientTransport>(
"http://127.0.0.1:" + std::to_string(port));
auto transport = std::make_unique<fastmcpp::client::SseClientTransport>("http://127.0.0.1:" +
std::to_string(port));
auto* sse_transport = transport.get();
fastmcpp::client::Client client(std::move(transport));

Expand Down Expand Up @@ -178,7 +180,8 @@ int main()
Json result;
try
{
result = session->send_request("sampling/createMessage", params, std::chrono::milliseconds(5000));
result = session->send_request("sampling/createMessage", params,
std::chrono::milliseconds(5000));
}
catch (const std::exception& e)
{
Expand All @@ -201,7 +204,8 @@ int main()
}
if (result.value("model", std::string()) != "fastmcpp-client")
{
std::cerr << "Unexpected model in sampling response: " << result.value("model", std::string()) << "\n";
std::cerr << "Unexpected model in sampling response: "
<< result.value("model", std::string()) << "\n";
sse_server->stop();
return 1;
}
Expand Down
3 changes: 2 additions & 1 deletion tests/server/streamable_http_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ void test_session_management()
transport.request("initialize", init_params);
assert(transport.has_session() && "Should have session after re-initialize");
assert(transport.session_id() != session_id && "Session ID should change after reset");
assert(server.session_count() == 2 && "Server should have 2 sessions after reset + initialize");
assert(server.session_count() == 2 &&
"Server should have 2 sessions after reset + initialize");

std::cout << "PASSED\n";
}
Expand Down
Loading
Loading