diff --git a/Makefile.common b/Makefile.common index 73152664bbc8..33304f624da3 100644 --- a/Makefile.common +++ b/Makefile.common @@ -2405,6 +2405,15 @@ ifeq ($(HAVE_NETWORKING), 1) network/netplay/netplay_frontend.o \ network/netplay/netplay_room_parse.o + # MCP Server + ifeq ($(HAVE_MCP), 1) + DEFINES += -DHAVE_MCP + OBJ += network/mcp/mcp_server.o \ + network/mcp/mcp_http_transport.o \ + network/mcp/mcp_adapter.o \ + network/mcp/mcp_adapter_tool_list.o + endif + # RetroAchievements ifeq ($(HAVE_CHEEVOS), 1) DEFINES += -DHAVE_CHEEVOS -DRC_CLIENT_SUPPORTS_HASH diff --git a/config.def.h b/config.def.h index 77165d4e81e7..cd83af17acd4 100644 --- a/config.def.h +++ b/config.def.h @@ -1469,6 +1469,11 @@ #define DEFAULT_NETWORK_REMOTE_BASE_PORT 55400 #define DEFAULT_STDIN_CMD_ENABLE false +/* Enable MCP (Model Context Protocol) server. */ +#define DEFAULT_MCP_SERVER_ENABLE false +#define DEFAULT_MCP_SERVER_ADDRESS "127.0.0.1" +#define DEFAULT_MCP_SERVER_PORT 7878 + #define DEFAULT_NETWORK_BUILDBOT_AUTO_EXTRACT_ARCHIVE true #define DEFAULT_NETWORK_BUILDBOT_SHOW_EXPERIMENTAL_CORES false diff --git a/configuration.c b/configuration.c index fce61f95d504..f007096b8d51 100644 --- a/configuration.c +++ b/configuration.c @@ -1602,6 +1602,10 @@ static struct config_array_setting *populate_settings_array( #ifdef HAVE_NETWORKING SETTING_ARRAY("netplay_mitm_server", settings->arrays.netplay_mitm_server, false, NULL, true); +#ifdef HAVE_MCP + SETTING_ARRAY("mcp_server_address", settings->arrays.mcp_server_address, true, DEFAULT_MCP_SERVER_ADDRESS, false); + SETTING_ARRAY("mcp_server_password", settings->arrays.mcp_server_password, false, NULL, true); +#endif SETTING_ARRAY("webdav_url", settings->arrays.webdav_url, false, NULL, true); SETTING_ARRAY("webdav_username", settings->arrays.webdav_username, false, NULL, true); SETTING_ARRAY("webdav_password", settings->arrays.webdav_password, false, NULL, true); @@ -2226,6 +2230,10 @@ static struct config_bool_setting *populate_settings_bool( SETTING_BOOL("stdin_cmd_enable", &settings->bools.stdin_cmd_enable, true, DEFAULT_STDIN_CMD_ENABLE, false); #endif +#ifdef HAVE_MCP + SETTING_BOOL("mcp_server_enable", &settings->bools.mcp_server_enable, true, DEFAULT_MCP_SERVER_ENABLE, false); +#endif + #ifdef HAVE_NETWORKING SETTING_BOOL("netplay_show_only_connectable", &settings->bools.netplay_show_only_connectable, true, DEFAULT_NETPLAY_SHOW_ONLY_CONNECTABLE, false); SETTING_BOOL("netplay_show_only_installed_cores", &settings->bools.netplay_show_only_installed_cores, true, DEFAULT_NETPLAY_SHOW_ONLY_INSTALLED_CORES, false); @@ -2624,6 +2632,9 @@ static struct config_uint_setting *populate_settings_uint( #ifdef HAVE_COMMAND SETTING_UINT("network_cmd_port", &settings->uints.network_cmd_port, true, DEFAULT_NETWORK_CMD_PORT, false); #endif +#ifdef HAVE_MCP + SETTING_UINT("mcp_server_port", &settings->uints.mcp_server_port, true, DEFAULT_MCP_SERVER_PORT, false); +#endif #ifdef HAVE_NETWORKGAMEPAD SETTING_UINT("network_remote_base_port", &settings->uints.network_remote_base_port, true, DEFAULT_NETWORK_REMOTE_BASE_PORT, false); #endif diff --git a/configuration.h b/configuration.h index 5a54a42611ef..a2010a02e811 100644 --- a/configuration.h +++ b/configuration.h @@ -243,6 +243,9 @@ typedef struct settings unsigned replay_max_keep; unsigned savestate_max_keep; unsigned network_cmd_port; +#ifdef HAVE_MCP + unsigned mcp_server_port; +#endif unsigned network_remote_base_port; unsigned keymapper_port; unsigned cloud_sync_sync_mode; @@ -539,6 +542,10 @@ typedef struct settings char audio_device[NAME_MAX_LENGTH]; char camera_device[NAME_MAX_LENGTH]; char netplay_mitm_server[NAME_MAX_LENGTH]; +#ifdef HAVE_MCP + char mcp_server_address[64]; + char mcp_server_password[NAME_MAX_LENGTH]; +#endif char webdav_url[NAME_MAX_LENGTH]; char webdav_username[NAME_MAX_LENGTH]; char webdav_password[NAME_MAX_LENGTH]; @@ -1061,6 +1068,9 @@ typedef struct settings bool save_file_compression; bool savestate_file_compression; bool network_cmd_enable; +#ifdef HAVE_MCP + bool mcp_server_enable; +#endif bool stdin_cmd_enable; bool keymapper_enable; bool network_remote_enable; diff --git a/griffin/griffin.c b/griffin/griffin.c index 8b84bc0ad3d6..213bb1134b35 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -1708,6 +1708,16 @@ STEAM INTEGRATION USING MIST #include "../network/presence.c" #endif +/*============================================================ +MCP SERVER +============================================================ */ +#ifdef HAVE_MCP +#include "../network/mcp/mcp_server.c" +#include "../network/mcp/mcp_http_transport.c" +#include "../network/mcp/mcp_adapter.c" +#include "../network/mcp/mcp_adapter_tool_list.c" +#endif + /*============================================================ CLOUD SYNC ============================================================ */ diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index c405b4258d63..41e746c60d67 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -2506,6 +2506,22 @@ MSG_HASH( MENU_ENUM_LABEL_NETWORK_CMD_PORT, "network_cmd_port" ) +MSG_HASH( + MENU_ENUM_LABEL_MCP_SERVER_ENABLE, + "mcp_server_enable" + ) +MSG_HASH( + MENU_ENUM_LABEL_MCP_SERVER_ADDRESS, + "mcp_server_address" + ) +MSG_HASH( + MENU_ENUM_LABEL_MCP_SERVER_PORT, + "mcp_server_port" + ) +MSG_HASH( + MENU_ENUM_LABEL_MCP_SERVER_PASSWORD, + "mcp_server_password" + ) MSG_HASH( MENU_ENUM_LABEL_NETWORK_INFORMATION, "network_information" diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index b4a3f2dff335..a57fc6e406ae 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -7606,6 +7606,38 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_NETWORK_CMD_PORT, "Network Command Port" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MCP_SERVER_ENABLE, + "MCP Server" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MCP_SERVER_ENABLE, + "Enable the Model Context Protocol (MCP) server for AI assistant integration." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MCP_SERVER_ADDRESS, + "MCP Server Address" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MCP_SERVER_ADDRESS, + "Bind address for the MCP HTTP server (e.g. 127.0.0.1)." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MCP_SERVER_PORT, + "MCP Server Port" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MCP_SERVER_PORT, + "Port for the MCP HTTP server to listen on." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_MCP_SERVER_PASSWORD, + "MCP Server Password" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_MCP_SERVER_PASSWORD, + "Password for MCP server authentication (Bearer token). Leave empty to disable auth." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_NETWORK_REMOTE_ENABLE, "Network RetroPad" diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index ed54de31cdb9..bb2758fc4775 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -944,6 +944,12 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_netplay_require_slaves, MENU_ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_netplay_check_frames, MENU_ENUM_SUBLABEL_NETPLAY_CHECK_FRAMES) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_netplay_nat_traversal, MENU_ENUM_SUBLABEL_NETPLAY_NAT_TRAVERSAL) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_stdin_cmd_enable, MENU_ENUM_SUBLABEL_STDIN_CMD_ENABLE) +#ifdef HAVE_MCP +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_mcp_server_enable, MENU_ENUM_SUBLABEL_MCP_SERVER_ENABLE) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_mcp_server_address, MENU_ENUM_SUBLABEL_MCP_SERVER_ADDRESS) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_mcp_server_port, MENU_ENUM_SUBLABEL_MCP_SERVER_PORT) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_mcp_server_password, MENU_ENUM_SUBLABEL_MCP_SERVER_PASSWORD) +#endif DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_mouse_enable, MENU_ENUM_SUBLABEL_MOUSE_ENABLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_pointer_enable, MENU_ENUM_SUBLABEL_POINTER_ENABLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_icon_thumbnails, MENU_ENUM_SUBLABEL_ICON_THUMBNAILS) @@ -3769,6 +3775,20 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_STDIN_CMD_ENABLE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_stdin_cmd_enable); break; +#ifdef HAVE_MCP + case MENU_ENUM_LABEL_MCP_SERVER_ENABLE: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_mcp_server_enable); + break; + case MENU_ENUM_LABEL_MCP_SERVER_ADDRESS: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_mcp_server_address); + break; + case MENU_ENUM_LABEL_MCP_SERVER_PORT: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_mcp_server_port); + break; + case MENU_ENUM_LABEL_MCP_SERVER_PASSWORD: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_mcp_server_password); + break; +#endif case MENU_ENUM_LABEL_NETPLAY_PUBLIC_ANNOUNCE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_netplay_public_announce); break; diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 9a53434ceeca..3f646727b37c 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -9474,6 +9474,29 @@ unsigned menu_displaylist_build_list( MENU_ENUM_LABEL_STDIN_CMD_ENABLE, PARSE_ONLY_BOOL, false) == 0) count++; + +#ifdef HAVE_MCP + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_MCP_SERVER_ENABLE, + PARSE_ONLY_BOOL, false) == 0) + count++; + + if (settings->bools.mcp_server_enable) + { + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_MCP_SERVER_ADDRESS, + PARSE_ONLY_STRING, false) == 0) + count++; + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_MCP_SERVER_PORT, + PARSE_ONLY_UINT, false) == 0) + count++; + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_MCP_SERVER_PASSWORD, + PARSE_ONLY_STRING, false) == 0) + count++; + } +#endif } break; case DISPLAYLIST_NETPLAY_LOBBY_FILTERS_LIST: diff --git a/menu/menu_setting.c b/menu/menu_setting.c index c146502dc270..19912803ea03 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -127,6 +127,10 @@ #endif #endif +#ifdef HAVE_MCP +#include "../network/mcp/mcp_server.h" +#endif + #if defined(HAVE_OVERLAY) #include "../input/input_overlay.h" #endif @@ -9633,6 +9637,22 @@ static void timezone_change_handler(rarch_setting_t *setting) } #endif +#ifdef HAVE_MCP +static void mcp_setting_change_handler(rarch_setting_t *setting) +{ + (void)setting; + mcp_server_deinit(); +} + +static size_t setting_get_string_representation_mcp_password( + rarch_setting_t *setting, char *s, size_t len) +{ + if (setting && !string_is_empty(setting->value.target.string)) + return strlcpy(s, "********", len); + return 0; +} +#endif + static void appicon_change_handler(rarch_setting_t *setting) { uico_driver_state_t *uico_st = uico_state_get_ptr(); @@ -23938,6 +23958,79 @@ static bool setting_append_list( general_write_handler, general_read_handler, SD_FLAG_ADVANCED); + +#ifdef HAVE_MCP + CONFIG_BOOL( + list, list_info, + &settings->bools.mcp_server_enable, + MENU_ENUM_LABEL_MCP_SERVER_ENABLE, + MENU_ENUM_LABEL_VALUE_MCP_SERVER_ENABLE, + DEFAULT_MCP_SERVER_ENABLE, + MENU_ENUM_LABEL_VALUE_OFF, + MENU_ENUM_LABEL_VALUE_ON, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler, + SD_FLAG_NONE); + (*list)[list_info->index - 1].action_ok = &setting_bool_action_left_with_refresh; + (*list)[list_info->index - 1].action_left = &setting_bool_action_left_with_refresh; + (*list)[list_info->index - 1].action_right = &setting_bool_action_right_with_refresh; + + CONFIG_STRING( + list, list_info, + settings->arrays.mcp_server_address, + sizeof(settings->arrays.mcp_server_address), + MENU_ENUM_LABEL_MCP_SERVER_ADDRESS, + MENU_ENUM_LABEL_VALUE_MCP_SERVER_ADDRESS, + "", + &group_info, + &subgroup_info, + parent_group, + NULL, + NULL); + SETTINGS_DATA_LIST_CURRENT_ADD_FLAGS(list, list_info, SD_FLAG_ALLOW_INPUT); + (*list)[list_info->index - 1].ui_type = ST_UI_TYPE_STRING_LINE_EDIT; + (*list)[list_info->index - 1].action_start = setting_generic_action_start_default; + (*list)[list_info->index - 1].change_handler = mcp_setting_change_handler; + + CONFIG_UINT( + list, list_info, + &settings->uints.mcp_server_port, + MENU_ENUM_LABEL_MCP_SERVER_PORT, + MENU_ENUM_LABEL_VALUE_MCP_SERVER_PORT, + DEFAULT_MCP_SERVER_PORT, + &group_info, + &subgroup_info, + parent_group, + NULL, + NULL); + (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint; + (*list)[list_info->index - 1].offset_by = 1; + menu_settings_list_current_add_range(list, list_info, 0, 65535, 1, true, true); + SETTINGS_DATA_LIST_CURRENT_ADD_FLAGS(list, list_info, SD_FLAG_ALLOW_INPUT); + (*list)[list_info->index - 1].change_handler = mcp_setting_change_handler; + + CONFIG_STRING( + list, list_info, + settings->arrays.mcp_server_password, + sizeof(settings->arrays.mcp_server_password), + MENU_ENUM_LABEL_MCP_SERVER_PASSWORD, + MENU_ENUM_LABEL_VALUE_MCP_SERVER_PASSWORD, + "", + &group_info, + &subgroup_info, + parent_group, + NULL, + NULL); + (*list)[list_info->index - 1].get_string_representation = + &setting_get_string_representation_mcp_password; + SETTINGS_DATA_LIST_CURRENT_ADD_FLAGS(list, list_info, SD_FLAG_ALLOW_INPUT); + (*list)[list_info->index - 1].ui_type = ST_UI_TYPE_PASSWORD_LINE_EDIT; + (*list)[list_info->index - 1].action_start = setting_generic_action_start_default; + (*list)[list_info->index - 1].change_handler = mcp_setting_change_handler; +#endif #endif CONFIG_BOOL( list, list_info, diff --git a/msg_hash.h b/msg_hash.h index f4b5d01093b0..510a0757e312 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -2924,6 +2924,10 @@ enum msg_hash_enums MENU_LABEL(NETWORK_CMD_ENABLE), MENU_LABEL(NETWORK_CMD_PORT), MENU_LABEL(STDIN_CMD_ENABLE), + MENU_LABEL(MCP_SERVER_ENABLE), + MENU_LABEL(MCP_SERVER_ADDRESS), + MENU_LABEL(MCP_SERVER_PORT), + MENU_LABEL(MCP_SERVER_PASSWORD), MENU_LABEL(NETWORK_REMOTE_ENABLE), MENU_LABEL(NETWORK_REMOTE_PORT), MENU_LABEL(NETWORK_ON_DEMAND_THUMBNAILS), diff --git a/network/mcp/mcp_adapter.c b/network/mcp/mcp_adapter.c new file mode 100644 index 000000000000..ec87aa51d868 --- /dev/null +++ b/network/mcp/mcp_adapter.c @@ -0,0 +1,346 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#include +#include + +#include +#include +#include +#include + +#include "mcp_adapter.h" +#include "mcp_defines.h" +#include "mcp_adapter_tool_list.h" +#include "mcp_adapter_utils.h" +#include "mcp_json_templates.h" +#include "../../version.h" +#include "../../verbosity.h" + +/* ---- JSON-RPC request parsing ---- */ + +typedef struct +{ + int64_t id; + bool has_id; + char method[128]; + char tool_name[128]; + char protocol_version[32]; + char args_json[MCP_JSON_MAX_REQUEST]; + size_t args_len; + bool is_notification; +} mcp_request_t; + +static bool mcp_parse_request(const char *json, size_t len, mcp_request_t *req) +{ + rjson_t *parser; + enum rjson_type type; + int depth = 0; + bool in_params = false; + int params_depth = 0; + bool in_arguments = false; + int args_depth = 0; + const char *args_key = NULL; + const char *key = NULL; + size_t key_len = 0; + + memset(req, 0, sizeof(*req)); + + parser = rjson_open_buffer(json, len); + if (!parser) + return false; + + for (;;) + { + type = rjson_next(parser); + + if (type == RJSON_DONE || type == RJSON_ERROR) + break; + + if (type == RJSON_OBJECT) + { + depth++; + if (in_arguments) + { + args_depth++; + } + else if (in_params) + { + params_depth++; + if (params_depth == 2 && key && string_is_equal(key, "arguments")) + { + in_arguments = true; + args_depth = 1; + args_key = NULL; + req->args_json[0] = '{'; + req->args_json[1] = '\0'; + req->args_len = 1; + key = NULL; + } + } + else if (depth == 2 && key && string_is_equal(key, "params")) + { + in_params = true; + params_depth = 1; + key = NULL; + } + continue; + } + if (type == RJSON_OBJECT_END) + { + if (in_arguments) + { + args_depth--; + if (args_depth <= 0) + { + if (req->args_len + 2 < sizeof(req->args_json)) + { + req->args_json[req->args_len++] = '}'; + req->args_json[req->args_len] = '\0'; + } + in_arguments = false; + } + } + if (in_params) + { + params_depth--; + if (params_depth <= 0) + in_params = false; + } + depth--; + continue; + } + if (type == RJSON_ARRAY) + { + if (in_arguments) + args_depth++; + else if (in_params) + params_depth++; + continue; + } + if (type == RJSON_ARRAY_END) + { + if (in_arguments) + args_depth--; + else if (in_params) + params_depth--; + continue; + } + + /* inside arguments - capture flat key/value pairs */ + if (in_arguments && args_depth == 1) + { + if (type == RJSON_STRING) + { + size_t slen; + const char *s = rjson_get_string(parser, &slen); + if (!args_key) + args_key = s; + else + { + /* string value: append "key":"value" */ + char tmp[512]; + char escaped[256]; + mcp_json_escape(escaped, sizeof(escaped), s); + if (req->args_len > 1) + req->args_json[req->args_len++] = ','; + snprintf(tmp, sizeof(tmp), "\"%s\":\"%s\"", args_key, escaped); + req->args_len += strlcpy( + req->args_json + req->args_len, + tmp, + sizeof(req->args_json) - req->args_len); + args_key = NULL; + } + } + else if (type == RJSON_NUMBER && args_key) + { + /* number value: append "key":123 */ + char tmp[256]; + if (req->args_len > 1) + req->args_json[req->args_len++] = ','; + snprintf(tmp, sizeof(tmp), "\"%s\":%d", args_key, + rjson_get_int(parser)); + req->args_len += strlcpy( + req->args_json + req->args_len, + tmp, + sizeof(req->args_json) - req->args_len); + args_key = NULL; + } + else if ((type == RJSON_TRUE || type == RJSON_FALSE) && args_key) + { + char tmp[256]; + if (req->args_len > 1) + req->args_json[req->args_len++] = ','; + snprintf(tmp, sizeof(tmp), "\"%s\":%s", args_key, + type == RJSON_TRUE ? "true" : "false"); + req->args_len += strlcpy( + req->args_json + req->args_len, + tmp, + sizeof(req->args_json) - req->args_len); + args_key = NULL; + } + else + args_key = NULL; + continue; + } + + /* inside params - look for "name" key for tools/call */ + if (in_params && params_depth == 1 && type == RJSON_STRING) + { + size_t slen; + const char *s = rjson_get_string(parser, &slen); + + if (key && string_is_equal(key, "name")) + { + strlcpy(req->tool_name, s, sizeof(req->tool_name)); + key = NULL; + } + else if (key && string_is_equal(key, "protocolVersion")) + { + strlcpy(req->protocol_version, s, sizeof(req->protocol_version)); + key = NULL; + } + else + key = s; + continue; + } + + if (in_params) + { + key = NULL; + continue; + } + + /* top-level key name (string when no key pending) */ + if (depth == 1 && type == RJSON_STRING && !key) + { + size_t slen; + key = rjson_get_string(parser, &slen); + key_len = slen; + continue; + } + + /* top-level string value */ + if (depth == 1 && key && type == RJSON_STRING) + { + size_t slen; + const char *s = rjson_get_string(parser, &slen); + + if (string_is_equal(key, "method")) + strlcpy(req->method, s, sizeof(req->method)); + + key = NULL; + continue; + } + + /* top-level non-string value */ + if (depth == 1 && key) + { + if (string_is_equal(key, "id") && type == RJSON_NUMBER) + { + req->id = (int64_t)rjson_get_int(parser); + req->has_id = true; + } + key = NULL; + continue; + } + + key = NULL; + } + + req->is_notification = (!req->has_id); + + rjson_free(parser); + return (req->method[0] != '\0'); +} + +/* ---- response builders ---- */ + +static void mcp_build_error(char *buf, size_t buf_size, + int64_t id, int code, const char *message) +{ + snprintf(buf, buf_size, mcp_error_fmt, (long long)id, code, message); +} + +static void mcp_build_initialize_response(char *buf, size_t buf_size, + int64_t id, const char *client_version) +{ + const char *version = (client_version && *client_version) + ? client_version : MCP_PROTOCOL_VERSION; + snprintf(buf, buf_size, + mcp_initialize_fmt, (long long)id, + version, PACKAGE_VERSION); +} + +/* ---- public API ---- */ + +void mcp_adapter_handle_request(const char *json_request, size_t len, + char *response, size_t response_size) +{ + mcp_request_t req; + + response[0] = '\0'; + + if (!mcp_parse_request(json_request, len, &req)) + { + RARCH_DBG("[MCP] Parse error for request: %.*s\n", + len > 200 ? 200 : (int)len, json_request); + mcp_build_error(response, response_size,0, -32700, "Parse error: Invalid JSON"); + return; + } + + RARCH_DBG("[MCP] Parsed request: method='%s', id=%lld, tool='%s'\n", + req.method, (long long)req.id, req.tool_name); + + if (string_is_equal(req.method, "initialize")) + { + mcp_build_initialize_response(response, response_size, req.id, req.protocol_version); + return; + } + + if (string_is_equal(req.method, "notifications/initialized")) + { + strlcpy(response, "{}", response_size); + return; + } + + if (string_is_equal(req.method, "tools/list")) + { + mcp_tools_build_list(req.id, response, response_size); + return; + } + + if (string_is_equal(req.method, "tools/call")) + { + if (string_is_empty(req.tool_name)) + { + mcp_build_error(response, response_size, req.id, -32602, + "Invalid params: missing tool name"); + return; + } + mcp_tools_call(req.id, req.tool_name, + req.args_json, req.args_len, + response, response_size); + return; + } + + if (string_is_equal(req.method, "resources/list")) + { + snprintf(response, response_size, mcp_resources_list_fmt, (long long)req.id); + return; + } + + RARCH_DBG("[MCP] Unknown method: '%s'\n", req.method); + mcp_build_error(response, response_size, req.id, -32601, "Method not found"); +} diff --git a/network/mcp/mcp_adapter.h b/network/mcp/mcp_adapter.h new file mode 100644 index 000000000000..57149fb88865 --- /dev/null +++ b/network/mcp/mcp_adapter.h @@ -0,0 +1,30 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_ADAPTER_H__ +#define MCP_ADAPTER_H__ + +#include +#include + +RETRO_BEGIN_DECLS + +/* Generate JSON-RPC response for a given request line. + * Writes the response into the provided buffer. */ +void mcp_adapter_handle_request(const char *json_request, size_t len, + char *response, size_t response_size); + +RETRO_END_DECLS + +#endif /* MCP_ADAPTER_H__ */ diff --git a/network/mcp/mcp_adapter_tool_list.c b/network/mcp/mcp_adapter_tool_list.c new file mode 100644 index 000000000000..f04d90fcf212 --- /dev/null +++ b/network/mcp/mcp_adapter_tool_list.c @@ -0,0 +1,108 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#include +#include + +#include +#include +#include + +#include "mcp_adapter_tool_list.h" +#include "mcp_defines.h" +#include "mcp_json_templates.h" +#include "../../retroarch.h" +#include "../../core_info.h" +#include "../../runloop.h" +#include "../../content.h" +#include "../../paths.h" +#include "../../verbosity.h" +#include "../../command.h" +#include "../../configuration.h" +#include "../../version.h" +#include "../../gfx/video_driver.h" +#include "../../audio/audio_driver.h" + +#include "mcp_adapter_tools.h" + +/* ---- tool registry ---- + * + * To add a new tool: + * 1. Define its JSON fragment and handler in mcp_adapter_tools.h + * 2. Add an entry to this array + */ + +static const mcp_tool_t mcp_tools[] = { + { "get_content_info", tool_get_content_info_json, mcp_tool_get_content_info }, + { "get_status", tool_get_status_json, mcp_tool_get_status }, + { "pause_resume", tool_pause_resume_json, mcp_tool_pause_resume }, + { "reset", tool_reset_json, mcp_tool_reset }, + { "save_state", tool_save_state_json, mcp_tool_save_state }, + { "load_state", tool_load_state_json, mcp_tool_load_state }, +}; + +static const size_t mcp_tools_count = sizeof(mcp_tools) / sizeof(mcp_tools[0]); + +/* ---- public API ---- */ + +void mcp_tools_build_list(int64_t id, char *buf, size_t buf_size) +{ + size_t pos = 0; + size_t i; + + pos += snprintf(buf + pos, buf_size - pos, + "{\"jsonrpc\":\"2.0\",\"id\":%lld," + "\"result\":{\"tools\":[", + (long long)id); + + for (i = 0; i < mcp_tools_count; i++) + { + if (i > 0) + buf[pos++] = ','; + pos += strlcpy(buf + pos, mcp_tools[i].json_fragment, buf_size - pos); + } + + strlcpy(buf + pos, "]}}", buf_size - pos); +} + +void mcp_tools_call(int64_t id, const char *tool_name, + const char *args_json, size_t args_len, + char *buf, size_t buf_size) +{ + char normalized[128]; + size_t i; + + /* normalize dots to underscores (vscode fails if tool names have dots) */ + strlcpy(normalized, tool_name, sizeof(normalized)); + for (i = 0; normalized[i]; i++) + { + if (normalized[i] == '.') + normalized[i] = '_'; + } + + for (i = 0; i < mcp_tools_count; i++) + { + if (string_is_equal(normalized, mcp_tools[i].name)) + { + char escaped[MCP_JSON_MAX_RESPONSE]; + mcp_tools[i].handler(args_json, args_len, buf, buf_size); + mcp_json_escape(escaped, sizeof(escaped), buf); + snprintf(buf, buf_size, mcp_content_info_fmt, (long long)id, escaped); + return; + } + } + + /* unknown tool */ + snprintf(buf, buf_size, mcp_error_fmt, (long long)id, -32601, "Unknown tool"); +} diff --git a/network/mcp/mcp_adapter_tool_list.h b/network/mcp/mcp_adapter_tool_list.h new file mode 100644 index 000000000000..e0cc6d62542c --- /dev/null +++ b/network/mcp/mcp_adapter_tool_list.h @@ -0,0 +1,49 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_ADAPTER_TOOL_LIST_H__ +#define MCP_ADAPTER_TOOL_LIST_H__ + +#include +#include + +RETRO_BEGIN_DECLS + +/* Handler function for a single MCP tool. + * Writes the raw JSON result object into the provided buffer. + * The dispatcher wraps it in the JSON-RPC response envelope. + * args_json / args_len contain the raw JSON of the + * "arguments" object (may be empty or NULL). */ +typedef void (*mcp_tool_handler_t)(const char *args_json, + size_t args_len, char *buf, size_t buf_size); + +/* Definition of one MCP tool. */ +typedef struct +{ + const char *name; /* tool name used for dispatch */ + const char *json_fragment; /* full JSON object for tools/list */ + mcp_tool_handler_t handler; /* implements tools/call */ +} mcp_tool_t; + +/* Build the JSON-RPC response for "tools/list". */ +void mcp_tools_build_list(int64_t id, char *buf, size_t buf_size); + +/* Dispatch a "tools/call" request to the matching tool handler. */ +void mcp_tools_call(int64_t id, const char *tool_name, + const char *args_json, size_t args_len, + char *buf, size_t buf_size); + +RETRO_END_DECLS + +#endif /* MCP_ADAPTER_TOOL_LIST_H__ */ diff --git a/network/mcp/mcp_adapter_tools.h b/network/mcp/mcp_adapter_tools.h new file mode 100644 index 000000000000..054a63feb2ff --- /dev/null +++ b/network/mcp/mcp_adapter_tools.h @@ -0,0 +1,389 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_ADAPTER_TOOLS_H__ +#define MCP_ADAPTER_TOOLS_H__ + +#include "mcp_adapter_utils.h" + +#include + +/* + * Tool definitions. + * + * Each tool has two parts: + * 1. A JSON fragment describing the tool + * 2. The handler function implementing the tool + * + * To add a new tool, add both parts here and then register it + * in the mcp_tools[] array in mcp_adapter_tool_list.c. + */ + +/* ================================================================ + * get_content_info + * ================================================================ */ + +static const char tool_get_content_info_json[] = + "{" + "\"name\":\"get_content_info\"," + "\"title\":\"Get Content Info\"," + "\"description\":\"Get information about the currently " + "running content in RetroArch (game name, core, system, " + "path, CRC32). Returns content status and details " + "if content is loaded.\"," + "\"inputSchema\":{" + "\"type\":\"object\"," + "\"properties\":{}," + "\"additionalProperties\":false" + "}" + "}"; + +static void mcp_tool_get_content_info(const char *args_json, size_t args_len, + char *buf, size_t buf_size) +{ + char result[4096] = "{"; + uint8_t flags = content_get_flags(); + + if (flags & CONTENT_ST_FLAG_IS_INITED) + { + core_info_t *core_info = NULL; + runloop_state_t *runloop_st = runloop_state_get_ptr(); + const char *content_path = path_get(RARCH_PATH_CONTENT); + const char *basename_path = path_get(RARCH_PATH_BASENAME); + const char *core_path = path_get(RARCH_PATH_CORE); + uint32_t crc = content_get_crc(); + + core_info_get_current_core(&core_info); + + mcp_json_add(result, sizeof(result), "status", + (runloop_st && (runloop_st->flags & RUNLOOP_FLAG_PAUSED)) + ? "paused" : "playing"); + + if (core_info) + { + if (core_info->core_name) + mcp_json_add(result, sizeof(result), "core_name", core_info->core_name); + if (core_info->systemname) + mcp_json_add(result, sizeof(result), "system_name", core_info->systemname); + if (core_info->system_id) + mcp_json_add(result, sizeof(result), "system_id", core_info->system_id); + if (core_info->display_name) + mcp_json_add(result, sizeof(result), "core_display_name", core_info->display_name); + } + else if (runloop_st && runloop_st->system.info.library_name) + { + mcp_json_add(result, sizeof(result), "core_name", runloop_st->system.info.library_name); + } + + if (content_path) + mcp_json_add(result, sizeof(result), "content_path", content_path); + + if (basename_path) + { + const char *basename = path_basename(basename_path); + if (basename) + mcp_json_add(result, sizeof(result), "content_name", basename); + } + + if (core_path) + mcp_json_add(result, sizeof(result), "core_path", core_path); + + if (crc != 0) + { + char crc_str[16]; + snprintf(crc_str, sizeof(crc_str), "%08lx", (unsigned long)crc); + mcp_json_add(result, sizeof(result), "crc32", crc_str); + } + } + else + mcp_json_add(result, sizeof(result), "status", "no_content"); + + strlcat(result, "}", sizeof(result)); + + strlcpy(buf, result, buf_size); +} + +/* ================================================================ + * get_status + * ================================================================ */ + +static const char tool_get_status_json[] = + "{" + "\"name\":\"get_status\"," + "\"title\":\"Get Status\"," + "\"description\":\"Get the current RetroArch status including " + "version, content state (playing, paused, or no content), " + "and frame count.\"," + "\"inputSchema\":{" + "\"type\":\"object\"," + "\"properties\":{}," + "\"additionalProperties\":false" + "}" + "}"; + +static void mcp_tool_get_status(const char *args_json, size_t args_len, + char *buf, size_t buf_size) +{ + char result[2048] = "{"; + uint8_t flags = content_get_flags(); + video_driver_state_t *video_st = video_state_get_ptr(); + + mcp_json_add(result, sizeof(result), "retroarch_version", PACKAGE_VERSION); + + if (flags & CONTENT_ST_FLAG_IS_INITED) + { + runloop_state_t *runloop_st = runloop_state_get_ptr(); + + if (runloop_st && (runloop_st->flags & RUNLOOP_FLAG_PAUSED)) + mcp_json_add(result, sizeof(result), "status", "paused"); + else if (runloop_st && (runloop_st->flags & RUNLOOP_FLAG_FASTMOTION)) + mcp_json_add(result, sizeof(result), "status", "fast_forward"); + else if (runloop_st && (runloop_st->flags & RUNLOOP_FLAG_SLOWMOTION)) + mcp_json_add(result, sizeof(result), "status", "slow_motion"); + else + mcp_json_add(result, sizeof(result), "status", "playing"); + + if (video_st) + mcp_json_add_int(result, sizeof(result), "frame_count", (int64_t)video_st->frame_count); + } + else + mcp_json_add(result, sizeof(result), "status", "no_content"); + + strlcat(result, "}", sizeof(result)); + strlcpy(buf, result, buf_size); +} + +/* ================================================================ + * pause_resume + * ================================================================ */ + +static const char tool_pause_resume_json[] = + "{" + "\"name\":\"pause_resume\"," + "\"title\":\"Pause or Resume\"," + "\"description\":\"Pause, resume, or toggle the currently " + "running content in RetroArch.\"," + "\"inputSchema\":{" + "\"type\":\"object\"," + "\"properties\":{" + "\"action\":{\"type\":\"string\"," + "\"enum\":[\"pause\",\"resume\"]," + "\"description\":\"The action to perform: pause or " + "resume the current pause state.\"}" + "}," + "\"required\":[\"action\"]," + "\"additionalProperties\":false" + "}" + "}"; + +static void mcp_tool_pause_resume(const char *args_json, size_t args_len, + char *buf, size_t buf_size) +{ + char action[32] = ""; + uint8_t flags = content_get_flags(); + + if (!(flags & CONTENT_ST_FLAG_IS_INITED)) + { + strlcpy(buf, "{\"error\":\"no content is currently running\"}", buf_size); + return; + } + + if ( !args_json + || !mcp_json_extract_string(args_json, args_len, + "action", action, sizeof(action)) + || action[0] == '\0') + { + strlcpy(buf, "{\"error\":\"missing required parameter: action\"}", buf_size); + return; + } + + if (string_is_equal(action, "pause")) + command_event(CMD_EVENT_PAUSE, NULL); + else if (string_is_equal(action, "resume")) + command_event(CMD_EVENT_UNPAUSE, NULL); + else + { + strlcpy(buf, "{\"error\":\"invalid action, use: pause or resume\"}", buf_size); + return; + } + + { + char result[256] = "{"; + runloop_state_t *runloop_st = runloop_state_get_ptr(); + mcp_json_add(result, sizeof(result), "result", "ok"); + mcp_json_add(result, sizeof(result), "status", + (runloop_st && (runloop_st->flags & RUNLOOP_FLAG_PAUSED)) + ? "paused" : "playing"); + strlcat(result, "}", sizeof(result)); + strlcpy(buf, result, buf_size); + } +} + +/* ================================================================ + * reset + * ================================================================ */ + +static const char tool_reset_json[] = + "{" + "\"name\":\"reset\"," + "\"title\":\"Reset\"," + "\"description\":\"Reset the currently running content " + "(equivalent to a soft reset of the emulated system).\"," + "\"inputSchema\":{" + "\"type\":\"object\"," + "\"properties\":{}," + "\"additionalProperties\":false" + "}" + "}"; + +static void mcp_tool_reset(const char *args_json, size_t args_len, + char *buf, size_t buf_size) +{ + uint8_t flags = content_get_flags(); + + if (!(flags & CONTENT_ST_FLAG_IS_INITED)) + { + strlcpy(buf, "{\"error\":\"no content is currently running\"}", buf_size); + return; + } + + command_event(CMD_EVENT_RESET, NULL); + + strlcpy(buf, "{\"result\":\"ok\"}", buf_size); +} + +/* ================================================================ + * save_state + * ================================================================ */ + +static const char tool_save_state_json[] = + "{" + "\"name\":\"save_state\"," + "\"title\":\"Save State\"," + "\"description\":\"Save the current emulation state to " + "a slot. If no slot is specified, the currently selected " + "slot is used.\"," + "\"inputSchema\":{" + "\"type\":\"object\"," + "\"properties\":{" + "\"slot\":{\"type\":\"integer\"," + "\"description\":\"The save state slot number " + "(0-999). If omitted, the current slot is used.\"}" + "}," + "\"additionalProperties\":false" + "}" + "}"; + +static void mcp_tool_save_state(const char *args_json, size_t args_len, + char *buf, size_t buf_size) +{ + char result[512] = "{"; + settings_t *settings = config_get_ptr(); + uint8_t flags = content_get_flags(); + int slot; + + if (!(flags & CONTENT_ST_FLAG_IS_INITED)) + { + strlcpy(buf, "{\"error\":\"no content is currently running\"}", buf_size); + return; + } + + if (!core_info_current_supports_savestate()) + { + strlcpy(buf, "{\"error\":\"current core does not support save states\"}", buf_size); + return; + } + + if (args_json && mcp_json_extract_int(args_json, args_len, "slot", &slot)) + configuration_set_int(settings, settings->ints.state_slot, slot); + + command_event(CMD_EVENT_SAVE_STATE, NULL); + + mcp_json_add(result, sizeof(result), "result", "ok"); + mcp_json_add_int(result, sizeof(result), "slot", settings->ints.state_slot); + strlcat(result, "}", sizeof(result)); + strlcpy(buf, result, buf_size); +} + +/* ================================================================ + * load_state + * ================================================================ */ + +static const char tool_load_state_json[] = + "{" + "\"name\":\"load_state\"," + "\"title\":\"Load State\"," + "\"description\":\"Load an emulation state from a slot. " + "If no slot is specified, the currently selected slot " + "is used.\"," + "\"inputSchema\":{" + "\"type\":\"object\"," + "\"properties\":{" + "\"slot\":{\"type\":\"integer\"," + "\"description\":\"The save state slot number " + "(0-999). If omitted, the current slot is used.\"}" + "}," + "\"additionalProperties\":false" + "}" + "}"; + +static void mcp_tool_load_state( + const char *args_json, size_t args_len, + char *buf, size_t buf_size) +{ + char result[512] = "{"; + settings_t *settings = config_get_ptr(); + uint8_t flags = content_get_flags(); + int slot; + + if (!(flags & CONTENT_ST_FLAG_IS_INITED)) + { + strlcpy(buf, "{\"error\":\"no content is currently running\"}", buf_size); + return; + } + + if (!core_info_current_supports_savestate()) + { + strlcpy(buf, "{\"error\":\"current core does not support save states\"}", buf_size); + return; + } + + if (args_json && mcp_json_extract_int(args_json, args_len, "slot", &slot)) + configuration_set_int(settings, settings->ints.state_slot, slot); + + { + char state_path[PATH_MAX_LENGTH]; + if (runloop_get_savestate_path(state_path, sizeof(state_path), settings->ints.state_slot)) + { + if (!path_is_valid(state_path)) + { + char err[512]; + snprintf(err, sizeof(err), + "{\"error\":\"no save state file found in slot %d\"}", + settings->ints.state_slot); + strlcpy(buf, err, buf_size); + return; + } + } + } + + command_event(CMD_EVENT_LOAD_STATE, NULL); + + mcp_json_add(result, sizeof(result), "result", "ok"); + mcp_json_add_int(result, sizeof(result), "slot", settings->ints.state_slot); + strlcat(result, "}", sizeof(result)); + strlcpy(buf, result, buf_size); +} + +#endif /* MCP_ADAPTER_TOOLS_H__ */ diff --git a/network/mcp/mcp_adapter_utils.h b/network/mcp/mcp_adapter_utils.h new file mode 100644 index 000000000000..c58eb812ff2d --- /dev/null +++ b/network/mcp/mcp_adapter_utils.h @@ -0,0 +1,213 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_ADAPTER_UTILS_H__ +#define MCP_ADAPTER_UTILS_H__ + +#include +#include +#include + +static void mcp_json_escape(char *dst, size_t dst_size, const char *src) +{ + size_t w = 0; + if (!src) + { + if (dst_size > 0) + dst[0] = '\0'; + return; + } + for (; *src && w + 6 < dst_size; src++) + { + switch (*src) + { + case '"': dst[w++] = '\\'; dst[w++] = '"'; break; + case '\\': dst[w++] = '\\'; dst[w++] = '\\'; break; + case '\n': dst[w++] = '\\'; dst[w++] = 'n'; break; + case '\r': dst[w++] = '\\'; dst[w++] = 'r'; break; + case '\t': dst[w++] = '\\'; dst[w++] = 't'; break; + default: + if ((unsigned char)*src < 0x20) + w += snprintf(dst + w, dst_size - w, "\\u%04x", + (unsigned)(unsigned char)*src); + else + dst[w++] = *src; + break; + } + } + if (w < dst_size) + dst[w] = '\0'; +} + +static void mcp_json_add(char *buf, size_t buf_size, + const char *key, const char *value) +{ + char escaped[2048]; + char tmp[2048 + 256]; + size_t len = strlen(buf); + mcp_json_escape(escaped, sizeof(escaped), value); + snprintf(tmp, sizeof(tmp), "\"%s\":\"%s\"", key, escaped); + if (len > 1) + strlcat(buf, ",", buf_size); + strlcat(buf, tmp, buf_size); +} + +static void mcp_json_add_int(char *buf, size_t buf_size, + const char *key, int64_t value) +{ + char tmp[256]; + size_t len = strlen(buf); + snprintf(tmp, sizeof(tmp), "\"%s\":%lld", key, (long long)value); + if (len > 1) + strlcat(buf, ",", buf_size); + strlcat(buf, tmp, buf_size); +} + +static void mcp_json_add_bool(char *buf, size_t buf_size, + const char *key, bool value) +{ + char tmp[256]; + size_t len = strlen(buf); + snprintf(tmp, sizeof(tmp), "\"%s\":%s", key, value ? "true" : "false"); + if (len > 1) + strlcat(buf, ",", buf_size); + strlcat(buf, tmp, buf_size); +} + +/* + * Extract a string value for a given key from a flat JSON object. + * Returns true if found. + */ +static bool mcp_json_extract_string( + const char *json, size_t json_len, + const char *key, char *value, size_t value_size) +{ + rjson_t *parser; + enum rjson_type type; + const char *pending_key = NULL; + int depth = 0; + + if (!json || !key || json_len == 0) + return false; + + parser = rjson_open_buffer(json, json_len); + if (!parser) + return false; + + for (;;) + { + type = rjson_next(parser); + if (type == RJSON_DONE || type == RJSON_ERROR) + break; + if (type == RJSON_OBJECT || type == RJSON_ARRAY) + { + depth++; + pending_key = NULL; + continue; + } + if (type == RJSON_OBJECT_END || type == RJSON_ARRAY_END) + { + depth--; + continue; + } + if (depth != 1) + continue; + if (type == RJSON_STRING) + { + size_t slen; + const char *s = rjson_get_string(parser, &slen); + if (!pending_key) + pending_key = s; + else + { + if (string_is_equal(pending_key, key)) + { + strlcpy(value, s, value_size); + rjson_free(parser); + return true; + } + pending_key = NULL; + } + } + else + pending_key = NULL; + } + + rjson_free(parser); + return false; +} + +/* + * Extract an integer value for a given key from a flat JSON object. + * Returns true if found. + */ +static bool mcp_json_extract_int( + const char *json, size_t json_len, + const char *key, int *value) +{ + rjson_t *parser; + enum rjson_type type; + const char *pending_key = NULL; + int depth = 0; + + if (!json || !key || json_len == 0) + return false; + + parser = rjson_open_buffer(json, json_len); + if (!parser) + return false; + + for (;;) + { + type = rjson_next(parser); + if (type == RJSON_DONE || type == RJSON_ERROR) + break; + if (type == RJSON_OBJECT || type == RJSON_ARRAY) + { + depth++; + pending_key = NULL; + continue; + } + if (type == RJSON_OBJECT_END || type == RJSON_ARRAY_END) + { + depth--; + continue; + } + if (depth != 1) + continue; + if (type == RJSON_STRING && !pending_key) + { + size_t slen; + pending_key = rjson_get_string(parser, &slen); + continue; + } + if (type == RJSON_NUMBER && pending_key) + { + if (string_is_equal(pending_key, key)) + { + *value = rjson_get_int(parser); + rjson_free(parser); + return true; + } + pending_key = NULL; + continue; + } + pending_key = NULL; + } + + rjson_free(parser); + return false; +} + +#endif /* MCP_ADAPTER_UTILS_H__ */ diff --git a/network/mcp/mcp_defines.h b/network/mcp/mcp_defines.h new file mode 100644 index 000000000000..df8aaea8570b --- /dev/null +++ b/network/mcp/mcp_defines.h @@ -0,0 +1,27 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_DEFINES_H__ +#define MCP_DEFINES_H__ + +#define MCP_JSON_MAX_REQUEST 4096 +#define MCP_JSON_MAX_RESPONSE 8192 + +#define MCP_HTTP_ENDPOINT_PATH "/mcp" +#define MCP_HTTP_RECV_BUF_SIZE 4096 +#define MCP_HTTP_MAX_REQUEST (64 * 1024) + +#define MCP_PROTOCOL_VERSION "2025-11-25" + +#endif /* MCP_DEFINES_H__ */ diff --git a/network/mcp/mcp_http_transport.c b/network/mcp/mcp_http_transport.c new file mode 100644 index 000000000000..b16bd18b2aa2 --- /dev/null +++ b/network/mcp/mcp_http_transport.c @@ -0,0 +1,519 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "mcp_http_transport.h" +#include "mcp_defines.h" +#include "../../verbosity.h" + +/* HTTP state machine for non-blocking I/O */ +enum mcp_http_state +{ + MCP_HTTP_STATE_IDLE = 0, /* listening, no client connected */ + MCP_HTTP_STATE_READING_HEADER, /* client connected, reading headers */ + MCP_HTTP_STATE_READING_BODY, /* headers parsed, reading body */ + MCP_HTTP_STATE_REQUEST_READY, /* full request parsed, ready to dispatch */ + MCP_HTTP_STATE_SENDING /* sending response */ +}; + +enum mcp_http_method +{ + MCP_HTTP_METHOD_UNKNOWN = 0, + MCP_HTTP_METHOD_OPTIONS, + MCP_HTTP_METHOD_GET, + MCP_HTTP_METHOD_POST +}; + +typedef struct mcp_http_state_data +{ + int server_fd; + int client_fd; + enum mcp_http_state state; + + /* receive buffer for current request */ + char recv_buf[MCP_HTTP_MAX_REQUEST]; + size_t recv_buf_len; + + /* parsed header info */ + int header_end; /* offset past \r\n\r\n */ + int content_length; + enum mcp_http_method method; + + /* config */ + char password[256]; + char bind_address[256]; + unsigned port; +} mcp_http_state_t; + +/* ---- helpers ---- */ + +static bool mcp_http_extract_path(const char *request, char *path_buf, size_t path_buf_size) +{ + const char *method_end; + const char *path_end; + size_t path_len; + + method_end = strchr(request, ' '); + if (!method_end) + return false; + + method_end++; + path_end = strchr(method_end, ' '); + if (!path_end) + return false; + + path_len = (size_t)(path_end - method_end); + if (path_len >= path_buf_size) + path_len = path_buf_size - 1; + memcpy(path_buf, method_end, path_len); + path_buf[path_len] = '\0'; + return true; +} + +static bool mcp_http_check_auth(const char *request, const char *password) +{ + const char *auth_header; + const char *value_start; + const char *line_end; + size_t pw_len; + + /* no password configured = no auth required */ + if (string_is_empty(password)) + return true; + + /* look for Authorization header (case-insensitive) */ + auth_header = strcasestr(request, "Authorization:"); + if (!auth_header) + return false; + + value_start = auth_header + STRLEN_CONST("Authorization:"); + while (*value_start == ' ' || *value_start == '\t') + value_start++; + + /* expect "Bearer " */ + if (strncmp(value_start, "Bearer ", STRLEN_CONST("Bearer ")) != 0) + return false; + + value_start += STRLEN_CONST("Bearer "); + line_end = strstr(value_start, "\r\n"); + pw_len = strlen(password); + + if (line_end) + { + if ((size_t)(line_end - value_start) != pw_len) + return false; + return (memcmp(value_start, password, pw_len) == 0); + } + + return string_is_equal(value_start, password); +} + +static int mcp_http_parse_content_length(const char *headers) +{ + const char *cl; + const char *val; + + cl = strcasestr(headers, "Content-Length:"); + if (!cl) + return 0; + + val = cl + STRLEN_CONST("Content-Length:"); + while (*val == ' ' || *val == '\t') + val++; + + return atoi(val); +} + +static void mcp_http_send_raw(int fd, const char *data, size_t len) +{ + size_t sent = 0; + while (sent < len) + { + ssize_t n = send(fd, data + sent, len - sent, 0); + if (n <= 0) + break; + sent += (size_t)n; + } +} + +static void mcp_http_send_error(int fd, int status_code, const char *status_text, const char *body) +{ + char header[512]; + size_t body_len = strlen(body); + int header_len = snprintf(header, sizeof(header), + "HTTP/1.1 %d %s\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: %u\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "\r\n", + status_code, status_text, (unsigned)body_len); + mcp_http_send_raw(fd, header, (size_t)header_len); + mcp_http_send_raw(fd, body, body_len); +} + +static void mcp_http_send_options(int fd) +{ + const char *response = + "HTTP/1.1 204 No Content\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: POST, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, Accept, Authorization, " + "MCP-Session-Id, MCP-Protocol-Version\r\n" + "Connection: close\r\n" + "\r\n"; + mcp_http_send_raw(fd, response, strlen(response)); +} + +static void mcp_http_close_client(mcp_http_state_t *st) +{ + if (st->client_fd >= 0) + { + socket_close(st->client_fd); + st->client_fd = -1; + } + st->state = MCP_HTTP_STATE_IDLE; + st->recv_buf_len = 0; + st->header_end = -1; + st->content_length = -1; + st->method = MCP_HTTP_METHOD_UNKNOWN; +} + +/* ---- interface implementation ---- */ + +static bool mcp_http_init(mcp_transport_t *transport, const char *bind_address, unsigned port, const char *password) +{ + mcp_http_state_t *st; + struct sockaddr_in addr; + int opt = 1; + int fd; + + if (!network_init()) + { + RARCH_ERR("[MCP] Failed to init network\n"); + return false; + } + + st = (mcp_http_state_t *)calloc(1, sizeof(*st)); + if (!st) + return false; + + st->server_fd = -1; + st->client_fd = -1; + st->state = MCP_HTTP_STATE_IDLE; + st->header_end = -1; + st->content_length = -1; + st->port = port; + + if (password && *password) + strlcpy(st->password, password, sizeof(st->password)); + if (bind_address && *bind_address) + strlcpy(st->bind_address, bind_address, sizeof(st->bind_address)); + else + strlcpy(st->bind_address, "127.0.0.1", sizeof(st->bind_address)); + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + { + RARCH_ERR("[MCP] Failed to create socket\n"); + free(st); + return false; + } + + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof(opt)); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + addr.sin_addr.s_addr = inet_addr(st->bind_address); + + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) + { + RARCH_ERR("[MCP] Failed to bind to %s:%u\n", st->bind_address, port); + socket_close(fd); + free(st); + return false; + } + + if (listen(fd, 1) < 0) + { + RARCH_ERR("[MCP] Failed to listen on socket\n"); + socket_close(fd); + free(st); + return false; + } + + /* set server socket to non-blocking */ + socket_nonblock(fd); + + st->server_fd = fd; + transport->state = st; + + RARCH_LOG("[MCP] HTTP server listening on http://%s:%u%s\n", + st->bind_address, + port, + MCP_HTTP_ENDPOINT_PATH); + return true; +} + +static bool mcp_http_poll(mcp_transport_t *transport, char *out_buf, size_t out_buf_size, size_t *out_len) +{ + mcp_http_state_t *st = (mcp_http_state_t *)transport->state; + char path_buf[256]; + + if (!st || st->server_fd < 0) + return false; + + switch (st->state) + { + case MCP_HTTP_STATE_IDLE: + { + /* Try to accept a new connection */ + struct sockaddr_in client_addr; + socklen_t client_len = sizeof(client_addr); + int client = accept(st->server_fd, + (struct sockaddr *)&client_addr, &client_len); + + if (client < 0) + return false; /* no connection pending */ + + socket_nonblock(client); + st->client_fd = client; + st->state = MCP_HTTP_STATE_READING_HEADER; + st->recv_buf_len = 0; + st->header_end = -1; + st->content_length = -1; + st->method = MCP_HTTP_METHOD_UNKNOWN; + + RARCH_DBG("[MCP] Client connected (fd=%d)\n", client); + return false; + } + + case MCP_HTTP_STATE_READING_HEADER: + case MCP_HTTP_STATE_READING_BODY: + { + char tmp[MCP_HTTP_RECV_BUF_SIZE]; + ssize_t n = recv(st->client_fd, tmp, sizeof(tmp), 0); + + if (n == 0) + { + /* client disconnected */ + RARCH_DBG("[MCP] Client disconnected\n"); + mcp_http_close_client(st); + return false; + } + + if (n < 0) + { + if (isagain((int)n) || isagain(errno)) + return false; /* no data yet */ + mcp_http_close_client(st); + return false; + } + + /* check size limit */ + if (st->recv_buf_len + (size_t)n > MCP_HTTP_MAX_REQUEST) + { + mcp_http_send_error(st->client_fd, 413, "Payload Too Large", "Request too large"); + mcp_http_close_client(st); + return false; + } + + memcpy(st->recv_buf + st->recv_buf_len, tmp, (size_t)n); + st->recv_buf_len += (size_t)n; + st->recv_buf[st->recv_buf_len] = '\0'; + + /* look for end of headers if not found yet */ + if (st->header_end < 0) + { + char *header_term = strstr(st->recv_buf, "\r\n\r\n"); + if (!header_term) + return false; /* need more data */ + + st->header_end = (int)(header_term - st->recv_buf) + 4; + + /* determine method */ + if (strncmp(st->recv_buf, "OPTIONS ", 8) == 0) + st->method = MCP_HTTP_METHOD_OPTIONS; + else if (strncmp(st->recv_buf, "GET ", 4) == 0) + st->method = MCP_HTTP_METHOD_GET; + else if (strncmp(st->recv_buf, "POST ", 5) == 0) + st->method = MCP_HTTP_METHOD_POST; + + st->content_length = mcp_http_parse_content_length(st->recv_buf); + + RARCH_DBG("[MCP] Headers complete: method=%s, content_length=%d, path=%s\n", + st->method == MCP_HTTP_METHOD_POST ? "POST" : + st->method == MCP_HTTP_METHOD_GET ? "GET" : + st->method == MCP_HTTP_METHOD_OPTIONS ? "OPTIONS" : "UNKNOWN", + st->content_length, + mcp_http_extract_path(st->recv_buf, path_buf, sizeof(path_buf)) ? path_buf : "?"); + + st->state = MCP_HTTP_STATE_READING_BODY; + } + + /* check if we have full body */ + if (st->header_end >= 0 && st->content_length >= 0) + { + int body_len = (int)st->recv_buf_len - st->header_end; + if (body_len < st->content_length) + return false; /* need more data */ + } + + /* full request received - now process */ + + /* CORS preflight */ + if (st->method == MCP_HTTP_METHOD_OPTIONS) + { + RARCH_DBG("[MCP] Handling OPTIONS (CORS preflight)\n"); + if (mcp_http_extract_path(st->recv_buf, path_buf, sizeof(path_buf)) + && !string_is_equal(path_buf, MCP_HTTP_ENDPOINT_PATH)) + { + mcp_http_send_error(st->client_fd, 404, "Not Found", "404 Not Found"); + } + else + mcp_http_send_options(st->client_fd); + + mcp_http_close_client(st); + return false; + } + + /* path validation */ + if (!mcp_http_extract_path(st->recv_buf, path_buf, sizeof(path_buf)) + || !string_is_equal(path_buf, MCP_HTTP_ENDPOINT_PATH)) + { + RARCH_DBG("[MCP] Rejecting invalid path: %s\n", path_buf); + mcp_http_send_error(st->client_fd, 404, "Not Found", "404 Not Found"); + mcp_http_close_client(st); + return false; + } + + /* auth check */ + if (!mcp_http_check_auth(st->recv_buf, st->password)) + { + RARCH_DBG("[MCP] Auth failed\n"); + mcp_http_send_error(st->client_fd, 401, "Unauthorized", "401 Unauthorized"); + mcp_http_close_client(st); + return false; + } + + /* GET (SSE not supported) */ + if (st->method == MCP_HTTP_METHOD_GET) + { + mcp_http_send_error(st->client_fd, 405, + "Method Not Allowed", + "{\"error\":\"SSE streaming not supported\"}"); + mcp_http_close_client(st); + return false; + } + + /* POST with body */ + if (st->method == MCP_HTTP_METHOD_POST && st->content_length > 0) + { + size_t body_len = (size_t)st->content_length; + RARCH_DBG("[MCP] POST body (%d bytes): %.*s\n", + st->content_length, + st->content_length > 200 ? 200 : st->content_length, + st->recv_buf + st->header_end); + + if (body_len >= out_buf_size) + body_len = out_buf_size - 1; + + memcpy(out_buf, st->recv_buf + st->header_end, body_len); + out_buf[body_len] = '\0'; + *out_len = body_len; + st->state = MCP_HTTP_STATE_REQUEST_READY; + return true; + } + + /* POST without body or other */ + mcp_http_close_client(st); + return false; + } + + case MCP_HTTP_STATE_REQUEST_READY: + case MCP_HTTP_STATE_SENDING: + /* waiting for send() to be called */ + return false; + + default: + break; + } + + return false; +} + +static bool mcp_http_send(mcp_transport_t *transport, const char *data, size_t len) +{ + mcp_http_state_t *st = (mcp_http_state_t *)transport->state; + char header[512]; + int header_len; + + if (!st || st->client_fd < 0) + return false; + + header_len = snprintf(header, sizeof(header), + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json\r\n" + "Content-Length: %u\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "\r\n", + (unsigned)len); + + RARCH_DBG("[MCP] Sending response (%u bytes): %.*s\n", (unsigned)len, len > 200 ? 200 : (int)len, data); + + mcp_http_send_raw(st->client_fd, header, (size_t)header_len); + mcp_http_send_raw(st->client_fd, data, len); + + mcp_http_close_client(st); + return true; +} + +static void mcp_http_close(mcp_transport_t *transport) +{ + mcp_http_state_t *st = (mcp_http_state_t *)transport->state; + if (!st) + return; + + mcp_http_close_client(st); + + if (st->server_fd >= 0) + { + socket_close(st->server_fd); + st->server_fd = -1; + } + + free(st); + transport->state = NULL; + + RARCH_LOG("[MCP] HTTP server closed\n"); +} + +const mcp_transport_interface_t mcp_http_transport = { + mcp_http_init, + mcp_http_poll, + mcp_http_send, + mcp_http_close +}; diff --git a/network/mcp/mcp_http_transport.h b/network/mcp/mcp_http_transport.h new file mode 100644 index 000000000000..dbd86ecb7fdd --- /dev/null +++ b/network/mcp/mcp_http_transport.h @@ -0,0 +1,26 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_HTTP_TRANSPORT_H__ +#define MCP_HTTP_TRANSPORT_H__ + +#include "mcp_transport.h" + +RETRO_BEGIN_DECLS + +extern const mcp_transport_interface_t mcp_http_transport; + +RETRO_END_DECLS + +#endif /* MCP_HTTP_TRANSPORT_H__ */ diff --git a/network/mcp/mcp_json_templates.h b/network/mcp/mcp_json_templates.h new file mode 100644 index 000000000000..e7ab595a3aec --- /dev/null +++ b/network/mcp/mcp_json_templates.h @@ -0,0 +1,71 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_JSON_TEMPLATES_H__ +#define MCP_JSON_TEMPLATES_H__ + +/* ---- JSON-RPC response templates ---- + * + * Each constant holds a complete JSON-RPC 2.0 response with + * printf-style placeholders for dynamic values. + */ + +static const char mcp_error_fmt[] = + "{" + "\"jsonrpc\":\"2.0\"," + "\"id\":%lld," + "\"error\":{" + "\"code\":%d," + "\"message\":\"%s\"" + "}" + "}"; + +static const char mcp_initialize_fmt[] = + "{" + "\"jsonrpc\":\"2.0\"," + "\"id\":%lld," + "\"result\":{" + "\"protocolVersion\":\"%s\"," + "\"capabilities\":{\"tools\":{},\"resources\":{}}," + "\"serverInfo\":{" + "\"name\":\"retroarch-mcp-server\"," + "\"title\":\"RetroArch MCP Server\"," + "\"description\":\"Query and control RetroArch frontend\"," + "\"version\":\"%s\"" + "}" + "}" + "}"; + +static const char mcp_content_info_fmt[] = + "{" + "\"jsonrpc\":\"2.0\"," + "\"id\":%lld," + "\"result\":{" + "\"content\":[{" + "\"type\":\"text\"," + "\"text\":\"%s\"" + "}]" + "}" + "}"; + +static const char mcp_resources_list_fmt[] = + "{" + "\"jsonrpc\":\"2.0\"," + "\"id\":%lld," + "\"result\":{" + "\"resources\":[]" + "}" + "}"; + +#endif /* MCP_JSON_TEMPLATES_H__ */ diff --git a/network/mcp/mcp_server.c b/network/mcp/mcp_server.c new file mode 100644 index 000000000000..5df70b86972a --- /dev/null +++ b/network/mcp/mcp_server.c @@ -0,0 +1,97 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#include + +#include + +#include "mcp_server.h" +#include "mcp_defines.h" +#include "mcp_transport.h" +#include "mcp_http_transport.h" +#include "mcp_adapter.h" +#include "../../configuration.h" +#include "../../verbosity.h" + +static mcp_transport_t mcp_transport; +static bool mcp_active = false; + +static bool mcp_server_init(const char *bind_address, unsigned port, const char *password) +{ + if (mcp_active) + return true; + + mcp_transport.iface = &mcp_http_transport; + mcp_transport.state = NULL; + + if (!mcp_transport.iface->init(&mcp_transport, bind_address, port, password)) + { + RARCH_ERR("[MCP] Failed to initialize transport\n"); + return false; + } + + mcp_active = true; + RARCH_LOG("[MCP] Server started\n"); + return true; +} + +void mcp_server_poll(void) +{ + settings_t *settings = config_get_ptr(); + + if (settings->bools.mcp_server_enable) + { + if (!mcp_active) + { + RARCH_DBG("[MCP] Initializing server on %s:%u\n", + settings->arrays.mcp_server_address, + settings->uints.mcp_server_port); + mcp_server_init( + settings->arrays.mcp_server_address, + settings->uints.mcp_server_port, + settings->arrays.mcp_server_password); + } + + if (mcp_active) + { + char request[MCP_JSON_MAX_REQUEST]; + size_t request_len = 0; + + if (mcp_transport.iface->poll(&mcp_transport, request, sizeof(request), &request_len)) + { + char response[MCP_JSON_MAX_RESPONSE]; + RARCH_DBG("[MCP] Request received (%u bytes)\n", (unsigned)request_len); + mcp_adapter_handle_request( request, request_len, response, sizeof(response)); + + if (response[0]) + { + RARCH_DBG("[MCP] Sending response (%u bytes)\n", (unsigned)strlen(response)); + mcp_transport.iface->send(&mcp_transport, response, strlen(response)); + } + } + } + } + else if (mcp_active) + mcp_server_deinit(); +} + +void mcp_server_deinit(void) +{ + if (!mcp_active) + return; + + mcp_transport.iface->close(&mcp_transport); + mcp_active = false; + RARCH_LOG("[MCP] Server stopped\n"); +} diff --git a/network/mcp/mcp_server.h b/network/mcp/mcp_server.h new file mode 100644 index 000000000000..bb9e98c17c46 --- /dev/null +++ b/network/mcp/mcp_server.h @@ -0,0 +1,33 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_SERVER_H__ +#define MCP_SERVER_H__ + +#include + +RETRO_BEGIN_DECLS + +/* Poll the MCP server (call once per frame from main loop). + * Reads settings internally: starts/stops the server + * when the enable toggle changes, and processes requests + * while running.*/ +void mcp_server_poll(void); + +/* Stop and clean up the MCP server. */ +void mcp_server_deinit(void); + +RETRO_END_DECLS + +#endif /* MCP_SERVER_H__ */ diff --git a/network/mcp/mcp_transport.h b/network/mcp/mcp_transport.h new file mode 100644 index 000000000000..554264cbd98e --- /dev/null +++ b/network/mcp/mcp_transport.h @@ -0,0 +1,52 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef MCP_TRANSPORT_H__ +#define MCP_TRANSPORT_H__ + +#include +#include + +RETRO_BEGIN_DECLS + +/* Generic transport interface for MCP server. + * Each transport implements init/poll/send/close. + * poll is non-blocking and returns data when available. */ + +typedef struct mcp_transport mcp_transport_t; + +typedef struct mcp_transport_interface +{ + bool (*init)(mcp_transport_t *transport, const char *bind_address, unsigned port, const char *password); + + /* poll: non-blocking check for incoming request. + * Returns true if a complete request body is available in out_buf. */ + bool (*poll)(mcp_transport_t *transport, char *out_buf, size_t out_buf_size, size_t *out_len); + + /* send: sends a JSON response to the current client. + * Returns true on success. */ + bool (*send)(mcp_transport_t *transport, const char *data, size_t len); + + void (*close)(mcp_transport_t *transport); +} mcp_transport_interface_t; + +struct mcp_transport +{ + const mcp_transport_interface_t *iface; + void *state; /* transport-specific state */ +}; + +RETRO_END_DECLS + +#endif /* MCP_TRANSPORT_H__ */ diff --git a/pkg/apple/BaseConfig.xcconfig b/pkg/apple/BaseConfig.xcconfig index 930a0974a21f..cbfa00ecc11a 100644 --- a/pkg/apple/BaseConfig.xcconfig +++ b/pkg/apple/BaseConfig.xcconfig @@ -59,6 +59,7 @@ OTHER_CFLAGS = $(inherited) -DHAVE_NEAREST_RESAMPLER OTHER_CFLAGS = $(inherited) -DHAVE_NETPLAYDISCOVERY OTHER_CFLAGS = $(inherited) -DHAVE_NETPLAYDISCOVERY_NSNET OTHER_CFLAGS = $(inherited) -DHAVE_NETWORKGAMEPAD +OTHER_CFLAGS = $(inherited) -DHAVE_MCP OTHER_CFLAGS = $(inherited) -DHAVE_NETWORKING OTHER_CFLAGS = $(inherited) -DHAVE_NETWORK_CMD OTHER_CFLAGS = $(inherited) -DHAVE_NO_BUILTINZLIB diff --git a/qb/config.libs.sh b/qb/config.libs.sh index 5c78baa8914f..754245b2b256 100644 --- a/qb/config.libs.sh +++ b/qb/config.libs.sh @@ -200,8 +200,10 @@ if [ "$HAVE_NETWORKING" != 'no' ]; then fi add_opt NETWORK_CMD yes + add_opt MCP yes else add_opt NETWORK_CMD no + add_opt MCP no fi check_enabled NETWORKING CHEEVOS cheevos 'Networking is' false diff --git a/retroarch.c b/retroarch.c index da23cbfafe9e..01d298a510cb 100644 --- a/retroarch.c +++ b/retroarch.c @@ -173,6 +173,9 @@ #ifdef HAVE_CLOUDSYNC #include "network/cloud_sync_driver.h" #endif +#ifdef HAVE_MCP +#include "network/mcp/mcp_server.h" +#endif #endif #ifdef HAVE_THREADS @@ -5903,6 +5906,11 @@ void main_exit(void *args) if (menu_st) menu_st->flags &= ~MENU_ST_FLAG_DATA_OWN; #endif + +#ifdef HAVE_MCP + mcp_server_deinit(); +#endif + retroarch_ctl(RARCH_CTL_MAIN_DEINIT, NULL); if (runloop_st->perfcnt_enable) @@ -6164,6 +6172,10 @@ int rarch_main(int argc, char *argv[], void *data) steam_poll(); #endif +#ifdef HAVE_MCP + mcp_server_poll(); +#endif + #ifdef HAVE_QT app_exit = ui_companion_qt.application->exiting; #endif diff --git a/ui/drivers/cocoa/cocoa_common.m b/ui/drivers/cocoa/cocoa_common.m index 743364fcab9f..f6da835997b8 100644 --- a/ui/drivers/cocoa/cocoa_common.m +++ b/ui/drivers/cocoa/cocoa_common.m @@ -59,6 +59,10 @@ #include "steam/steam.h" #endif +#ifdef HAVE_MCP +#include "network/mcp/mcp_server.h" +#endif + #if IOS #import extern bool RAIsVoiceOverRunning(void) @@ -119,6 +123,10 @@ static void rarch_draw_observer(CFRunLoopObserverRef observer, steam_poll(); #endif +#ifdef HAVE_MCP + mcp_server_poll(); +#endif + runloop_flags = runloop_get_flags(); #if !TARGET_OS_TV if (runloop_flags & RUNLOOP_FLAG_FASTMOTION) diff --git a/ui/drivers/ui_cocoa.m b/ui/drivers/ui_cocoa.m index 1d7b2fd370e8..799779d2f3c0 100644 --- a/ui/drivers/ui_cocoa.m +++ b/ui/drivers/ui_cocoa.m @@ -60,6 +60,10 @@ #include "steam/steam.h" #endif +#ifdef HAVE_MCP +#include "network/mcp/mcp_server.h" +#endif + typedef struct ui_application_cocoa { void *empty; @@ -829,6 +833,10 @@ - (void) rarch_main steam_poll(); #endif +#ifdef HAVE_MCP + mcp_server_poll(); +#endif + while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.002, FALSE) == kCFRunLoopRunHandledSource); if (ret == -1)