BareGit
#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