diff --git a/include/fastmcpp/client/client.hpp b/include/fastmcpp/client/client.hpp index 530913c..f498cfd 100644 --- a/include/fastmcpp/client/client.hpp +++ b/include/fastmcpp/client/client.hpp @@ -1050,10 +1050,8 @@ class Client // ValidationError so older servers / partial responses do not crash the client. // - "content" present but not an array is treated as empty (do not crash). if (body.contains("content") && body["content"].is_array()) - { for (const auto& c : body["content"]) result.content.push_back(parse_content_block(c)); - } // else: leave result.content empty if (body.contains("structuredContent")) diff --git a/include/fastmcpp/client/types.hpp b/include/fastmcpp/client/types.hpp index 62f23a5..c8b736c 100644 --- a/include/fastmcpp/client/types.hpp +++ b/include/fastmcpp/client/types.hpp @@ -401,9 +401,8 @@ inline void from_json(const fastmcpp::Json& j, ToolInfo& t) // Python fastmcp >= 2.x exposes per-tool version via _meta.fastmcp.version (see // fastmcp_slim/fastmcp/utilities/components.py:get_meta). Surface it as ToolInfo.version // if no top-level "version" was provided so the proxy passthrough preserves the field. - if (!t.version && j["_meta"].is_object() && j["_meta"].contains("fastmcp") - && j["_meta"]["fastmcp"].is_object() - && j["_meta"]["fastmcp"].contains("version")) + if (!t.version && j["_meta"].is_object() && j["_meta"].contains("fastmcp") && + j["_meta"]["fastmcp"].is_object() && j["_meta"]["fastmcp"].contains("version")) { const auto& v = j["_meta"]["fastmcp"]["version"]; if (v.is_string()) diff --git a/include/fastmcpp/tools/tool_transform.hpp b/include/fastmcpp/tools/tool_transform.hpp index 8949964..ca6e20e 100644 --- a/include/fastmcpp/tools/tool_transform.hpp +++ b/include/fastmcpp/tools/tool_transform.hpp @@ -157,7 +157,7 @@ build_transformed_schema(const Json& parent_schema, if (k == "$defs" && v.is_object()) { hoisted_defs = v; - continue; // do not also write it under the property + continue; // do not also write it under the property } new_prop[k] = v; } diff --git a/src/client/sampling_handlers.cpp b/src/client/sampling_handlers.cpp index f20ba04..5094b73 100644 --- a/src/client/sampling_handlers.cpp +++ b/src/client/sampling_handlers.cpp @@ -261,8 +261,7 @@ static fastmcpp::Json build_openai_messages(const fastmcpp::Json& params) "' content not yet supported (F16 / fastmcp #3550); cannot dispatch sampling " "request"); // Unknown type — surface clearly so callers don't get silent data loss. - throw std::runtime_error( - "OpenAI sampling handler: unhandled content type '" + t + "'"); + throw std::runtime_error("OpenAI sampling handler: unhandled content type '" + t + "'"); } std::string text = join_text_blocks(content); diff --git a/src/providers/openapi_provider.cpp b/src/providers/openapi_provider.cpp index 1a41d13..d7700b6 100644 --- a/src/providers/openapi_provider.cpp +++ b/src/providers/openapi_provider.cpp @@ -444,7 +444,8 @@ Json OpenAPIProvider::invoke_route(const RouteDefinition& route, const Json& arg std::ostringstream query; bool first = true; - auto append_pair = [&](const std::string& key, const std::string& val) { + auto append_pair = [&](const std::string& key, const std::string& val) + { query << (first ? "?" : "&"); first = false; query << url_encode_component(key) << "=" << url_encode_component(val); diff --git a/src/server/response_limiting_middleware.cpp b/src/server/response_limiting_middleware.cpp index d90c6ae..0743c23 100644 --- a/src/server/response_limiting_middleware.cpp +++ b/src/server/response_limiting_middleware.cpp @@ -71,7 +71,8 @@ AfterHook ResponseLimitingMiddleware::make_hook() const // outputSchema after truncation) and signal bypass via `_meta = {}` so MCP SDK // clients accept the response as a vanilla CallToolResult instead of failing // outputSchema validation. Apply at both shapes (route payload + JSON-RPC envelope). - auto bypass_output_schema = [](fastmcpp::Json& obj) { + auto bypass_output_schema = [](fastmcpp::Json& obj) + { if (obj.contains("structuredContent")) obj.erase("structuredContent"); if (!obj.contains("_meta") || !obj["_meta"].is_object()) diff --git a/src/util/json_schema_type.cpp b/src/util/json_schema_type.cpp index 965e140..31f2b19 100644 --- a/src/util/json_schema_type.cpp +++ b/src/util/json_schema_type.cpp @@ -58,7 +58,8 @@ const std::regex& cached_regex_required(const std::string& key, const std::strin { auto* p = cached_regex(key, pattern); if (!p) - throw fastmcpp::ValidationError("internal regex compile failure for built-in pattern: " + key); + throw fastmcpp::ValidationError("internal regex compile failure for built-in pattern: " + + key); return *p; } @@ -152,8 +153,8 @@ SchemaValue handle_string(const fastmcpp::Json& schema, const fastmcpp::Json& in } else if (fmt == "date-time") { - const auto& dt_re = - cached_regex_required("date-time", R"(^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$)"); + const auto& dt_re = cached_regex_required( + "date-time", R"(^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$)"); if (!std::regex_match(value, dt_re)) throw fastmcpp::ValidationError("Invalid date-time format at " + path); } @@ -363,7 +364,7 @@ SchemaValue convert(const fastmcpp::Json& schema, const fastmcpp::Json& instance if (schema.is_boolean()) { if (schema.get()) - return instance; // true: accept-any, pass through + return instance; // true: accept-any, pass through throw fastmcpp::ValidationError("schema=false rejects all values at " + path); } diff --git a/tests/tools/test_tool_timeout.cpp b/tests/tools/test_tool_timeout.cpp index 517892c..c8fa107 100644 --- a/tests/tools/test_tool_timeout.cpp +++ b/tests/tools/test_tool_timeout.cpp @@ -27,11 +27,14 @@ void test_tool_timeout_triggers() Tool slow_tool("slow", Json::object(), Json::object(), [](const Json&) -> Json { - sleep_for_at_least(50ms); + // Large sleep margin so the timeout fires reliably even on + // slow CI runners (macOS Debug) where scheduling jitter can + // delay future::wait_for() past the worker's sleep duration. + sleep_for_at_least(5s); return Json{{"ok", true}}; }); - slow_tool.set_timeout(10ms); + slow_tool.set_timeout(50ms); bool threw = false; try @@ -72,11 +75,11 @@ void test_manager_timeout_toggle() Tool slow_tool("slow_manager", Json::object(), Json::object(), [](const Json&) -> Json { - sleep_for_at_least(40ms); + sleep_for_at_least(5s); return Json{{"ok", true}}; }); - slow_tool.set_timeout(10ms); + slow_tool.set_timeout(50ms); ToolManager tm; tm.register_tool(slow_tool); diff --git a/tests/transports/stdio_lifecycle.cpp b/tests/transports/stdio_lifecycle.cpp index 8d878e9..e96d2a7 100644 --- a/tests/transports/stdio_lifecycle.cpp +++ b/tests/transports/stdio_lifecycle.cpp @@ -7,6 +7,10 @@ #include #include +#ifndef _WIN32 +#include +#endif + static std::string find_stdio_server_binary() { namespace fs = std::filesystem; @@ -30,6 +34,15 @@ int main() using fastmcpp::Json; using fastmcpp::client::StdioTransport; +#ifndef _WIN32 + // Ignore SIGPIPE: writing to a closed subprocess stdin (e.g. when the child + // has already exited, as in Test 1's `sh -c "exit 42"`) must produce + // EPIPE/return -1, not kill this test binary. macOS Debug runners under + // CI load can race the child's exit ahead of our first write, surfacing + // this as a SIGPIPE-induced test failure. + signal(SIGPIPE, SIG_IGN); +#endif + // Test 1: Server process crash surfaces TransportError with context std::cout << "Test: server crash surfaces TransportError...\n"; {