From 98beecaaf1d6a0cccaf6e80f72779a499029c7e9 Mon Sep 17 00:00:00 2001 From: Elias Bachaalany Date: Tue, 19 May 2026 10:05:20 -0700 Subject: [PATCH 1/2] fix(ci): clang-format sweep + widen tool-timeout test margins for macOS Debug Fixes the two CI failures on main (PR #44): - format-check: 7 files needed clang-format normalization (left over from the v3.1.0->v3.3.1 catch-up cycle and community PR merges); ran clang-format -i on the flagged set. - macos-latest (Debug): fastmcpp_tools_timeout::test_tool_timeout_triggers and test_manager_timeout_toggle could race on macOS Debug runners under load -- the 10ms timeout / 40-50ms worker-sleep window was tight enough that scheduling jitter occasionally let future::wait_for() return 'ready' after the worker completed, instead of 'timeout' before it did. Widened both to 50ms timeout / 5s worker-sleep -- 100x margin -- without slowing the test (it still returns within ~100ms). Files formatted: - include/fastmcpp/client/{client,types}.hpp - include/fastmcpp/tools/tool_transform.hpp - src/client/sampling_handlers.cpp - src/providers/openapi_provider.cpp - src/server/response_limiting_middleware.cpp - src/util/json_schema_type.cpp - tests/tools/test_tool_timeout.cpp (also widened; format swept) Local verification: fastmcpp Debug ctest 103/103 GREEN. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- include/fastmcpp/client/client.hpp | 2 -- include/fastmcpp/client/types.hpp | 5 ++--- include/fastmcpp/tools/tool_transform.hpp | 2 +- src/client/sampling_handlers.cpp | 3 +-- src/providers/openapi_provider.cpp | 3 ++- src/server/response_limiting_middleware.cpp | 3 ++- src/util/json_schema_type.cpp | 9 +++++---- tests/tools/test_tool_timeout.cpp | 11 +++++++---- 8 files changed, 20 insertions(+), 18 deletions(-) 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); From 5e824ca0c3f8ed00af2ad7f67539ab625749692f Mon Sep 17 00:00:00 2001 From: Elias Bachaalany Date: Tue, 19 May 2026 10:24:14 -0700 Subject: [PATCH 2/2] fix(test): mask SIGPIPE in fastmcpp_stdio_lifecycle (macOS Debug) Test 1 ('server crash surfaces TransportError') launches sh -c \"exit 42\" and then writes to the subprocess's stdin. On macOS Debug under CI load, the subprocess can exit before our first write completes, causing the kernel to raise SIGPIPE in the test binary -- which terminates the test process before TransportError can propagate. Result: test #76 reports SIGPIPE***Exception in 0.03s with no useful diagnostic. Fix: ignore SIGPIPE in main() on POSIX so write(2) returns -1/EPIPE instead of killing the process. The C++ port code already handles EPIPE correctly; this just stops the kernel from short-circuiting that path. Windows is unaffected (no SIGPIPE). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/transports/stdio_lifecycle.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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"; {