17#include <nlohmann/json.hpp>
25#include <sys/random.h>
26#include <sys/socket.h>
30namespace entropic::http {
34auto logger = log::get(
"mcp.http_listener");
51 return nlohmann::json{{
"error",
message}}.dump();
68 got +=
n > 0 ?
static_cast<size_t>(
n) : 0;
70 static const char*
hex =
"0123456789abcdef";
73 for (
unsigned char b :
buf) {
74 out.push_back(
hex[
b >> 4]);
75 out.push_back(
hex[
b & 0x0f]);
91 if (
a.size() !=
b.size() ||
a.empty()) {
return false; }
92 volatile unsigned char diff = 0;
93 for (
size_t i = 0;
i <
a.size(); ++
i) {
108 const std::string
suffix =
":" + std::to_string(
port);
120 static const std::string
scheme =
"Bearer ";
133 const char*
home = std::getenv(
"HOME");
134 const std::filesystem::path base =
home !=
nullptr ?
home :
"/tmp";
135 return base /
".entropic" /
"serve" / (std::to_string(
port) +
".token");
148 std::filesystem::create_directories(
dir,
ec);
153 err =
dir.string() +
" is not a plain directory";
155 err =
dir.string() +
" is owned by another user";
156 }
else if ((
st.st_mode & 0777) != 0700 &&
::chmod(
dir.c_str(), 0700) != 0) {
157 err =
dir.string() +
": cannot set mode 0700";
172 const std::string& token, std::string&
err) {
173 const std::string
tmp = path.string() +
".tmp." + std::to_string(
::getpid());
174 const int fd = ::open(
tmp.c_str(),
178 &&
::write(fd, token.data(), token.size())
179 ==
static_cast<ssize_t>(token.size())
181 if (fd >= 0) { ::close(fd); }
184 err =
"cannot write " + path.string() +
": " + std::strerror(
errno);
198 std::lock_guard<std::mutex> lock(mu_);
199 return now < blocked_until_;
209 std::lock_guard<std::mutex> lock(mu_);
268 while ((
nl =
buf.find(
'\n')) != std::string::npos) {
269 const std::string
event =
"data: " +
buf.substr(0,
nl) +
"\n\n";
271 buf.erase(0,
nl + 1);
282void pump_sse(
int fd, httplib::DataSink& sink) {
292 static const char ka[] =
": keepalive\n\n";
297 if (
n <= 0) {
break; }
298 buf.append(
chunk,
static_cast<size_t>(
n));
301 if (
live) { sink.done(); }
313 httplib::Response&
res) {
317 res.set_content(
error_body(
"socketpair failed"),
"application/json");
320 auto job = std::make_shared<SseJob>();
321 job->read_fd =
sv[0];
322 job->worker = std::thread([
bridge, body, fd =
sv[1]] {
323 const std::string
reply =
bridge->dispatch(body, fd) +
"\n";
327 res.set_header(
"Cache-Control",
"no-cache");
328 res.set_chunked_content_provider(
330 [
job](
size_t, httplib::DataSink& sink) {
346 HttpListener::Impl& impl) {
347 const auto now = AuthThrottle::Clock::now();
349 if (
req.has_header(
"Origin")) {
353 }
else if (impl.throttle.blocked(now)) {
357 impl.throttle.record_failure(now);
361 logger->warn(
"[http] refused {} {} from {}: {}",
req.method,
req.path,
362 req.remote_addr, status);
376 httplib::Response&
res) {
377 const auto msg = nlohmann::json::parse(
req.body,
nullptr,
false);
378 if (
msg.is_discarded() || !
msg.is_object()) {
382 }
else if (!
msg.contains(
"id") || !
msg.contains(
"method")) {
384 }
else if (
msg.value(
"method",
"") ==
"tools/call") {
387 res.set_content(
bridge->dispatch(
req.body, -1),
"application/json");
418 const std::atomic<int>&
port) {
421 s.set_read_timeout(30, 0);
422 s.set_write_timeout(30, 0);
423 s.new_task_queue = [] {
return new httplib::ThreadPool(32); };
424 s.Post(
"/mcp", [&impl,
bridge, &
port](
const httplib::Request&
req,
425 httplib::Response&
res) {
434 auto not_allowed = [](
const httplib::Request&, httplib::Response&
res) {
450 return port == 0 ? server.bind_to_any_port(
"127.0.0.1")
451 : (server.bind_to_port(
"127.0.0.1",
port) ?
port : -1);
461std::string HttpListener::publish_token(
int bound) {
463 impl_->token = generate_token();
464 if (impl_->token.empty()) {
465 err =
"no entropy for the bearer token";
467 token_file_ = token_path_for(
bound);
468 if (prepare_private_dir(token_file_.parent_path(),
err)) {
469 write_token_file(token_file_, impl_->token,
err);
486 err =
"cannot bind 127.0.0.1:" + std::to_string(port_.load());
492 thread_ = std::thread([
this] {
493 impl_->server.listen_after_bind();
494 impl_->listen_returned =
true;
496 err = await_running();
499 logger->info(
"[http] serving MCP at http://127.0.0.1:{}/mcp "
500 "(token: {})",
bound, token_file_.string());
520std::string HttpListener::await_running() {
521 for (
int i = 0;
i < 5000; ++
i) {
522 if (impl_->server.is_running() || impl_->listen_returned) {
break; }
523 std::this_thread::sleep_for(std::chrono::milliseconds(1));
525 return impl_->server.is_running() ? std::string{}
526 : std::string(
"listener did not start");
535 impl_->server.stop();
536 if (thread_.joinable()) { thread_.join(); }
537 if (!token_file_.empty()) {
539 std::filesystem::remove(token_file_,
ec);
Unix socket MCP bridge for external client access.
Refuses after repeated authentication failures (decision #84).
void record_failure(Clock::time_point now)
Record one failed authentication at now.
bool blocked(Clock::time_point now) const
Whether requests are refused at now.
void stop()
Stop serving and remove the token file.
~HttpListener()
Stops the server and removes the token file.
HttpListener(ExternalBridge *bridge, int port)
Bind to 127.0.0.1:port on start(); 0 picks a free port.
bool start(std::string &err)
Write the token, bind, and start serving.
Unix socket MCP bridge — exposes a running engine to external clients.
std::string err
size_exceeded, when set
std::filesystem::path token_path_for(int port)
Where the token for a port is written.
bool host_allowed(const std::string &host, int port)
Whether a Host header names this listener's loopback endpoint.
bool write_token_file(const std::filesystem::path &path, const std::string &token, std::string &err)
Write token to path atomically (temp + rename), mode 0600.
bool tokens_equal(const std::string &a, const std::string &b)
Compare two tokens in time independent of where they differ.
static int bind_loopback(httplib::Server &server, int port)
Bind 127.0.0.1 on port, or on any free port when it is 0.
bool prepare_private_dir(const std::filesystem::path &dir, std::string &err)
Create or verify the token directory: 0700, ours, not a symlink.
std::string generate_token()
A fresh bearer token: 256 bits from getrandom(), hex-encoded.
std::thread worker
Runs dispatch()
std::string bearer_token(const std::string &header)
The token in an Authorization: Bearer <t> header, or "".
static void configure_server(HttpListener::Impl &impl, ExternalBridge *bridge, const std::atomic< int > &port)
Install routes, limits and the worker pool on the server.
Serve the external MCP bridge over Streamable HTTP (gh#204, v2.14.0).
spdlog initialization and logger access.
auto now()
Get current time for timing measurements.
@ ok
Tool dispatched, returned non-empty content.
@ refuse
false: every escape refused ("Path escapes project root")
@ optional
optional: every escape asks the host's path approver
@ read
read_file, list_directory
@ write
write_file, edit_file
httplib state, kept out of the header (no third-party headers).
std::atomic< bool > listen_returned
Listen thread has exited.
httplib::Server server
The HTTP server.
std::string token
This run's bearer token.
AuthThrottle throttle
Repeated-failure block.