Skip to content
Closed
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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ jobs:
uses: actions/cache@v4
with:
path: build/_deps
key: deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('CMakeLists.txt') }}
key: deps-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('CMakeLists.txt') }}
restore-keys: |
deps-${{ runner.os }}-${{ runner.arch }}-
deps-v2-${{ runner.os }}-${{ runner.arch }}-

- name: Configure (Unix)
if: runner.os != 'Windows'
Expand All @@ -49,9 +49,11 @@ jobs:

- name: Configure (Windows)
if: runner.os == 'Windows'
# Use x64 host tools for more compiler heap space (avoids C1060 errors on template-heavy code)
run: >
cmake -B build -S .
-G "Visual Studio 17 2022"
-T host=x64
-DFASTMCPP_BUILD_TESTS=ON
-DFASTMCPP_BUILD_EXAMPLES=ON

Expand Down
51 changes: 48 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ if(FASTMCPP_BUILD_TESTS)
target_link_libraries(fastmcpp_http_integration PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_http_integration COMMAND fastmcpp_http_integration)

add_executable(fastmcpp_http_auth tests/server/http_auth.cpp)
target_link_libraries(fastmcpp_http_auth PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_http_auth COMMAND fastmcpp_http_auth)

add_executable(fastmcpp_json_schema tests/schema/json_schema.cpp)
target_link_libraries(fastmcpp_json_schema PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_json_schema COMMAND fastmcpp_json_schema)
Expand Down Expand Up @@ -202,12 +206,15 @@ if(FASTMCPP_BUILD_TESTS)
add_executable(fastmcpp_sse_server tests/server/sse.cpp)
target_link_libraries(fastmcpp_sse_server PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_sse_server COMMAND fastmcpp_sse_server)
# Avoid port conflicts with parallel tests; SSE tests can be flaky on CI
set_tests_properties(fastmcpp_sse_server PROPERTIES RUN_SERIAL TRUE)

# MCP SSE format compliance test (regression test for GitHub Issue #1)
add_executable(fastmcpp_sse_mcp_format tests/server/sse_mcp_format.cpp)
target_link_libraries(fastmcpp_sse_mcp_format PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_sse_mcp_format COMMAND fastmcpp_sse_mcp_format)

set_tests_properties(fastmcpp_sse_mcp_format PROPERTIES RUN_SERIAL TRUE)
# Advanced test suites (Task 3.4)
add_executable(fastmcpp_tools_validation tests/tools/validation.cpp)
target_link_libraries(fastmcpp_tools_validation PRIVATE fastmcpp_core)
Expand All @@ -229,9 +236,47 @@ if(FASTMCPP_BUILD_TESTS)
target_link_libraries(fastmcpp_server_patterns PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_patterns COMMAND fastmcpp_server_patterns)

add_executable(fastmcpp_server_interactions tests/server/interactions.cpp)
target_link_libraries(fastmcpp_server_interactions PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_interactions COMMAND fastmcpp_server_interactions)
# Server interactions helpers library (compiled separately to avoid MSVC heap exhaustion)
# Each helper part as separate library to avoid MSVC heap exhaustion in parallel builds
add_library(fastmcpp_helpers_p1 STATIC tests/server/interactions_helpers_p1.cpp)
target_link_libraries(fastmcpp_helpers_p1 PRIVATE fastmcpp_core)

add_library(fastmcpp_helpers_p2 STATIC tests/server/interactions_helpers_p2.cpp)
target_link_libraries(fastmcpp_helpers_p2 PRIVATE fastmcpp_core)

add_library(fastmcpp_helpers_p3 STATIC tests/server/interactions_helpers_p3.cpp)
target_link_libraries(fastmcpp_helpers_p3 PRIVATE fastmcpp_core)

add_library(fastmcpp_helpers_p4 STATIC tests/server/interactions_helpers_p4.cpp)
target_link_libraries(fastmcpp_helpers_p4 PRIVATE fastmcpp_core)

add_library(fastmcpp_helpers_p5 STATIC tests/server/interactions_helpers_p5.cpp)
target_link_libraries(fastmcpp_helpers_p5 PRIVATE fastmcpp_core)

# Server interactions split into 6 parts
add_executable(fastmcpp_server_interactions_p1 tests/server/interactions_p1.cpp)
target_link_libraries(fastmcpp_server_interactions_p1 PRIVATE fastmcpp_core fastmcpp_helpers_p1 fastmcpp_helpers_p2 fastmcpp_helpers_p3 fastmcpp_helpers_p4 fastmcpp_helpers_p5)
add_test(NAME fastmcpp_server_interactions_p1 COMMAND fastmcpp_server_interactions_p1)

add_executable(fastmcpp_server_interactions_p2 tests/server/interactions_p2.cpp)
target_link_libraries(fastmcpp_server_interactions_p2 PRIVATE fastmcpp_core fastmcpp_helpers_p1 fastmcpp_helpers_p2 fastmcpp_helpers_p3 fastmcpp_helpers_p4 fastmcpp_helpers_p5)
add_test(NAME fastmcpp_server_interactions_p2 COMMAND fastmcpp_server_interactions_p2)

add_executable(fastmcpp_server_interactions_p3 tests/server/interactions_p3.cpp)
target_link_libraries(fastmcpp_server_interactions_p3 PRIVATE fastmcpp_core fastmcpp_helpers_p1 fastmcpp_helpers_p2 fastmcpp_helpers_p3 fastmcpp_helpers_p4 fastmcpp_helpers_p5)
add_test(NAME fastmcpp_server_interactions_p3 COMMAND fastmcpp_server_interactions_p3)

add_executable(fastmcpp_server_interactions_p4 tests/server/interactions_p4.cpp)
target_link_libraries(fastmcpp_server_interactions_p4 PRIVATE fastmcpp_core fastmcpp_helpers_p1 fastmcpp_helpers_p2 fastmcpp_helpers_p3 fastmcpp_helpers_p4 fastmcpp_helpers_p5)
add_test(NAME fastmcpp_server_interactions_p4 COMMAND fastmcpp_server_interactions_p4)

add_executable(fastmcpp_server_interactions_p5 tests/server/interactions_p5.cpp)
target_link_libraries(fastmcpp_server_interactions_p5 PRIVATE fastmcpp_core fastmcpp_helpers_p1 fastmcpp_helpers_p2 fastmcpp_helpers_p3 fastmcpp_helpers_p4 fastmcpp_helpers_p5)
add_test(NAME fastmcpp_server_interactions_p5 COMMAND fastmcpp_server_interactions_p5)

add_executable(fastmcpp_server_interactions_p6 tests/server/interactions_p6.cpp)
target_link_libraries(fastmcpp_server_interactions_p6 PRIVATE fastmcpp_core fastmcpp_helpers_p1 fastmcpp_helpers_p2 fastmcpp_helpers_p3 fastmcpp_helpers_p4 fastmcpp_helpers_p5)
add_test(NAME fastmcpp_server_interactions_p6 COMMAND fastmcpp_server_interactions_p6)

add_executable(fastmcpp_server_context_meta tests/server/context_meta.cpp)
target_link_libraries(fastmcpp_server_context_meta PRIVATE fastmcpp_core)
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ fastmcpp is a C++ port of the Python [fastmcp](https://github.com/jlowin/fastmcp
- Middleware for request/response processing.
- Integration with MCP‑compatible CLI tools.
- Cross‑platform: Windows, Linux, macOS.
- Server hardening knobs: optional auth tokens, CORS allowlist, payload/queue limits, and
per-connection SSE session binding.

## Requirements

Expand Down Expand Up @@ -155,6 +157,10 @@ int main() {
}
```

To enable hardening on HTTP/SSE servers, provide the optional auth token, allowed origin, and
payload limit arguments. For SSE, POST requests must include the session_id provided by the initial
`event: endpoint` message; connections without the bearer token (when set) are rejected.

### HTTP client

```cpp
Expand Down
6 changes: 5 additions & 1 deletion include/fastmcpp/server/http_server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class HttpServerWrapper
{
public:
HttpServerWrapper(std::shared_ptr<Server> core, std::string host = "127.0.0.1",
int port = 18080);
int port = 18080, std::string auth_token = "",
std::string allowed_origin = "", size_t payload_limit = 1024 * 1024);
~HttpServerWrapper();

bool start();
Expand All @@ -40,6 +41,9 @@ class HttpServerWrapper
std::shared_ptr<Server> core_;
std::string host_;
int port_;
std::string auth_token_;
std::string allowed_origin_;
size_t payload_limit_;
std::unique_ptr<httplib::Server> svr_;
std::thread thread_;
std::atomic<bool> running_{false};
Expand Down
28 changes: 26 additions & 2 deletions src/server/http_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
namespace fastmcpp::server
{

HttpServerWrapper::HttpServerWrapper(std::shared_ptr<Server> core, std::string host, int port)
: core_(std::move(core)), host_(std::move(host)), port_(port)
HttpServerWrapper::HttpServerWrapper(std::shared_ptr<Server> core, std::string host, int port,
std::string auth_token, std::string allowed_origin,
size_t payload_limit)
: core_(std::move(core)), host_(std::move(host)), port_(port),
auth_token_(std::move(auth_token)), allowed_origin_(std::move(allowed_origin)),
payload_limit_(payload_limit)
{
}

Expand All @@ -24,16 +28,36 @@ bool HttpServerWrapper::start()
if (running_)
return false;
svr_ = std::make_unique<httplib::Server>();
if (payload_limit_ > 0)
svr_->set_payload_max_length(payload_limit_);
// Generic POST: /<route>
svr_->Post(R"(/(.*))",
[this](const httplib::Request& req, httplib::Response& res)
{
if (!auth_token_.empty())
{
std::string auth = req.get_header_value("Authorization");
if (auth.empty())
auth = req.get_header_value("authorization");
const std::string expected = "Bearer " + auth_token_;
if (auth != expected)
{
res.status = 401;
res.set_content("{\"error\":\"unauthorized\"}", "application/json");
return;
}
}
try
{
auto route = req.matches[1].str();
auto payload = fastmcpp::util::json::parse(req.body);
auto out = core_->handle(route, payload);
res.set_content(out.dump(), "application/json");
if (!allowed_origin_.empty())
{
res.set_header("Access-Control-Allow-Origin", allowed_origin_.c_str());
res.set_header("Vary", "Origin");
}
res.status = 200;
}
catch (const fastmcpp::NotFoundError& e)
Expand Down
67 changes: 67 additions & 0 deletions tests/server/http_auth.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include "fastmcpp/server/http_server.hpp"
#include "fastmcpp/server/server.hpp"
#include "fastmcpp/util/json.hpp"

#include <cassert>
#include <chrono>
#include <httplib.h>
#include <string>
#include <thread>

int main()
{
using namespace fastmcpp;
auto core = std::make_shared<server::Server>();
core->route("sum", [](const Json& j) { return j.at("a").get<int>() + j.at("b").get<int>(); });

const int port = 18082;
const std::string token = "secret-token";
const std::string origin = "https://example.com";
server::HttpServerWrapper http{core, "127.0.0.1", port,
token, origin, static_cast<size_t>(1024 * 16)};
if (!http.start())
{
std::cerr << "failed to start HTTP server\n";
return 1;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));

httplib::Client cli("127.0.0.1", port);

// Missing auth should be rejected
auto res = cli.Post("/sum", Json{{"a", 1}, {"b", 2}}.dump(), "application/json");
if (!res || res->status != 401)
{
std::cerr << "expected 401 for missing auth (got "
<< (res ? std::to_string(res->status) : std::string("no response")) << ")\n";
http.stop();
return 1;
}

// Authorized request should succeed and include CORS header
httplib::Headers headers = {{"Authorization", std::string("Bearer ") + token}};
res = cli.Post("/sum", headers, Json{{"a", 5}, {"b", 7}}.dump(), "application/json");
if (!res || res->status != 200)
{
std::cerr << "expected 200 for authorized request\n";
http.stop();
return 1;
}
auto out = Json::parse(res->body);
if (out.get<int>() != 12)
{
std::cerr << "unexpected sum result\n";
http.stop();
return 1;
}
auto cors = res->get_header_value("Access-Control-Allow-Origin");
if (cors != origin)
{
std::cerr << "missing/invalid CORS header\n";
http.stop();
return 1;
}

http.stop();
return 0;
}
Loading
Loading