Skip to content

Commit b2277d8

Browse files
authored
Merge pull request #26 from 0xeb/feature/mcp-timeout-transport-ping
Add telemetry spans and trace propagation
2 parents e590e3d + 972e62c commit b2277d8

25 files changed

Lines changed: 1373 additions & 77 deletions

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ add_library(fastmcpp_core STATIC
4040
src/client/client.cpp
4141
src/client/sampling_handlers.cpp
4242
src/client/transports.cpp
43+
src/telemetry.cpp
4344
src/util/json_schema.cpp
4445
src/util/json_schema_type.cpp
4546
src/settings.cpp
@@ -226,6 +227,10 @@ if(FASTMCPP_BUILD_TESTS)
226227
target_link_libraries(fastmcpp_tools_manager PRIVATE fastmcpp_core)
227228
add_test(NAME fastmcpp_tools_manager COMMAND fastmcpp_tools_manager)
228229

230+
add_executable(fastmcpp_tools_timeout tests/tools/test_tool_timeout.cpp)
231+
target_link_libraries(fastmcpp_tools_timeout PRIVATE fastmcpp_core)
232+
add_test(NAME fastmcpp_tools_timeout COMMAND fastmcpp_tools_timeout)
233+
229234
add_executable(fastmcpp_integration tests/integration.cpp)
230235
target_link_libraries(fastmcpp_integration PRIVATE fastmcpp_core)
231236
add_test(NAME fastmcpp_integration COMMAND fastmcpp_integration)
@@ -442,6 +447,10 @@ if(FASTMCPP_BUILD_TESTS)
442447
target_link_libraries(fastmcpp_server_middleware_pipeline PRIVATE fastmcpp_core)
443448
add_test(NAME fastmcpp_server_middleware_pipeline COMMAND fastmcpp_server_middleware_pipeline)
444449

450+
add_executable(fastmcpp_telemetry tests/telemetry/tracing.cpp)
451+
target_link_libraries(fastmcpp_telemetry PRIVATE fastmcpp_core)
452+
add_test(NAME fastmcpp_telemetry COMMAND fastmcpp_telemetry)
453+
445454
add_executable(fastmcpp_stdio_client tests/transports/stdio_client.cpp)
446455
target_link_libraries(fastmcpp_stdio_client PRIVATE fastmcpp_core)
447456
add_test(NAME fastmcpp_stdio_client COMMAND fastmcpp_stdio_client)

examples/stdio_mcp_server.cpp

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,23 @@ int main()
3131
tm.register_tool(add);
3232

3333
fastmcpp::tools::Tool counter{
34-
"counter",
35-
Json{{"type", "object"}, {"properties", Json::object()}},
34+
"counter", Json{{"type", "object"}, {"properties", Json::object()}},
3635
Json{{"type", "array"},
37-
{"items",
38-
Json::array({Json{{"type", "object"},
39-
{"properties", Json{{"type", Json{{"type", "string"}}},
40-
{"text", Json{{"type", "string"}}}}},
41-
{"required", Json::array({"type", "text"})}}})}},
36+
{"items", Json::array({Json{{"type", "object"},
37+
{"properties", Json{{"type", Json{{"type", "string"}}},
38+
{"text", Json{{"type", "string"}}}}},
39+
{"required", Json::array({"type", "text"})}}})}},
4240
[&counter_value](const Json&) -> Json
4341
{
4442
counter_value += 1;
45-
return Json{{"content",
46-
Json::array({Json{{"type", "text"}, {"text", std::to_string(counter_value)}}})}};
43+
return Json{{"content", Json::array({Json{{"type", "text"},
44+
{"text", std::to_string(counter_value)}}})}};
4745
}};
4846
tm.register_tool(counter);
4947

50-
auto handler =
51-
fastmcpp::mcp::make_mcp_handler("demo_stdio", "0.1.0", tm,
52-
{{"add", "Add two numbers"},
53-
{"counter", "Increment and return an in-process counter"}});
48+
auto handler = fastmcpp::mcp::make_mcp_handler(
49+
"demo_stdio", "0.1.0", tm,
50+
{{"add", "Add two numbers"}, {"counter", "Increment and return an in-process counter"}});
5451
fastmcpp::server::StdioServerWrapper server(handler);
5552
server.run();
5653
return 0;

include/fastmcpp/app.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "fastmcpp/server/server.hpp"
88
#include "fastmcpp/tools/manager.hpp"
99

10+
#include <chrono>
1011
#include <memory>
1112
#include <optional>
1213
#include <string>
@@ -65,6 +66,7 @@ class FastMCP
6566
std::vector<std::string> exclude_args;
6667
TaskSupport task_support{TaskSupport::Forbidden};
6768
Json output_schema{Json::object()};
69+
std::optional<std::chrono::milliseconds> timeout;
6870
};
6971

7072
struct PromptOptions
@@ -250,7 +252,7 @@ class FastMCP
250252
// =========================================================================
251253

252254
/// Invoke a tool by name (handles prefixed routing)
253-
Json invoke_tool(const std::string& name, const Json& args) const;
255+
Json invoke_tool(const std::string& name, const Json& args, bool enforce_timeout = true) const;
254256

255257
/// Read a resource by URI (handles prefixed routing)
256258
resources::ResourceContent read_resource(const std::string& uri,

include/fastmcpp/client/client.hpp

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "fastmcpp/client/types.hpp"
88
#include "fastmcpp/exceptions.hpp"
99
#include "fastmcpp/server/server.hpp"
10+
#include "fastmcpp/telemetry.hpp"
1011
#include "fastmcpp/types.hpp"
1112
#include "fastmcpp/util/json_schema.hpp"
1213
#include "fastmcpp/util/json_schema_type.hpp"
@@ -68,6 +69,15 @@ class IServerRequestTransport
6869
virtual void set_server_request_handler(ServerRequestHandler handler) = 0;
6970
};
7071

72+
/// Optional transport interface: some transports expose MCP session IDs.
73+
class ISessionTransport
74+
{
75+
public:
76+
virtual ~ISessionTransport() = default;
77+
virtual std::string session_id() const = 0;
78+
virtual bool has_session() const = 0;
79+
};
80+
7181
/// Loopback transport for in-process server testing
7282
class LoopbackTransport : public ITransport
7383
{
@@ -240,12 +250,15 @@ class Client
240250
CallToolResult call_tool_mcp(const std::string& name, const fastmcpp::Json& arguments,
241251
const CallToolOptions& options = CallToolOptions{})
242252
{
253+
auto span =
254+
telemetry::client_span("tool " + name, "tools/call", name, transport_session_id());
243255

244256
fastmcpp::Json payload = {{"name", name}, {"arguments", arguments}};
245257

246258
// Add _meta if provided
247-
if (options.meta)
248-
payload["_meta"] = *options.meta;
259+
auto propagated_meta = telemetry::inject_trace_context(options.meta);
260+
if (propagated_meta)
261+
payload["_meta"] = *propagated_meta;
249262

250263
if (options.progress_handler)
251264
options.progress_handler(0.0f, std::nullopt, "request started");
@@ -439,7 +452,13 @@ class Client
439452
/// Read a resource by URI
440453
ReadResourceResult read_resource_mcp(const std::string& uri)
441454
{
442-
auto response = call("resources/read", {{"uri", uri}});
455+
auto span = telemetry::client_span("resource " + uri, "resources/read", uri,
456+
transport_session_id());
457+
fastmcpp::Json payload = {{"uri", uri}};
458+
auto propagated_meta = telemetry::inject_trace_context(std::nullopt);
459+
if (propagated_meta)
460+
payload["_meta"] = *propagated_meta;
461+
auto response = call("resources/read", payload);
443462
return parse_read_resource_result(response);
444463
}
445464

@@ -477,7 +496,8 @@ class Client
477496
GetPromptResult get_prompt_mcp(const std::string& name,
478497
const fastmcpp::Json& arguments = fastmcpp::Json::object())
479498
{
480-
499+
auto span =
500+
telemetry::client_span("prompt " + name, "prompts/get", name, transport_session_id());
481501
fastmcpp::Json payload = {{"name", name}};
482502
if (!arguments.empty())
483503
{
@@ -491,6 +511,10 @@ class Client
491511
payload["arguments"] = stringArgs;
492512
}
493513

514+
auto propagated_meta = telemetry::inject_trace_context(std::nullopt);
515+
if (propagated_meta)
516+
payload["_meta"] = *propagated_meta;
517+
494518
auto response = call("prompts/get", payload);
495519
return parse_get_prompt_result(response);
496520
}
@@ -810,6 +834,18 @@ class Client
810834
}
811835
}
812836

837+
std::optional<std::string> transport_session_id() const
838+
{
839+
if (!transport_)
840+
return std::nullopt;
841+
if (auto* session_transport = dynamic_cast<ISessionTransport*>(transport_.get()))
842+
{
843+
if (session_transport->has_session())
844+
return session_transport->session_id();
845+
}
846+
return std::nullopt;
847+
}
848+
813849
// Internal constructor for cloning
814850
Client(std::shared_ptr<ITransport> t, std::shared_ptr<CallbackState> callbacks,
815851
bool /*internal*/)
@@ -1541,6 +1577,9 @@ inline std::shared_ptr<ResourceTask> Client::read_resource_task(const std::strin
15411577

15421578
fastmcpp::Json task_meta = {{"ttl", ttl_ms}};
15431579
payload["_meta"] = fastmcpp::Json{{"modelcontextprotocol.io/task", std::move(task_meta)}};
1580+
auto propagated_meta = telemetry::inject_trace_context(payload["_meta"]);
1581+
if (propagated_meta)
1582+
payload["_meta"] = *propagated_meta;
15441583

15451584
auto response = call("resources/read", payload);
15461585

@@ -1575,6 +1614,9 @@ Client::get_prompt_task(const std::string& name, const fastmcpp::Json& arguments
15751614

15761615
fastmcpp::Json task_meta = {{"ttl", ttl_ms}};
15771616
payload["_meta"] = fastmcpp::Json{{"modelcontextprotocol.io/task", std::move(task_meta)}};
1617+
auto propagated_meta = telemetry::inject_trace_context(payload["_meta"]);
1618+
if (propagated_meta)
1619+
payload["_meta"] = *propagated_meta;
15781620

15791621
auto response = call("prompts/get", payload);
15801622

include/fastmcpp/client/transports.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ class StdioTransport : public ITransport
114114
/// 3. Server sends JSON-RPC responses back via the SSE stream
115115
class SseClientTransport : public ITransport,
116116
public IServerRequestTransport,
117-
public IResettableTransport
117+
public IResettableTransport,
118+
public ISessionTransport
118119
{
119120
public:
120121
/// Construct an SSE client transport
@@ -179,7 +180,9 @@ class SseClientTransport : public ITransport,
179180
/// 3. Session ID management via Mcp-Session-Id header
180181
///
181182
/// Reference: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/
182-
class StreamableHttpTransport : public ITransport, public IResettableTransport
183+
class StreamableHttpTransport : public ITransport,
184+
public IResettableTransport,
185+
public ISessionTransport
183186
{
184187
public:
185188
/// Construct a Streamable HTTP client transport

include/fastmcpp/exceptions.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ struct ValidationError : public Error
2020
using Error::Error;
2121
};
2222

23+
struct ToolTimeoutError : public Error
24+
{
25+
using Error::Error;
26+
};
27+
2328
struct TransportError : public Error
2429
{
2530
using Error::Error;

include/fastmcpp/proxy.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ class ProxyApp
107107

108108
/// Invoke a tool by name
109109
/// Tries local tools first, falls back to remote
110-
client::CallToolResult invoke_tool(const std::string& name, const Json& args) const;
110+
client::CallToolResult invoke_tool(const std::string& name, const Json& args,
111+
bool enforce_timeout = true) const;
111112

112113
/// Read a resource by URI
113114
/// Tries local resources first, falls back to remote

include/fastmcpp/server/context.hpp

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ enum class LogLevel
3636
Error
3737
};
3838

39+
enum class TransportType
40+
{
41+
Stdio,
42+
Sse,
43+
StreamableHttp
44+
};
45+
3946
// ============================================================================
4047
// Sampling types (for Context.sample())
4148
// ============================================================================
@@ -146,6 +153,21 @@ inline std::string to_string(LogLevel level)
146153
}
147154
}
148155

156+
inline std::string to_string(TransportType transport)
157+
{
158+
switch (transport)
159+
{
160+
case TransportType::Stdio:
161+
return "stdio";
162+
case TransportType::Sse:
163+
return "sse";
164+
case TransportType::StreamableHttp:
165+
return "streamable-http";
166+
default:
167+
return "unknown";
168+
}
169+
}
170+
149171
using LogCallback = std::function<void(LogLevel, const std::string&, const std::string&)>;
150172
using ProgressCallback =
151173
std::function<void(const std::string&, double, double, const std::string&)>;
@@ -158,7 +180,8 @@ class Context
158180
Context(const resources::ResourceManager& rm, const prompts::PromptManager& pm,
159181
std::optional<fastmcpp::Json> request_meta,
160182
std::optional<std::string> request_id = std::nullopt,
161-
std::optional<std::string> session_id = std::nullopt);
183+
std::optional<std::string> session_id = std::nullopt,
184+
std::optional<TransportType> transport = std::nullopt);
162185

163186
std::vector<resources::Resource> list_resources() const;
164187
std::vector<prompts::Prompt> list_prompts() const;
@@ -177,6 +200,16 @@ class Context
177200
{
178201
return session_id_;
179202
}
203+
std::optional<std::string> transport() const
204+
{
205+
if (!transport_.has_value())
206+
return std::nullopt;
207+
return to_string(*transport_);
208+
}
209+
std::optional<TransportType> transport_type() const
210+
{
211+
return transport_;
212+
}
180213

181214
std::optional<std::string> client_id() const
182215
{
@@ -398,6 +431,7 @@ class Context
398431
std::optional<fastmcpp::Json> request_meta_;
399432
std::optional<std::string> request_id_;
400433
std::optional<std::string> session_id_;
434+
std::optional<TransportType> transport_;
401435
mutable std::unordered_map<std::string, std::any> state_;
402436
LogCallback log_callback_;
403437
ProgressCallback progress_callback_;

0 commit comments

Comments
 (0)