13#include <Poco/Dynamic/Var.h>
14#include <Poco/JSON/Array.h>
15#include <Poco/JSON/Object.h>
28constexpr int kDefaultConsoleRead = 200;
29constexpr int kMaxConsoleRead = 1000;
31constexpr int kDefaultScreenshotBytes = 2 * 1024 * 1024;
32constexpr int kMinScreenshotBytes = 1024;
33constexpr int kMaxScreenshotBytes = 32 * 1024 * 1024;
36const std::vector<std::string>& consoleLevels() {
37 static const std::vector<std::string> levels{
"debug",
"info",
"warn",
"error",
"print",
"cmd",
"result",
"engine"};
42const std::vector<std::string>& markerLevels() {
43 static const std::vector<std::string> levels{
"debug",
"info",
"warn",
"error"};
47bool isListed(
const std::vector<std::string>& allowed,
const std::string&
value) {
48 return std::find(allowed.begin(), allowed.end(),
value) != allowed.end();
51std::string joined(
const std::vector<std::string>&
values) {
54 if (!out.empty()) out +=
'|';
61std::int64_t getArgSeq(Poco::JSON::Object::Ptr args,
const char*
key) {
63 return value > 0 ?
static_cast<std::int64_t
>(
value) : 0;
66std::string textPayload(Poco::JSON::Object::Ptr
object,
bool isError =
false) {
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);
74 return textPayload(out,
true);
77std::string consoleRead(Poco::JSON::Object::Ptr args) {
79 if (!
level.empty() && !isListed(consoleLevels(),
level)) {
80 return errorPayload(
"unknown level '" +
level +
"'; expected one of " + joined(consoleLevels()));
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"));
87 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
91 out->set(
"firstSeq",
static_cast<Poco::Int64
>(slice.firstSeq));
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());
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);
108 out->set(
"lines",
lines);
109 return textPayload(out);
112std::string consoleWrite(Poco::JSON::Object::Ptr args) {
114 if (
text.empty())
return errorPayload(
"missing text");
116 if (!isListed(markerLevels(),
level)) {
117 return errorPayload(
"unknown level '" +
level +
"'; expected one of " + joined(markerLevels()));
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);
130std::string consoleClear() {
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);
150constexpr std::size_t kCrashLogWindowBytes = 512 * 1024;
166struct SessionSegment {
171bool contains(
const std::string& haystack,
const char* needle) {
return haystack.find(needle) != std::string::npos; }
174std::string readLogWindow(
const std::string&
path, std::uint64_t* totalBytes) {
175 std::ifstream in(
path, std::ios::binary);
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);
182 const std::streamoff
window =
static_cast<std::streamoff
>(kCrashLogWindowBytes);
185 std::ostringstream
buffer;
189 const auto newline =
text.find(
'\n');
190 text.erase(0, newline == std::string::npos ?
text.size() : newline + 1);
195CrashLogScan scanCrashLog(
int tailLines) {
199 if (
path.empty() || !std::filesystem::exists(
path, ec))
return scan;
202 const std::string
window = readLogWindow(
path, &scan.bytes);
203 scan.scannedBytes =
window.size();
205 std::istringstream stream(window);
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();
214 segments.emplace_back();
217 if (segments.empty())
continue;
219 segments.back().ended =
true;
222 if (contains(
line,
"] crash | ")) {
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);
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;
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());
248Poco::JSON::Object::Ptr crashLogScanObject(
const CrashLogScan& scan) {
249 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
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);
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);
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());
272 out->set(
"tail",
tail);
275 "no crash/error log yet; common/CrashLog.h opens it at process start (EVE_LOG_DIR overrides the "
277 return textPayload(out);
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");
285 std::clamp(
getArgInt(args,
"maxBytes", kDefaultScreenshotBytes), kMinScreenshotBytes, kMaxScreenshotBytes);
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) {
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);
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);
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);
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);
327std::string softRestart(Poco::JSON::Object::Ptr args) {
329 if (!
vm)
return errorPayload(
"no VM attached; start the game with eve run --mcp-port ...");
333 if (args && args->has(
"args")) {
335 request.args = args->getObject(
"args");
337 return errorPayload(
"args must be a JSON object");
343 const auto&
status = result.status();
344 const auto* diag =
status.primaryDiagnostic();
345 return errorPayload(diag && !diag->message().empty() ? diag->message()
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);
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";
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) +
"'");
381 Poco::JSON::Object::Ptr out = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
383 out->set(
"path",
path);
385 const bool exists = !
path.empty() && std::filesystem::exists(
path, ec);
386 out->set(
"exists",
exists);
387 Poco::Int64
bytes = 0;
389 const auto size = std::filesystem::file_size(
path, ec);
390 if (!ec)
bytes =
static_cast<Poco::Int64
>(
size);
392 out->set(
"bytes",
bytes);
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";
struct SQVM * HSQUIRRELVM
Persistent, thread-safe record of engine crash and problem events.
std::map< std::string, Var > values
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
graphics::Canvas * previous
std::unique_ptr< gpgpu::GpuBuffer > buffer
Soft-restart a live game without unloading the resource cache.
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.
static Debugger & instance()
bool getArgBool(Poco::JSON::Object::Ptr args, const char *key, bool def=false)
Boolean argument, or def when absent or not convertible.
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.
std::string textContentResult(const std::string &text, bool isError=false)
MCP tools/call result object carrying one text content item.
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.
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.
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.
std::string getArgString(Poco::JSON::Object::Ptr args, const char *key, const std::string &def={})
String argument, or def when absent or not convertible.
WidgetDesc window(std::string title, std::vector< WidgetDesc > children, std::string id)
Top-level window widget with a title bar.
constexpr const char * kSessionStartMarker
Marker text that opens a session block in eve.log.
constexpr const char * kSessionEndMarker
Marker text that closes a session block (followed by the exit code). @ownership Borrowed pointer to a...
constexpr std::string_view statusCodeName(StatusCode code) noexcept
Return the stable protocol spelling of a status code.
std::string crashLogPath()
Path of the open crash/error log file.
Render surface / frame state snapshot (value type).