载入中...
搜索中...
未找到
Mcp.cpp
浏览该文件的文档.
1#include "cmdline.h"
2
3#include "common/Capability.h"
4#include "common/EditorHost.h"
5#include "common/Module.h"
6#include "common/Runtime.h"
7#include "common/config.h"
11
12#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU)
13#include "devtools/DevTool.hpp"
15#endif
16
17#include <simplesquirrel/simplesquirrel.hpp>
18#include <CLI11.hpp>
19#include <squirrel.h>
20
21#include <chrono>
22#include <cstdarg>
23#include <cstdio>
24#include <filesystem>
25#include <iostream>
26#include <map>
27#include <string>
28#include <thread>
29
30namespace eve::cmd {
31namespace {
32
34void hostPrintFunc(HSQUIRRELVM, const SQChar* s, ...) {
35 char buf[4096];
36 va_list args;
37 va_start(args, s);
38 vsnprintf(buf, sizeof(buf), s, args);
39 va_end(args);
40 fputs(buf, stderr);
41}
42
43bool endsWith(const std::string& value, const std::string& suffix) {
44 return value.size() >= suffix.size() &&
45 value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0;
46}
47
48bool isEditorHostResource(const std::string& path) {
49 if (path == "mcp.nut") return true;
50 if (path.rfind("mcp/", 0) == 0 && endsWith(path, ".nut")) return true;
51 if (path.rfind("editors/", 0) != 0) return false;
52 return endsWith(path, ".vm.nut") || endsWith(path, ".editor.json");
53}
54
55} // namespace
56
57struct McpArgs : Handler {
58 std::string root_path;
59 int port = 0;
60
61 void setup(CLI::App& app, std::shared_ptr<CLI::Formatter> formatter) override {
62 auto mcp = app.add_subcommand(
63 "mcp", "Run a headless MCP host for AI agents (stdio by default; --port for TCP)");
64 mcp->allow_extras()->formatter(formatter);
65 mcp->add_option("--port", port,
66 "Listen on this TCP port instead of stdio (Codex uses stdio)");
67 mcp->add_option("-r,--root", root_path, "Project root directory (default: current dir)");
68 }
69
70 int parse(CLI::App& app, Cmdline& cmd) override {
71 auto mcp = app.get_subcommand("mcp");
72 if (mcp->parsed()) {
73 std::string current_path = cmd.get_remaining(mcp);
74 if (!root_path.empty()) current_path = root_path;
75 return cmd.McpHost(current_path, port);
76 }
77 return -1;
78 }
79};
80
82
83int Cmdline::McpHost(std::string path, int port) {
84#if defined(EVENGINE_ANDROID) || defined(EVENGINE_IOS) || defined(EVENGINE_WEBGPU)
85 (void)path;
86 (void)port;
87 std::cerr << "eve mcp: headless MCP host is desktop-only" << std::endl;
88 return 4;
89#else
90 try {
91 std::string gameDir = path;
92 {
93 std::error_code ec;
94 if (path.empty() || path == ".")
95 gameDir = std::filesystem::current_path(ec).string();
96 else
97 gameDir = std::filesystem::absolute(path, ec).string();
98 if (ec) gameDir = path;
99 }
100
101 if (!gameDir.empty() && gameDir != ".") {
102 std::error_code ec;
103 std::filesystem::current_path(gameDir, ec);
104 if (ec) {
105 std::cerr << "Cannot chdir to project path '" << gameDir << "': " << ec.message()
106 << std::endl;
107 return 2;
108 }
109 }
110
111 Runtime runtime(2048, ssq::Libs::ALL);
112 runtime.initialize();
113 runtime.vm().setPrintFunc(&hostPrintFunc, &hostPrintFunc);
114
115 // Mount the project for PhysFS so script file APIs / editors/ resolve.
116 // Optional for the headless host: keep running when the environment
117 // cannot initialize PhysFS (sandboxed/CI shells), scripts just lose
118 // the PhysFS-backed file API.
119 eve::filesystem::Filesystem* projectFs = nullptr;
120 try {
121 projectFs = eve::filesystem::Filesystem::create();
122 if (projectFs) {
123 std::error_code ec;
124 auto cwd = std::filesystem::current_path(ec);
125 if (!ec) {
126 try {
127 projectFs->setSource(cwd.string());
128 } catch (...) {
129 }
130 }
131 }
133 } catch (const std::exception& e) {
134 std::cerr << "eve mcp: filesystem mount skipped (" << e.what() << ")" << std::endl;
135 }
136
137 // DevTools: attach the VM so eve_eval / eve_run_script / snapshot work;
138 // expose eve.dev. No statement-level local sampling (host is a service).
139 auto& dt = eve::dev::DevTool::instance();
140 dt.attach(runtime, /*sampleLocals=*/false);
141 dt.exposeScriptApi(runtime.vm());
142
143 // The headless host never runs load.nut, so registered modules are not
144 // instantiated automatically and their capability providers
145 // (IEditorHost, IRenderCapture, ISceneQuery, ...) are absent. Mirror
146 // load.nut's binding loop so the host exposes the full tool surface.
148
149 // Editor host: JSON Views <-> Squirrel ViewModel binding + window/render.
150 auto* host = eve::cap::query<eve::IEditorHost>();
151 if (host) {
152 host->start(runtime.vm(), gameDir, /*allowWindow=*/true);
153 host->exposeScriptApi(runtime.vm());
154 } else {
155 std::cerr << "eve mcp: editor host unavailable (ui module missing)"
156 << std::endl;
157 return 3;
158 }
159
160 // Unlike `eve run`, the MCP host does not execute load.nut. Start the
161 // same project watcher here and route editor/host scripts through the
162 // EditorHost while other assets keep using the shared asset reloaders.
163 auto* hot = eve::ModuleManager::getInstance<eve::filesystem::HotReload>("HotReload");
164 const int watchCount = projectFs && hot ? hot->watchTree(".") : 0;
165 host->setHotReloadWatchCount(watchCount);
166 if (std::filesystem::is_regular_file(std::filesystem::path(gameDir) / "mcp.nut")) {
167 const std::string initial = host->reloadResource("mcp.nut");
168 if (initial.rfind("error:", 0) == 0)
169 std::cerr << "MCP host script load failed: " << initial << std::endl;
170 }
171 std::cerr << "MCP hot-reload: watching " << watchCount << " path(s)" << std::endl;
172
173 auto& mcp = dt.mcp();
174 mcp.setGameRoot(gameDir);
175
176 if (port > 0) {
177 const int bound = dt.startMcp(static_cast<uint16_t>(port));
178 if (bound <= 0) {
179 std::cerr << "Failed to start MCP server on port " << port << std::endl;
180 return 3;
181 }
182 std::cerr << "MCP listening on 127.0.0.1:" << bound
183 << " (newline JSON-RPC; use tools/eve-mcp for Cursor stdio)" << std::endl;
184 } else {
185 if (!mcp.listenStdio()) {
186 std::cerr << "Failed to start stdio MCP transport" << std::endl;
187 return 3;
188 }
189 std::cerr << "MCP stdio transport ready (headless editor host; project=" << gameDir
190 << ")" << std::endl;
191 }
192
193 std::map<std::string, std::chrono::steady_clock::time_point> pendingReloads;
194 constexpr auto reloadDebounce = std::chrono::milliseconds(100);
195 while (!host->exitRequested()) {
196 dt.poll(); // drains MCP requests on the main thread
197 if (projectFs) {
198 while (true) {
199 const std::string kind = projectFs->pollWatch();
200 if (kind.empty()) break;
201 if (kind != "modified" && kind != "added" && kind != "movedTo") continue;
202 std::string changed = projectFs->getLastWatchPath();
203 changed = eve::filesystem::HotReload::normalizePath(std::move(changed));
204 if (!changed.empty()) pendingReloads[changed] = std::chrono::steady_clock::now();
205 }
206 }
207 const auto now = std::chrono::steady_clock::now();
208 for (auto it = pendingReloads.begin(); it != pendingReloads.end();) {
209 if (now - it->second < reloadDebounce) {
210 ++it;
211 continue;
212 }
213 const std::string changed = it->first;
214 if (isEditorHostResource(changed)) {
215 const std::string result = host->reloadResource(changed);
216 std::cerr << "MCP hot-reload: " << changed << ": " << result << std::endl;
217 } else if (hot && hot->tryReload(changed)) {
218 std::cerr << "MCP hot-reload asset: " << changed << std::endl;
219 }
220 it = pendingReloads.erase(it);
221 }
222 host->frame();
223 if (mcp.transport() == eve::dev::McpServer::Transport::Stdio &&
224 mcp.stdinClosed()) {
225 std::cerr << "MCP stdio closed; host exiting" << std::endl;
226 break;
227 }
228 std::this_thread::sleep_for(std::chrono::milliseconds(2));
229 }
230
231 host->stop();
232 dt.detach();
233 return 0;
234 } catch (const std::exception& e) {
235 std::cerr << "eve mcp failed: " << e.what() << std::endl;
236 return 3;
237 }
238#endif
239}
240
241} // namespace eve::cmd
double value
const std::string & s
struct SQVM * HSQUIRRELVM
int watchCount
TokenKind kind
std::string path
Definition PlayHost.cpp:110
UIHostHandle host
static void requireAll()
Instantiates every registered module through its factory.
Definition Module.cpp:200
EVENGINE_API_FOUNDATION public API.
Definition Runtime.h:189
命令行模块(eve.cmd):run / build / package / test / zip / dev-server 等子命令入口。
Definition cmdline.h:33
static std::string get_remaining(CLI::App *sub, std::string default_path=".")
子命令剩余位置参数。
Definition cmdline.cpp:80
int McpHost(std::string path, int port)
无头 MCP 宿主:stdio(默认)或 TCP,供 AI 代理驱动。
Definition Mcp.cpp:83
static DevTool & instance()
Definition DevTool.cpp:132
EVENGINE_API_FOUNDATION public API.
Definition Filesystem.h:43
virtual bool setSource(std::string source)=0
Sets the path to the game source. This can only be set once.
virtual std::string pollWatch()=0
Pop next watch event kind: "added"|"removed"|"modified"|"movedFrom"|"movedTo". Empty string if queue ...
virtual std::string getLastWatchPath() const =0
static std::string normalizePath(std::string path)
Normalize path.
#define CMD_REG(name)
Definition cmdline.h:101
void installScriptFileApi(ssq::VM &vm)
用 PhysFS 版本覆盖 Squirrel 的 file / dofile / loadfile 全局, 使脚本与资源从已挂载的游戏源(真实目录或内存挂载的 .eve 归档)解析; PhysFS 中不...
Definition FileApi.cpp:213
One eve <subcommand> handler: CLI setup + argument parsing.
Definition cmdline.h:23
int parse(CLI::App &app, Cmdline &cmd) override
Parses arguments and runs the command; returns the exit code.
Definition Mcp.cpp:70
void setup(CLI::App &app, std::shared_ptr< CLI::Formatter > formatter) override
Registers options on the CLI11 sub-app.
Definition Mcp.cpp:61
std::string root_path
Definition Mcp.cpp:58