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)