#include "mcp_server.hpp"
#include <string>
#include <string_view>
#include <utility>
namespace nethack_mcp
{
namespace
{
bool supportedLegacyProtocolVersion(const std::string& version)
{
return version == "2025-11-25"
|| version == "2025-06-18"
|| version == "2025-03-26";
}
constexpr std::string_view MODERN_PROTOCOL_VERSION = "2026-07-28";
constexpr std::string_view SERVER_INFO_META_KEY =
"io.modelcontextprotocol/serverInfo";
Json objectSchema(Json properties, Json required = Json::array())
{
return {
{"type", "object"},
{"properties", std::move(properties)},
{"required", std::move(required)},
{"additionalProperties", false},
};
}
Json stringProperty()
{
return {{"type", "string"}};
}
Json gameIdProperty()
{
return {
{"type", "string"},
{"minLength", 36},
{"maxLength", 36},
{"pattern",
"^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-"
"[0-9a-f]{12}$"},
};
}
Json controlTokenProperty()
{
return {
{"type", "string"},
{"minLength", 43},
{"maxLength", 43},
{"pattern", "^[A-Za-z0-9_-]{43}$"},
};
}
} // namespace
McpServer::McpServer(GameManager& manager)
: manager_(manager)
{}
Json McpServer::handleMessage(const Json& request, bool& should_respond,
const std::string& client_id,
bool modern_protocol)
{
Json reply = handleMessageInternal(
request, should_respond, client_id, modern_protocol);
return addModernMetadata(std::move(reply), request, modern_protocol);
}
Json McpServer::handleMessageInternal(const Json& request,
bool& should_respond,
const std::string& client_id,
bool modern_protocol)
{
should_respond = true;
if(!request.is_object()
|| !request.contains("jsonrpc")
|| !request.at("jsonrpc").is_string()
|| request.at("jsonrpc").get<std::string>() != "2.0"
|| !request.contains("method") || !request.at("method").is_string())
{
const Json id = request.is_object() && request.contains("id")
? request.at("id") : Json(nullptr);
return jsonRpcError(id, -32600, "invalid JSON-RPC request");
}
const Json id = request.contains("id") ? request.at("id")
: Json(nullptr);
const std::string method = request.at("method").get<std::string>();
if(!request.contains("id"))
{
should_respond = false;
}
if(method == "notifications/initialized"
|| method == "notifications/cancelled")
{
should_respond = false;
return Json();
}
if(method == "initialize")
{
if(modern_protocol)
{
return jsonRpcError(id, -32601,
"initialize is not available in this protocol");
}
const Json params = request.value("params", Json::object());
if(!params.is_object())
{
return jsonRpcError(id, -32602,
"initialize params must be an object");
}
if(params.contains("protocolVersion")
&& !params.at("protocolVersion").is_string())
{
return jsonRpcError(id, -32602,
"protocolVersion must be a string");
}
const std::string version = params.value("protocolVersion", "");
if(!version.empty() && !supportedLegacyProtocolVersion(version))
{
return jsonRpcError(id, -32602,
"unsupported MCP protocol version");
}
const std::string negotiated_version = version.empty()
? "2025-11-25" : version;
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", {
{"protocolVersion", negotiated_version},
{"capabilities", {{"tools", Json::object()}}},
{"serverInfo", {
{"name", "nethack-mcp"},
{"version", "0.1.0"},
}},
}},
};
}
if(method == "ping")
{
if(modern_protocol)
{
return jsonRpcError(id, -32601,
"ping is not available in this protocol");
}
return {{"jsonrpc", "2.0"}, {"id", id}, {"result", {}}};
}
if(method == "server/discover")
{
if(!modern_protocol)
{
return jsonRpcError(id, -32601, "method not found: server/discover");
}
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", {
{"supportedVersions", {std::string(MODERN_PROTOCOL_VERSION)}},
{"capabilities", {{"tools", Json::object()}}},
}},
};
}
if(method == "resources/list")
{
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", {{"resources", Json::array()}}},
};
}
if(method == "resources/templates/list")
{
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", {{"resourceTemplates", Json::array()}}},
};
}
if(method == "tools/list")
{
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", {{"tools", tools()}}},
};
}
if(method == "tools/call")
{
const Json params = request.value("params", Json::object());
if(!params.is_object() || !params.contains("name")
|| !params.at("name").is_string())
{
return jsonRpcError(id, -32602, "tools/call needs a tool name");
}
const Json arguments = params.value("arguments", Json::object());
if(!arguments.is_object())
{
return jsonRpcError(id, -32602, "tool arguments must be an object");
}
const std::string name = params.at("name").get<std::string>();
ToolResult result;
if(name == "new_game") result = manager_.createGame(arguments, client_id);
else if(name == "observe" || name == "press"
|| name == "select_menu" || name == "respond"
|| name == "quit_game")
result = manager_.dispatch(name, arguments, client_id);
else
{
return jsonRpcError(id, -32602, "unknown tool");
}
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", toolResult(result)},
};
}
return jsonRpcError(id, -32601, "method not found");
}
Json McpServer::addModernMetadata(Json response, const Json& request,
bool modern_protocol) const
{
if(!modern_protocol || !response.is_object())
{
return response;
}
const std::string method = request.is_object()
&& request.contains("method") && request.at("method").is_string()
? request.at("method").get<std::string>() : std::string();
const Json server_info = {
{"name", "nethack-mcp"},
{"version", "0.1.0"},
};
if(response.contains("result") && response.at("result").is_object())
{
Json& result = response["result"];
result["_meta"][std::string(SERVER_INFO_META_KEY)] = server_info;
result["resultType"] = "complete";
if(method == "server/discover" || method == "tools/list"
|| method == "resources/list"
|| method == "resources/templates/list")
{
result["ttlMs"] = 300000;
result["cacheScope"] = "public";
}
}
return response;
}
Json McpServer::tools() const
{
const Json game_id = gameIdProperty();
const Json selection = objectSchema(
{
{"entry_id", {{"type", "integer"}, {"minimum", 1}}},
{"count", {{"type", "integer"}, {"minimum", 1}}},
},
{"entry_id"});
return Json::array({
{
{"name", "new_game"},
{"description", "Start one fresh NetHack game and return its control token."},
{"inputSchema", objectSchema({
{"name", {
{"type", "string"}, {"minLength", 1},
{"maxLength", 30}, {"pattern", "^[ -~]+$"},
}},
{"model_slug", {
{"type", "string"}, {"minLength", 1},
{"maxLength", 128}, {"pattern", "^[ -~]+$"},
}},
{"role", stringProperty()},
{"race", stringProperty()},
{"gender", stringProperty()},
{"alignment", stringProperty()},
}, {"model_slug"})},
},
{
{"name", "observe"},
{"description", "Read the latest complete game observation."},
{"inputSchema", objectSchema({
{"game_id", game_id},
{"control_token", controlTokenProperty()},
{"detail", {{"type", "string"},
{"enum", {"compact", "full"}}}},
{"after_message_id", {{"type", "integer"}, {"minimum", 0}}},
{"wait_ms", {{"type", "integer"},
{"minimum", 0}, {"maximum", 10000}}},
}, {"game_id", "control_token"})},
},
{
{"name", "press"},
{"description", "Send one key to a pending key boundary."},
{"inputSchema", objectSchema({
{"game_id", game_id},
{"control_token", controlTokenProperty()},
{"input_id", {{"type", "integer"}, {"minimum", 1}}},
{"key", stringProperty()},
}, {"game_id", "control_token", "input_id", "key"})},
},
{
{"name", "select_menu"},
{"description", "Submit complete selections for a menu."},
{"inputSchema", objectSchema({
{"game_id", game_id},
{"control_token", controlTokenProperty()},
{"input_id", {{"type", "integer"}, {"minimum", 1}}},
{"selections", {{"type", "array"},
{"items", selection}}},
{"cancel", {{"type", "boolean"}}},
}, {"game_id", "control_token", "input_id", "selections"})},
},
{
{"name", "respond"},
{"description", "Answer a non-key pending boundary."},
{"inputSchema", {
{"type", "object"},
{"properties", {
{"game_id", game_id},
{"control_token", controlTokenProperty()},
{"input_id", {{"type", "integer"}, {"minimum", 1}}},
{"text", stringProperty()},
{"choice", stringProperty()},
{"command", stringProperty()},
{"acknowledge", {{"type", "boolean"}}},
{"cancel", {{"type", "boolean"}}},
}},
{"required", {"game_id", "control_token", "input_id"}},
{"additionalProperties", false},
{"oneOf", {
{{"required", {"text"}}},
{{"required", {"choice"}}},
{{"required", {"command"}}},
{{"required", {"acknowledge"}}},
{{"required", {"cancel"}}},
}},
}},
},
{
{"name", "quit_game"},
{"description", "Stop the active worker administratively."},
{"inputSchema", objectSchema({
{"game_id", game_id},
{"control_token", controlTokenProperty()},
}, {"game_id", "control_token"})},
},
});
}
Json McpServer::toolResult(const ToolResult& result) const
{
Json structured = result.value;
Json text_value = result.success
? result.value
: Json({
{"error", {
{"code", result.code},
{"message", result.message},
}},
{"state", result.value},
});
Json response = {
{"content", Json::array({
{{"type", "text"}, {"text", text_value.dump()}},
})},
{"structuredContent", structured},
};
if(!result.success)
{
response["isError"] = true;
}
return response;
}
Json McpServer::jsonRpcError(const Json& id, int code,
const std::string& message) const
{
return {
{"jsonrpc", "2.0"},
{"id", id},
{"error", {{"code", code}, {"message", message}}},
};
}
} // namespace nethack_mcp