From 879e6f6b6f28213a2c1b7372008494ad22ba79eb Mon Sep 17 00:00:00 2001 From: Elias Bachaalany Date: Tue, 20 Jan 2026 10:49:10 -0800 Subject: [PATCH 1/2] Fix JSON-RPC 2.0 notification handling in StreamableHttpServerWrapper According to JSON-RPC 2.0 specification section 4.1: "A Notification is a Request object without an 'id' member... The Server MUST NOT reply to a Notification." Previously, the StreamableHttpServerWrapper would pass notifications to the handler and return whatever response it produced (typically an error for unknown methods like 'notifications/initialized'). This caused MCP clients like OpenAI Codex CLI to fail during handshaking. This fix: - Detects notifications by checking for missing/null 'id' field - Returns 202 Accepted with no response body for notifications - Still calls the handler so notification processing can occur - Silently ignores any handler errors for notifications Added test_notification_handling() to verify: - notifications/initialized returns 202 with empty body - notifications/cancelled returns 202 with empty body Fixes compatibility with MCP clients that follow the JSON-RPC 2.0 spec. --- src/server/streamable_http_server.cpp | 21 ++++++ tests/server/streamable_http_integration.cpp | 73 ++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/server/streamable_http_server.cpp b/src/server/streamable_http_server.cpp index 62d4251..e7bc20c 100644 --- a/src/server/streamable_http_server.cpp +++ b/src/server/streamable_http_server.cpp @@ -211,6 +211,27 @@ bool StreamableHttpServerWrapper::start() return; } + // Check if this is a notification (no "id" field means notification) + // JSON-RPC 2.0 spec: server MUST NOT reply to notifications + bool is_notification = !message.contains("id") || message["id"].is_null(); + + if (is_notification) + { + // For notifications, call handler but don't send response body + // This is required by JSON-RPC 2.0 spec and MCP protocol + try + { + handler_(message); // Process but ignore result + } + catch (...) + { + // Silently ignore errors for notifications + } + res.set_header("Mcp-Session-Id", session_id); + res.status = 202; // Accepted, no content + return; + } + // Normal request - process with handler auto response = handler_(message); diff --git a/tests/server/streamable_http_integration.cpp b/tests/server/streamable_http_integration.cpp index 4e86a46..17dfc56 100644 --- a/tests/server/streamable_http_integration.cpp +++ b/tests/server/streamable_http_integration.cpp @@ -452,6 +452,78 @@ void test_default_timeout_allows_slow_tool() server.stop(); } +void test_notification_handling() +{ + std::cout << " test_notification_handling... " << std::flush; + + const int port = 18356; + const std::string host = "127.0.0.1"; + + // Create minimal handler + tools::ToolManager tool_mgr; + std::unordered_map descriptions; + auto handler = mcp::make_mcp_handler("notification_test", "1.0.0", tool_mgr, descriptions); + + server::StreamableHttpServerWrapper server(handler, host, port, "/mcp"); + bool started = server.start(); + assert(started && "Server failed to start"); + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + try + { + httplib::Client cli(host, port); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(5, 0); + + // First initialize to get a session + Json init_request = {{"jsonrpc", "2.0"}, + {"id", 1}, + {"method", "initialize"}, + {"params", + {{"protocolVersion", "2024-11-05"}, + {"capabilities", Json::object()}, + {"clientInfo", {{"name", "test"}, {"version", "1.0"}}}}}}; + + auto init_res = cli.Post("/mcp", init_request.dump(), "application/json"); + assert(init_res && init_res->status == 200); + + std::string session_id = init_res->get_header_value("Mcp-Session-Id"); + assert(!session_id.empty() && "Should have session ID"); + + // Now send a notification (no "id" field = notification per JSON-RPC 2.0) + // JSON-RPC 2.0 spec: server MUST NOT reply to notifications + Json notification = {{"jsonrpc", "2.0"}, {"method", "notifications/initialized"}}; + + httplib::Headers headers = {{"Mcp-Session-Id", session_id}}; + auto notif_res = cli.Post("/mcp", headers, notification.dump(), "application/json"); + + // Server should return 202 Accepted with no content body + assert(notif_res && "Notification request should succeed"); + assert(notif_res->status == 202 && "Notification should return 202 Accepted"); + assert(notif_res->body.empty() && "Notification response should have no body"); + + // Test another common notification: notifications/cancelled + Json cancel_notification = {{"jsonrpc", "2.0"}, + {"method", "notifications/cancelled"}, + {"params", {{"requestId", "123"}, {"reason", "timeout"}}}}; + + auto cancel_res = cli.Post("/mcp", headers, cancel_notification.dump(), "application/json"); + assert(cancel_res && cancel_res->status == 202 && "Cancel notification should return 202"); + assert(cancel_res->body.empty() && "Cancel notification response should have no body"); + + std::cout << "PASSED\n"; + } + catch (const std::exception& e) + { + std::cout << "FAILED: " << e.what() << "\n"; + server.stop(); + throw; + } + + server.stop(); +} + int main() { std::cout << "Streamable HTTP Integration Tests\n"; @@ -465,6 +537,7 @@ int main() test_server_info(); test_error_handling(); test_default_timeout_allows_slow_tool(); + test_notification_handling(); std::cout << "\nAll tests passed!\n"; return 0; From 811840aae5f80106df8fe25ef7786f7a49a054e2 Mon Sep 17 00:00:00 2001 From: Elias Bachaalany Date: Tue, 20 Jan 2026 10:52:55 -0800 Subject: [PATCH 2/2] Add pre-commit hook for automatic clang-format Adds .githooks/pre-commit that automatically formats staged C++ files before commit. This prevents formatting issues from reaching CI. To enable: git config core.hooksPath .githooks The hook: - Finds clang-format (prefers versioned like clang-format-19) - Formats only staged .cpp/.hpp/.h/.c files - Re-stages formatted files automatically --- .githooks/README.md | 27 +++++++++++++++++++++++++++ .githooks/pre-commit | 30 +++++++++++++++++++----------- 2 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 .githooks/README.md diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100644 index 0000000..bfa83ac --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,27 @@ +# Git Hooks + +This directory contains git hooks for the fastmcpp project. + +## Installation + +Run one of the following commands to enable the hooks: + +```bash +# Option 1: Configure git to use this directory for hooks +git config core.hooksPath .githooks + +# Option 2: Symlink (Linux/macOS) +ln -sf ../../.githooks/pre-commit .git/hooks/pre-commit +``` + +## Available Hooks + +### pre-commit + +Automatically formats staged C++ files with clang-format before commit. + +- Finds clang-format (prefers versioned like clang-format-19) +- Formats only staged `.cpp`, `.hpp`, `.h`, `.c` files +- Re-stages formatted files automatically + +This ensures all committed code follows the project's formatting style. diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 4180e12..be55cd8 100644 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,26 +1,34 @@ #!/bin/bash -# Pre-commit hook: auto-format staged C++ files -# -# Enable with: git config core.hooksPath .githooks +# Pre-commit hook to automatically format C++ files with clang-format +# Install: git config core.hooksPath .githooks -STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|hpp)$') +# Find clang-format (prefer versioned, fall back to unversioned) +CLANG_FORMAT="" +for cf in clang-format-19 clang-format-18 clang-format-17 clang-format; do + if command -v "$cf" &> /dev/null; then + CLANG_FORMAT="$cf" + break + fi +done -if [ -z "$STAGED_FILES" ]; then +if [ -z "$CLANG_FORMAT" ]; then + echo "Warning: clang-format not found, skipping formatting" exit 0 fi -# Check if clang-format is available -if ! command -v clang-format &> /dev/null; then - echo "Warning: clang-format not found, skipping auto-format" +# Get staged C++ files +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|hpp|h|c)$') + +if [ -z "$STAGED_FILES" ]; then exit 0 fi -# Auto-format and re-stage +# Format each staged file for file in $STAGED_FILES; do if [ -f "$file" ]; then - clang-format -i "$file" + $CLANG_FORMAT -i "$file" git add "$file" fi done -exit 0 +echo "Formatted $(echo "$STAGED_FILES" | wc -w) file(s) with $CLANG_FORMAT"