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
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ add_library(fastmcpp_core STATIC
src/client/client.cpp
src/client/sampling_handlers.cpp
src/client/transports.cpp
src/telemetry.cpp
src/util/json_schema.cpp
src/util/json_schema_type.cpp
src/settings.cpp
Expand Down Expand Up @@ -226,6 +227,10 @@ if(FASTMCPP_BUILD_TESTS)
target_link_libraries(fastmcpp_tools_manager PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_tools_manager COMMAND fastmcpp_tools_manager)

add_executable(fastmcpp_tools_timeout tests/tools/test_tool_timeout.cpp)
target_link_libraries(fastmcpp_tools_timeout PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_tools_timeout COMMAND fastmcpp_tools_timeout)

add_executable(fastmcpp_integration tests/integration.cpp)
target_link_libraries(fastmcpp_integration PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_integration COMMAND fastmcpp_integration)
Expand Down Expand Up @@ -442,6 +447,10 @@ if(FASTMCPP_BUILD_TESTS)
target_link_libraries(fastmcpp_server_middleware_pipeline PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_middleware_pipeline COMMAND fastmcpp_server_middleware_pipeline)

add_executable(fastmcpp_telemetry tests/telemetry/tracing.cpp)
target_link_libraries(fastmcpp_telemetry PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_telemetry COMMAND fastmcpp_telemetry)

add_executable(fastmcpp_stdio_client tests/transports/stdio_client.cpp)
target_link_libraries(fastmcpp_stdio_client PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_stdio_client COMMAND fastmcpp_stdio_client)
Expand Down
23 changes: 10 additions & 13 deletions examples/stdio_mcp_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,23 @@ int main()
tm.register_tool(add);

fastmcpp::tools::Tool counter{
"counter",
Json{{"type", "object"}, {"properties", Json::object()}},
"counter", Json{{"type", "object"}, {"properties", Json::object()}},
Json{{"type", "array"},
{"items",
Json::array({Json{{"type", "object"},
{"properties", Json{{"type", Json{{"type", "string"}}},
{"text", Json{{"type", "string"}}}}},
{"required", Json::array({"type", "text"})}}})}},
{"items", Json::array({Json{{"type", "object"},
{"properties", Json{{"type", Json{{"type", "string"}}},
{"text", Json{{"type", "string"}}}}},
{"required", Json::array({"type", "text"})}}})}},
[&counter_value](const Json&) -> Json
{
counter_value += 1;
return Json{{"content",
Json::array({Json{{"type", "text"}, {"text", std::to_string(counter_value)}}})}};
return Json{{"content", Json::array({Json{{"type", "text"},
{"text", std::to_string(counter_value)}}})}};
}};
tm.register_tool(counter);

auto handler =
fastmcpp::mcp::make_mcp_handler("demo_stdio", "0.1.0", tm,
{{"add", "Add two numbers"},
{"counter", "Increment and return an in-process counter"}});
auto handler = fastmcpp::mcp::make_mcp_handler(
"demo_stdio", "0.1.0", tm,
{{"add", "Add two numbers"}, {"counter", "Increment and return an in-process counter"}});
fastmcpp::server::StdioServerWrapper server(handler);
server.run();
return 0;
Expand Down
4 changes: 3 additions & 1 deletion include/fastmcpp/app.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "fastmcpp/server/server.hpp"
#include "fastmcpp/tools/manager.hpp"

#include <chrono>
#include <memory>
#include <optional>
#include <string>
Expand Down Expand Up @@ -65,6 +66,7 @@ class FastMCP
std::vector<std::string> exclude_args;
TaskSupport task_support{TaskSupport::Forbidden};
Json output_schema{Json::object()};
std::optional<std::chrono::milliseconds> timeout;
};

struct PromptOptions
Expand Down Expand Up @@ -250,7 +252,7 @@ class FastMCP
// =========================================================================

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

/// Read a resource by URI (handles prefixed routing)
resources::ResourceContent read_resource(const std::string& uri,
Expand Down
50 changes: 46 additions & 4 deletions include/fastmcpp/client/client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "fastmcpp/client/types.hpp"
#include "fastmcpp/exceptions.hpp"
#include "fastmcpp/server/server.hpp"
#include "fastmcpp/telemetry.hpp"
#include "fastmcpp/types.hpp"
#include "fastmcpp/util/json_schema.hpp"
#include "fastmcpp/util/json_schema_type.hpp"
Expand Down Expand Up @@ -68,6 +69,15 @@ class IServerRequestTransport
virtual void set_server_request_handler(ServerRequestHandler handler) = 0;
};

/// Optional transport interface: some transports expose MCP session IDs.
class ISessionTransport
{
public:
virtual ~ISessionTransport() = default;
virtual std::string session_id() const = 0;
virtual bool has_session() const = 0;
};

/// Loopback transport for in-process server testing
class LoopbackTransport : public ITransport
{
Expand Down Expand Up @@ -240,12 +250,15 @@ class Client
CallToolResult call_tool_mcp(const std::string& name, const fastmcpp::Json& arguments,
const CallToolOptions& options = CallToolOptions{})
{
auto span =
telemetry::client_span("tool " + name, "tools/call", name, transport_session_id());

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

// Add _meta if provided
if (options.meta)
payload["_meta"] = *options.meta;
auto propagated_meta = telemetry::inject_trace_context(options.meta);
if (propagated_meta)
payload["_meta"] = *propagated_meta;

if (options.progress_handler)
options.progress_handler(0.0f, std::nullopt, "request started");
Expand Down Expand Up @@ -439,7 +452,13 @@ class Client
/// Read a resource by URI
ReadResourceResult read_resource_mcp(const std::string& uri)
{
auto response = call("resources/read", {{"uri", uri}});
auto span = telemetry::client_span("resource " + uri, "resources/read", uri,
transport_session_id());
fastmcpp::Json payload = {{"uri", uri}};
auto propagated_meta = telemetry::inject_trace_context(std::nullopt);
if (propagated_meta)
payload["_meta"] = *propagated_meta;
auto response = call("resources/read", payload);
return parse_read_resource_result(response);
}

Expand Down Expand Up @@ -477,7 +496,8 @@ class Client
GetPromptResult get_prompt_mcp(const std::string& name,
const fastmcpp::Json& arguments = fastmcpp::Json::object())
{

auto span =
telemetry::client_span("prompt " + name, "prompts/get", name, transport_session_id());
fastmcpp::Json payload = {{"name", name}};
if (!arguments.empty())
{
Expand All @@ -491,6 +511,10 @@ class Client
payload["arguments"] = stringArgs;
}

auto propagated_meta = telemetry::inject_trace_context(std::nullopt);
if (propagated_meta)
payload["_meta"] = *propagated_meta;

auto response = call("prompts/get", payload);
return parse_get_prompt_result(response);
}
Expand Down Expand Up @@ -810,6 +834,18 @@ class Client
}
}

std::optional<std::string> transport_session_id() const
{
if (!transport_)
return std::nullopt;
if (auto* session_transport = dynamic_cast<ISessionTransport*>(transport_.get()))
{
if (session_transport->has_session())
return session_transport->session_id();
}
return std::nullopt;
}

// Internal constructor for cloning
Client(std::shared_ptr<ITransport> t, std::shared_ptr<CallbackState> callbacks,
bool /*internal*/)
Expand Down Expand Up @@ -1541,6 +1577,9 @@ inline std::shared_ptr<ResourceTask> Client::read_resource_task(const std::strin

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

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

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

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

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

Expand Down
7 changes: 5 additions & 2 deletions include/fastmcpp/client/transports.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ class StdioTransport : public ITransport
/// 3. Server sends JSON-RPC responses back via the SSE stream
class SseClientTransport : public ITransport,
public IServerRequestTransport,
public IResettableTransport
public IResettableTransport,
public ISessionTransport
{
public:
/// Construct an SSE client transport
Expand Down Expand Up @@ -179,7 +180,9 @@ class SseClientTransport : public ITransport,
/// 3. Session ID management via Mcp-Session-Id header
///
/// Reference: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/
class StreamableHttpTransport : public ITransport, public IResettableTransport
class StreamableHttpTransport : public ITransport,
public IResettableTransport,
public ISessionTransport
{
public:
/// Construct a Streamable HTTP client transport
Expand Down
5 changes: 5 additions & 0 deletions include/fastmcpp/exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ struct ValidationError : public Error
using Error::Error;
};

struct ToolTimeoutError : public Error
{
using Error::Error;
};

struct TransportError : public Error
{
using Error::Error;
Expand Down
3 changes: 2 additions & 1 deletion include/fastmcpp/proxy.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ class ProxyApp

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

/// Read a resource by URI
/// Tries local resources first, falls back to remote
Expand Down
36 changes: 35 additions & 1 deletion include/fastmcpp/server/context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ enum class LogLevel
Error
};

enum class TransportType
{
Stdio,
Sse,
StreamableHttp
};

// ============================================================================
// Sampling types (for Context.sample())
// ============================================================================
Expand Down Expand Up @@ -146,6 +153,21 @@ inline std::string to_string(LogLevel level)
}
}

inline std::string to_string(TransportType transport)
{
switch (transport)
{
case TransportType::Stdio:
return "stdio";
case TransportType::Sse:
return "sse";
case TransportType::StreamableHttp:
return "streamable-http";
default:
return "unknown";
}
}

using LogCallback = std::function<void(LogLevel, const std::string&, const std::string&)>;
using ProgressCallback =
std::function<void(const std::string&, double, double, const std::string&)>;
Expand All @@ -158,7 +180,8 @@ class Context
Context(const resources::ResourceManager& rm, const prompts::PromptManager& pm,
std::optional<fastmcpp::Json> request_meta,
std::optional<std::string> request_id = std::nullopt,
std::optional<std::string> session_id = std::nullopt);
std::optional<std::string> session_id = std::nullopt,
std::optional<TransportType> transport = std::nullopt);

std::vector<resources::Resource> list_resources() const;
std::vector<prompts::Prompt> list_prompts() const;
Expand All @@ -177,6 +200,16 @@ class Context
{
return session_id_;
}
std::optional<std::string> transport() const
{
if (!transport_.has_value())
return std::nullopt;
return to_string(*transport_);
}
std::optional<TransportType> transport_type() const
{
return transport_;
}

std::optional<std::string> client_id() const
{
Expand Down Expand Up @@ -398,6 +431,7 @@ class Context
std::optional<fastmcpp::Json> request_meta_;
std::optional<std::string> request_id_;
std::optional<std::string> session_id_;
std::optional<TransportType> transport_;
mutable std::unordered_map<std::string, std::any> state_;
LogCallback log_callback_;
ProgressCallback progress_callback_;
Expand Down
Loading
Loading