Skip to content

Commit e381c8d

Browse files
committed
Add Streamable HTTP transport, server progress/notifications, and ping support
Features: - StreamableHttpTransport client: single POST endpoint with session management - StreamableHttpServerWrapper: MCP 2025-03-26 compliant server - ServerSession::send_notification() and send_progress() for server-to-client messages - ping method support in all MCP handlers Implementation: - Client transport uses Mcp-Session-Id header for session tracking - Server wrapper manages sessions via thread-safe session map - Progress notifications sent via existing SSE infrastructure - All 7 handler variants support ping method Tests: - 59/59 unit tests passing - Streamable HTTP integration tests - Interop tests (T5: C++ server + Python fastmcp client) Spec: MCP 2025-03-26 (Streamable HTTP transport)
1 parent c6b06bf commit e381c8d

14 files changed

Lines changed: 1302 additions & 24 deletions

CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ add_library(fastmcpp_core
3232
src/server/http_server.cpp
3333
src/server/stdio_server.cpp
3434
src/server/sse_server.cpp
35+
src/server/streamable_http_server.cpp
3536
src/client/client.cpp
3637
src/client/transports.cpp
3738
src/util/json_schema.cpp
@@ -303,6 +304,11 @@ if(FASTMCPP_BUILD_TESTS)
303304
target_link_libraries(fastmcpp_server_sse_http_integration PRIVATE fastmcpp_core)
304305
add_test(NAME fastmcpp_server_sse_http_integration COMMAND fastmcpp_server_sse_http_integration)
305306

307+
# Streamable HTTP integration (MCP spec 2025-03-26)
308+
add_executable(fastmcpp_server_streamable_http_integration tests/server/streamable_http_integration.cpp)
309+
target_link_libraries(fastmcpp_server_streamable_http_integration PRIVATE fastmcpp_core)
310+
add_test(NAME fastmcpp_server_streamable_http_integration COMMAND fastmcpp_server_streamable_http_integration)
311+
306312
add_executable(fastmcpp_server_auth_cors_security tests/server/auth_cors_security.cpp)
307313
target_link_libraries(fastmcpp_server_auth_cors_security PRIVATE fastmcpp_core)
308314
add_test(NAME fastmcpp_server_auth_cors_security COMMAND fastmcpp_server_auth_cors_security)

README.md

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,22 @@ 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.13.0
18+
**Current version:** 2.14.0
1919

2020
## Features
2121

2222
- Core MCP protocol implementation (JSON‑RPC).
23-
- Multiple transports: STDIO, HTTP (SSE), WebSocket.
23+
- Multiple transports: STDIO, HTTP (SSE), Streamable HTTP, WebSocket.
24+
- Streamable HTTP transport (MCP spec 2025-03-26) with session management.
2425
- Tool management and invocation.
2526
- Resources and prompts support.
27+
- Resource templates with URI pattern matching.
2628
- JSON Schema validation.
27-
- Middleware for request/response processing.
29+
- McpApp high-level application class.
30+
- ProxyApp for backend server proxying.
31+
- ServerSession for bidirectional communication, sampling, and server-initiated notifications.
32+
- Built-in middleware: Logging, Timing, Caching, RateLimiting, ErrorHandling.
33+
- Tool transforms for input/output processing.
2834
- Integration with MCP‑compatible CLI tools.
2935
- Cross‑platform: Windows, Linux, macOS.
3036

@@ -105,12 +111,6 @@ ctest --test-dir build -C Release -R fastmcp_smoke --output-on-failure
105111
ctest --test-dir build -C Release -N
106112
```
107113

108-
Current status (CI / WSL configuration):
109-
110-
- 24/24 tests passing (100% success rate).
111-
- 3 streaming tests disabled due to infrastructure dependencies.
112-
- C++ test line count is much smaller than the Python `fastmcp` suite (see CCSDK parity docs).
113-
114114
## Basic Usage
115115

116116
### STDIO MCP server
@@ -177,6 +177,61 @@ int main() {
177177
}
178178
```
179179

180+
### Streamable HTTP server (MCP spec 2025-03-26)
181+
182+
```cpp
183+
#include <fastmcpp/tools/manager.hpp>
184+
#include <fastmcpp/mcp/handler.hpp>
185+
#include <fastmcpp/server/streamable_http_server.hpp>
186+
187+
int main() {
188+
fastmcpp::tools::ToolManager tm;
189+
// register tools on tm...
190+
191+
auto handler = fastmcpp::mcp::make_mcp_handler(
192+
"myserver", "1.0.0", tm
193+
);
194+
195+
// Streamable HTTP server on /mcp endpoint
196+
fastmcpp::server::StreamableHttpServerWrapper server(
197+
handler, "127.0.0.1", 8080, "/mcp"
198+
);
199+
server.start(); // non-blocking
200+
201+
std::this_thread::sleep_for(std::chrono::hours(1));
202+
server.stop();
203+
return 0;
204+
}
205+
```
206+
207+
### Streamable HTTP client
208+
209+
```cpp
210+
#include <fastmcpp/client/transports.hpp>
211+
212+
int main() {
213+
fastmcpp::client::StreamableHttpTransport transport(
214+
"http://localhost:8080", "/mcp"
215+
);
216+
217+
// Send initialize request
218+
auto init_response = transport.request("mcp", {
219+
{"jsonrpc", "2.0"},
220+
{"id", 1},
221+
{"method", "initialize"},
222+
{"params", {
223+
{"protocolVersion", "2024-11-05"},
224+
{"capabilities", {}},
225+
{"clientInfo", {{"name", "client"}, {"version", "1.0"}}}
226+
}}
227+
});
228+
229+
// Session ID is automatically managed via Mcp-Session-Id header
230+
std::cout << "Session: " << transport.session_id() << std::endl;
231+
return 0;
232+
}
233+
```
234+
180235
## Examples
181236

182237
See the `examples/` directory for complete programs, including:

include/fastmcpp/client/transports.hpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,57 @@ class SseClientTransport : public ITransport
137137
std::unordered_map<int64_t, std::promise<fastmcpp::Json>> pending_requests_;
138138
};
139139

140+
/// Streamable HTTP client transport for connecting to MCP servers using the
141+
/// Streamable HTTP protocol (MCP spec version 2025-03-26).
142+
///
143+
/// This transport is simpler than SSE:
144+
/// 1. Client sends POST requests to a single endpoint (default: /mcp)
145+
/// 2. Server responds with JSON or SSE stream
146+
/// 3. Session ID management via Mcp-Session-Id header
147+
///
148+
/// Reference: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/
149+
class StreamableHttpTransport : public ITransport
150+
{
151+
public:
152+
/// Construct a Streamable HTTP client transport
153+
/// @param base_url The base URL of the MCP server (e.g., "http://127.0.0.1:8080")
154+
/// @param mcp_path Path for the MCP endpoint (default: "/mcp")
155+
/// @param headers Additional headers to include in requests
156+
explicit StreamableHttpTransport(std::string base_url, std::string mcp_path = "/mcp",
157+
std::unordered_map<std::string, std::string> headers = {});
158+
159+
~StreamableHttpTransport();
160+
161+
/// Send a JSON-RPC request and wait for response
162+
fastmcpp::Json request(const std::string& route, const fastmcpp::Json& payload) override;
163+
164+
/// Get the session ID (set after successful initialize)
165+
std::string session_id() const;
166+
167+
/// Check if a session ID has been set
168+
bool has_session() const;
169+
170+
/// Set callback for handling server-initiated notifications during streaming responses
171+
void set_notification_callback(std::function<void(const fastmcpp::Json&)> callback);
172+
173+
private:
174+
void parse_session_id_from_response(const std::string& headers);
175+
fastmcpp::Json parse_response(const std::string& body, const std::string& content_type);
176+
void process_sse_line(const std::string& line, std::vector<fastmcpp::Json>& messages);
177+
178+
std::string base_url_;
179+
std::string mcp_path_;
180+
std::unordered_map<std::string, std::string> headers_;
181+
182+
// Session management
183+
mutable std::mutex session_mutex_;
184+
std::string session_id_;
185+
186+
// Notification handling
187+
std::function<void(const fastmcpp::Json&)> notification_callback_;
188+
189+
// Request ID generation
190+
std::atomic<int64_t> next_id_{1};
191+
};
192+
140193
} // namespace fastmcpp::client

include/fastmcpp/server/session.hpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,41 @@ class ServerSession
257257
return true;
258258
}
259259

260+
/**
261+
* Send a notification to the client (fire-and-forget, no response expected).
262+
*
263+
* @param method The JSON-RPC method name (e.g., "notifications/progress")
264+
* @param params Notification parameters
265+
*/
266+
void send_notification(const std::string& method, const Json& params = Json::object())
267+
{
268+
Json notification = {{"jsonrpc", "2.0"}, {"method", method}, {"params", params}};
269+
270+
if (send_callback_)
271+
send_callback_(notification);
272+
}
273+
274+
/**
275+
* Send a progress notification to the client.
276+
*
277+
* @param progress_token Token identifying the operation (from request _meta.progressToken)
278+
* @param progress Current progress value
279+
* @param total Total progress value (optional)
280+
* @param message Progress message (optional)
281+
*/
282+
void send_progress(const std::string& progress_token, double progress, double total = 0.0,
283+
const std::string& message = "")
284+
{
285+
Json params = {{"progressToken", progress_token}, {"progress", progress}};
286+
287+
if (total > 0.0)
288+
params["total"] = total;
289+
if (!message.empty())
290+
params["message"] = message;
291+
292+
send_notification("notifications/progress", params);
293+
}
294+
260295
/**
261296
* Check if a JSON message is a response (has id, no method).
262297
*/
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#pragma once
2+
#include "fastmcpp/server/session.hpp"
3+
#include "fastmcpp/types.hpp"
4+
5+
#include <atomic>
6+
#include <condition_variable>
7+
#include <deque>
8+
#include <functional>
9+
#include <httplib.h>
10+
#include <memory>
11+
#include <mutex>
12+
#include <string>
13+
#include <thread>
14+
#include <unordered_map>
15+
16+
namespace fastmcpp::server
17+
{
18+
19+
/**
20+
* Streamable HTTP MCP server wrapper.
21+
*
22+
* This transport implements the Streamable HTTP protocol for MCP communication
23+
* per MCP spec version 2025-03-26:
24+
* - Single POST endpoint (default: /mcp)
25+
* - Session ID management via Mcp-Session-Id header
26+
* - Responses can be JSON or SSE stream
27+
*
28+
* This is a simpler transport than SSE with a single endpoint.
29+
* Clients send JSON-RPC requests via POST and receive responses in the
30+
* same HTTP response (either as JSON or SSE stream for long-running operations).
31+
*
32+
* Usage:
33+
* auto handler = fastmcpp::mcp::make_mcp_handler("myserver", "1.0.0", tools);
34+
* StreamableHttpServerWrapper server(handler);
35+
* server.start(); // Non-blocking - runs in background thread
36+
* // ... server runs ...
37+
* server.stop(); // Graceful shutdown
38+
*
39+
* Reference: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/
40+
*/
41+
class StreamableHttpServerWrapper
42+
{
43+
public:
44+
using McpHandler = std::function<fastmcpp::Json(const fastmcpp::Json&)>;
45+
46+
/**
47+
* Construct a Streamable HTTP server with an MCP handler.
48+
*
49+
* @param handler Function that processes JSON-RPC requests and returns responses
50+
* @param host Host address to bind to (default: "127.0.0.1")
51+
* @param port Port to listen on (default: 18080)
52+
* @param mcp_path Path for the MCP POST endpoint (default: "/mcp")
53+
* @param auth_token Optional auth token for Bearer authentication (empty = no auth required)
54+
* @param cors_origin Optional CORS origin to allow (empty = no CORS header, use "*" for
55+
* wildcard)
56+
*/
57+
explicit StreamableHttpServerWrapper(McpHandler handler, std::string host = "127.0.0.1",
58+
int port = 18080, std::string mcp_path = "/mcp",
59+
std::string auth_token = "", std::string cors_origin = "");
60+
61+
~StreamableHttpServerWrapper();
62+
63+
/**
64+
* Start the server in background (non-blocking).
65+
*
66+
* Launches a background thread that runs the HTTP server.
67+
* Use stop() to terminate.
68+
*
69+
* @return true if server started successfully
70+
*/
71+
bool start();
72+
73+
/**
74+
* Stop the server.
75+
*
76+
* Signals the server to stop and joins the background thread.
77+
* Safe to call multiple times.
78+
*/
79+
void stop();
80+
81+
/**
82+
* Check if server is currently running.
83+
*/
84+
bool running() const
85+
{
86+
return running_.load();
87+
}
88+
89+
/**
90+
* Get the port the server is listening on.
91+
*/
92+
int port() const
93+
{
94+
return port_;
95+
}
96+
97+
/**
98+
* Get the host address the server is bound to.
99+
*/
100+
const std::string& host() const
101+
{
102+
return host_;
103+
}
104+
105+
/**
106+
* Get the MCP endpoint path.
107+
*/
108+
const std::string& mcp_path() const
109+
{
110+
return mcp_path_;
111+
}
112+
113+
/**
114+
* Get the ServerSession for a given session ID.
115+
*
116+
* This allows server-initiated requests (sampling, elicitation) via
117+
* the session's bidirectional transport.
118+
*
119+
* @param session_id The session to get
120+
* @return Shared pointer to ServerSession, or nullptr if not found
121+
*/
122+
std::shared_ptr<ServerSession> get_session(const std::string& session_id) const
123+
{
124+
std::lock_guard<std::mutex> lock(sessions_mutex_);
125+
auto it = sessions_.find(session_id);
126+
if (it == sessions_.end())
127+
return nullptr;
128+
return it->second;
129+
}
130+
131+
/**
132+
* Get the number of active sessions.
133+
*/
134+
size_t session_count() const
135+
{
136+
std::lock_guard<std::mutex> lock(sessions_mutex_);
137+
return sessions_.size();
138+
}
139+
140+
private:
141+
void run_server();
142+
std::string generate_session_id();
143+
bool check_auth(const std::string& auth_header) const;
144+
145+
McpHandler handler_;
146+
std::string host_;
147+
int port_;
148+
std::string mcp_path_;
149+
std::string auth_token_; // Optional Bearer token for authentication
150+
std::string cors_origin_; // Optional CORS origin (empty = no CORS)
151+
152+
std::unique_ptr<httplib::Server> svr_;
153+
std::thread thread_;
154+
std::atomic<bool> running_{false};
155+
156+
// Security limits
157+
static constexpr size_t MAX_SESSIONS = 1000;
158+
159+
// Active sessions mapped by session ID
160+
std::unordered_map<std::string, std::shared_ptr<ServerSession>> sessions_;
161+
mutable std::mutex sessions_mutex_;
162+
};
163+
164+
} // namespace fastmcpp::server

0 commit comments

Comments
 (0)