Changes
diff --git a/README.md b/README.md
index 27e0ec6..03fdfe6 100644
--- a/README.md
+++ b/README.md
@@ -38,9 +38,12 @@ games and records them as interrupted.
Startup settings can be loaded from a TOML file with `--config PATH`. The
configuration is read first; command-line options override it. The Arch
package ships a sample configuration at
-`packages/arch/nethack-mcp.toml`. Set `[server].listen_address` to choose the
-network address; it defaults to `127.0.0.1`. The `--listen-address` option
-overrides the config file.
+`packages/arch/nethack-mcp.toml`. Set `[server].listen_address` to choose an
+IP address or a Unix domain socket path; it defaults to `127.0.0.1`. For
+example, `/run/nethack-mcp/http.sock` selects a socket file and makes the
+configured port irrelevant. Set `listen_socket_permission = 0o660` in
+`[server]` to set the socket file's permission bits. The `--listen-address`
+option overrides the config file.
Create games with the MCP `new_game` tool and a caller-supplied `model_slug`.
Keep the returned `game_id` and `control_token`; every later game tool call
diff --git a/include/game_http_server.hpp b/include/game_http_server.hpp
index a4ece85..cb36788 100644
--- a/include/game_http_server.hpp
+++ b/include/game_http_server.hpp
@@ -4,7 +4,6 @@
#include <cstdint>
#include <string>
#include <string_view>
-#include <thread>
#include <vector>
#include <mw/http_server.hpp>
@@ -31,10 +30,10 @@ public:
GameHttpServer(const GameHttpServer&) = delete;
GameHttpServer& operator=(const GameHttpServer&) = delete;
- /// Bind synchronously so an occupied port fails promptly.
+ /// Start serving and wait until the listener is ready.
bool startServer(std::string& error);
- /// Stop the listener and join its serving thread.
+ /// Stop the listener and wait for its serving thread.
void stopServer();
/// Report whether the HTTP listener is still serving.
@@ -63,7 +62,6 @@ private:
GameManager& manager_;
McpServer& mcp_;
ServerConfig config_;
- std::thread server_thread_;
std::atomic<bool> started_ = false;
mutable std::atomic<std::size_t> active_requests_ = 0;
mutable std::atomic<std::uint64_t> request_count_ = 0;
diff --git a/include/server_config.hpp b/include/server_config.hpp
index bf98e0f..7f84d83 100644
--- a/include/server_config.hpp
+++ b/include/server_config.hpp
@@ -3,6 +3,7 @@
#include <chrono>
#include <cstddef>
#include <filesystem>
+#include <optional>
#include <string>
namespace nethack_mcp
@@ -11,10 +12,12 @@ namespace nethack_mcp
/// Startup-only listener settings, limits, and public URL policy.
struct ServerConfig
{
- /// Network address used by the HTTP listener.
+ /// Network address or Unix domain socket path used by the HTTP listener.
std::string listen_address = "127.0.0.1";
- /// HTTP listener port.
+ /// TCP listener port; ignored when listening on a Unix socket.
int port = 8765;
+ /// Optional permission bits for a Unix domain socket file.
+ std::optional<unsigned int> listen_socket_permission;
/// Private per-game worker files, normally on temporary storage.
std::filesystem::path data_root;
/// Persistent SQLite database path, outside temporary storage.
diff --git a/packages/arch/PKGBUILD b/packages/arch/PKGBUILD
index c625017..3777354 100644
--- a/packages/arch/PKGBUILD
+++ b/packages/arch/PKGBUILD
@@ -12,13 +12,15 @@ source=(
'nethack-mcp::git+https://git.xeno.darksair.org/nethack-mcp.git'
'nethack-mcp.service'
'nethack-mcp.sysusers'
+ 'nethack-mcp.tmpfiles'
'nethack-mcp.toml'
)
sha256sums=(
'SKIP'
'a9ef1632e3afffa4785cbe0de2cf8cb9478ca67629eb41bca52b2fd15b129ab8'
'1c1622cbd9842aa7a466abed401de52b273b0f20eb8ac590b67d6fea7c15c2fd'
- '813c726c9b3026fe32e0f8f9edd952c90d1fcf2d507b52c15636454e9f40b4e8'
+ 'bca01370008de9029018618153139f446a9b287245213bdb2ff6858945be2c6c'
+ '55e5a896e8fbad99d0136f6b8a978b115e7fedb628c47018b440963a62a8a01d'
)
pkgver() {
@@ -53,6 +55,8 @@ package() {
"$pkgdir/etc/nethack-mcp.toml"
install -Dm644 "$srcdir/nethack-mcp.sysusers" \
"$pkgdir/usr/lib/sysusers.d/nethack-mcp.conf"
+ install -Dm644 "$srcdir/nethack-mcp.tmpfiles" \
+ "$pkgdir/usr/lib/tmpfiles.d/nethack-mcp.conf"
install -Dm644 "$srcdir/nethack-mcp.service" \
"$pkgdir/usr/lib/systemd/system/nethack-mcp.service"
diff --git a/packages/arch/README.md b/packages/arch/README.md
index ede0dbe..3e07d15 100644
--- a/packages/arch/README.md
+++ b/packages/arch/README.md
@@ -22,6 +22,9 @@ sudo systemctl enable --now nethack-mcp.service
The defaults listen on loopback. To publish the service through an HTTPS
reverse proxy, update the public URL and capacity settings in the TOML file.
-Configure Apache to accept the intended hostname. Requests from proxied
+Configure Apache to accept the intended hostname. You can set
+`[server].listen_address` to a Unix socket path and set
+`listen_socket_permission = 0o660` for the socket mode. The Apache service
+user also needs access to the socket's parent directory. Requests from proxied
clients share the proxy's peer address for per-client limits. Keep the
configured data directory and database path writable by the service account.
diff --git a/packages/arch/nethack-mcp.tmpfiles b/packages/arch/nethack-mcp.tmpfiles
new file mode 100644
index 0000000..6b0b4a6
--- /dev/null
+++ b/packages/arch/nethack-mcp.tmpfiles
@@ -0,0 +1 @@
+d /var/lib/nethack-mcp 0750 nethack-mcp nethack-mcp -
diff --git a/packages/arch/nethack-mcp.toml b/packages/arch/nethack-mcp.toml
index e190144..ea422a2 100644
--- a/packages/arch/nethack-mcp.toml
+++ b/packages/arch/nethack-mcp.toml
@@ -1,5 +1,8 @@
[server]
listen_address = "127.0.0.1"
+# For a Unix socket, use a path and optionally set its permission bits:
+# listen_address = "/run/nethack-mcp/http.sock"
+# listen_socket_permission = 0o660
port = 8765
public_base_url = "http://127.0.0.1:8765/"
diff --git a/src/config_file.cpp b/src/config_file.cpp
index 94a3aa3..0c8c593 100644
--- a/src/config_file.cpp
+++ b/src/config_file.cpp
@@ -138,6 +138,27 @@ bool readPort(const toml::table& table, std::string_view key,
return true;
}
+bool readSocketPermission(const toml::table& table, std::string_view key,
+ std::optional<unsigned int>& value,
+ const std::filesystem::path& path,
+ std::string& error)
+{
+ const toml::node* node = table.get(key);
+ if(node == nullptr)
+ {
+ return true;
+ }
+ const auto parsed = node->value<std::int64_t>();
+ if(!parsed || *parsed < 0 || *parsed > 0777)
+ {
+ return fail(path,
+ "[server].listen_socket_permission must be an octal TOML "
+ "integer from 0o000 to 0o777", error);
+ }
+ value = static_cast<unsigned int>(*parsed);
+ return true;
+}
+
bool readSeconds(const toml::table& table, std::string_view key,
const std::string& section, std::chrono::seconds& value,
const std::filesystem::path& path, std::string& error)
@@ -159,10 +180,13 @@ bool readServerSection(const toml::table& table, ServerConfig& config,
const std::filesystem::path& path, std::string& error)
{
if(!validateKeys(table, {
- "listen_address", "port", "public_base_url",
+ "listen_address", "listen_socket_permission", "port",
+ "public_base_url",
}, "server", path, error)
|| !readString(table, "listen_address", "server",
config.listen_address, path, error)
+ || !readSocketPermission(table, "listen_socket_permission",
+ config.listen_socket_permission, path, error)
|| !readPort(table, "port", config.port, path, error)
|| !readString(table, "public_base_url", "server",
config.public_base_url, path, error))
diff --git a/src/game_http_server.cpp b/src/game_http_server.cpp
index 033ec11..42be3ba 100644
--- a/src/game_http_server.cpp
+++ b/src/game_http_server.cpp
@@ -21,6 +21,22 @@ namespace nethack_mcp
namespace
{
+bool isUnixSocketPath(std::string_view listen_address)
+{
+ return listen_address.find('/') != std::string_view::npos;
+}
+
+mw::HTTPServer::ListenAddress makeListenAddress(const ServerConfig& config)
+{
+ if(isUnixSocketPath(config.listen_address))
+ {
+ mw::SocketFileInfo socket(config.listen_address);
+ socket.permission = config.listen_socket_permission;
+ return socket;
+ }
+ return mw::IPSocketInfo{config.listen_address, config.port};
+}
+
class CounterSlot
{
public:
@@ -196,8 +212,7 @@ std::string depthText(std::optional<int> depth)
} // namespace
GameHttpServer::GameHttpServer(GameManager& manager, McpServer& mcp)
- : mw::HTTPServer(mw::IPSocketInfo{
- manager.config().listen_address, manager.config().port}),
+ : mw::HTTPServer(makeListenAddress(manager.config())),
manager_(manager), mcp_(mcp), config_(manager.config())
{}
@@ -208,35 +223,28 @@ GameHttpServer::~GameHttpServer()
bool GameHttpServer::startServer(std::string& error)
{
- setup();
- if(!server.bind_to_port(config_.listen_address, config_.port))
+ auto result = mw::HTTPServer::start();
+ if(!result)
{
- error = "could not bind HTTP server to " + config_.listen_address
- + ":"
- + std::to_string(config_.port);
+ error = mw::errorMsg(result.error());
return false;
}
started_ = true;
- server_thread_ = std::thread([this] {
- server.listen_after_bind();
- started_ = false;
- });
return true;
}
void GameHttpServer::stopServer()
{
- server.stop();
- if(server_thread_.joinable())
+ if(started_.exchange(false))
{
- server_thread_.join();
+ mw::HTTPServer::stop();
+ mw::HTTPServer::wait();
}
- started_ = false;
}
bool GameHttpServer::running() const
{
- return started_;
+ return started_ && server.is_running();
}
void GameHttpServer::setup()
diff --git a/src/main.cpp b/src/main.cpp
index 4ab99ba..43ab773 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -107,7 +107,7 @@ void printUsage()
std::fputs(
"Usage: nethack_mcp [options]\n"
" --config PATH Read TOML configuration\n"
- " --listen-address ADDRESS HTTP bind address (127.0.0.1)\n"
+ " --listen-address ADDRESS HTTP address or UDS path\n"
" --port PORT HTTP port (8765)\n"
" --data-root PATH Temporary game files\n"
" --database PATH Persistent SQLite records\n"