载入中...
搜索中...
未找到
McpRuntimeTools.cpp
浏览该文件的文档.
2
8
9#include "common/Capability.h"
10#include "common/CrashLog.h"
12
13#include <Poco/Dynamic/Var.h>
14#include <Poco/JSON/Array.h>
15#include <Poco/JSON/Object.h>
16
17#include <algorithm>
18#include <cstdint>
19#include <filesystem>
20#include <fstream>
21#include <sstream>
22#include <string>
23#include <vector>
24
25namespace eve::dev {
26namespace {
27
28constexpr int kDefaultConsoleRead = 200;
29constexpr int kMaxConsoleRead = 1000;
30
31constexpr int kDefaultScreenshotBytes = 2 * 1024 * 1024;
32constexpr int kMinScreenshotBytes = 1024;
33constexpr int kMaxScreenshotBytes = 32 * 1024 * 1024;
34
36const std::vector<std::string>& consoleLevels() {
37 static const std::vector<std::string> levels{"debug", "info", "warn", "error", "print", "cmd", "result", "engine"};
38 return levels;
39}
40
42const std::vector<std::string>& markerLevels() {
43 static const std::vector<std::string> levels{"debug", "info", "warn", "error"};
44 return levels;
45}
46
47bool isListed(const std::vector<std::string>& allowed, const std::string& value) {
48 return std::find(allowed.begin(), allowed.end(), value) != allowed.end();
49}
50
51std::string joined(const std::vector<std::string>& values) {
52 std::string out;
53 for (const auto& value : values) {
54 if (!out.empty()) out += '|';
55 out += value;
56 }
57 return out;
58}
59
61std::int64_t getArgSeq(Poco::JSON::Object::Ptr args, const char* key) {
62 const long long value = getArgInt64(args, key, 0);
63 return value > 0 ? static_cast<std::int64_t>(value) : 0;
64}
65
66std::string textPayload(Poco::JSON::Object::Ptr object, bool isError = false) {
67 return textContentResult(mcpStringify(Poco::Dynamic::Var(object)), isError);
68}
69
70std::string errorPayload(const std::string& message) {
71 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
72 out->set("ok", false);
73 out->set("error", message);
74 return textPayload(out, true);
75}
76
77std::string consoleRead(Poco::JSON::Object::Ptr args) {
78 const std::string level = getArgString(args, "level");
79 if (!level.empty() && !isListed(consoleLevels(), level)) {
80 return errorPayload("unknown level '" + level + "'; expected one of " + joined(consoleLevels()));
81 }
82 const int limit = std::clamp(getArgInt(args, "limit", kDefaultConsoleRead), 1, kMaxConsoleRead);
83 const std::uint64_t afterSeq = static_cast<std::uint64_t>(getArgSeq(args, "sinceSeq"));
84
85 const ConsoleSlice slice = ConsolePanel::instance().read(afterSeq, static_cast<std::size_t>(limit), level);
86
87 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
88 out->set("ok", true);
89 out->set("attached", ConsolePanel::instance().isAttached());
90 out->set("stderrCaptured", ConsolePanel::instance().isCapturingStderr());
91 out->set("firstSeq", static_cast<Poco::Int64>(slice.firstSeq));
92 // `cursor` is the resumable value for the next sinceSeq; `nextSeq` is only
93 // the seq the next appended line will get, which is not a valid cursor.
94 out->set("cursor", static_cast<Poco::Int64>(slice.cursor));
95 out->set("nextSeq", static_cast<Poco::Int64>(slice.nextSeq));
96 out->set("droppedThrough", static_cast<Poco::Int64>(slice.droppedThrough));
97 out->set("truncated", slice.truncated);
98 out->set("count", static_cast<int>(slice.lines.size()));
99 Poco::JSON::Array::Ptr lines = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
100 for (const auto& line : slice.lines) {
101 Poco::JSON::Object::Ptr item = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
102 item->set("seq", static_cast<Poco::Int64>(line.seq));
103 item->set("time", line.timestamp);
104 item->set("level", line.level);
105 item->set("text", line.text);
106 lines->add(item);
107 }
108 out->set("lines", lines);
109 return textPayload(out);
110}
111
112std::string consoleWrite(Poco::JSON::Object::Ptr args) {
113 const std::string text = getArgString(args, "text");
114 if (text.empty()) return errorPayload("missing text");
115 const std::string level = getArgString(args, "level", "info");
116 if (!isListed(markerLevels(), level)) {
117 return errorPayload("unknown level '" + level + "'; expected one of " + joined(markerLevels()));
118 }
119
120 ConsolePanel& console = ConsolePanel::instance();
121 console.addLog(level, text);
122
123 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
124 out->set("ok", true);
125 out->set("seq", static_cast<Poco::Int64>(console.nextSeq() - 1));
126 out->set("nextSeq", static_cast<Poco::Int64>(console.nextSeq()));
127 return textPayload(out);
128}
129
130std::string consoleClear() {
131 ConsolePanel& console = ConsolePanel::instance();
132 console.clear();
133
134 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
135 out->set("ok", true);
136 out->set("nextSeq", static_cast<Poco::Int64>(console.nextSeq()));
137 out->set("droppedThrough", static_cast<Poco::Int64>(console.droppedThrough()));
138 return textPayload(out);
139}
140
141// ---------------------------------------------------------------------------
142// Crash / previous-session evidence
143//
144// The server dies with the process it serves, so "why did the last run die" is
145// only answerable from the persistent log written by common/CrashLog.h.
146// ---------------------------------------------------------------------------
147
148// One window is enough to answer the question; a runaway log is never loaded
149// whole.
150constexpr std::size_t kCrashLogWindowBytes = 512 * 1024;
151
152struct CrashLogScan {
153 bool exists = false;
154 std::uint64_t bytes = 0;
155 std::uint64_t scannedBytes = 0;
156 int sessions = 0;
157 int crashes = 0;
158 std::string lastCrashAt;
159 bool hasPreviousSession = false;
162 std::vector<std::string> tail;
163};
164
166struct SessionSegment {
167 bool ended = false;
168 bool crashed = false;
169};
170
171bool contains(const std::string& haystack, const char* needle) { return haystack.find(needle) != std::string::npos; }
172
174std::string readLogWindow(const std::string& path, std::uint64_t* totalBytes) {
175 std::ifstream in(path, std::ios::binary);
176 if (!in) return {};
177 in.seekg(0, std::ios::end);
178 const std::streamoff size = in.tellg();
179 if (size < 0) return {};
180 *totalBytes = static_cast<std::uint64_t>(size);
181
182 const std::streamoff window = static_cast<std::streamoff>(kCrashLogWindowBytes);
183 const std::streamoff start = size > window ? size - window : 0;
184 in.seekg(start);
185 std::ostringstream buffer;
186 buffer << in.rdbuf();
187 std::string text = buffer.str();
188 if (start > 0) {
189 const auto newline = text.find('\n');
190 text.erase(0, newline == std::string::npos ? text.size() : newline + 1);
191 }
192 return text;
193}
194
195CrashLogScan scanCrashLog(int tailLines) {
196 CrashLogScan scan;
197 const std::string path = eve::crashLogPath();
198 std::error_code ec;
199 if (path.empty() || !std::filesystem::exists(path, ec)) return scan;
200 scan.exists = true;
201
202 const std::string window = readLogWindow(path, &scan.bytes);
203 scan.scannedBytes = window.size();
204
205 std::istringstream stream(window);
206 std::string line;
207 std::vector<std::string> all;
208 std::vector<SessionSegment> segments;
209 while (std::getline(stream, line)) {
210 if (!line.empty() && line.back() == '\r') line.pop_back();
211 all.push_back(line);
212 if (contains(line, eve::kSessionStartMarker)) {
213 ++scan.sessions;
214 segments.emplace_back();
215 continue;
216 }
217 if (segments.empty()) continue; // pre-session noise in a truncated window
218 if (contains(line, eve::kSessionEndMarker)) {
219 segments.back().ended = true;
220 continue;
221 }
222 if (contains(line, "] crash | ")) {
223 ++scan.crashes;
224 segments.back().crashed = true;
225 const auto open = line.find('[');
226 const auto close = line.find(']');
227 if (open != std::string::npos && close != std::string::npos && close > open + 1)
228 scan.lastCrashAt = line.substr(open + 1, close - open - 1);
229 }
230 }
231
232 // The newest start marker belongs to the session that is reading this log, so
233 // the interesting verdict is about the one before it: "did the run that came
234 // before me finish, or die?".
235 if (segments.size() >= 2) {
236 const SessionSegment& previous = segments[segments.size() - 2];
237 scan.hasPreviousSession = true;
238 scan.previousSessionEnded = previous.ended;
239 scan.previousSessionCrashed = previous.crashed;
240 }
241
242 const std::size_t keep = tailLines > 0 ? static_cast<std::size_t>(tailLines) : 0;
243 const std::size_t from = all.size() > keep ? all.size() - keep : 0;
244 scan.tail.assign(all.begin() + static_cast<std::ptrdiff_t>(from), all.end());
245 return scan;
246}
247
248Poco::JSON::Object::Ptr crashLogScanObject(const CrashLogScan& scan) {
249 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
250 out->set("path", eve::crashLogPath());
251 out->set("exists", scan.exists);
252 out->set("bytes", static_cast<Poco::Int64>(scan.bytes));
253 out->set("scannedBytes", static_cast<Poco::Int64>(scan.scannedBytes));
254 out->set("sessions", scan.sessions);
255 out->set("crashes", scan.crashes);
256 if (!scan.lastCrashAt.empty()) out->set("lastCrashAt", scan.lastCrashAt);
257 if (scan.hasPreviousSession) {
258 out->set("previousSessionEnded", scan.previousSessionEnded);
259 out->set("previousSessionCrashed", scan.previousSessionCrashed);
260 }
261 return out;
262}
263
264std::string crashReport(Poco::JSON::Object::Ptr args) {
265 const int lines = std::clamp(getArgInt(args, "lines", 200), 1, 2000);
266 CrashLogScan scan = scanCrashLog(lines);
267
268 Poco::JSON::Object::Ptr out = crashLogScanObject(scan);
269 out->set("ok", true);
270 Poco::JSON::Array::Ptr tail = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
271 for (const auto& line : scan.tail) tail->add(line);
272 out->set("tail", tail);
273 if (!scan.exists)
274 out->set("message",
275 "no crash/error log yet; common/CrashLog.h opens it at process start (EVE_LOG_DIR overrides the "
276 "directory)");
277 return textPayload(out);
278}
279
280std::string screenshotImage(Poco::JSON::Object::Ptr args) {
281 auto* capture = eve::cap::query<eve::IRenderCapture>();
282 if (!capture) return errorPayload("graphics module not available");
283
284 const int budget =
285 std::clamp(getArgInt(args, "maxBytes", kDefaultScreenshotBytes), kMinScreenshotBytes, kMaxScreenshotBytes);
286
287 static const std::string kDataUrlPrefix = "data:image/png;base64,";
288 const std::string dataUrl = capture->capturePngDataUrl();
289 if (dataUrl.size() <= kDataUrlPrefix.size() || dataUrl.compare(0, kDataUrlPrefix.size(), kDataUrlPrefix) != 0) {
290 // Enabling readback and reading it back cannot happen inside one
291 // presented frame: the swapchain copy is recorded by the next present.
292 // This is a retryable state, not a failed call, so isError stays false.
293 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
294 out->set("ok", false);
295 out->set("retryable", true);
296 out->set("reason", "no-presented-frame");
297 out->set("readbackEnabled", capture->status().readbackEnabled);
298 out->set("message",
299 "readback was just enabled or no frame has been presented yet; wait for one rendered "
300 "frame and call again (eve_render_status.readbackEnabled flips to true)");
301 return textPayload(out);
302 }
303
304 const std::string payload = dataUrl.substr(kDataUrlPrefix.size());
305 if (static_cast<int>(payload.size()) > budget) {
306 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
307 out->set("ok", false);
308 out->set("retryable", false);
309 out->set("reason", "image-too-large");
310 out->set("base64Bytes", static_cast<Poco::Int64>(payload.size()));
311 out->set("maxBytes", budget);
312 out->set("message", "raise maxBytes or use eve_screenshot with a path");
313 return textPayload(out);
314 }
315
316 const eve::RenderStatusInfo status = capture->status();
317 Poco::JSON::Object::Ptr caption = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
318 caption->set("ok", true);
319 caption->set("mimeType", "image/png");
320 caption->set("width", status.width);
321 caption->set("height", status.height);
322 caption->set("base64Bytes", static_cast<Poco::Int64>(payload.size()));
323 caption->set("backend", status.backend);
324 return imageContentResult(payload, "image/png", mcpStringify(Poco::Dynamic::Var(caption)));
325}
326
327std::string softRestart(Poco::JSON::Object::Ptr args) {
329 if (!vm) return errorPayload("no VM attached; start the game with eve run --mcp-port ...");
330
331 SoftRestartRequest request;
332 request.reloadScripts = getArgBool(args, "reloadScripts", true);
333 if (args && args->has("args")) {
334 try {
335 request.args = args->getObject("args");
336 } catch (...) {
337 return errorPayload("args must be a JSON object");
338 }
339 }
340
341 auto result = executeSoftRestart(vm, std::move(request));
342 if (!result) {
343 const auto& status = result.status();
344 const auto* diag = status.primaryDiagnostic();
345 return errorPayload(diag && !diag->message().empty() ? diag->message()
347 }
348
349 const SoftRestartReport& report = result.value();
350 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
351 out->set("ok", true);
352 out->set("keptResources", true);
353 out->set("resourceCountBefore", static_cast<Poco::Int64>(report.resourceCountBefore));
354 out->set("resourceCountAfter", static_cast<Poco::Int64>(report.resourceCountAfter));
355 out->set("scriptsReloaded", report.scriptsReloaded);
356 out->set("initCalled", report.initCalled);
357 out->set("args", report.args ? Poco::Dynamic::Var(report.args) : Poco::Dynamic::Var(Poco::JSON::Object::Ptr(new Poco::JSON::Object())));
358 return textPayload(out);
359}
360
361} // namespace
362
363bool isMcpRuntimeTool(std::string_view name) {
364 return name == "eve_console_read" || name == "eve_console_write" || name == "eve_console_clear" ||
365 name == "eve_screenshot_image" || name == "eve_crash_report" || name == "eve_restart";
366}
367
368std::string callMcpRuntimeTool(std::string_view name, Poco::JSON::Object::Ptr args) {
369 if (name == "eve_console_read") return consoleRead(args);
370 if (name == "eve_console_write") return consoleWrite(args);
371 if (name == "eve_console_clear") return consoleClear();
372 if (name == "eve_screenshot_image") return screenshotImage(args);
373 if (name == "eve_crash_report") return crashReport(args);
374 if (name == "eve_restart") return softRestart(args);
375 return errorPayload("unknown runtime tool '" + std::string(name) + "'");
376}
377
378Poco::JSON::Object::Ptr crashLogSummary() {
379 // Cheap by design: `eve_status` must not read the whole log. Counts and the
380 // tail come from eve_crash_report.
381 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
382 const std::string path = eve::crashLogPath();
383 out->set("path", path);
384 std::error_code ec;
385 const bool exists = !path.empty() && std::filesystem::exists(path, ec);
386 out->set("exists", exists);
387 Poco::Int64 bytes = 0;
388 if (exists) {
389 const auto size = std::filesystem::file_size(path, ec);
390 if (!ec) bytes = static_cast<Poco::Int64>(size);
391 }
392 out->set("bytes", bytes);
393 return out;
394}
395
396std::string_view mcpRuntimeToolSchemas() {
397 return R"json({"name":"eve_console_read","description":"Read retained runtime console lines: Squirrel print/error, engine stderr (level engine), eve.dev.console.* and agent markers. Lines carry a monotonic seq; pass the returned cursor back as sinceSeq for an incremental read (sinceSeq=0 reads the newest lines). truncated=true means lines were dropped before sinceSeq.","inputSchema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":1000},"sinceSeq":{"type":"integer","minimum":0},"level":{"type":"string","enum":["debug","info","warn","error","print","cmd","result","engine"]}}}},{"name":"eve_console_write","description":"Append one agent marker line to the runtime console so it shares the game's ordered diagnosis stream.","inputSchema":{"type":"object","properties":{"text":{"type":"string"},"level":{"type":"string","enum":["debug","info","warn","error"]}},"required":["text"]}},{"name":"eve_console_clear","description":"Drop retained console lines. The sequence cursor keeps advancing, so an existing sinceSeq never re-reads or stalls.","inputSchema":{"type":"object","properties":{}}},{"name":"eve_screenshot_image","description":"Capture the presented frame and return it as an MCP image content item, so a client without filesystem access can look at the game. After readback is enabled the first call reports ok=false with retryable=true; wait one rendered frame and call again.","inputSchema":{"type":"object","properties":{"maxBytes":{"type":"integer","minimum":1024,"maximum":33554432,"description":"base64 payload budget (default 2097152)"}}}},{"name":"eve_crash_report","description":"Explain why a previous run ended: reads the persistent eve.log written by common/CrashLog.h and returns session/crash counts (window-scoped), the last crash timestamp, whether the session before the current one ended cleanly or crashed, and a bounded tail. Use it after reconnecting to a restarted game, since the MCP server dies with the process it served.","inputSchema":{"type":"object","properties":{"lines":{"type":"integer","minimum":1,"maximum":2000,"description":"tail lines to return (default 200)"}}}},{"name":"eve_restart","description":"Soft-restart the running game without unloading cached resources or killing the process (keeps Vulkan/MCP/ResourceManager). Optional args become eve.restartArgs for eve_init / eve_before_restart / eve_after_restart. Prefer this over relaunching eve when iterating on gameplay.","inputSchema":{"type":"object","properties":{"args":{"type":"object","description":"Game restart parameters (seed, scene, level, ...)"},"reloadScripts":{"type":"boolean","description":"Re-dofile tracked scripts and clear persist roots before eve_init (default true). When false, only reset native providers and call eve_restart(args) if present else eve_init()."}}}})json";
398}
399
400} // namespace eve::dev
double value
Value::Object payload
Duration start
std::string from
struct SQVM * HSQUIRRELVM
Persistent, thread-safe record of engine crash and problem events.
HSQUIRRELVM vm
Definition ECS.cpp:20
std::map< std::string, Var > values
std::string message
DiagnosticCode code
const GltfImportRequest & request
std::uint32_t key
wgpu::PopErrorScopeStatus status
std::string text
bool previousSessionCrashed
std::uint64_t scannedBytes
std::uint64_t bytes
int sessions
bool exists
bool hasPreviousSession
bool crashed
int crashes
std::string lastCrashAt
bool previousSessionEnded
std::vector< std::string > tail
bool ended
std::string name
int level
graphics::Canvas * previous
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
std::string path
Definition PlayHost.cpp:110
std::string string
Soft-restart a live game without unloading the resource cache.
int limit
Definition TreeMesh.cpp:164
float size
Definition TreeMesh.cpp:156
std::vector< std::string_view > lines
static ConsolePanel & instance()
ConsoleSlice read(std::uint64_t afterSeq, size_t max=64, const std::string &level={}) const
Read retained lines relative to a sequence cursor.
HSQUIRRELVM vm() const
Definition Debugger.hpp:96
static Debugger & instance()
Definition Debugger.cpp:387
bool getArgBool(Poco::JSON::Object::Ptr args, const char *key, bool def=false)
Boolean argument, or def when absent or not convertible.
Definition McpArgs.hpp:65
bool isMcpRuntimeTool(std::string_view name)
Whether name belongs to the MCP runtime console / frame-capture surface.
std::string mcpStringify(const Poco::Dynamic::Var &value)
Compact single-line JSON text for a newline-framed MCP payload.
Definition McpJson.hpp:23
std::string textContentResult(const std::string &text, bool isError=false)
MCP tools/call result object carrying one text content item.
Definition McpJson.hpp:59
std::string imageContentResult(const std::string &base64Png, const std::string &mimeType, const std::string &caption)
MCP tools/call result object carrying one image content item.
Definition McpJson.hpp:77
Result< SoftRestartReport > executeSoftRestart(HSQUIRRELVM vm, SoftRestartRequest request)
Soft-restart the game attached to vm.
Poco::JSON::Object::Ptr crashLogSummary()
Summary of the persistent crash/error log (eve.log).
std::string callMcpRuntimeTool(std::string_view name, Poco::JSON::Object::Ptr args)
Execute one runtime tool.
long long getArgInt64(Poco::JSON::Object::Ptr args, const char *key, long long def=0)
64-bit integer argument (sequence numbers, handles), or def.
Definition McpArgs.hpp:45
std::string_view mcpRuntimeToolSchemas()
Comma-separated MCP tool schema fragments without surrounding array brackets.
int getArgInt(Poco::JSON::Object::Ptr args, const char *key, int def=0)
Integer argument, or def when absent or not convertible.
Definition McpArgs.hpp:35
std::string getArgString(Poco::JSON::Object::Ptr args, const char *key, const std::string &def={})
String argument, or def when absent or not convertible.
Definition McpArgs.hpp:25
WidgetDesc window(std::string title, std::vector< WidgetDesc > children, std::string id)
Top-level window widget with a title bar.
Definition Widget.cpp:292
constexpr const char * kSessionStartMarker
Marker text that opens a session block in eve.log.
Definition CrashLog.h:39
constexpr const char * kSessionEndMarker
Marker text that closes a session block (followed by the exit code). @ownership Borrowed pointer to a...
Definition CrashLog.h:46
constexpr std::string_view statusCodeName(StatusCode code) noexcept
Return the stable protocol spelling of a status code.
Definition Status.h:45
std::string crashLogPath()
Path of the open crash/error log file.
Definition CrashLog.cpp:89
Render surface / frame state snapshot (value type).