Skip to content

Commit 82220d9

Browse files
committed
Add HTTP/SSE auth hardening and tests
1 parent 5b3522a commit 82220d9

11 files changed

Lines changed: 5706 additions & 134 deletions

File tree

CMakeLists.txt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ if(FASTMCPP_BUILD_TESTS)
151151
target_link_libraries(fastmcpp_http_integration PRIVATE fastmcpp_core)
152152
add_test(NAME fastmcpp_http_integration COMMAND fastmcpp_http_integration)
153153

154+
add_executable(fastmcpp_http_auth tests/server/http_auth.cpp)
155+
target_link_libraries(fastmcpp_http_auth PRIVATE fastmcpp_core)
156+
add_test(NAME fastmcpp_http_auth COMMAND fastmcpp_http_auth)
157+
154158
add_executable(fastmcpp_json_schema tests/schema/json_schema.cpp)
155159
target_link_libraries(fastmcpp_json_schema PRIVATE fastmcpp_core)
156160
add_test(NAME fastmcpp_json_schema COMMAND fastmcpp_json_schema)
@@ -229,9 +233,18 @@ if(FASTMCPP_BUILD_TESTS)
229233
target_link_libraries(fastmcpp_server_patterns PRIVATE fastmcpp_core)
230234
add_test(NAME fastmcpp_server_patterns COMMAND fastmcpp_server_patterns)
231235

232-
add_executable(fastmcpp_server_interactions tests/server/interactions.cpp)
233-
target_link_libraries(fastmcpp_server_interactions PRIVATE fastmcpp_core)
234-
add_test(NAME fastmcpp_server_interactions COMMAND fastmcpp_server_interactions)
236+
# Server interactions split into 3 parts to avoid MSVC heap exhaustion
237+
add_executable(fastmcpp_server_interactions_p1 tests/server/interactions_p1.cpp)
238+
target_link_libraries(fastmcpp_server_interactions_p1 PRIVATE fastmcpp_core)
239+
add_test(NAME fastmcpp_server_interactions_p1 COMMAND fastmcpp_server_interactions_p1)
240+
241+
add_executable(fastmcpp_server_interactions_p2 tests/server/interactions_p2.cpp)
242+
target_link_libraries(fastmcpp_server_interactions_p2 PRIVATE fastmcpp_core)
243+
add_test(NAME fastmcpp_server_interactions_p2 COMMAND fastmcpp_server_interactions_p2)
244+
245+
add_executable(fastmcpp_server_interactions_p3 tests/server/interactions_p3.cpp)
246+
target_link_libraries(fastmcpp_server_interactions_p3 PRIVATE fastmcpp_core)
247+
add_test(NAME fastmcpp_server_interactions_p3 COMMAND fastmcpp_server_interactions_p3)
235248

236249
add_executable(fastmcpp_server_context_meta tests/server/context_meta.cpp)
237250
target_link_libraries(fastmcpp_server_context_meta PRIVATE fastmcpp_core)

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ fastmcpp is a C++ port of the Python [fastmcp](https://github.com/jlowin/fastmcp
2727
- Middleware for request/response processing.
2828
- Integration with MCP‑compatible CLI tools.
2929
- Cross‑platform: Windows, Linux, macOS.
30+
- Server hardening knobs: optional auth tokens, CORS allowlist, payload/queue limits, and
31+
per-connection SSE session binding.
3032

3133
## Requirements
3234

@@ -155,6 +157,10 @@ int main() {
155157
}
156158
```
157159

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

160166
```cpp

include/fastmcpp/server/http_server.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ class HttpServerWrapper
1818
{
1919
public:
2020
HttpServerWrapper(std::shared_ptr<Server> core, std::string host = "127.0.0.1",
21-
int port = 18080);
21+
int port = 18080, std::string auth_token = "",
22+
std::string allowed_origin = "", size_t payload_limit = 1024 * 1024);
2223
~HttpServerWrapper();
2324

2425
bool start();
@@ -40,6 +41,9 @@ class HttpServerWrapper
4041
std::shared_ptr<Server> core_;
4142
std::string host_;
4243
int port_;
44+
std::string auth_token_;
45+
std::string allowed_origin_;
46+
size_t payload_limit_;
4347
std::unique_ptr<httplib::Server> svr_;
4448
std::thread thread_;
4549
std::atomic<bool> running_{false};

src/server/http_server.cpp

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
namespace fastmcpp::server
99
{
1010

11-
HttpServerWrapper::HttpServerWrapper(std::shared_ptr<Server> core, std::string host, int port)
12-
: core_(std::move(core)), host_(std::move(host)), port_(port)
11+
HttpServerWrapper::HttpServerWrapper(std::shared_ptr<Server> core, std::string host, int port,
12+
std::string auth_token, std::string allowed_origin,
13+
size_t payload_limit)
14+
: core_(std::move(core)), host_(std::move(host)), port_(port),
15+
auth_token_(std::move(auth_token)), allowed_origin_(std::move(allowed_origin)),
16+
payload_limit_(payload_limit)
1317
{
1418
}
1519

@@ -24,16 +28,36 @@ bool HttpServerWrapper::start()
2428
if (running_)
2529
return false;
2630
svr_ = std::make_unique<httplib::Server>();
31+
if (payload_limit_ > 0)
32+
svr_->set_payload_max_length(payload_limit_);
2733
// Generic POST: /<route>
2834
svr_->Post(R"(/(.*))",
2935
[this](const httplib::Request& req, httplib::Response& res)
3036
{
37+
if (!auth_token_.empty())
38+
{
39+
std::string auth = req.get_header_value("Authorization");
40+
if (auth.empty())
41+
auth = req.get_header_value("authorization");
42+
const std::string expected = "Bearer " + auth_token_;
43+
if (auth != expected)
44+
{
45+
res.status = 401;
46+
res.set_content("{\"error\":\"unauthorized\"}", "application/json");
47+
return;
48+
}
49+
}
3150
try
3251
{
3352
auto route = req.matches[1].str();
3453
auto payload = fastmcpp::util::json::parse(req.body);
3554
auto out = core_->handle(route, payload);
3655
res.set_content(out.dump(), "application/json");
56+
if (!allowed_origin_.empty())
57+
{
58+
res.set_header("Access-Control-Allow-Origin", allowed_origin_.c_str());
59+
res.set_header("Vary", "Origin");
60+
}
3761
res.status = 200;
3862
}
3963
catch (const fastmcpp::NotFoundError& e)

tests/server/http_auth.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#include "fastmcpp/server/http_server.hpp"
2+
#include "fastmcpp/server/server.hpp"
3+
#include "fastmcpp/util/json.hpp"
4+
5+
#include <cassert>
6+
#include <chrono>
7+
#include <httplib.h>
8+
#include <string>
9+
#include <thread>
10+
11+
int main()
12+
{
13+
using namespace fastmcpp;
14+
auto core = std::make_shared<server::Server>();
15+
core->route("sum", [](const Json& j) { return j.at("a").get<int>() + j.at("b").get<int>(); });
16+
17+
const int port = 18082;
18+
const std::string token = "secret-token";
19+
const std::string origin = "https://example.com";
20+
server::HttpServerWrapper http{core, "127.0.0.1", port, token, origin,
21+
static_cast<size_t>(1024 * 16)};
22+
if (!http.start())
23+
{
24+
std::cerr << "failed to start HTTP server\n";
25+
return 1;
26+
}
27+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
28+
29+
httplib::Client cli("127.0.0.1", port);
30+
31+
// Missing auth should be rejected
32+
auto res = cli.Post("/sum", Json{{"a", 1}, {"b", 2}}.dump(), "application/json");
33+
if (!res || res->status != 401)
34+
{
35+
std::cerr << "expected 401 for missing auth (got "
36+
<< (res ? std::to_string(res->status) : std::string("no response")) << ")\n";
37+
http.stop();
38+
return 1;
39+
}
40+
41+
// Authorized request should succeed and include CORS header
42+
httplib::Headers headers = {{"Authorization", std::string("Bearer ") + token}};
43+
res = cli.Post("/sum", headers, Json{{"a", 5}, {"b", 7}}.dump(), "application/json");
44+
if (!res || res->status != 200)
45+
{
46+
std::cerr << "expected 200 for authorized request\n";
47+
http.stop();
48+
return 1;
49+
}
50+
auto out = Json::parse(res->body);
51+
if (out.get<int>() != 12)
52+
{
53+
std::cerr << "unexpected sum result\n";
54+
http.stop();
55+
return 1;
56+
}
57+
auto cors = res->get_header_value("Access-Control-Allow-Origin");
58+
if (cors != origin)
59+
{
60+
std::cerr << "missing/invalid CORS header\n";
61+
http.stop();
62+
return 1;
63+
}
64+
65+
http.stop();
66+
return 0;
67+
}

tests/server/interactions.cpp

Lines changed: 2 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "fastmcpp/server/server.hpp"
88
#include "fastmcpp/tools/manager.hpp"
99
#include "fastmcpp/tools/tool.hpp"
10+
#include "interactions_fixture.hpp"
1011

1112
#include <cassert>
1213
#include <iostream>
@@ -19,116 +20,8 @@ using namespace fastmcpp;
1920
// Test Server Fixture - creates a server with multiple tools
2021
// ============================================================================
2122

22-
std::shared_ptr<server::Server> create_interaction_server()
23-
{
24-
auto srv = std::make_shared<server::Server>();
25-
26-
// Tool: add - basic arithmetic
27-
srv->route(
28-
"tools/list",
29-
[](const Json&)
30-
{
31-
Json tools = Json::array();
32-
33-
tools.push_back(
34-
Json{{"name", "add"},
35-
{"description", "Add two numbers"},
36-
{"inputSchema", Json{{"type", "object"},
37-
{"properties", Json{{"x", {{"type", "integer"}}},
38-
{"y", {{"type", "integer"}}}}},
39-
{"required", Json::array({"x", "y"})}}}});
40-
41-
tools.push_back(
42-
Json{{"name", "greet"},
43-
{"description", "Greet a person"},
44-
{"inputSchema", Json{{"type", "object"},
45-
{"properties", Json{{"name", {{"type", "string"}}}}},
46-
{"required", Json::array({"name"})}}}});
47-
48-
tools.push_back(Json{{"name", "error_tool"},
49-
{"description", "Always fails"},
50-
{"inputSchema", Json{{"type", "object"}}}});
51-
52-
tools.push_back(Json{{"name", "list_tool"},
53-
{"description", "Returns a list"},
54-
{"inputSchema", Json{{"type", "object"}}}});
55-
56-
tools.push_back(Json{{"name", "nested_tool"},
57-
{"description", "Returns nested data"},
58-
{"inputSchema", Json{{"type", "object"}}}});
59-
60-
tools.push_back(Json{
61-
{"name", "optional_params"},
62-
{"description", "Has optional params"},
63-
{"inputSchema",
64-
Json{{"type", "object"},
65-
{"properties", Json{{"required_param", {{"type", "string"}}},
66-
{"optional_param",
67-
{{"type", "string"}, {"default", "default_value"}}}}},
68-
{"required", Json::array({"required_param"})}}}});
69-
70-
return Json{{"tools", tools}};
71-
});
23+
// create_interaction_server moved to interactions_fixture.hpp
7224

73-
srv->route(
74-
"tools/call",
75-
[](const Json& in)
76-
{
77-
std::string name = in.at("name").get<std::string>();
78-
Json args = in.value("arguments", Json::object());
79-
80-
if (name == "add")
81-
{
82-
int x = args.at("x").get<int>();
83-
int y = args.at("y").get<int>();
84-
int result = x + y;
85-
return Json{{"content", Json::array({Json{{"type", "text"},
86-
{"text", std::to_string(result)}}})},
87-
{"structuredContent", Json{{"result", result}}},
88-
{"isError", false}};
89-
}
90-
if (name == "greet")
91-
{
92-
std::string greeting = "Hello, " + args.at("name").get<std::string>() + "!";
93-
return Json{{"content", Json::array({Json{{"type", "text"}, {"text", greeting}}})},
94-
{"isError", false}};
95-
}
96-
if (name == "error_tool")
97-
{
98-
return Json{
99-
{"content", Json::array({Json{{"type", "text"}, {"text", "Test error"}}})},
100-
{"isError", true}};
101-
}
102-
if (name == "list_tool")
103-
{
104-
return Json{
105-
{"content", Json::array({Json{{"type", "text"}, {"text", "[\"x\",2]"}}})},
106-
{"structuredContent", Json{{"result", Json::array({"x", 2})}}},
107-
{"isError", false}};
108-
}
109-
if (name == "nested_tool")
110-
{
111-
Json nested = {{"level1", {{"level2", {{"value", 42}}}}}};
112-
return Json{
113-
{"content", Json::array({Json{{"type", "text"}, {"text", nested.dump()}}})},
114-
{"structuredContent", Json{{"result", nested}}},
115-
{"isError", false}};
116-
}
117-
if (name == "optional_params")
118-
{
119-
std::string req = args.at("required_param").get<std::string>();
120-
std::string opt = args.value("optional_param", "default_value");
121-
return Json{
122-
{"content", Json::array({Json{{"type", "text"}, {"text", req + ":" + opt}}})},
123-
{"isError", false}};
124-
}
125-
return Json{
126-
{"content", Json::array({Json{{"type", "text"}, {"text", "Unknown tool"}}})},
127-
{"isError", true}};
128-
});
129-
130-
return srv;
131-
}
13225

13326
// ============================================================================
13427
// TestTools - Basic tool operations

0 commit comments

Comments
 (0)