Files
EVEngine/src/engine/cmdline/Mcp.cpp
T
西风逍遥游 4b24d1a157 feat(mcp): headless eve mcp host with AI-crafted MVVM editors
Adds 'eve mcp': a desktop-only headless MCP host (stdio by default, --port for TCP, --root for the project). It runs the full runtime, attaches DevTools (eve_eval / eve_run_script / snapshot), and starts the new EditorHost module that renders JSON-defined editor Views (ImGui) bound two-way to Squirrel ViewModel tables (MVVM), persists editors under editors/, and exposes the eve_host_* tool family (apply/state/set_value/events/capture/script/shutdown...). The stdio transport uses a background stdin reader and keeps stdout JSON-RPC-only; script print() is routed to stderr.

Tests: stdio handshake + tools/list round trip, host editor binding (VM<->View sync, set_value -> VM write + onChange events, save/reload persistence). Docs updated (AI与MCP支持.md, 命令行设计.md, tools/eve-mcp README) with the editor JSON protocol and Codex stdio setup.
2026-08-17 22:43:08 +08:00

173 lines
5.8 KiB
C++

#include "cmdline.h"
#include "common/Module.h"
#include "common/Runtime.h"
#include "common/config.h"
#include "filesystem/Filesystem.h"
#include "filesystem/physfs/FileApi.h"
#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU)
#include "devtools/DevTool.hpp"
#include "devtools/McpServer.hpp"
#include "ui/EditorHost.h"
#endif
#include <simplesquirrel/simplesquirrel.hpp>
#include <CLI11.hpp>
#include <squirrel.h>
#include <chrono>
#include <cstdarg>
#include <cstdio>
#include <filesystem>
#include <iostream>
#include <string>
#include <thread>
namespace eve::cmd {
namespace {
/** Route Squirrel print() to stderr so stdio MCP stdout stays JSON-RPC-only. */
void hostPrintFunc(HSQUIRRELVM, const SQChar* s, ...) {
char buf[4096];
va_list args;
va_start(args, s);
vsnprintf(buf, sizeof(buf), s, args);
va_end(args);
fputs(buf, stderr);
}
} // namespace
struct McpArgs : Handler {
std::string root_path;
int port = 0;
void setup(CLI::App& app, std::shared_ptr<CLI::Formatter> formatter) override {
auto mcp = app.add_subcommand(
"mcp", "Run a headless MCP host for AI agents (stdio by default; --port for TCP)");
mcp->allow_extras()->formatter(formatter);
mcp->add_option("--port", port,
"Listen on this TCP port instead of stdio (Codex uses stdio)");
mcp->add_option("-r,--root", root_path, "Project root directory (default: current dir)");
}
int parse(CLI::App& app, Cmdline& cmd) override {
auto mcp = app.get_subcommand("mcp");
if (mcp->parsed()) {
std::string current_path = cmd.get_remaining(mcp);
if (!root_path.empty()) current_path = root_path;
return cmd.McpHost(current_path, port);
}
return -1;
}
};
CMD_REG(McpArgs);
int Cmdline::McpHost(std::string path, int port) {
#if defined(EVENGINE_ANDROID) || defined(EVENGINE_IOS) || defined(EVENGINE_WEBGPU)
(void)path;
(void)port;
std::cerr << "eve mcp: headless MCP host is desktop-only" << std::endl;
return 4;
#else
try {
std::string gameDir = path;
{
std::error_code ec;
if (path.empty() || path == ".")
gameDir = std::filesystem::current_path(ec).string();
else
gameDir = std::filesystem::absolute(path, ec).string();
if (ec) gameDir = path;
}
if (!gameDir.empty() && gameDir != ".") {
std::error_code ec;
std::filesystem::current_path(gameDir, ec);
if (ec) {
std::cerr << "Cannot chdir to project path '" << gameDir << "': " << ec.message()
<< std::endl;
return 2;
}
}
Runtime runtime(2048, ssq::Libs::ALL);
runtime.initialize();
runtime.vm().setPrintFunc(&hostPrintFunc, &hostPrintFunc);
// Mount the project for PhysFS so script file APIs / editors/ resolve.
// Optional for the headless host: keep running when the environment
// cannot initialize PhysFS (sandboxed/CI shells), scripts just lose
// the PhysFS-backed file API.
try {
auto* fs = eve::filesystem::Filesystem::create();
if (fs) {
std::error_code ec;
auto cwd = std::filesystem::current_path(ec);
if (!ec) {
try {
fs->setSource(cwd.string());
} catch (...) {
}
}
}
eve::filesystem::physfs::installScriptFileApi(runtime.vm());
} catch (const std::exception& e) {
std::cerr << "eve mcp: filesystem mount skipped (" << e.what() << ")" << std::endl;
}
// DevTools: attach the VM so eve_eval / eve_run_script / snapshot work;
// expose eve.dev. No statement-level local sampling (host is a service).
auto& dt = eve::dev::DevTool::instance();
dt.attach(runtime.vm(), /*sampleLocals=*/false);
dt.exposeScriptApi(runtime.vm());
// Editor host: JSON Views <-> Squirrel ViewModel binding + window/render.
auto& host = eve::ui::EditorHost::instance();
host.start(runtime.vm(), gameDir, /*allowWindow=*/true);
host.exposeScriptApi(runtime.vm());
auto& mcp = dt.mcp();
mcp.setGameRoot(gameDir);
if (port > 0) {
const int bound = dt.startMcp(static_cast<uint16_t>(port));
if (bound <= 0) {
std::cerr << "Failed to start MCP server on port " << port << std::endl;
return 3;
}
std::cerr << "MCP listening on 127.0.0.1:" << bound
<< " (newline JSON-RPC; use tools/eve-mcp for Cursor stdio)" << std::endl;
} else {
if (!mcp.listenStdio()) {
std::cerr << "Failed to start stdio MCP transport" << std::endl;
return 3;
}
std::cerr << "MCP stdio transport ready (headless editor host; project=" << gameDir
<< ")" << std::endl;
}
while (!host.exitRequested()) {
dt.poll(); // drains MCP requests on the main thread
host.frame();
if (mcp.transport() == eve::dev::McpServer::Transport::Stdio &&
mcp.stdinClosed()) {
std::cerr << "MCP stdio closed; host exiting" << std::endl;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
host.stop();
dt.detach();
return 0;
} catch (const std::exception& e) {
std::cerr << "eve mcp failed: " << e.what() << std::endl;
return 3;
}
#endif
}
} // namespace eve::cmd