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
27 changes: 27 additions & 0 deletions .githooks/README.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 19 additions & 11 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -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"
21 changes: 21 additions & 0 deletions src/server/streamable_http_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
73 changes: 73 additions & 0 deletions tests/server/streamable_http_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string, std::string> 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";
Expand All @@ -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;
Expand Down
Loading