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
19 changes: 16 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 @@ -229,9 +233,18 @@ 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 split into 3 parts to avoid MSVC heap exhaustion
add_executable(fastmcpp_server_interactions_p1 tests/server/interactions_p1.cpp)
target_link_libraries(fastmcpp_server_interactions_p1 PRIVATE fastmcpp_core)
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)
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)
add_test(NAME fastmcpp_server_interactions_p3 COMMAND fastmcpp_server_interactions_p3)

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;
}
111 changes: 2 additions & 109 deletions tests/server/interactions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "fastmcpp/server/server.hpp"
#include "fastmcpp/tools/manager.hpp"
#include "fastmcpp/tools/tool.hpp"
#include "interactions_fixture.hpp"

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

std::shared_ptr<server::Server> create_interaction_server()
{
auto srv = std::make_shared<server::Server>();

// Tool: add - basic arithmetic
srv->route(
"tools/list",
[](const Json&)
{
Json tools = Json::array();

tools.push_back(
Json{{"name", "add"},
{"description", "Add two numbers"},
{"inputSchema", Json{{"type", "object"},
{"properties", Json{{"x", {{"type", "integer"}}},
{"y", {{"type", "integer"}}}}},
{"required", Json::array({"x", "y"})}}}});

tools.push_back(
Json{{"name", "greet"},
{"description", "Greet a person"},
{"inputSchema", Json{{"type", "object"},
{"properties", Json{{"name", {{"type", "string"}}}}},
{"required", Json::array({"name"})}}}});

tools.push_back(Json{{"name", "error_tool"},
{"description", "Always fails"},
{"inputSchema", Json{{"type", "object"}}}});

tools.push_back(Json{{"name", "list_tool"},
{"description", "Returns a list"},
{"inputSchema", Json{{"type", "object"}}}});

tools.push_back(Json{{"name", "nested_tool"},
{"description", "Returns nested data"},
{"inputSchema", Json{{"type", "object"}}}});

tools.push_back(Json{
{"name", "optional_params"},
{"description", "Has optional params"},
{"inputSchema",
Json{{"type", "object"},
{"properties", Json{{"required_param", {{"type", "string"}}},
{"optional_param",
{{"type", "string"}, {"default", "default_value"}}}}},
{"required", Json::array({"required_param"})}}}});

return Json{{"tools", tools}};
});
// create_interaction_server moved to interactions_fixture.hpp

srv->route(
"tools/call",
[](const Json& in)
{
std::string name = in.at("name").get<std::string>();
Json args = in.value("arguments", Json::object());

if (name == "add")
{
int x = args.at("x").get<int>();
int y = args.at("y").get<int>();
int result = x + y;
return Json{{"content", Json::array({Json{{"type", "text"},
{"text", std::to_string(result)}}})},
{"structuredContent", Json{{"result", result}}},
{"isError", false}};
}
if (name == "greet")
{
std::string greeting = "Hello, " + args.at("name").get<std::string>() + "!";
return Json{{"content", Json::array({Json{{"type", "text"}, {"text", greeting}}})},
{"isError", false}};
}
if (name == "error_tool")
{
return Json{
{"content", Json::array({Json{{"type", "text"}, {"text", "Test error"}}})},
{"isError", true}};
}
if (name == "list_tool")
{
return Json{
{"content", Json::array({Json{{"type", "text"}, {"text", "[\"x\",2]"}}})},
{"structuredContent", Json{{"result", Json::array({"x", 2})}}},
{"isError", false}};
}
if (name == "nested_tool")
{
Json nested = {{"level1", {{"level2", {{"value", 42}}}}}};
return Json{
{"content", Json::array({Json{{"type", "text"}, {"text", nested.dump()}}})},
{"structuredContent", Json{{"result", nested}}},
{"isError", false}};
}
if (name == "optional_params")
{
std::string req = args.at("required_param").get<std::string>();
std::string opt = args.value("optional_param", "default_value");
return Json{
{"content", Json::array({Json{{"type", "text"}, {"text", req + ":" + opt}}})},
{"isError", false}};
}
return Json{
{"content", Json::array({Json{{"type", "text"}, {"text", "Unknown tool"}}})},
{"isError", true}};
});

return srv;
}

// ============================================================================
// TestTools - Basic tool operations
Expand Down
Loading
Loading