载入中...
搜索中...
未找到
McpServer.cpp
浏览该文件的文档.
2
7
11#include "devtools/Debugger.hpp"
12#include "devtools/DevTool.hpp"
13#include "devtools/McpArgs.hpp"
16#include "devtools/McpJson.hpp"
20#include "devtools/PlayHost.h"
23#include "devtools/Snapshot.hpp"
24
25#include "scripts.h"
26
27#include "common/AudioQuery.h"
29#include "common/Capability.h"
30#include "common/EditorHost.h"
31#include "common/Module.h"
33#include "common/PhysicsQuery.h"
35#include "common/ProcgenQuery.h"
37#include "common/SceneQuery.h"
39#include "common/ScriptError.h"
40#include "common/UIAutomation.h"
41
42#include <Poco/Dynamic/Var.h>
43#include <Poco/Exception.h>
44#include <Poco/JSON/Array.h>
45#include <Poco/JSON/Object.h>
46#include <Poco/JSON/Parser.h>
47#include <Poco/JSON/Stringifier.h>
48#include <Poco/Net/NetException.h>
49#include <Poco/Net/ServerSocket.h>
50#include <Poco/Net/SocketAddress.h>
51#include <Poco/Net/StreamSocket.h>
52#include <Poco/Timespan.h>
53
54#include <squirrel.h>
55#include <glm/glm.hpp>
56
57#include <cmath>
58#include <cstdio>
59#include <cstdlib>
60#include <filesystem>
61#include <fstream>
62#include <sstream>
63#include <vector>
64
65namespace eve::dev {
66namespace {
67
68constexpr const SQChar* kMcpSnippetSource = _SC("memory://mcp/snippet");
69constexpr const SQChar* kMcpSceneDirectorSource = _SC("memory://mcp/scene-director");
70
71std::string idToJson(const Poco::Dynamic::Var& id) {
72 if (id.isEmpty()) return "null";
73 try {
74 if (id.isInteger() || id.isNumeric()) return std::to_string(id.convert<Poco::Int64>());
75 } catch (...) {
76 }
77 try {
78 return std::string("\"") + mcpJsonEscape(id.convert<std::string>()) + "\"";
79 } catch (...) {
80 return "null";
81 }
82}
83
84std::string makeResult(const std::string& idJson, const std::string& resultJson) {
85 return std::string("{\"jsonrpc\":\"2.0\",\"id\":") + idJson + ",\"result\":" + resultJson + "}";
86}
87
88std::string makeError(const std::string& idJson, int code, const std::string& message) {
89 return std::string("{\"jsonrpc\":\"2.0\",\"id\":") + idJson + ",\"error\":{\"code\":" + std::to_string(code) +
90 ",\"message\":\"" + mcpJsonEscape(message) + "\"}}";
91}
92
93std::string pauseReasonName(PauseReason r) {
94 switch (r) {
95 case PauseReason::Breakpoint: return "breakpoint";
96 case PauseReason::Step: return "step";
97 case PauseReason::Exception: return "exception";
98 case PauseReason::Snapshot: return "snapshot";
99 case PauseReason::PauseKey: return "pause";
100 default: return "none";
101 }
102}
103
104std::string engineStatusJson(const McpServer& mcp) {
105 auto& dbg = Debugger::instance();
106 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
107 o->set("attached", mcpDevAttached());
108 o->set("paused", dbg.isPaused());
109 o->set("pauseReason", pauseReasonName(dbg.lastPauseReason()));
110 const auto& loc = dbg.pauseLocation();
111 o->set("source", loc.source);
112 o->set("line", loc.line);
113 o->set("function", loc.function);
114 o->set("mcpPort", mcp.port());
115 o->set("mcpConnected", mcp.hasClient());
116 switch (mcp.transport()) {
117 case McpServer::Transport::Stdio: o->set("transport", "stdio"); break;
118 case McpServer::Transport::Tcp: o->set("transport", "tcp"); break;
119 default: o->set("transport", "none"); break;
120 }
121 o->set("dapPort", DebugAdapter::instance().port());
122 o->set("gameRoot", mcp.gameRoot());
123 o->set("host", eve::cap::query<eve::IEditorHost>() ? eve::cap::query<eve::IEditorHost>()->status()
124 : std::string("unavailable"));
125 o->set("callgraphEvents", static_cast<int>(mcpCallgraphEvents()));
126 o->set("ai", AiPanel::instance().statusLine());
127 // The server dies with the process it serves; point the agent at the
128 // persistent log that outlives a crash.
129 o->set("crashLog", crashLogSummary());
130 o->set("scriptTools", scriptToolCount());
131 return mcpStringify(Poco::Dynamic::Var(o));
132}
133
134glm::vec3 argVec3(Poco::JSON::Object::Ptr args, const char* key, const glm::vec3& def = {}) {
135 if (!args || !args->has(key)) return def;
136 try {
137 auto arr = args->getArray(key);
138 if (arr && arr->size() >= 3) {
139 return glm::vec3(static_cast<float>(arr->get(0).convert<double>()),
140 static_cast<float>(arr->get(1).convert<double>()),
141 static_cast<float>(arr->get(2).convert<double>()));
142 }
143 } catch (...) {
144 }
145 return def;
146}
147
148Poco::JSON::Array::Ptr vec3ToArray(const glm::vec3& v) {
149 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
150 arr->add(v.x);
151 arr->add(v.y);
152 arr->add(v.z);
153 return arr;
154}
155
156// ============================= Scene Director (AI scene-authoring) =============
157// Thin C++ dispatchers over the Squirrel `scene_director` kit (src/scripts/
158// scene_director.nut, embedded as eve::scene_director_content). Agents build /
159// inspect scenes through `eve_scene_modify` / `eve_scene_info` /
160// `eve_camera_generate` / `eve_scene_reset`; the kit owns the live Renderable3D /
161// Camera3D / lighting state.
162
163// Escape a string as a Squirrel single-line string literal (JSON escapes are
164// a subset Squirrel understands: \n \r \t \\ \" and \uXXXX).
165std::string sqStringLiteralEscape(const std::string& s) { return mcpJsonEscape(s); }
166
167// Encode a Poco JSON value as a Squirrel literal (table/array/scalar/null).
168std::string sqLiteralValue(const Poco::Dynamic::Var& v) {
169 if (v.isEmpty()) return "null";
170 if (v.isBoolean()) return v.convert<bool>() ? "true" : "false";
171 if (v.isInteger()) return std::to_string(v.convert<Poco::Int64>());
172 if (v.isNumeric()) {
173 const double d = v.convert<double>();
174 char buf[64];
175 std::snprintf(buf, sizeof(buf), "%g", d);
176 return buf;
177 }
178 if (v.isString()) return std::string("\"") + sqStringLiteralEscape(v.convert<std::string>()) + "\"";
179 if (v.isArray()) {
180 std::string out = "[";
181 try {
182 auto arr = v.extract<Poco::JSON::Array::Ptr>();
183 for (size_t i = 0; i < arr->size(); ++i) {
184 if (i) out += ",";
185 out += sqLiteralValue(arr->get(static_cast<unsigned int>(i)));
186 }
187 } catch (...) {
188 }
189 out += "]";
190 return out;
191 }
192 if (v.isStruct()) {
193 std::string out = "{";
194 bool first = true;
195 try {
196 auto obj = v.extract<Poco::JSON::Object::Ptr>();
197 for (const auto& kv : *obj) {
198 if (!first) out += ",";
199 first = false;
200 out += "\"" + sqStringLiteralEscape(kv.first) + "\"=" + sqLiteralValue(kv.second);
201 }
202 } catch (...) {
203 }
204 out += "}";
205 return out;
206 }
207 return "null";
208}
209
210std::string snippetErrorText(HSQUIRRELVM vm, bool compile) {
213 if (!ctx.empty()) return eve::script::formatScriptError(ctx);
214 return compile ? "compile failed" : "runtime failed";
215}
216
217// Compile + run a snippet against the live VM (no return value captured).
218bool runVmSnippet(HSQUIRRELVM vm, const std::string& source, std::string* err) {
219 const SQInteger top = sq_gettop(vm);
220 // MCP snippets are transient input, not project scripts. Compile them
221 // directly from memory so the unified project compiler cannot register a
222 // file-shaped source that hot reload may later treat as user content.
223 if (SQ_FAILED(
224 sq_compilebuffer(vm, source.c_str(), static_cast<SQInteger>(source.size()), kMcpSnippetSource, SQTrue))) {
225 sq_settop(vm, top);
226 if (err) *err = snippetErrorText(vm, true);
227 return false;
228 }
229 sq_pushroottable(vm);
230 if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
231 sq_settop(vm, top);
232 if (err) *err = snippetErrorText(vm, false);
233 return false;
234 }
235 sq_settop(vm, top);
236 return true;
237}
238
239// Serialize a Squirrel value (at stack idx) to compact JSON.
240// sqValueToJson lives in devtools/McpSquirrelJson.hpp so the script tool
241// registry serializes handler results exactly the same way.
242
245std::string callSceneDirectorReturn(HSQUIRRELVM vm, const std::string& snippet, std::string* err) {
246 const SQInteger top = sq_gettop(vm);
247 if (SQ_FAILED(sq_compilebuffer(vm, snippet.c_str(), static_cast<SQInteger>(snippet.size()), kMcpSceneDirectorSource,
248 SQTrue))) {
249 sq_settop(vm, top);
250 if (err) *err = snippetErrorText(vm, true);
251 return {};
252 }
253 sq_pushroottable(vm);
254 if (SQ_FAILED(sq_call(vm, 1, SQTrue, SQTrue))) {
255 sq_settop(vm, top);
256 std::string text = snippetErrorText(vm, false);
257 if (err)
258 *err = text == "runtime failed" ? "runtime failed (is the scene_director kit installed?)" : std::move(text);
259 return {};
260 }
261 std::string json = sqValueToJson(vm, -1);
262 sq_settop(vm, top);
263 return json;
264}
265
266// Install the scene_director kit into the live VM (idempotent).
267bool ensureSceneDirectorInstalled(HSQUIRRELVM vm, std::string* err) {
268 const std::string check = "return (\"scene_director\" in getroottable()) ? (scene_director != null) : false;";
269 const std::string out = callSceneDirectorReturn(vm, check, nullptr);
270 if (!out.empty() && out.find("true") != std::string::npos) return true;
271 const char* kit = eve::scene_director_content;
272 if (!kit || !*kit) {
273 if (err) *err = "scene_director.nut not embedded (rebuild EVScripts)";
274 return false;
275 }
276 return runVmSnippet(vm, kit, err);
277}
278
279std::string sceneDirectorToolError(const std::string& name, const std::string& err) {
280 return "error: " + name + ": " + (err.empty() ? "unknown" : err);
281}
282
283std::string renderStatusText(eve::IRenderCapture* cap) {
284 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
285 if (!cap) {
286 o->set("error", "Graphics module not available");
287 return mcpStringify(Poco::Dynamic::Var(o));
288 }
289 const eve::RenderStatusInfo s = cap->status();
290 o->set("width", s.width);
291 o->set("height", s.height);
292 o->set("pixelWidth", s.pixelWidth);
293 o->set("pixelHeight", s.pixelHeight);
294 o->set("had3DThisFrame", s.had3DThisFrame);
295 o->set("readbackEnabled", s.readbackEnabled);
296 o->set("backend", s.backend);
297 o->set("renderFlowEvents", static_cast<int>(DevTool::instance().renderFlow().eventCount()));
298 return mcpStringify(Poco::Dynamic::Var(o));
299}
300
301eve::IRenderCapture* mcpCapture() { return eve::cap::query<eve::IRenderCapture>(); }
302eve::ISceneQuery* mcpScene() { return eve::cap::query<eve::ISceneQuery>(); }
303eve::IPhysicsQuery* mcpPhysics() { return eve::cap::query<eve::IPhysicsQuery>(); }
304eve::IProcgenQuery* mcpProcgen() { return eve::cap::query<eve::IProcgenQuery>(); }
305eve::IParticlesQuery* mcpParticles() { return eve::cap::query<eve::IParticlesQuery>(); }
306eve::IAudioQuery* mcpAudio() { return eve::cap::query<eve::IAudioQuery>(); }
307eve::IEditorHost* mcpHost() { return eve::cap::query<eve::IEditorHost>(); }
308eve::IUIAutomation* mcpUI() { return eve::cap::query<eve::IUIAutomation>(); }
309eve::IEditorAutomation* mcpEditor() { return eve::cap::query<eve::IEditorAutomation>(); }
310eve::IPixelWorldAutomation* mcpPixelWorld() {
311 return eve::cap::query<eve::IPixelWorldAutomation>();
312}
313
320void runPendingVisionDump() {
321 if (!RenderVision::instance().pending()) return;
322 auto* cap = mcpCapture();
323 if (cap) RenderVision::instance().pollPending(cap, renderStatusText(cap));
324}
325
326Poco::JSON::Object::Ptr renderableObservation(eve::IRenderCapture& capture, int entityId, int generation) {
327 Poco::JSON::Object::Ptr output = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
328 auto inspected = capture.inspectRenderable3D(static_cast<std::uint32_t>(entityId),
329 static_cast<std::uint32_t>(generation));
330 if (!inspected) {
331 output->set("status", "stale");
332 output->set("entityId", entityId);
333 output->set("generation", generation);
334 output->set("error", inspected.status().describe());
335 return output;
336 }
337 const auto& info = inspected.value();
338 output->set("status", "ok");
339 output->set("entityId", info.entityId);
340 output->set("generation", info.generation);
341 Poco::JSON::Array::Ptr position = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
342 position->add(info.x);
343 position->add(info.y);
344 position->add(info.z);
345 output->set("position", position);
346 Poco::JSON::Array::Ptr tint = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
347 tint->add(info.tintR);
348 tint->add(info.tintG);
349 tint->add(info.tintB);
350 tint->add(info.tintA);
351 output->set("tint", tint);
352 output->set("metallic", info.metallic);
353 output->set("roughness", info.roughness);
354 output->set("parallaxScale", info.parallaxScale);
355 output->set("visible", info.visible);
356 output->set("receiveLight", info.receiveLight);
357 output->set("castShadow", info.castShadow);
358 output->set("receiveShadow", info.receiveShadow);
359 output->set("hasPackedMaterial", info.hasPackedMaterial);
360 output->set("hasTexture", info.hasTexture);
361 output->set("hasShader", info.hasShader);
362 return output;
363}
364
365Poco::JSON::Object::Ptr sceneNodeObservation(eve::ISceneQuery& scene, const std::string& host,
366 const std::string& node) {
367 Poco::JSON::Object::Ptr output = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
368 output->set("observer", "scene-node");
369 output->set("host", host.empty() ? scene.activeHost() : host);
370 output->set("node", node);
372 const bool found = host.empty() ? scene.getNode(node, &info) : scene.getNodeIn(host, node, &info);
373 if (node.empty() || !found) {
374 output->set("status", "not-found");
375 output->set("error", "Scene node is missing from the observed host");
376 return output;
377 }
378 output->set("status", "ok");
379 output->set("id", info.id);
380 output->set("name", info.name);
381 output->set("path", info.path);
382 output->set("visible", info.visible);
383 output->set("x", info.x);
384 output->set("y", info.y);
385 output->set("z", info.z);
386 output->set("yaw", info.yaw);
387 output->set("pitch", info.pitch);
388 output->set("roll", info.roll);
389 output->set("sx", info.sx);
390 output->set("sy", info.sy);
391 output->set("sz", info.sz);
392 output->set("parent", info.parent);
393 Poco::JSON::Array::Ptr children = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
394 for (const auto& child : info.children) children->add(child);
395 output->set("children", children);
396 return output;
397}
398
399Poco::JSON::Object::Ptr runtimeObservation(Poco::JSON::Object::Ptr args, std::string* error) {
400 const std::string observer = getArgString(args, "observer", "renderable3d");
401 if (observer == "renderable3d") {
402 auto* capture = mcpCapture();
403 if (!capture) {
404 if (error) *error = "graphics module not available";
405 return nullptr;
406 }
407 const int entityId = getArgInt(args, "entityId", -1);
408 const int generation = getArgInt(args, "generation", -1);
409 if (entityId < 0 || generation < 0) {
410 if (error) *error = "entityId and generation are required";
411 return nullptr;
412 }
413 auto output = renderableObservation(*capture, entityId, generation);
414 output->set("observer", observer);
415 return output;
416 }
417 if (observer == "scene-node") {
418 auto* scene = mcpScene();
419 if (!scene) {
420 if (error) *error = "scene module not available";
421 return nullptr;
422 }
423 return sceneNodeObservation(*scene, getArgString(args, "host"), getArgString(args, "node"));
424 }
425 if (error) *error = "unsupported observer: " + observer;
426 return nullptr;
427}
428
429struct ExpectationMatch {
430 bool valid = true;
431 double maxError = 0.0;
432 std::vector<std::string> mismatches;
433 std::string error;
434};
435
436bool isJsonObject(const Poco::Dynamic::Var& value) {
437 return value.type() == typeid(Poco::JSON::Object::Ptr);
438}
439
440bool isJsonArray(const Poco::Dynamic::Var& value) {
441 return value.type() == typeid(Poco::JSON::Array::Ptr);
442}
443
444void compareExpectation(const Poco::Dynamic::Var& actual, const Poco::Dynamic::Var& expected,
445 const std::string& path, double tolerance, ExpectationMatch* match) {
446 if (!match || !match->valid) return;
447 if (isJsonObject(expected)) {
448 if (!isJsonObject(actual)) {
449 match->valid = false;
450 match->error = "Expected object at " + path;
451 return;
452 }
453 auto actualObject = actual.extract<Poco::JSON::Object::Ptr>();
454 auto expectedObject = expected.extract<Poco::JSON::Object::Ptr>();
455 for (const auto& item : *expectedObject) {
456 const std::string childPath = path.empty() ? item.first : path + "." + item.first;
457 if (!actualObject->has(item.first)) {
458 match->valid = false;
459 match->error = "Observed value has no field " + childPath;
460 return;
461 }
462 compareExpectation(actualObject->get(item.first), item.second, childPath, tolerance, match);
463 }
464 return;
465 }
466 if (isJsonArray(expected)) {
467 if (!isJsonArray(actual)) {
468 match->valid = false;
469 match->error = "Expected array at " + path;
470 return;
471 }
472 auto actualArray = actual.extract<Poco::JSON::Array::Ptr>();
473 auto expectedArray = expected.extract<Poco::JSON::Array::Ptr>();
474 if (actualArray->size() < expectedArray->size()) {
475 match->valid = false;
476 match->error = "Observed array is shorter than expectation at " + path;
477 return;
478 }
479 for (std::size_t index = 0; index < expectedArray->size(); ++index) {
480 compareExpectation(actualArray->get(static_cast<unsigned>(index)),
481 expectedArray->get(static_cast<unsigned>(index)),
482 path + "[" + std::to_string(index) + "]", tolerance, match);
483 }
484 return;
485 }
486 if (expected.isNumeric()) {
487 if (!actual.isNumeric()) {
488 match->valid = false;
489 match->error = "Expected numeric value at " + path;
490 return;
491 }
492 const double error = std::abs(actual.convert<double>() - expected.convert<double>());
493 match->maxError = std::max(match->maxError, error);
494 if (error > tolerance) match->mismatches.push_back(path);
495 return;
496 }
497 if (expected.isBoolean()) {
498 if (!actual.isBoolean()) {
499 match->valid = false;
500 match->error = "Expected boolean value at " + path;
501 return;
502 }
503 if (actual.convert<bool>() != expected.convert<bool>()) match->mismatches.push_back(path);
504 return;
505 }
506 if (expected.isString()) {
507 if (!actual.isString()) {
508 match->valid = false;
509 match->error = "Expected string value at " + path;
510 return;
511 }
512 if (actual.convert<std::string>() != expected.convert<std::string>()) match->mismatches.push_back(path);
513 return;
514 }
515 if (expected.isEmpty()) {
516 if (!actual.isEmpty()) match->mismatches.push_back(path);
517 return;
518 }
519 match->valid = false;
520 match->error = "Unsupported expectation value at " + path;
521}
522
523Poco::JSON::Object::Ptr expectationValue(const ExpectationMatch& match) {
524 Poco::JSON::Object::Ptr output = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
525 output->set("valid", match.valid);
526 output->set("converged", match.valid && match.mismatches.empty());
527 output->set("maxError", match.maxError);
528 Poco::JSON::Array::Ptr mismatches = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
529 for (const auto& path : match.mismatches) mismatches->add(path);
530 output->set("mismatches", mismatches);
531 if (!match.error.empty()) output->set("error", match.error);
532 return output;
533}
534
535Poco::JSON::Object::Ptr observationEvent(Poco::JSON::Object::Ptr descriptor,
536 Poco::JSON::Object::Ptr observation,
537 std::string* error) {
538 Poco::JSON::Object::Ptr event = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
539 event->set("observation", observation);
540 if (!observation || observation->optValue<std::string>("status", "unavailable") != "ok") {
541 event->set("status", "observation-unavailable");
542 event->set("converged", false);
543 return event;
544 }
545 if (!descriptor || !descriptor->has("expect")) return event;
546
547 double tolerance = 0.001;
548 if (descriptor->has("tolerance")) {
549 try {
550 tolerance = descriptor->get("tolerance").convert<double>();
551 } catch (...) {
552 tolerance = -1.0;
553 }
554 }
555 if (!std::isfinite(tolerance) || tolerance < 0.0) {
556 if (error) *error = "tolerance must be a finite non-negative number";
557 return nullptr;
558 }
559 const Poco::Dynamic::Var expected = descriptor->get("expect");
560 if (!isJsonObject(expected)) {
561 if (error) *error = "expect must be a JSON object";
562 return nullptr;
563 }
564 ExpectationMatch match;
565 compareExpectation(Poco::Dynamic::Var(observation), expected, "", tolerance, &match);
566 if (!match.valid) {
567 if (error) *error = match.error;
568 return nullptr;
569 }
570 event->set("expectation", expectationValue(match));
571 event->set("converged", match.mismatches.empty());
572 event->set("maxError", match.maxError);
573 return event;
574}
575
576std::string callTool(McpServer& mcp, const std::string& name, Poco::JSON::Object::Ptr args) {
578
579 auto& dbg = Debugger::instance();
580 auto& dap = DebugAdapter::instance();
581
582 if (name == "eve_status") return engineStatusJson(mcp);
583
584 if (name == "eve_api_search") {
585 const std::string query = getArgString(args, "query");
586 const std::string module = getArgString(args, "module");
587 const std::string scriptClass = getArgString(args, "class");
588 int limit = getArgInt(args, "limit", 20);
589 if (limit < 0) limit = 0;
590 const auto hits =
591 eve::script::generatedBindingContracts().search(query, module, scriptClass, static_cast<size_t>(limit));
592 std::ostringstream json;
593 json << "{\"ok\":true,\"schema\":\"eve.binding-api\",\"schemaVersion\":1,\"query\":\"" << mcpJsonEscape(query)
594 << "\",\"module\":\"" << mcpJsonEscape(module) << "\",\"class\":\"" << mcpJsonEscape(scriptClass)
595 << "\",\"count\":" << hits.size() << ",\"matches\":[";
596 for (size_t i = 0; i < hits.size(); ++i) {
597 if (i != 0) json << ',';
598 json << "{\"score\":" << hits[i].score << ",\"contract\":" << hits[i].contract.toJson() << '}';
599 }
600 json << "]}";
601 return json.str();
602 }
603
604 if (name == "eve_api_get") {
605 const std::string key = getArgString(args, "key");
606 const std::string scriptClass = getArgString(args, "class");
607 const std::string method = getArgString(args, "method");
608 const auto& registry = eve::script::generatedBindingContracts();
609 const eve::script::BindingContract* contract = nullptr;
610 if (!key.empty()) {
611 contract = registry.find(key);
612 } else if (!scriptClass.empty() && !method.empty()) {
613 contract = registry.findMethod(scriptClass, method);
614 } else if (!method.empty()) {
615 contract = registry.findMethod(method);
616 } else {
617 return "{\"ok\":false,\"error\":\"missing key or method\"}";
618 }
619 if (!contract) return "{\"ok\":false,\"error\":\"binding not found or method name is ambiguous\"}";
620 return std::string("{\"ok\":true,\"schema\":\"eve.binding-api\",\"schemaVersion\":1,\"contract\":") +
621 contract->toJson() + "}";
622 }
623
624 if (name == "eve_gameplay") {
625 if (!args || !args->has("request")) return "error: missing request";
626 std::string request;
627 try {
628 request = mcpStringify(args->get("request"));
629 } catch (const std::exception& error) {
630 return std::string("error: invalid request: ") + error.what();
631 }
633 return response ? std::move(response).takeValue()
634 : std::string("error: ") + response.status().describe();
635 }
636
637 if (name == "eve_play") {
638 if (!args || !args->has("request")) return "error: missing request";
639 std::string request;
640 try {
641 request = mcpStringify(args->get("request"));
642 } catch (const std::exception& error) {
643 return std::string("error: invalid request: ") + error.what();
644 }
645 auto response = executePlayJson(request);
646 return response ? std::move(response).takeValue()
647 : std::string("error: ") + response.status().describe();
648 }
649
650 if (name.rfind("eve_pixelworld_", 0) == 0) {
651 auto* provider = mcpPixelWorld();
652 if (!provider) return "{\"ok\":false,\"error\":\"pixelworld module not available\"}";
653 return provider->invoke(name.substr(std::string("eve_pixelworld_").size()),
654 mcpStringify(Poco::Dynamic::Var(args)));
655 }
656
657 // ============================= Game UI =============================
658 if (name == "eve_ui_tree") {
659 return mcpUI() ? mcpUI()->tree(getArgString(args, "host")) : "error: ui module not available";
660 }
661 if (name == "eve_ui_get") {
662 return mcpUI() ? mcpUI()->get(getArgString(args, "host"), getArgString(args, "widget"))
663 : "error: ui module not available";
664 }
665 if (name == "eve_ui_click") {
666 return mcpUI() ? mcpUI()->click(getArgString(args, "host"), getArgString(args, "widget"))
667 : "error: ui module not available";
668 }
669
670 // ============================= Editor command protocol =============================
671 const auto editorInvoke = [&](const char* operation) {
672 return mcpEditor() ? mcpEditor()->invoke(operation, mcpStringify(Poco::Dynamic::Var(args)))
673 : std::string("error: editor module not available");
674 };
675 if (name == "eve_editor_commands") return editorInvoke("commands");
676 if (name == "eve_editor_target_list") return editorInvoke("target-list");
677 if (name == "eve_editor_target_create") return editorInvoke("target-create");
678 if (name == "eve_editor_target_close") return editorInvoke("target-close");
679 if (name == "eve_editor_inspect") return editorInvoke("inspect");
680 if (name == "eve_editor_plan") return editorInvoke("plan");
681 if (name == "eve_editor_commit") return editorInvoke("commit");
682 if (name == "eve_editor_execute") return editorInvoke("execute");
683 if (name == "eve_editor_observe_start") {
684 auto* editor = mcpEditor();
685 if (!editor) return "error: editor module not available";
686 std::string error;
687 Poco::JSON::Object::Ptr observation = runtimeObservation(args, &error);
688 if (!observation) return "error: " + error;
689 if (observation->getValue<std::string>("status") != "ok")
690 return mcpStringify(Poco::Dynamic::Var(observation));
691 Poco::JSON::Object::Ptr event = observationEvent(args, observation, &error);
692 if (!event) {
693 Poco::JSON::Object::Ptr invalid = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
694 invalid->set("status", "invalid-expectation");
695 invalid->set("error", error);
696 invalid->set("observation", observation);
697 return mcpStringify(Poco::Dynamic::Var(invalid));
698 }
699 Poco::JSON::Object::Ptr request = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
700 request->set("descriptor", args);
701 request->set("event", event);
702 return editor->invoke("observe-start", mcpStringify(Poco::Dynamic::Var(request)));
703 }
704 if (name == "eve_editor_observe_poll") {
705 auto* editor = mcpEditor();
706 if (!editor) return "error: editor module not available";
707 try {
708 Poco::JSON::Parser describeParser;
709 Poco::Dynamic::Var described = describeParser.parse(editor->invoke(
710 "observe-describe", mcpStringify(Poco::Dynamic::Var(args))));
711 auto describedObject = described.extract<Poco::JSON::Object::Ptr>();
712 if (!describedObject || describedObject->optValue<std::string>("status", "") != "applied")
713 return mcpStringify(described);
714 auto descriptor = describedObject->getObject("descriptor");
715 std::string error;
716 Poco::JSON::Object::Ptr observation = runtimeObservation(descriptor, &error);
717 if (!observation) {
718 observation = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
719 observation->set("status", "unavailable");
720 observation->set("error", error);
721 }
722 Poco::JSON::Object::Ptr event = observationEvent(descriptor, observation, &error);
723 if (!event) {
724 event = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
725 event->set("observation", observation);
726 event->set("status", "expectation-observation-failed");
727 event->set("error", error);
728 }
729 Poco::JSON::Object::Ptr publish = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
730 publish->set("sessionId", getArgString(args, "sessionId"));
731 publish->set("event", event);
732 Poco::JSON::Parser publishParser;
733 Poco::Dynamic::Var published = publishParser.parse(editor->invoke(
734 "observe-publish", mcpStringify(Poco::Dynamic::Var(publish))));
735 auto publishedObject = published.extract<Poco::JSON::Object::Ptr>();
736 if (!publishedObject || publishedObject->optValue<std::string>("status", "") != "applied")
737 return mcpStringify(published);
738 return editor->invoke("observe-poll", mcpStringify(Poco::Dynamic::Var(args)));
739 } catch (const std::exception& exception) {
740 return std::string("error: could not poll observation session: ") + exception.what();
741 }
742 }
743 if (name == "eve_editor_observe_close") return editorInvoke("observe-close");
744 if (name == "eve_editor_execute_observe") {
745 auto* editor = mcpEditor();
746 if (!editor) return "error: editor module not available";
747 std::string error;
748 Poco::JSON::Object::Ptr before = runtimeObservation(args, &error);
749 if (!before) return "error: " + error;
750 if (before->getValue<std::string>("status") != "ok")
751 return mcpStringify(Poco::Dynamic::Var(before));
752
753 Poco::JSON::Object::Ptr output = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
754 output->set("before", before);
755 const bool hasExpectation = args && args->has("expect");
756 double tolerance = 0.001;
757 if (args && args->has("tolerance")) {
758 try {
759 tolerance = args->get("tolerance").convert<double>();
760 } catch (...) {
761 tolerance = -1.0;
762 }
763 }
764 if (!std::isfinite(tolerance) || tolerance < 0.0) {
765 output->set("status", "invalid-expectation");
766 output->set("error", "tolerance must be a finite non-negative number");
767 return mcpStringify(Poco::Dynamic::Var(output));
768 }
769 Poco::Dynamic::Var expected;
770 if (hasExpectation) {
771 expected = args->get("expect");
772 if (!isJsonObject(expected)) {
773 output->set("status", "invalid-expectation");
774 output->set("error", "expect must be a JSON object");
775 return mcpStringify(Poco::Dynamic::Var(output));
776 }
777 ExpectationMatch validation;
778 compareExpectation(Poco::Dynamic::Var(before), expected, "", tolerance, &validation);
779 if (!validation.valid) {
780 output->set("status", "invalid-expectation");
781 output->set("expectation", expectationValue(validation));
782 output->set("error", validation.error);
783 return mcpStringify(Poco::Dynamic::Var(output));
784 }
785 }
786 bool transactionAccepted = false;
787 try {
788 Poco::JSON::Parser parser;
789 Poco::Dynamic::Var transaction = parser.parse(editor->invoke("execute", mcpStringify(Poco::Dynamic::Var(args))));
790 output->set("transaction", transaction);
791 Poco::JSON::Object::Ptr transactionObject = transaction.extract<Poco::JSON::Object::Ptr>();
792 transactionAccepted = transactionObject && transactionObject->optValue<bool>("accepted", false);
793 output->set("status", transactionAccepted ? "observed" : "transaction-rejected");
794 } catch (const std::exception& exception) {
795 output->set("status", "failed");
796 output->set("error", std::string("Could not parse Editor transaction response: ") + exception.what());
797 }
798 error.clear();
799 Poco::JSON::Object::Ptr after = runtimeObservation(args, &error);
800 if (!after) {
801 after = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
802 after->set("status", "unavailable");
803 after->set("error", error);
804 }
805 output->set("after", after);
806 if (transactionAccepted && after->getValue<std::string>("status") != "ok")
807 output->set("status", "runtime-observation-failed");
808 if (hasExpectation && after->getValue<std::string>("status") == "ok") {
809 ExpectationMatch match;
810 compareExpectation(Poco::Dynamic::Var(after), expected, "", tolerance, &match);
811 output->set("expectation", expectationValue(match));
812 output->set("converged", match.valid && match.mismatches.empty());
813 output->set("maxError", match.maxError);
814 if (!match.valid) {
815 output->set("status", "expectation-observation-failed");
816 output->set("error", match.error);
817 }
818 } else if (hasExpectation) {
819 output->set("converged", false);
820 }
821 try {
822 Poco::JSON::Parser parser;
823 Poco::Dynamic::Var editorState = parser.parse(editor->invoke("inspect", mcpStringify(Poco::Dynamic::Var(args))));
824 output->set("editor", editorState);
825 } catch (const std::exception& exception) {
826 output->set("editorError", std::string("Could not parse Editor observation: ") + exception.what());
827 }
828 return mcpStringify(Poco::Dynamic::Var(output));
829 }
830 if (name == "eve_editor_cancel") return editorInvoke("cancel");
831 if (name == "eve_editor_undo") return editorInvoke("undo");
832 if (name == "eve_editor_redo") return editorInvoke("redo");
833 if (name == "eve_editor_diagnostics") return editorInvoke("diagnostics");
834
835 if (name == "eve_renderable3d_get") {
836 auto* capture = mcpCapture();
837 if (!capture) return "error: graphics module not available";
838 const int entityId = getArgInt(args, "entityId", -1);
839 const int generation = getArgInt(args, "generation", -1);
840 if (entityId < 0 || generation < 0) return "error: entityId and generation are required";
841 return mcpStringify(Poco::Dynamic::Var(renderableObservation(*capture, entityId, generation)));
842 }
843
844 // ============================= Scene / Entity =============================
845 if (name == "eve_scene_status") {
846 auto* scene = mcpScene();
847 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
848 if (!scene) {
849 o->set("error", "Scene module not available");
850 return mcpStringify(Poco::Dynamic::Var(o));
851 }
852 const std::string host = scene->activeHost();
853 if (host.empty()) {
854 o->set("activeHost", Poco::Dynamic::Var());
855 o->set("nodeCount", 0);
856 return mcpStringify(Poco::Dynamic::Var(o));
857 }
858 o->set("activeHost", host);
859 o->set("nodeCount", scene->nodeCount());
860 o->set("rootId", scene->rootId());
861 return mcpStringify(Poco::Dynamic::Var(o));
862 }
863
864 if (name == "eve_scene_nodes") {
865 auto* scene = mcpScene();
866 if (!scene || scene->activeHost().empty()) return "error: no active scene host";
867 const int limit = getArgInt(args, "limit", 500);
868 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
869 for (const auto& n : scene->nodes(limit)) {
870 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
871 o->set("id", n.id);
872 o->set("name", n.name);
873 o->set("path", n.path);
874 o->set("visible", n.visible);
875 arr->add(o);
876 }
877 return mcpStringify(Poco::Dynamic::Var(arr));
878 }
879
880 if (name == "eve_scene_node_get") {
881 auto* scene = mcpScene();
882 if (!scene || scene->activeHost().empty()) return "error: no active scene host";
883 const std::string id = getArgString(args, "id");
885 if (id.empty() || !scene->getNode(id, &n)) return "error: node not found: " + id;
886 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
887 o->set("id", n.id);
888 o->set("name", n.name);
889 o->set("path", n.path);
890 o->set("visible", n.visible);
891 o->set("x", n.x);
892 o->set("y", n.y);
893 o->set("z", n.z);
894 o->set("yaw", n.yaw);
895 o->set("pitch", n.pitch);
896 o->set("roll", n.roll);
897 o->set("sx", n.sx);
898 o->set("sy", n.sy);
899 o->set("sz", n.sz);
900 o->set("parent", n.parent);
901 Poco::JSON::Array::Ptr kids = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
902 for (const auto& c : n.children) kids->add(c);
903 o->set("children", kids);
904 return mcpStringify(Poco::Dynamic::Var(o));
905 }
906
907 if (name == "eve_scene_node_set") {
908 auto* scene = mcpScene();
909 if (!scene || scene->activeHost().empty()) return "error: no active scene host";
910 const std::string id = getArgString(args, "id");
911 if (id.empty()) return "error: node not found: " + id;
912 bool changed = false;
913 if (args && args->has("x") && args->has("y") && args->has("z")) {
914 changed =
915 scene->setNodeTransform(id, getArgFloat(args, "x"), getArgFloat(args, "y"), getArgFloat(args, "z"));
916 }
917 if (args && args->has("visible")) {
918 changed = scene->setNodeVisible(id, getArgBool(args, "visible")) || changed;
919 }
920 return changed ? "ok" : "error: node not found: " + id;
921 }
922
923 // ============================= Procgen =============================
924 if (name == "eve_procgen_recipes") {
925 auto* pg = mcpProcgen();
926 if (!pg) return "error: Procgen module not available";
927 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
928 auto addList = [&](const char* key, const std::vector<std::string>& items) {
929 Poco::JSON::Array::Ptr a = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
930 for (const auto& it : items) a->add(it);
931 o->set(key, a);
932 };
933 addList("algorithms", pg->algorithms());
934 addList("meshRecipes", pg->meshRecipes());
935 addList("textureRecipes", pg->textureRecipes());
936 addList("pbrRecipes", pg->pbrRecipes());
937 return mcpStringify(Poco::Dynamic::Var(o));
938 }
939
940 if (name == "eve_procgen_map") {
941 auto* pg = mcpProcgen();
942 if (!pg) return "error: Procgen module not available";
943 const std::string algorithm = getArgString(args, "algorithm");
944 if (algorithm.empty()) return "error: missing algorithm";
945 std::vector<std::pair<std::string, std::string>> params;
946 for (const auto& key : {"roomCount", "roomMin", "roomMax", "corridorWidth", "autotile", "scale", "octaves"}) {
947 if (args && args->has(key)) params.emplace_back(key, std::to_string(getArgInt(args, key)));
948 }
949 if (args && args->has("corridorStyle"))
950 params.emplace_back("corridorStyle", getArgString(args, "corridorStyle"));
951 std::string err;
952 std::string json = pg->generateMap(algorithm, getArgInt(args, "width", 32), getArgInt(args, "height", 32),
953 static_cast<uint32_t>(getArgInt(args, "seed", 0)), params, &err);
954 if (json.empty()) return "error: " + (err.empty() ? std::string("empty grid") : err);
955 return json;
956 }
957
958 if (name == "eve_procgen_mesh") {
959 auto* pg = mcpProcgen();
960 if (!pg) return "error: Procgen module not available";
961 const std::string recipe = getArgString(args, "recipe");
962 if (recipe.empty()) return "error: missing recipe";
963 std::string err;
964 std::string json =
965 pg->buildMesh(recipe, static_cast<uint32_t>(getArgInt(args, "seed", 0)), getArgInt(args, "width", -1),
966 getArgInt(args, "height", -1), getArgInt(args, "depth", -1), &err);
967 if (json.empty()) return "error: " + (err.empty() ? std::string("build failed") : err);
968 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
969 o->set("recipe", recipe);
970 try {
971 Poco::JSON::Parser parser;
972 Poco::Dynamic::Var parsed = parser.parse(json);
973 Poco::JSON::Object::Ptr jo = parsed.extract<Poco::JSON::Object::Ptr>();
974 o->set("vertices", jo->get("vertices"));
975 o->set("triangles", jo->get("triangles"));
976 } catch (...) {
977 return json;
978 }
979 return mcpStringify(Poco::Dynamic::Var(o));
980 }
981
982 // ============================= Physics =============================
983 if (name == "eve_physics_new_world") {
984 auto* ph = mcpPhysics();
985 if (!ph) return "error: Physics module not available";
986 const int id = ph->newWorld(getArgFloat(args, "gravityX", 0.f), getArgFloat(args, "gravityY", 900.f));
987 if (id < 0) return "error: failed to create world";
988 float gx = 0.f, gy = 0.f;
989 ph->worldGravity(id, &gx, &gy);
990 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
991 o->set("id", id);
992 o->set("gravityX", gx);
993 o->set("gravityY", gy);
994 return mcpStringify(Poco::Dynamic::Var(o));
995 }
996
997 if (name == "eve_physics_list_worlds") {
998 auto* ph = mcpPhysics();
999 if (!ph) return "error: Physics module not available";
1000 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1001 for (int i = 0; i < ph->worldCount(); ++i) {
1002 float gx = 0.f, gy = 0.f;
1003 if (!ph->worldGravity(i, &gx, &gy)) continue;
1004 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1005 o->set("id", i);
1006 o->set("gravityX", gx);
1007 o->set("gravityY", gy);
1008 arr->add(o);
1009 }
1010 return mcpStringify(Poco::Dynamic::Var(arr));
1011 }
1012
1013 if (name == "eve_physics_raycast") {
1014 auto* ph = mcpPhysics();
1015 if (!ph) return "error: Physics module not available";
1017 if (!ph->rayCast(getArgInt(args, "world", 0), getArgFloat(args, "x1"), getArgFloat(args, "y1"),
1018 getArgFloat(args, "x2"), getArgFloat(args, "y2"), &h))
1019 return "error: unknown physics world id";
1020 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1021 o->set("hit", h.hit);
1022 if (h.hit) {
1023 o->set("bodyId", h.bodyId);
1024 o->set("x", h.x);
1025 o->set("y", h.y);
1026 o->set("normalX", h.normalX);
1027 o->set("normalY", h.normalY);
1028 o->set("fraction", h.fraction);
1029 }
1030 return mcpStringify(Poco::Dynamic::Var(o));
1031 }
1032
1033 if (name == "eve_physics_remove_world") {
1034 auto* ph = mcpPhysics();
1035 if (!ph) return "error: Physics module not available";
1036 return ph->removeWorld(getArgInt(args, "world", -1)) ? "ok" : "error: unknown physics world id";
1037 }
1038
1039 // ============================= Render =============================
1040 if (name == "eve_render_status") {
1041 return renderStatusText(mcpCapture());
1042 }
1043
1044 if (name == "eve_render_describe") {
1045 const bool fresh = getArgBool(args, "fresh", false);
1046 const std::string reason = getArgString(args, "reason");
1047 return RenderVision::instance().describe(mcpCapture(), renderStatusText(mcpCapture()), fresh, reason);
1048 }
1049
1050 if (name == "eve_render_vision_config") {
1051 auto& rv = RenderVision::instance();
1052 if (args && args->has("baseUrl")) rv.setBaseUrl(getArgString(args, "baseUrl"));
1053 if (args && args->has("apiKey")) rv.setApiKey(getArgString(args, "apiKey"));
1054 if (args && args->has("model")) rv.setModel(getArgString(args, "model"));
1055 if (args && args->has("path")) rv.setPath(getArgString(args, "path"));
1056 if (args && args->has("timeoutMs")) rv.setTimeoutMs(getArgInt(args, "timeoutMs", 20000));
1057 return rv.configJson();
1058 }
1059
1060 if (name == "eve_screenshot") {
1061 auto* cap = mcpCapture();
1062 if (!cap) return "error: Graphics module not available";
1063 std::string path = getArgString(args, "path");
1064 if (path.empty()) path = "mcp_screenshot.png";
1065 // A relative path resolves against the project root: the host process may
1066 // run from any working directory, and an agent has to be able to read the
1067 // artifact back from the path it receives.
1068 if (std::filesystem::path(path).is_relative() && !mcp.gameRoot().empty())
1069 path = (std::filesystem::path(mcp.gameRoot()) / path).string();
1070 // Report one separator convention: the project root already uses '/',
1071 // and agents echo this path back into file tooling.
1072 for (char& c : path) {
1073 if (c == '\\') c = '/';
1074 }
1075
1076 const eve::RenderStatusInfo status = cap->status();
1077 int w = 0;
1078 int h = 0;
1079 std::string err;
1080 if (!cap->savePng(path, &w, &h, &err)) {
1081 // Enabling readback and reading it back cannot happen inside one
1082 // presented frame: the swapchain copy is recorded by the next
1083 // present. Report that as a retryable state instead of a failure so
1084 // an unattended loop keeps polling instead of giving up.
1085 const bool retryable = !status.readbackEnabled;
1086 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1087 o->set("ok", false);
1088 o->set("retryable", retryable);
1089 o->set("reason", retryable ? "no-presented-frame" : "capture-failed");
1090 o->set("readbackEnabled", status.readbackEnabled);
1091 o->set("message", err);
1092 const std::string payload = mcpStringify(Poco::Dynamic::Var(o));
1093 return retryable ? payload : "error: " + payload;
1094 }
1095
1096 std::error_code sizeError;
1097 const auto bytes = std::filesystem::file_size(path, sizeError);
1098 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1099 o->set("ok", true);
1100 o->set("path", path);
1101 o->set("bytes", sizeError ? Poco::Int64(0) : static_cast<Poco::Int64>(bytes));
1102 o->set("width", w);
1103 o->set("height", h);
1104 return mcpStringify(Poco::Dynamic::Var(o));
1105 }
1106
1107 // ============================= Particles / Weather =============================
1108 if (name == "eve_particles_status") {
1109 auto* part = mcpParticles();
1110 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1111 if (!part) {
1112 o->set("error", "Particles module not available");
1113 return mcpStringify(Poco::Dynamic::Var(o));
1114 }
1115 o->set("emitterCount", part->emitterCount());
1116 return mcpStringify(Poco::Dynamic::Var(o));
1117 }
1118
1119 if (name == "eve_particles_emit") {
1120 auto* part = mcpParticles();
1121 if (!part) return "error: Particles module not available";
1122 float ex = 0.f, ey = 0.f;
1123 int cnt = 0;
1124 if (!part->createEmitter(getArgInt(args, "buffer", 1000), getArgFloat(args, "x"), getArgFloat(args, "y"),
1125 getArgString(args, "preset"), getArgInt(args, "count", 100), &ex, &ey, &cnt))
1126 return "error: failed to create emitter";
1127 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1128 o->set("x", ex);
1129 o->set("y", ey);
1130 o->set("count", cnt);
1131 return mcpStringify(Poco::Dynamic::Var(o));
1132 }
1133
1134 // ============================= Audio =============================
1135 if (name == "eve_audio_status") {
1136 auto* audio = mcpAudio();
1137 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1138 if (!audio) {
1139 o->set("error", "Audio module not available");
1140 return mcpStringify(Poco::Dynamic::Var(o));
1141 }
1142 o->set("volume", audio->volume());
1143 return mcpStringify(Poco::Dynamic::Var(o));
1144 }
1145
1146 if (name == "eve_audio_set_volume") {
1147 auto* audio = mcpAudio();
1148 if (!audio) return "error: Audio module not available";
1149 audio->setVolume(getArgFloat(args, "volume", 1.f));
1150 return "ok";
1151 }
1152
1153 if (name == "eve_audio_stop_all") {
1154 auto* audio = mcpAudio();
1155 if (!audio) return "error: Audio module not available";
1156 audio->stopAll();
1157 return "ok";
1158 }
1159
1160 // ============================= Host UI (headless MCP editor host) =====
1161 if (name == "eve_host_status") {
1162 return mcpHost() ? mcpHost()->status() : "error: ui module not available";
1163 }
1164 if (name == "eve_host_window_open") {
1165 const std::string title = getArgString(args, "title", "EVEngine AI Host");
1166 return mcpHost() ? mcpHost()->openWindow(title, getArgInt(args, "width", 1280), getArgInt(args, "height", 800))
1167 : "error: ui module not available";
1168 }
1169 if (name == "eve_host_window_close") {
1170 return mcpHost() ? mcpHost()->closeWindow() : "error: ui module not available";
1171 }
1172 if (name == "eve_host_window_state") {
1173 return mcpHost() ? mcpHost()->windowState() : "error: ui module not available";
1174 }
1175 if (name == "eve_host_editor_apply") {
1176 std::string json;
1177 if (args && args->has("editor")) {
1178 try {
1179 Poco::JSON::Object::Ptr o = args->getObject("editor");
1180 if (o)
1181 json = mcpStringify(Poco::Dynamic::Var(o));
1182 else
1183 json = getArgString(args, "editor");
1184 } catch (...) {
1185 json = getArgString(args, "editor");
1186 }
1187 }
1188 if (json.empty()) return "error: missing editor";
1189 return mcpHost() ? mcpHost()->applyEditor(json) : "error: ui module not available";
1190 }
1191 if (name == "eve_host_editor_remove") {
1192 return mcpHost() ? mcpHost()->removeEditor(getArgString(args, "id")) : "error: ui module not available";
1193 }
1194 if (name == "eve_host_editor_list") {
1195 return mcpHost() ? mcpHost()->listEditors() : "error: ui module not available";
1196 }
1197 if (name == "eve_host_editor_state") {
1198 return mcpHost() ? mcpHost()->editorState(getArgString(args, "id")) : "error: ui module not available";
1199 }
1200 if (name == "eve_host_editor_set_value") {
1201 if (!args || !args->has("value")) return "error: missing value";
1202 return mcpHost() ? mcpHost()->setEditorValue(getArgString(args, "editor"), getArgString(args, "widget"),
1203 mcpStringify(getArgVar(args, "value")))
1204 : "error: ui module not available";
1205 }
1206 if (name == "eve_host_editor_save") {
1207 return mcpHost() ? mcpHost()->saveEditor(getArgString(args, "id")) : "error: ui module not available";
1208 }
1209 if (name == "eve_host_editor_unload") {
1210 return mcpHost() ? mcpHost()->unloadEditor(getArgString(args, "id")) : "error: ui module not available";
1211 }
1212 if (name == "eve_host_vm_register") {
1213 return mcpHost() ? mcpHost()->registerVM(getArgString(args, "name"), getArgString(args, "source"))
1214 : "error: ui module not available";
1215 }
1216 if (name == "eve_host_vm_unregister") {
1217 return mcpHost() ? mcpHost()->unregisterVM(getArgString(args, "name")) : "error: ui module not available";
1218 }
1219 if (name == "eve_host_events") {
1220 return mcpHost() ? mcpHost()->consumeEvents(getArgString(args, "editor")) : "error: ui module not available";
1221 }
1222 if (name == "eve_host_widget_rect") {
1223 return mcpHost() ? mcpHost()->widgetRect(getArgString(args, "editor"), getArgString(args, "widget"))
1224 : "error: ui module not available";
1225 }
1226 if (name == "eve_host_capture") {
1227 return mcpHost() ? mcpHost()->capture(getArgString(args, "path")) : "error: ui module not available";
1228 }
1229 if (name == "eve_host_script") {
1230 return mcpHost() ? mcpHost()->runScript(getArgString(args, "source")) : "error: ui module not available";
1231 }
1232 if (name == "eve_host_resource_reload") {
1233 return mcpHost() ? mcpHost()->reloadResource(getArgString(args, "path")) : "error: ui module not available";
1234 }
1235 if (name == "eve_host_hot_reload_status") {
1236 return mcpHost() ? mcpHost()->hotReloadStatus() : "error: ui module not available";
1237 }
1238 if (name == "eve_host_shutdown") {
1239 if (mcpHost()) mcpHost()->requestExit();
1240 return "ok";
1241 }
1242
1243 if (name == "eve_eval") {
1244 const std::string expr = getArgString(args, "expression");
1245 if (expr.empty()) return "error: missing expression";
1246 auto info = dbg.evaluate(expr);
1247 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1248 o->set("expression", expr);
1249 o->set("name", info.name);
1250 o->set("value", info.value);
1251 o->set("type", info.type);
1252 o->set("ok", !info.value.empty() || info.type == "null" || !info.name.empty());
1253 return mcpStringify(Poco::Dynamic::Var(o));
1254 }
1255
1256 if (name == "eve_skeleton_inspect") {
1257 const std::string actor = getArgString(args, "actor");
1258 const std::string bone = getArgString(args, "bone");
1259 if (actor.empty()) return "error: missing actor";
1260 const std::string expr = "eve_mcp_skeleton_inspect(\"" + sqStringLiteralEscape(actor) +
1261 "\",\"" + sqStringLiteralEscape(bone) + "\")";
1262 auto info = dbg.evaluate(expr);
1263 if (info.value.empty()) return "error: eve_mcp_skeleton_inspect is unavailable";
1264 // Debugger string values are display-quoted. The project hook returns
1265 // canonical JSON, so remove only that outer presentation pair.
1266 if (info.value.size() >= 2 && info.value.front() == '"' && info.value.back() == '"')
1267 return info.value.substr(1, info.value.size() - 2);
1268 return info.value;
1269 }
1270
1271 if (name == "eve_pause") {
1272 dbg.pause(PauseReason::PauseKey);
1273 dap.notifyStopped(PauseReason::PauseKey, dbg.pauseLocation());
1274 return "paused";
1275 }
1276 if (name == "eve_continue") {
1277 dbg.resume();
1278 dap.notifyContinued();
1279 return "continued";
1280 }
1281 if (name == "eve_step_over") {
1282 dbg.stepOver();
1283 dap.notifyContinued();
1284 return "step_over";
1285 }
1286 if (name == "eve_step_into") {
1287 dbg.stepInto();
1288 dap.notifyContinued();
1289 return "step_into";
1290 }
1291 if (name == "eve_step_out") {
1292 dbg.stepOut();
1293 dap.notifyContinued();
1294 return "step_out";
1295 }
1296 if (name == "eve_step_frame") {
1297 dbg.stepFrame();
1298 dap.notifyContinued();
1299 return "step_frame";
1300 }
1301
1302 if (name == "eve_stack") {
1303 auto frames = dbg.stackTrace();
1304 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1305 for (const auto& f : frames) {
1306 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1307 o->set("id", f.id);
1308 o->set("name", f.name);
1309 o->set("source", f.loc.source);
1310 o->set("line", f.loc.line);
1311 o->set("function", f.loc.function);
1312 arr->add(o);
1313 }
1314 return mcpStringify(Poco::Dynamic::Var(arr));
1315 }
1316
1317 if (name == "eve_locals") {
1318 const int level = getArgInt(args, "level", 1);
1319 auto vars = dbg.locals(level);
1320 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1321 for (const auto& v : vars) {
1322 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1323 o->set("name", v.name);
1324 o->set("value", v.value);
1325 o->set("type", v.type);
1326 arr->add(o);
1327 }
1328 return mcpStringify(Poco::Dynamic::Var(arr));
1329 }
1330
1331 if (name == "eve_set_breakpoint") {
1332 const std::string source = getArgString(args, "source");
1333 const int line = getArgInt(args, "line");
1334 if (source.empty() || line <= 0) return "error: need source and line";
1335 const std::string condition = getArgString(args, "condition");
1336 const int id = dbg.setBreakpoint(source, line, true, condition);
1337 return "ok id=" + std::to_string(id);
1338 }
1339 if (name == "eve_clear_breakpoint") {
1340 const std::string source = getArgString(args, "source");
1341 const int line = getArgInt(args, "line");
1342 if (source.empty() || line <= 0) return "error: need source and line";
1343 return dbg.clearBreakpoint(source, line) ? "cleared" : "not_found";
1344 }
1345 if (name == "eve_list_breakpoints") {
1346 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1347 for (const auto& bp : dbg.breakpoints()) {
1348 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1349 o->set("id", bp.id);
1350 o->set("source", bp.source);
1351 o->set("line", bp.line);
1352 o->set("enabled", bp.enabled);
1353 o->set("condition", bp.condition);
1354 arr->add(o);
1355 }
1356 return mcpStringify(Poco::Dynamic::Var(arr));
1357 }
1358
1359 if (name == "eve_watch_add") {
1360 const std::string expr = getArgString(args, "expression");
1361 if (expr.empty()) return "error: missing expression";
1362 dbg.addWatch(expr);
1363 dbg.refreshWatches();
1364 return "ok";
1365 }
1366 if (name == "eve_watch_list") {
1367 dbg.refreshWatches();
1368 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1369 for (const auto& w : dbg.watches()) {
1370 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1371 o->set("expression", w.expression);
1372 o->set("value", w.value);
1373 o->set("ok", w.ok);
1374 arr->add(o);
1375 }
1376 return mcpStringify(Poco::Dynamic::Var(arr));
1377 }
1378
1379 if (name == "eve_snapshot_capture") {
1380 HSQUIRRELVM vm = dbg.vm();
1381 if (!vm) return "error: no VM";
1382 std::string err;
1383 std::string json = Snapshot::instance().capture(vm, &err);
1384 if (!err.empty() && json.empty()) return "error: " + err;
1385 return json;
1386 }
1387 if (name == "eve_snapshot_restore") {
1388 HSQUIRRELVM vm = dbg.vm();
1389 if (!vm) return "error: no VM";
1390 const std::string json = getArgString(args, "json");
1391 std::string err;
1392 if (!Snapshot::instance().restore(vm, json, &err)) return "error: " + err;
1393 return "ok";
1394 }
1395 if (name == "eve_snapshot_save") {
1396 HSQUIRRELVM vm = dbg.vm();
1397 if (!vm) return "error: no VM";
1398 const std::string path = getArgString(args, "path");
1399 std::string err;
1400 if (!Snapshot::instance().saveFile(vm, path, &err)) return "error: " + err;
1401 return "ok";
1402 }
1403 if (name == "eve_snapshot_load") {
1404 HSQUIRRELVM vm = dbg.vm();
1405 if (!vm) return "error: no VM";
1406 const std::string path = getArgString(args, "path");
1407 std::string err;
1408 if (!Snapshot::instance().loadFile(vm, path, &err)) return "error: " + err;
1409 return "ok";
1410 }
1411
1412 if (name == "eve_error_slice") {
1413 const std::string& last = mcpLastReport();
1414 if (!last.empty()) return last;
1415 return mcpFormatError("no prior error; callgraph slice at latest site");
1416 }
1417
1418 if (name == "eve_run_script") {
1419 HSQUIRRELVM vm = dbg.vm();
1420 if (!vm) return "error: no VM";
1421 const std::string source = getArgString(args, "source");
1422 if (source.empty()) return "error: missing source";
1423 std::string error;
1424 if (!runVmSnippet(vm, source, &error)) return "error: " + error;
1425 return "ok";
1426 }
1427
1428 if (name == "eve_ai_note") {
1429 const std::string text = getArgString(args, "text");
1430 if (text.empty()) return "error: missing text";
1432 return "ok";
1433 }
1434 if (name == "eve_ai_log") return AiPanel::instance().formatLog(100);
1435
1436 // ===================== Scene Director (AI scene-authoring) =====================
1437 // Agent-drivable scene construction backed by the `scene_director` script kit.
1438 if (name == "eve_scene_director_install") {
1439 HSQUIRRELVM vm = dbg.vm();
1440 if (!vm) return "error: no VM";
1441 std::string err;
1442 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1443 return "ok";
1444 }
1445
1446 if (name == "eve_scene_director_status") {
1447 HSQUIRRELVM vm = dbg.vm();
1448 if (!vm) return "error: no VM";
1449 std::string err;
1450 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1451 return callSceneDirectorReturn(vm, "return ::scene_director.status();", &err);
1452 }
1453
1454 if (name == "eve_scene_reset") {
1455 HSQUIRRELVM vm = dbg.vm();
1456 if (!vm) return "error: no VM";
1457 std::string err;
1458 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1459 const std::string out = callSceneDirectorReturn(vm, "return ::scene_director.reset();", &err);
1460 if (!err.empty()) return sceneDirectorToolError(name, err);
1461 return out;
1462 }
1463
1464 if (name == "eve_scene_modify") {
1465 HSQUIRRELVM vm = dbg.vm();
1466 if (!vm) return "error: no VM";
1467 std::string err;
1468 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1469 const std::string action = getArgString(args, "action");
1470 const std::string target = getArgString(args, "target");
1471 Poco::Dynamic::Var paramsVar;
1472 if (args && args->has("params")) {
1473 try {
1474 paramsVar = Poco::Dynamic::Var(args->getObject("params"));
1475 } catch (...) {
1476 }
1477 }
1478 const std::string snippet = "return ::scene_director.modify(" + sqLiteralValue(Poco::Dynamic::Var(action)) +
1479 "," + sqLiteralValue(Poco::Dynamic::Var(target)) + "," + sqLiteralValue(paramsVar) +
1480 ");";
1481 const std::string out = callSceneDirectorReturn(vm, snippet, &err);
1482 if (!err.empty()) return sceneDirectorToolError(name, err);
1483 return out;
1484 }
1485
1486 if (name == "eve_camera_generate") {
1487 HSQUIRRELVM vm = dbg.vm();
1488 if (!vm) return "error: no VM";
1489 std::string err;
1490 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1491 const int count = getArgInt(args, "count", 6);
1492 const std::string snippet = "return ::scene_director.cameras(" + std::to_string(count) + ");";
1493 const std::string out = callSceneDirectorReturn(vm, snippet, &err);
1494 if (!err.empty()) return sceneDirectorToolError(name, err);
1495 return out;
1496 }
1497
1498 if (name == "eve_scene_info") {
1499 HSQUIRRELVM vm = dbg.vm();
1500 if (!vm) return "error: no VM";
1501 std::string err;
1502 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1503 const std::string out = callSceneDirectorReturn(vm, "return ::scene_director.info();", &err);
1504 if (!err.empty()) return sceneDirectorToolError(name, err);
1505 return out;
1506 }
1507
1508 // ---- 场景巡检工具集(图像与 3D 几何数据严格同步) ----
1509 if (name == "inspect_generate_scene_camera_views") {
1510 const glm::vec3 center = argVec3(args, "center");
1511 const float fov = getArgFloat(args, "fov", 60.f);
1512 auto views = SceneInspect::instance().generateViews(center, fov);
1513 Poco::JSON::Object::Ptr root = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1514 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1515 for (const auto& v : views) {
1516 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1517 o->set("name", v.name);
1518 o->set("kind", v.kind);
1519 o->set("eye", vec3ToArray(v.eye));
1520 o->set("target", vec3ToArray(v.target));
1521 o->set("fov", static_cast<double>(v.fovYDeg));
1522 arr->add(o);
1523 }
1524 root->set("views", arr);
1525 return mcpStringify(Poco::Dynamic::Var(root));
1526 }
1527
1528 if (name == "set_camera_pose") {
1529 const glm::vec3 pos = argVec3(args, "pos", glm::vec3(0.f, 1.8f, 0.f));
1530 const glm::vec3 rot = argVec3(args, "rot", glm::vec3(0.f));
1531 const float fov = getArgFloat(args, "fov", 0.f);
1532 const bool ok = SceneInspect::instance().setCameraPose(pos, rot, fov);
1533 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1534 o->set("ok", ok);
1535 if (!ok) {
1536 o->set("error", "failed to set camera pose (no Graphics/Camera3D)");
1537 } else {
1538 try {
1539 Poco::JSON::Parser parser;
1540 o->set("pose", parser.parse(SceneInspect::instance().currentPoseJson()));
1541 } catch (...) {
1542 }
1543 }
1544 return mcpStringify(Poco::Dynamic::Var(o));
1545 }
1546
1547 if (name == "capture_render_frame") {
1548 const std::string dir = getArgString(args, "dir");
1549 const std::string tag = getArgString(args, "tag", "frame");
1550 std::vector<std::string> buffers;
1551 if (args && args->has("buffers")) {
1552 try {
1553 auto arr = args->getArray("buffers");
1554 if (arr) {
1555 for (size_t i = 0; i < arr->size(); ++i)
1556 buffers.push_back(arr->get(static_cast<unsigned int>(i)).convert<std::string>());
1557 }
1558 } catch (...) {
1559 }
1560 }
1561 const auto res = SceneInspect::instance().capture(dir, tag, buffers);
1562 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1563 o->set("ok", res.ok);
1564 if (res.ok) {
1565 o->set("png", res.pngPath);
1566 o->set("json", res.jsonPath);
1567 o->set("width", res.width);
1568 o->set("height", res.height);
1569 o->set("entityCount", res.entityCount);
1570 if (!res.depthPngPath.empty()) o->set("depthPng", res.depthPngPath);
1571 if (!res.normalPngPath.empty()) o->set("normalPng", res.normalPngPath);
1572 if (!res.idPngPath.empty()) o->set("idPng", res.idPngPath);
1573 if (!res.idJsonPath.empty()) o->set("idJson", res.idJsonPath);
1574 if (!res.unsupported.empty()) {
1575 Poco::JSON::Array::Ptr uns = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1576 for (const auto& u : res.unsupported) uns->add(u);
1577 o->set("unsupported", uns);
1578 }
1579 } else {
1580 o->set("error", res.error);
1581 }
1582 return mcpStringify(Poco::Dynamic::Var(o));
1583 }
1584
1585 if (name == "get_visible_entities_screen_bbox") {
1586 const bool hasPos = args && args->has("pos");
1587 const bool hasTarget = args && args->has("target");
1588 if (hasPos && hasTarget) {
1589 const glm::vec3 eye = argVec3(args, "pos");
1590 const glm::vec3 tgt = argVec3(args, "target");
1591 const float fov = getArgFloat(args, "fov", 0.f);
1592 return SceneInspect::instance().visibleEntitiesJson(&eye, &tgt, fov);
1593 }
1594 const float fov = getArgFloat(args, "fov", 0.f);
1595 return SceneInspect::instance().visibleEntitiesJson(nullptr, nullptr, fov);
1596 }
1597
1598 return "error: unknown tool " + name;
1599}
1600
1601// Revisions this server actually implements. The newline-delimited JSON-RPC
1602// shape and the tools/resources/prompts surface are shared by all of them, so a
1603// client asking for one of these may keep its own framing assumptions.
1604constexpr const char* kMcpProtocolLatest = "2025-06-18";
1605
1606bool isSupportedProtocol(const std::string& requested) {
1607 return requested == "2025-06-18" || requested == "2025-03-26" || requested == "2024-11-05";
1608}
1609
1610std::string handleInitialize(McpServer& mcp, const std::string& idJson, Poco::JSON::Object::Ptr params) {
1611 std::string clientName = "mcp-client";
1612 std::string requested = kMcpProtocolLatest;
1613 if (params) {
1614 try {
1615 if (params->has("clientInfo")) {
1616 auto info = params->getObject("clientInfo");
1617 if (info && info->has("name")) clientName = info->get("name").convert<std::string>();
1618 }
1619 if (params->has("protocolVersion")) requested = params->get("protocolVersion").convert<std::string>();
1620 } catch (...) {
1621 }
1622 }
1623 (void)mcp;
1624 AiPanel::instance().setClientName(clientName);
1625 AiPanel::instance().addLog("system", "mcp.initialize", clientName);
1626
1627 // The server must answer with a revision it implements; echoing an
1628 // arbitrary client string would claim support for a protocol that was never
1629 // built, and the client would then frame requests accordingly.
1630 const std::string protocol = isSupportedProtocol(requested) ? requested : kMcpProtocolLatest;
1631
1632 // Hand-built JSON keeps initialize compact (newline framing) without a Poco Object tree.
1633 const std::string resultJson =
1634 std::string("{\"protocolVersion\":\"") + mcpJsonEscape(protocol) +
1635 "\",\"capabilities\":{\"tools\":{},\"resources\":{},\"prompts\":{}},"
1636 "\"serverInfo\":{\"name\":\"evengine\",\"title\":\"EVEngine MCP\",\"version\":\"0.5.1\"},"
1637 "\"instructions\":\"EVEngine MCP for AI-assisted game development. Call eve_api_search/eve_api_get to look up "
1638 "this build's EveScript bindings before writing .nut (do not invent Unity C# names); prefer eve_play and "
1639 "editor commands over eve_eval for gameplay. Diagnose through the game's own channels: "
1640 "eve_console_read returns the runtime console (Squirrel print/error plus engine log lines) with a monotonic "
1641 "seq cursor, eve_error_slice returns the last script/render error slice, and eve_render_status with "
1642 "eve_screenshot/eve_screenshot_image capture engine-owned frames. Drive a run with eve_pause/eve_step_* or "
1643 "eve_play (clock/step/observe/capture/checkpoint/act), assert with eve_eval/eve_run_script and snapshot "
1644 "tools, and inspect live state with eve_editor_*/eve_scene_*/eve_ui_*. Use eve_restart (optional args) to "
1645 "soft-restart the game without reloading the resource cache — do not kill the process between AI debug "
1646 "iterations. eve_host_* tools create JSON-defined editor windows bound to Squirrel ViewModels (MVVM) for "
1647 "AI-crafted terrain/material/event editors.\"}";
1648 return makeResult(idJson, resultJson);
1649}
1650
1651std::string handleToolsList(const std::string& idJson) {
1652 // Complete tool objects, comma-separated; the array wrapper is added by
1653 // handleToolsList so project-exported tools can be appended last.
1654 static const char* const kToolsParts[] = {
1655 "{\"name\":\"eve_status\",\"description\":\"Runtime + debugger + MCP/DAP status JSON.\","
1656 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1657 "{\"name\":\"eve_api_search\",\"description\":\"Search this build's EveScript Binding Contracts "
1658 "(module/class/method signatures). Look up APIs before writing .nut. Do not use this to invent Unity C# "
1659 "names.\","
1660 "\"inputSchema\":{\"type\":\"object\",\"properties\":{"
1661 "\"query\":{\"type\":\"string\",\"description\":\"case-insensitive method, class, key, or space-separated "
1662 "tokens\"},"
1663 "\"module\":{\"type\":\"string\",\"description\":\"optional exact module id\"},"
1664 "\"class\":{\"type\":\"string\",\"description\":\"optional exact script class\"},"
1665 "\"limit\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":64}},"
1666 "\"required\":[]}},"
1667 "{\"name\":\"eve_api_get\",\"description\":\"Fetch one EveScript Binding Contract by key (module/Class.method) "
1668 "or unique method name. Prefer this after eve_api_search.\","
1669 "\"inputSchema\":{\"type\":\"object\",\"properties\":{"
1670 "\"key\":{\"type\":\"string\"},\"class\":{\"type\":\"string\"},\"method\":{\"type\":\"string\"}}}},"
1671 "{\"name\":\"eve_gameplay\",\"description\":\"Observe, discover, submit or advance player-equivalent gameplay "
1672 "through the versioned shared control protocol.\","
1673 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"request\":{\"type\":\"object\"}},\"required\":["
1674 "\"request\"]}},"
1675 "{\"name\":\"eve_play\",\"description\":\"Unified Play Host: pause/play, step host frames, observe "
1676 "game.agent.json script roots, run mapped act, capture engine screenshots, checkpoint restore, and "
1677 "record/replay play traces without eve_eval.\","
1678 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"request\":{\"type\":\"object\"}},\"required\":["
1679 "\"request\"]}},"
1680 "{\"name\":\"eve_pixelworld_worlds\",\"description\":\"List live PixelWorld simulations and status.\","
1681 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1682 "{\"name\":\"eve_pixelworld_catalog_builtin\",\"description\":\"Return the canonical versioned built-in "
1683 "material/reaction Catalog document.\","
1684 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1685 "{\"name\":\"eve_pixelworld_catalog_validate\",\"description\":\"Transactionally validate and canonicalize a "
1686 "material/reaction Catalog document.\","
1687 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"catalog\":{}},\"required\":[\"catalog\"]}},"
1688 "{\"name\":\"eve_pixelworld_catalog_apply\",\"description\":\"Hot-reload a compatible Catalog into a paused "
1689 "world using optimistic fingerprint concurrency.\","
1690 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},\"catalog\":{},"
1691 "\"expectedFingerprint\":{\"type\":\"string\"}},\"required\":[\"world\",\"catalog\",\"expectedFingerprint\"]}},"
1692 "{\"name\":\"eve_pixelworld_pause\",\"description\":\"Pause or resume one PixelWorld.\","
1693 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},"
1694 "\"paused\":{\"type\":\"boolean\"}},\"required\":[\"world\",\"paused\"]}},"
1695 "{\"name\":\"eve_pixelworld_step\",\"description\":\"Explicitly advance one PixelWorld by 1-1024 ticks, "
1696 "including while paused.\","
1697 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},"
1698 "\"count\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":1024}},\"required\":[\"world\"]}},"
1699 "{\"name\":\"eve_pixelworld_edit\",\"description\":\"Apply a strictly sequenced paint, heat, or explosion "
1700 "edit.\","
1701 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},"
1702 "\"sequence\":{\"type\":\"integer\"},\"kind\":{\"type\":\"string\",\"enum\":[\"paint\",\"heat\",\"explosion\"]}"
1703 ","
1704 "\"x\":{\"type\":\"integer\"},\"y\":{\"type\":\"integer\"},\"radius\":{\"type\":\"integer\"},"
1705 "\"material\":{\"type\":\"integer\"},\"strength\":{\"type\":\"integer\"},"
1706 "\"temperatureDelta\":{\"type\":\"integer\"}},\"required\":[\"world\",\"sequence\",\"kind\"]}},"
1707 "{\"name\":\"eve_pixelworld_diagnostics\",\"description\":\"Inspect bounded per-Chunk activity, material and "
1708 "temperature diagnostics.\","
1709 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},"
1710 "\"minX\":{\"type\":\"integer\"},\"minY\":{\"type\":\"integer\"},\"maxX\":{\"type\":\"integer\"},"
1711 "\"maxY\":{\"type\":\"integer\"}},\"required\":[\"world\"]}},"
1712 "{\"name\":\"eve_pixelworld_samples\",\"description\":\"Read the latest bounded PixelWorld simulation "
1713 "performance samples.\","
1714 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},"
1715 "\"limit\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":256}},\"required\":[\"world\"]}},"
1716 "{\"name\":\"eve_pixelworld_snapshot_capture\",\"description\":\"Capture a canonical PixelWorld snapshot as "
1717 "hex.\","
1718 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"}},\"required\":["
1719 "\"world\"]}},"
1720 "{\"name\":\"eve_pixelworld_snapshot_restore\",\"description\":\"Transactionally restore a canonical hex "
1721 "PixelWorld snapshot.\","
1722 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},"
1723 "\"snapshot\":{\"type\":\"string\"}},\"required\":[\"world\",\"snapshot\"]}},"
1724 "{\"name\":\"eve_ui_tree\",\"description\":\"Inspect retained game UI hosts and semantic widget trees (not "
1725 "only eve_host editors).\","
1726 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"host\":{\"type\":\"string\",\"description\":\"optional "
1727 "exact host name\"}}}},"
1728 "{\"name\":\"eve_ui_get\",\"description\":\"Read one retained game UI widget by semantic id.\","
1729 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"host\":{\"type\":\"string\"},\"widget\":{\"type\":"
1730 "\"string\"}},\"required\":[\"widget\"]}},"
1731 "{\"name\":\"eve_ui_click\",\"description\":\"Queue a semantic click by retained game UI widget id through the "
1732 "normal event path.\","
1733 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"host\":{\"type\":\"string\"},\"widget\":{\"type\":"
1734 "\"string\"}},\"required\":[\"widget\"]}},"
1735 "{\"name\":\"eve_editor_commands\",\"description\":\"List editor commands allowed for automation in the active "
1736 "game/editor.\","
1737 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1738 "{\"name\":\"eve_editor_target_list\",\"description\":\"Discover editor targets and creatable types: every "
1739 "registered target id with its type/revision/generation plus bound/owned flags, and the target type names the "
1740 "currently loaded adapters accept. Targets a project script created and bound (tile layer, height map, voxel "
1741 "world, ...) appear here, which is the only way to learn their id.\","
1742 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1743 "{\"name\":\"eve_editor_target_create\",\"description\":\"Create an Editor-owned document target or bind a "
1744 "live "
1745 "SceneHost/Renderable3D for Agent workflows. Accepted type names are runtime-dependent: call "
1746 "eve_editor_target_list and read supportedTypes instead of assuming a fixed set.\","
1747 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"target\":{\"type\":\"string\"},\"type\":{\"type\":"
1748 "\"string\"},\"host\":{\"type\":\"string\"},\"object\":{\"type\":\"string\"},\"entityId\":{\"type\":"
1749 "\"integer\",\"minimum\":0},\"generation\":{\"type\":\"integer\",\"minimum\":0}},\"required\":[\"target\","
1750 "\"type\"]}},"
1751 "{\"name\":\"eve_editor_target_close\",\"description\":\"Unregister and destroy an Editor-owned automation "
1752 "target.\","
1753 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"target\":{\"type\":\"string\"}},\"required\":["
1754 "\"target\"]}},"
1755 "{\"name\":\"eve_editor_inspect\",\"description\":\"Inspect authoritative structured state for an editor "
1756 "target.\","
1757 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"target\":{\"type\":\"string\"}},\"required\":["
1758 "\"target\"]}},"
1759 "{\"name\":\"eve_editor_plan\",\"description\":\"Validate and retain a side-effect-free editor command plan.\","
1760 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"command\":{\"type\":\"string\"},\"payload\":{},"
1761 "\"expectedRevision\":{\"type\":\"integer\"}},\"required\":[\"command\"]}},"
1762 "{\"name\":\"eve_editor_commit\",\"description\":\"Commit a retained editor command plan.\","
1763 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"planId\":{\"type\":\"string\"}},\"required\":["
1764 "\"planId\"]}},"
1765 "{\"name\":\"eve_editor_execute\",\"description\":\"Execute an editor command immediately through the same "
1766 "transaction path as UI and scripts.\","
1767 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"command\":{\"type\":\"string\"},\"payload\":{}},"
1768 "\"required\":[\"command\"]}},"
1769 "{\"name\":\"eve_editor_execute_observe\",\"description\":\"Execute one Editor transaction and return "
1770 "correlated before/after runtime observations plus the Editor target snapshot. An optional expect object "
1771 "declares the desired observed subset; tolerance controls numeric matching and the response reports converged, "
1772 "maxError, and mismatch paths. The observer is renderable3d by default; scene-node observes a node in an "
1773 "optional named host. Missing or stale subjects and invalid expectations are rejected before mutation.\","
1774 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"target\":{\"type\":\"string\"},\"command\":{\"type\":"
1775 "\"string\"},\"payload\":{},\"observer\":{\"type\":\"string\",\"enum\":[\"renderable3d\",\"scene-node\"]},"
1776 "\"entityId\":{\"type\":\"integer\",\"minimum\":0},\"generation\":{\"type\":\"integer\",\"minimum\":0},"
1777 "\"host\":{\"type\":\"string\"},\"node\":{\"type\":\"string\"},\"expect\":{\"type\":\"object\"},\"tolerance\":{"
1778 "\"type\":\"number\",\"minimum\":0}},"
1779 "\"required\":[\"target\",\"command\"]}},"
1780 "{\"name\":\"eve_editor_observe_start\",\"description\":\"Start an RX-backed runtime observation session. The "
1781 "initial sample is validated immediately; consecutive duplicate samples are suppressed until the session is "
1782 "closed.\","
1783 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"observer\":{\"type\":\"string\",\"enum\":["
1784 "\"renderable3d\",\"scene-node\"]},\"entityId\":{\"type\":\"integer\",\"minimum\":0},\"generation\":{\"type\":"
1785 "\"integer\",\"minimum\":0},\"host\":{\"type\":\"string\"},\"node\":{\"type\":\"string\"},\"expect\":{\"type\":"
1786 "\"object\"},\"tolerance\":{\"type\":\"number\",\"minimum\":0}}}},"
1787 "{\"name\":\"eve_editor_observe_poll\",\"description\":\"Sample and drain changed events from an RX-backed "
1788 "observation session.\","
1789 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"sessionId\":{\"type\":\"string\"}},\"required\":["
1790 "\"sessionId\"]}},"
1791 "{\"name\":\"eve_editor_observe_close\",\"description\":\"Explicitly cancel and remove an RX-backed "
1792 "observation session.\","
1793 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"sessionId\":{\"type\":\"string\"}},\"required\":["
1794 "\"sessionId\"]}},"
1795 "{\"name\":\"eve_editor_cancel\",\"description\":\"Discard a retained editor command plan without side "
1796 "effects.\","
1797 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"planId\":{\"type\":\"string\"}},\"required\":["
1798 "\"planId\"]}},"
1799 "{\"name\":\"eve_editor_undo\",\"description\":\"Undo the last editor transaction in the automation session.\","
1800 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1801 "{\"name\":\"eve_editor_redo\",\"description\":\"Redo the last editor transaction in the automation session.\","
1802 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1803 "{\"name\":\"eve_editor_diagnostics\",\"description\":\"Read structured diagnostics from the last editor "
1804 "automation operation.\","
1805 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1806 "{\"name\":\"eve_renderable3d_get\",\"description\":\"Read generation-qualified live Renderable3D transform "
1807 "and field-backed material state for Agent verification.\","
1808 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"entityId\":{\"type\":\"integer\",\"minimum\":0},"
1809 "\"generation\":{\"type\":\"integer\",\"minimum\":0}},\"required\":[\"entityId\",\"generation\"]}},"
1810 "{\"name\":\"eve_eval\",\"description\":\"Evaluate a Squirrel expression (local or roottable).\","
1811 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"expression\":{\"type\":\"string\"}},\"required\":["
1812 "\"expression\"]}},"
1813 "{\"name\":\"eve_skeleton_inspect\",\"description\":\"Inspect a published runtime skeleton hierarchy and "
1814 "bind/current bone transforms. The game provides eve_mcp_skeleton_inspect.\","
1815 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"actor\":{\"type\":\"string\"},\"bone\":{\"type\":"
1816 "\"string\"}},\"required\":[\"actor\"]}},"
1817 "{\"name\":\"eve_pause\",\"description\":\"Pause the game / script.\","
1818 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1819 "{\"name\":\"eve_continue\",\"description\":\"Continue from pause.\","
1820 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1821 "{\"name\":\"eve_step_over\",\"description\":\"Step over next statement.\","
1822 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1823 "{\"name\":\"eve_step_into\",\"description\":\"Step into next statement.\","
1824 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1825 "{\"name\":\"eve_step_out\",\"description\":\"Step out of current function.\","
1826 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1827 "{\"name\":\"eve_step_frame\",\"description\":\"Run one game frame then pause.\","
1828 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1829 "{\"name\":\"eve_stack\",\"description\":\"Return current script stack frames.\","
1830 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1831 "{\"name\":\"eve_locals\",\"description\":\"List locals at a stack level (default 1).\","
1832 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1833 "{\"name\":\"eve_set_breakpoint\",\"description\":\"Set a script breakpoint.\","
1834 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"},\"line\":{\"type\":"
1835 "\"integer\"},\"condition\":{\"type\":\"string\",\"description\":\"Optional Squirrel expression; empty always "
1836 "hits.\"}},\"required\":[\"source\",\"line\"]}},"
1837 "{\"name\":\"eve_clear_breakpoint\",\"description\":\"Clear a script breakpoint.\","
1838 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"},\"line\":{\"type\":"
1839 "\"integer\"}},\"required\":[\"source\",\"line\"]}},"
1840 "{\"name\":\"eve_list_breakpoints\",\"description\":\"List breakpoints.\","
1841 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1842 "{\"name\":\"eve_watch_add\",\"description\":\"Add a watch expression.\","
1843 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"expression\":{\"type\":\"string\"}},\"required\":["
1844 "\"expression\"]}},"
1845 "{\"name\":\"eve_watch_list\",\"description\":\"List watches with last values.\","
1846 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1847 "{\"name\":\"eve_snapshot_capture\",\"description\":\"Capture script-state snapshot JSON.\","
1848 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1849 "{\"name\":\"eve_snapshot_restore\",\"description\":\"Restore script-state from snapshot JSON.\","
1850 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"json\":{\"type\":\"string\"}},\"required\":[\"json\"]}}"
1851 ","
1852 "{\"name\":\"eve_snapshot_save\",\"description\":\"Save snapshot to a file path.\","
1853 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}}"
1854 ","
1855 "{\"name\":\"eve_snapshot_load\",\"description\":\"Load snapshot from a file path.\","
1856 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}}"
1857 ","
1858 "{\"name\":\"eve_error_slice\",\"description\":\"Return last error report / backward slice (script + "
1859 "render).\","
1860 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1861 "{\"name\":\"eve_run_script\",\"description\":\"Compile and run a short Squirrel snippet in the live VM.\","
1862 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"}},\"required\":["
1863 "\"source\"]}},"
1864 "{\"name\":\"eve_ai_note\",\"description\":\"Append a note to the DevTools AI session log.\","
1865 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"required\":[\"text\"]}}"
1866 ","
1867 "{\"name\":\"eve_ai_log\",\"description\":\"Read the DevTools AI session log.\","
1868 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1869 "{\"name\":\"eve_scene_director_install\",\"description\":\"Install the scene-director authoring kit into the "
1870 "live VM (idempotent).\","
1871 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1872 "{\"name\":\"eve_scene_director_status\",\"description\":\"Scene-director kit status (installed / propCount / "
1873 "hasCamera).\","
1874 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1875 "{\"name\":\"eve_scene_reset\",\"description\":\"Clear all staged props, camera and reset lighting to "
1876 "defaults.\","
1877 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1878 "{\"name\":\"eve_scene_modify\",\"description\":\"Agent scene action: action in "
1879 "add_object|spawn|place|move_object|move|scale|rotate|rotation|remove_object|remove|visibility|material|"
1880 "lighting|set_lighting|camera|cameras|info|list|reset. spawn/move params: "
1881 "{id,kind,x,y,z,sx,sy,sz,yaw_deg,scale,pos,tint,seed,mesh_params,...}; lighting params: "
1882 "{timeOfDay,atmosphere,intensity,background}; camera params: {eye,target,fov}. Returns JSON.\","
1883 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\"},\"target\":{\"type\":"
1884 "\"string\"},\"params\":{\"type\":\"object\"}},\"required\":[\"action\"]}},"
1885 "{\"name\":\"eve_camera_generate\",\"description\":\"Generate standardized QC camera rigs (eye/target/fov) "
1886 "orbiting the staged scene.\","
1887 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"count\":{\"type\":\"integer\"}}}},"
1888 "{\"name\":\"eve_scene_info\",\"description\":\"Authoritative staged-scene truth JSON: props "
1889 "(id/kind/pos/scale/yaw_deg/tint) + count.\","
1890 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1891 "{\"name\":\"inspect_generate_scene_camera_views\",\"description\":\"Generate a standard set of inspection "
1892 "camera views (road-level, bird's-eye, corner close-up, vista) around a center point.\","
1893 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"center\":{\"type\":\"array\",\"items\":{\"type\":"
1894 "\"number\"},\"description\":\"[x,y,z] center point to orbit (default "
1895 "[0,0,0])\"},\"fov\":{\"type\":\"number\",\"description\":\"base vertical FOV in degrees (default 60)\"}}}}"
1896 ","
1897 "{\"name\":\"set_camera_pose\",\"description\":\"Set the active camera pose from position + Euler rotation + "
1898 "optional FOV.\","
1899 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"pos\":{\"type\":\"array\",\"items\":{\"type\":"
1900 "\"number\"},\"description\":\"[x,y,z] camera "
1901 "position\"},\"rot\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},\"description\":\"[yawDeg,pitchDeg] "
1902 "facing\"},\"fov\":{\"type\":\"number\",\"description\":\"vertical FOV in degrees (0=keep "
1903 "current)\"}},\"required\":[\"pos\",\"rot\"]}}"
1904 ","
1905 "{\"name\":\"capture_render_frame\",\"description\":\"Atomically capture the current view and export matching "
1906 "buffers. PNG color frame + geometry JSON always; 'buffers' may add depth/normal (GBuffer), id (per-pixel "
1907 "render ID mask with JSON mapping) — shadow is unsupported on current backend.\","
1908 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"dir\":{\"type\":\"string\",\"description\":\"output "
1909 "directory (default: cache dir)\"},\"tag\":{\"type\":\"string\",\"description\":\"file name tag (default "
1910 "'frame')\"},\"buffers\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"optional: "
1911 "'color'|'depth'|'normal'|'id' (default ['color'])\"}}}}"
1912 ","
1913 "{\"name\":\"get_visible_entities_screen_bbox\",\"description\":\"Return visible scene entities in the frustum "
1914 "with their screen-space bbox, world AABB, id and asset label.\","
1915 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"pos\":{\"type\":\"array\",\"items\":{\"type\":"
1916 "\"number\"},\"description\":\"optional [x,y,z] camera eye "
1917 "override\"},\"target\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},\"description\":\"optional [x,y,z] "
1918 "look target override\"},\"fov\":{\"type\":\"number\",\"description\":\"optional FOV override (degrees)\"}}}}"
1919 ","
1920 "{\"name\":\"eve_scene_status\",\"description\":\"Active scene host name, node count and root id.\","
1921 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1922 "{\"name\":\"eve_scene_nodes\",\"description\":\"List nodes of the active scene host (id/name/path/visible).\","
1923 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"limit\":{\"type\":\"integer\"}}}},"
1924 "{\"name\":\"eve_scene_node_get\",\"description\":\"Read transform/visibility/parent/children of a scene node "
1925 "by id.\","
1926 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},"
1927 "{\"name\":\"eve_scene_node_set\",\"description\":\"Set position (x,y,z) or visibility of a scene node by "
1928 "id.\","
1929 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"x\":{\"type\":\"number\"},"
1930 "\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"},\"visible\":{\"type\":\"boolean\"}},\"required\":["
1931 "\"id\"]}},"
1932 "{\"name\":\"eve_procgen_recipes\",\"description\":\"List available procgen map algorithms and "
1933 "mesh/texture/PBR recipes.\","
1934 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1935 "{\"name\":\"eve_procgen_map\",\"description\":\"Generate a semantic tile grid with a procgen map algorithm.\","
1936 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"algorithm\":{\"type\":\"string\"},\"width\":{\"type\":"
1937 "\"integer\"},\"height\":{\"type\":\"integer\"},\"seed\":{\"type\":\"integer\"}},\"required\":[\"algorithm\"]}}"
1938 ","
1939 "{\"name\":\"eve_procgen_mesh\",\"description\":\"Build a procedural CPU mesh (mesh.* recipe) and return "
1940 "stats.\","
1941 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"recipe\":{\"type\":\"string\"},\"seed\":{\"type\":"
1942 "\"integer\"}},\"required\":[\"recipe\"]}},"
1943 "{\"name\":\"eve_physics_new_world\",\"description\":\"Create a 2D physics world and return its id.\","
1944 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"gravityX\":{\"type\":\"number\"},\"gravityY\":{"
1945 "\"type\":\"number\"}}}},"
1946 "{\"name\":\"eve_physics_list_worlds\",\"description\":\"List live 2D physics worlds with gravity.\","
1947 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1948 "{\"name\":\"eve_physics_raycast\",\"description\":\"Raycast a segment in a physics world and report the "
1949 "closest hit.\","
1950 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},\"x1\":{\"type\":"
1951 "\"number\"},\"y1\":{\"type\":\"number\"},\"x2\":{\"type\":\"number\"},\"y2\":{\"type\":\"number\"}},"
1952 "\"required\":[\"world\",\"x1\",\"y1\",\"x2\",\"y2\"]}},"
1953 "{\"name\":\"eve_physics_remove_world\",\"description\":\"Destroy a 2D physics world by id.\","
1954 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"}},\"required\":["
1955 "\"world\"]}},"
1956 "{\"name\":\"eve_render_status\",\"description\":\"Render window size, 3D frame flag, readback state and "
1957 "RenderFlow event count.\","
1958 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1959 "{\"name\":\"eve_screenshot\",\"description\":\"Capture the current frame to a PNG file (enables readback). "
1960 "Relative paths resolve against the project root and the response carries the absolute path, byte size and "
1961 "pixel size. The swapchain copy lands on the next present, so the first call can return ok=false with "
1962 "retryable=true and readbackEnabled=false; wait one rendered frame and call again.\","
1963 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}},"
1964 "{\"name\":\"eve_render_describe\",\"description\":\"Capture the current frame and ask the configured vision "
1965 "model to describe it and relate it to render parameters. Cached unless fresh=true.\","
1966 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"fresh\":{\"type\":\"boolean\"},\"reason\":{\"type\":"
1967 "\"string\"}}}},"
1968 "{\"name\":\"eve_render_vision_config\",\"description\":\"Set/read the vision model config "
1969 "(baseUrl/apiKey/model/path/timeoutMs). No args returns current config (key masked).\","
1970 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"baseUrl\":{\"type\":\"string\"},\"apiKey\":{\"type\":"
1971 "\"string\"},\"model\":{\"type\":\"string\"},\"path\":{\"type\":\"string\"},\"timeoutMs\":{\"type\":"
1972 "\"integer\"}}}},"
1973 "{\"name\":\"eve_particles_status\",\"description\":\"Report live particle emitter count.\","
1974 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1975 "{\"name\":\"eve_particles_emit\",\"description\":\"Spawn a particle emitter at a position (optionally a "
1976 "preset) and emit particles.\","
1977 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},"
1978 "\"preset\":{\"type\":\"string\"},\"count\":{\"type\":\"integer\"}}}},"
1979 "{\"name\":\"eve_audio_status\",\"description\":\"Report master audio volume.\","
1980 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1981 "{\"name\":\"eve_audio_set_volume\",\"description\":\"Set master audio volume (0..1).\","
1982 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"volume\":{\"type\":\"number\"}},\"required\":["
1983 "\"volume\"]}},"
1984 "{\"name\":\"eve_audio_stop_all\",\"description\":\"Stop all playing audio sources.\","
1985 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1986 "{\"name\":\"eve_host_status\",\"description\":\"Headless editor host status: window, editors, registered "
1987 "ViewModels, project root.\","
1988 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1989 "{\"name\":\"eve_host_window_open\",\"description\":\"Create the host OS window (lazy; editors can also "
1990 "auto-open it).\","
1991 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"width\":{\"type\":"
1992 "\"integer\"},\"height\":{\"type\":\"integer\"}}}},"
1993 "{\"name\":\"eve_host_window_close\",\"description\":\"Close the host OS window (MCP server stays alive).\","
1994 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1995 "{\"name\":\"eve_host_window_state\",\"description\":\"Host window open state + size.\","
1996 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1997 "{\"name\":\"eve_host_editor_apply\",\"description\":\"Apply an editor View (JSON widget tree). Auto-opens the "
1998 "host window on first use.\","
1999 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"editor\":{\"type\":\"object\"}},\"required\":["
2000 "\"editor\"]}},",
2001 "{\"name\":\"eve_host_editor_remove\",\"description\":\"Remove an editor panel from the session.\","
2002 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},",
2003 "{\"name\":\"eve_host_editor_list\",\"description\":\"List editors (id/title/vm).\","
2004 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
2005 "{\"name\":\"eve_host_editor_state\",\"description\":\"Editor values + pending events (id omitted = all "
2006 "editors).\","
2007 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}}}},"
2008 "{\"name\":\"eve_host_editor_set_value\",\"description\":\"Set a widget value (JSON value). Writes the bound "
2009 "ViewModel and emits a change event.\","
2010 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"editor\":{\"type\":\"string\"},\"widget\":{\"type\":"
2011 "\"string\"},\"value\":{}},\"required\":[\"editor\",\"widget\",\"value\"]}},",
2012 "{\"name\":\"eve_host_editor_save\",\"description\":\"Persist editor as editors/<id>.editor.json + <id>.vm.nut "
2013 "in the project.\","
2014 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},",
2015 "{\"name\":\"eve_host_editor_unload\",\"description\":\"Remove an editor from the session (files stay on "
2016 "disk).\","
2017 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},",
2018 "{\"name\":\"eve_host_vm_register\",\"description\":\"Compile a Squirrel ViewModel and register it by table "
2019 "name.\","
2020 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"source\":{\"type\":"
2021 "\"string\"}},\"required\":[\"name\",\"source\"]}},",
2022 "{\"name\":\"eve_host_vm_unregister\",\"description\":\"Unregister a ViewModel table.\","
2023 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}}"
2024 ",",
2025 "{\"name\":\"eve_host_events\",\"description\":\"Read and clear interaction events (human clicks/sliders or AI "
2026 "set_value).\","
2027 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"editor\":{\"type\":\"string\"}}}},"
2028 "{\"name\":\"eve_host_widget_rect\",\"description\":\"Last-frame screen rect of a widget (for script drawing "
2029 "in viewports).\","
2030 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"editor\":{\"type\":\"string\"},\"widget\":{\"type\":"
2031 "\"string\"}},\"required\":[\"editor\",\"widget\"]}},",
2032 "{\"name\":\"eve_host_capture\",\"description\":\"Capture the host window to a PNG and return path/size.\","
2033 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}},"
2034 "{\"name\":\"eve_host_script\",\"description\":\"Run a Squirrel snippet in the host VM (full engine API + "
2035 "eve.host).\","
2036 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"}},\"required\":["
2037 "\"source\"]}},",
2038 "{\"name\":\"eve_host_resource_reload\",\"description\":\"Reload a project-scoped MCP host script or editor "
2039 "View/ViewModel resource without restarting.\","
2040 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}}"
2041 ",",
2042 "{\"name\":\"eve_host_hot_reload_status\",\"description\":\"MCP host watcher count, reload counters and latest "
2043 "reload diagnostic.\","
2044 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
2045 "{\"name\":\"eve_host_shutdown\",\"description\":\"Exit the headless MCP host process.\","
2046 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}",
2047 };
2048 static const std::string kToolsBody = [] {
2049 // Core table first (the last entry carries no trailing comma), then every
2050 // satellite tool family. Satellites return comma-free object lists and
2051 // the separator is added here, so adding a family cannot silently break
2052 // the array (a missing separator makes the whole tools/list unusable).
2053 std::string out;
2054 for (const char* part : kToolsParts) out += part;
2055 for (std::string_view family :
2057 if (family.empty()) continue;
2058 out += ',';
2059 out += family;
2060 }
2061 return out;
2062 }();
2063
2064 // Project-exported tools come last and are re-read every call: a script may
2065 // register or drop one at any time (hot reload of mcp.nut).
2066 std::string tools = "{\"tools\":[";
2067 tools += kToolsBody;
2068 const std::string scripted = scriptToolSchemas();
2069 if (!scripted.empty()) {
2070 tools += ',';
2071 tools += scripted;
2072 }
2073 tools += "]}";
2074 return makeResult(idJson, tools);
2075}
2076
2077std::string handleToolsCall(McpServer& mcp, const std::string& idJson, Poco::JSON::Object::Ptr params) {
2078 if (!params || !params->has("name")) return makeError(idJson, -32602, "tools/call requires params.name");
2079 std::string name;
2080 try {
2081 name = params->get("name").convert<std::string>();
2082 } catch (...) {
2083 return makeError(idJson, -32602, "tools/call params.name must be a string");
2084 }
2085 Poco::JSON::Object::Ptr args = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2086 if (params->has("arguments")) {
2087 try {
2088 args = params->getObject("arguments");
2089 } catch (...) {
2090 }
2091 }
2092
2093 std::string detail;
2094 try {
2095 if (args) detail = mcpStringify(Poco::Dynamic::Var(args));
2096 } catch (...) {
2097 }
2098 AiPanel::instance().addLog("tool", name, detail);
2099
2100 // Runtime-observation tools build their own content envelope: a console
2101 // payload is text, a frame capture is an MCP image item.
2102 if (isMcpRuntimeTool(name)) {
2103 try {
2104 return makeResult(idJson, callMcpRuntimeTool(name, args));
2105 } catch (const std::exception& e) {
2106 AiPanel::instance().addLog("error", name, e.what());
2107 return makeResult(idJson, textContentResult(std::string("error: ") + e.what(), true));
2108 }
2109 }
2110
2111 // Capability-backed domain families (decals, ...). They report a named
2112 // `unavailable` reason when their provider was trimmed out of the build.
2113 if (isMcpDomainTool(name)) {
2114 try {
2115 return makeResult(idJson, callMcpDomainTool(name, args));
2116 } catch (const std::exception& e) {
2117 AiPanel::instance().addLog("error", name, e.what());
2118 return makeResult(idJson, textContentResult(std::string("error: ") + e.what(), true));
2119 }
2120 }
2121
2122 // Project-exported script tools. Built-ins win: a script tool can never
2123 // shadow a compiled tool, which is why registration rejects their prefixes.
2124 if (isScriptTool(name)) {
2125 try {
2126 return makeResult(idJson, callScriptTool(name, args));
2127 } catch (const std::exception& e) {
2128 AiPanel::instance().addLog("error", name, e.what());
2129 return makeResult(idJson, textContentResult(std::string("error: ") + e.what(), true));
2130 }
2131 }
2132
2133 try {
2134 const std::string out = callTool(mcp, name, args);
2135 const bool isErr = out.rfind("error:", 0) == 0;
2136 return makeResult(idJson, textContentResult(out, isErr));
2137 } catch (const std::exception& e) {
2138 AiPanel::instance().addLog("error", name, e.what());
2139 return makeResult(idJson, textContentResult(std::string("error: ") + e.what(), true));
2140 }
2141}
2142
2143std::string handleResourcesList(const std::string& idJson) {
2144 return makeResult(idJson,
2145 "{\"resources\":["
2146 "{\"uri\":\"eve://status\",\"name\":\"status\",\"description\":\"Debugger / MCP / DAP "
2147 "status\",\"mimeType\":\"application/json\"},"
2148 "{\"uri\":\"eve://error-report\",\"name\":\"error-report\",\"description\":\"Last DevTools error "
2149 "slice report\",\"mimeType\":\"text/plain\"},"
2150 "{\"uri\":\"eve://ai-session\",\"name\":\"ai-session\",\"description\":\"AI / MCP session "
2151 "log\",\"mimeType\":\"text/plain\"},"
2152 "{\"uri\":\"eve://callgraph\",\"name\":\"callgraph\",\"description\":\"CallGraph event "
2153 "summary\",\"mimeType\":\"application/json\"},"
2154 "{\"uri\":\"eve://crash-log\",\"name\":\"crash-log\",\"description\":\"Persistent crash/error "
2155 "log (eve.log) that survives a process death\",\"mimeType\":\"text/plain\"}"
2156 "]}");
2157}
2158
2159std::string handleResourcesRead(const std::string& idJson, Poco::JSON::Object::Ptr params) {
2160 if (!params || !params->has("uri")) return makeError(idJson, -32602, "resources/read requires params.uri");
2161 std::string uri;
2162 try {
2163 uri = params->get("uri").convert<std::string>();
2164 } catch (...) {
2165 return makeError(idJson, -32602, "resources/read params.uri must be a string");
2166 }
2167 std::string text;
2168 std::string mime = "text/plain";
2169 if (uri == "eve://status") {
2170 text = engineStatusJson(McpServer::instance());
2171 mime = "application/json";
2172 } else if (uri == "eve://error-report") {
2173 text = mcpLastReport();
2174 if (text.empty()) text = "(no error report yet)\n";
2175 } else if (uri == "eve://ai-session") {
2177 if (text.empty()) text = "(empty)\n";
2178 } else if (uri == "eve://callgraph") {
2179 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2180 o->set("events", static_cast<int>(mcpCallgraphEvents()));
2181 o->set("stackDepth", static_cast<int>(mcpCallgraphStackDepth()));
2182 text = mcpStringify(Poco::Dynamic::Var(o));
2183 mime = "application/json";
2184 } else if (uri == "eve://crash-log") {
2185 // Same evidence eve_crash_report exposes, as a resource so a client can
2186 // attach it without a tool round trip. Bounded to the tail: a resource
2187 // read must not turn a long log into one enormous MCP frame.
2188 constexpr std::streamoff kCrashResourceBytes = 64 * 1024;
2189 text = "(no crash log yet)\n";
2190 const std::string path = crashLogSummary()->getValue<std::string>("path");
2191 std::error_code exists = std::error_code();
2192 if (!path.empty() && std::filesystem::exists(path, exists)) {
2193 std::ifstream in(path, std::ios::binary);
2194 if (in) {
2195 in.seekg(0, std::ios::end);
2196 const std::streamoff size = in.tellg();
2197 const std::streamoff start = size > kCrashResourceBytes ? size - kCrashResourceBytes : 0;
2198 in.seekg(start < 0 ? 0 : start);
2199 std::ostringstream buffer;
2200 buffer << in.rdbuf();
2201 text = buffer.str();
2202 if (start > 0) {
2203 const auto newline = text.find('\n');
2204 text.erase(0, newline == std::string::npos ? text.size() : newline + 1);
2205 text = "[…truncated; full log at " + path + "]\n" + text;
2206 }
2207 if (text.empty()) text = "(crash log is empty)\n";
2208 }
2209 }
2210 } else {
2211 return makeError(idJson, -32002, "Unknown resource: " + uri);
2212 }
2213
2214 Poco::JSON::Object::Ptr contentsItem = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2215 contentsItem->set("uri", uri);
2216 contentsItem->set("mimeType", mime);
2217 contentsItem->set("text", text);
2218 Poco::JSON::Array::Ptr contents = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
2219 contents->add(contentsItem);
2220 Poco::JSON::Object::Ptr result = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2221 result->set("contents", contents);
2222 return makeResult(idJson, mcpStringify(Poco::Dynamic::Var(result)));
2223}
2224
2225std::string handlePromptsList(const std::string& idJson) {
2226 return makeResult(
2227 idJson,
2228 "{\"prompts\":["
2229 "{\"name\":\"debug_failure\",\"description\":\"Investigate the latest script/render error using MCP tools and "
2230 "the error slice.\"},"
2231 "{\"name\":\"test_scenario\",\"description\":\"Drive a reproducible in-engine test: pause, snapshot, eval "
2232 "assertions, continue.\"},"
2233 "{\"name\":\"ai_game_review\",\"description\":\"Review live game state for AI-generated content issues.\"}"
2234 "]}");
2235}
2236
2237std::string handlePromptsGet(const std::string& idJson, Poco::JSON::Object::Ptr params) {
2238 if (!params || !params->has("name")) return makeError(idJson, -32602, "prompts/get requires params.name");
2239 std::string name;
2240 try {
2241 name = params->get("name").convert<std::string>();
2242 } catch (...) {
2243 return makeError(idJson, -32602, "prompts/get params.name must be a string");
2244 }
2245 std::string text;
2246 if (name == "debug_failure") {
2247 text =
2248 "You are debugging an EVEngine game via MCP.\n"
2249 "1) Call eve_status and read resource eve://error-report.\n"
2250 "2) Use eve_stack / eve_locals / eve_eval to inspect state.\n"
2251 "3) Use eve_error_slice to narrow script/render causes.\n"
2252 "4) Propose a minimal fix; verify with eve_run_script or snapshot restore.";
2253 } else if (name == "test_scenario") {
2254 text =
2255 "Design a short automated test against the live EVEngine session:\n"
2256 "1) Read game.agent.json via eve_play op=status, then observe declared script roots.\n"
2257 "2) Pause, step host frames, capture an engine screenshot, and checkpoint restore with eve_play.\n"
2258 "3) Use eve_gameplay for player-equivalent domain commands when the contract declares a gameplay-domain.\n"
2259 "4) Do not use eve_eval as the official observation path.";
2260 } else if (name == "ai_game_review") {
2261 text =
2262 "Review this AI-generated or AI-assisted game build:\n"
2263 "1) eve_status + eve_ai_log for recent agent actions.\n"
2264 "2) Spot-check critical roots with eve_eval.\n"
2265 "3) If unstable, pause and capture a snapshot.\n"
2266 "4) Summarize risks and suggested script fixes.";
2267 } else {
2268 return makeError(idJson, -32602, "Unknown prompt: " + name);
2269 }
2270
2271 Poco::JSON::Object::Ptr msg = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2272 msg->set("role", "user");
2273 Poco::JSON::Array::Ptr content = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
2274 Poco::JSON::Object::Ptr part = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2275 part->set("type", "text");
2276 part->set("text", text);
2277 content->add(part);
2278 msg->set("content", content);
2279 Poco::JSON::Array::Ptr messages = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
2280 messages->add(msg);
2281 Poco::JSON::Object::Ptr result = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2282 result->set("description", name);
2283 result->set("messages", messages);
2284 return makeResult(idJson, mcpStringify(Poco::Dynamic::Var(result)));
2285}
2286
2287} // namespace
2288
2290 // Process-immortal singleton: the stdio reader thread (detached when
2291 // reading stdin) and cross-singleton calls make destruction unsafe; see
2292 // devtools/Immortal.hpp.
2293 return Immortal<McpServer>::get();
2294}
2295
2296McpServer::McpServer() = default;
2297
2298McpServer::~McpServer() {
2299 // Only reached if someone deletes the instance; keep sockets tidy without
2300 // touching AiPanel (may already be torn down in other lifetime models).
2301 std::lock_guard<std::mutex> lock(ioMu_);
2302 if (client_) {
2303 try {
2304 client_->close();
2305 } catch (...) {
2306 }
2307 client_.reset();
2308 }
2309 if (server_) {
2310 try {
2311 server_->close();
2312 } catch (...) {
2313 }
2314 server_.reset();
2315 }
2316 stdioQueue_.clear();
2317 listening_.store(false);
2318 hasClient_.store(false);
2319 port_.store(0);
2320 transport_.store(Transport::None);
2321}
2322
2323void McpServer::setGameRoot(std::string root) {
2324 for (char& c : root) {
2325 if (c == '\\') c = '/';
2326 }
2327 while (!root.empty() && (root.back() == '/' || root.back() == '\\')) root.pop_back();
2328 gameRoot_ = std::move(root);
2329}
2330
2331int McpServer::listen(uint16_t port) {
2332 stop();
2333 try {
2334 Poco::Net::SocketAddress addr("127.0.0.1", port);
2335 server_ = std::make_unique<Poco::Net::ServerSocket>(addr);
2336 server_->setBlocking(false);
2337 const int bound = static_cast<int>(server_->address().port());
2338 port_.store(bound);
2339 listening_.store(true);
2340 initialized_ = false;
2341 try {
2342 if (gameRoot_.empty()) setGameRoot(std::filesystem::current_path().string());
2343 } catch (...) {
2344 }
2347 AiPanel::instance().addLog("system", "mcp.listen", "listening on 127.0.0.1:" + std::to_string(bound));
2348 transport_.store(Transport::Tcp);
2349 return bound;
2350 } catch (...) {
2351 server_.reset();
2352 listening_.store(false);
2353 port_.store(0);
2354 transport_.store(Transport::None);
2356 return 0;
2357 }
2358}
2359
2360bool McpServer::listenStdio(std::istream& in, std::ostream& out) {
2361 stop();
2362 {
2363 std::lock_guard<std::mutex> lock(ioMu_);
2364 stdioQueue_.clear();
2365 }
2366 transport_.store(Transport::Stdio);
2367 listening_.store(true);
2368 port_.store(0);
2369 initialized_ = false;
2370 stdinClosed_.store(false);
2371 hasClient_.store(true);
2372 stdioIn_ = &in;
2373 stdioOut_ = &out;
2374 joinReader_ = (&in != &std::cin);
2375 try {
2376 stdioReader_ = std::thread([this, &in]() {
2377 std::string line;
2378 while (listening_.load() && std::getline(in, line)) {
2379 if (!line.empty() && line.back() == '\r') line.pop_back();
2380 if (line.empty()) continue;
2381 {
2382 std::lock_guard<std::mutex> lock(ioMu_);
2383 stdioQueue_.push_back(std::move(line));
2384 }
2385 }
2386 stdinClosed_.store(true);
2387 hasClient_.store(false);
2388 });
2389 } catch (...) {
2390 transport_.store(Transport::None);
2391 listening_.store(false);
2392 hasClient_.store(false);
2393 return false;
2394 }
2397 AiPanel::instance().addLog("system", "mcp.listen", "stdio transport ready (newline JSON-RPC)");
2398 return true;
2399}
2400
2401void McpServer::stop() {
2402 listening_.store(false);
2403 hasClient_.store(false);
2404 if (stdioReader_.joinable()) {
2405 if (joinReader_) {
2406 stdioReader_.join();
2407 } else {
2408 stdioReader_.detach();
2409 }
2410 }
2411 std::lock_guard<std::mutex> lock(ioMu_);
2412 if (client_) {
2413 try {
2414 client_->close();
2415 } catch (...) {
2416 }
2417 client_.reset();
2418 }
2419 if (server_) {
2420 try {
2421 server_->close();
2422 } catch (...) {
2423 }
2424 server_.reset();
2425 }
2426 stdioQueue_.clear();
2427 recvBuf_.clear();
2428 port_.store(0);
2429 transport_.store(Transport::None);
2430 initialized_ = false;
2431 stdioIn_ = nullptr;
2432 stdioOut_ = nullptr;
2433 AiPanel::instance().setMcpPort(0);
2434 AiPanel::instance().setMcpConnected(false);
2435 AiPanel::instance().setClientName({});
2436}
2437
2438void McpServer::poll() {
2439 if (!listening_.load()) return;
2440 if (transport_.load() == Transport::Stdio) {
2441 std::vector<std::string> batch;
2442 {
2443 std::lock_guard<std::mutex> lock(ioMu_);
2444 batch.swap(stdioQueue_);
2445 }
2446 for (const auto& line : batch) handleMessage(line);
2447 runPendingVisionDump();
2448 return;
2449 }
2450 acceptNonBlocking();
2451 readAndDispatch();
2452 runPendingVisionDump();
2453}
2454
2455void McpServer::acceptNonBlocking() {
2456 if (!server_ || client_) return;
2457 try {
2458 Poco::Net::SocketAddress clientAddr;
2459 Poco::Net::StreamSocket ss = server_->acceptConnection(clientAddr);
2460 ss.setBlocking(true);
2461 ss.setReceiveTimeout(Poco::Timespan(0, 1000));
2462 client_ = std::make_unique<Poco::Net::StreamSocket>(ss);
2463 hasClient_.store(true);
2464 recvBuf_.clear();
2465 initialized_ = false;
2466 AiPanel::instance().setMcpConnected(true);
2467 AiPanel::instance().addLog("system", "mcp.accept", "client connected");
2468 } catch (const Poco::TimeoutException&) {
2469 } catch (const Poco::Net::NetException&) {
2470 } catch (...) {
2471 }
2472}
2473
2474bool McpServer::sendLine(const std::string& json) {
2475 if (transport_.load() == Transport::Stdio) {
2476 std::lock_guard<std::mutex> lock(ioMu_);
2477 if (!stdioOut_) return false;
2478 try {
2479 const std::string frame = json + "\n";
2480 (*stdioOut_) << frame;
2481 stdioOut_->flush();
2482 return true;
2483 } catch (...) {
2484 return false;
2485 }
2486 }
2487 if (!client_) return false;
2488 const std::string frame = json + "\n";
2489 try {
2490 const int sent = client_->sendBytes(frame.data(), static_cast<int>(frame.size()));
2491 return sent == static_cast<int>(frame.size());
2492 } catch (...) {
2493 client_.reset();
2494 hasClient_.store(false);
2495 AiPanel::instance().setMcpConnected(false);
2496 return false;
2497 }
2498}
2499
2500void McpServer::readAndDispatch() {
2501 if (!client_) return;
2502 char buf[8192];
2503 try {
2504 const int n = client_->receiveBytes(buf, sizeof(buf));
2505 if (n <= 0) {
2506 if (n == 0) {
2507 client_.reset();
2508 hasClient_.store(false);
2509 AiPanel::instance().setMcpConnected(false);
2510 AiPanel::instance().addLog("system", "mcp.disconnect", "client closed");
2511 }
2512 return;
2513 }
2514 recvBuf_.append(buf, static_cast<size_t>(n));
2515 } catch (const Poco::TimeoutException&) {
2516 return;
2517 } catch (const Poco::Net::NetException&) {
2518 return;
2519 } catch (...) {
2520 client_.reset();
2521 hasClient_.store(false);
2522 AiPanel::instance().setMcpConnected(false);
2523 return;
2524 }
2525
2526 while (true) {
2527 const auto nl = recvBuf_.find('\n');
2528 if (nl == std::string::npos) break;
2529 std::string line = recvBuf_.substr(0, nl);
2530 recvBuf_.erase(0, nl + 1);
2531 if (!line.empty() && line.back() == '\r') line.pop_back();
2532 if (line.empty()) continue;
2533 handleMessage(line);
2534 }
2535}
2536
2537void McpServer::handleMessage(const std::string& json) {
2538 try {
2539 Poco::JSON::Parser parser;
2540 auto var = parser.parse(json);
2541 auto obj = var.extract<Poco::JSON::Object::Ptr>();
2542 if (!obj) return;
2543
2544 std::string method;
2545 if (obj->has("method")) {
2546 try {
2547 method = obj->get("method").convert<std::string>();
2548 } catch (const Poco::Exception& e) {
2549 sendLine(makeError("null", -32600, std::string("bad method: ") + e.displayText()));
2550 return;
2551 }
2552 }
2553 const bool hasId = obj->has("id");
2554 Poco::Dynamic::Var idVar;
2555 if (hasId) idVar = obj->get("id");
2556 std::string idJson = "null";
2557 try {
2558 if (hasId) idJson = idToJson(idVar);
2559 } catch (const Poco::Exception& e) {
2560 sendLine(makeError("null", -32600, std::string("bad id: ") + e.displayText()));
2561 return;
2562 }
2563
2564 if (!hasId) {
2565 if (method == "notifications/initialized" || method == "initialized") {
2566 initialized_ = true;
2567 AiPanel::instance().addLog("system", "mcp.initialized", "handshake complete");
2568 }
2569 return;
2570 }
2571
2572 auto params = [&]() -> Poco::JSON::Object::Ptr {
2573 if (!obj->has("params")) return Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2574 try {
2575 return obj->getObject("params");
2576 } catch (...) {
2577 return Poco::JSON::Object::Ptr(new Poco::JSON::Object());
2578 }
2579 };
2580
2581 if (method == "initialize") {
2582 sendLine(handleInitialize(*this, idJson, params()));
2583 return;
2584 }
2585 if (method == "ping") {
2586 sendLine(makeResult(idJson, "{}"));
2587 return;
2588 }
2589 if (method == "tools/list") {
2590 sendLine(handleToolsList(idJson));
2591 return;
2592 }
2593 if (method == "tools/call") {
2594 sendLine(handleToolsCall(*this, idJson, params()));
2595 return;
2596 }
2597 if (method == "resources/list") {
2598 sendLine(handleResourcesList(idJson));
2599 return;
2600 }
2601 if (method == "resources/read") {
2602 sendLine(handleResourcesRead(idJson, params()));
2603 return;
2604 }
2605 if (method == "prompts/list") {
2606 sendLine(handlePromptsList(idJson));
2607 return;
2608 }
2609 if (method == "prompts/get") {
2610 sendLine(handlePromptsGet(idJson, params()));
2611 return;
2612 }
2613
2614 sendLine(makeError(idJson, -32601, "Method not found: " + method));
2615 } catch (const Poco::Exception& e) {
2616 sendLine(makeError("null", -32700, std::string("Parse error: ") + e.displayText()));
2617 } catch (const std::exception& e) {
2618 sendLine(makeError("null", -32603, e.what()));
2619 } catch (...) {
2620 sendLine(makeError("null", -32603, "Internal error"));
2621 }
2622}
2623
2624} // namespace eve::dev
LogicalId target
ActionParameterOperation operation
double value
Value::Object payload
Duration start
SQInteger top
float w
Definition AnimClip.cpp:738
int observer
Definition AnimSmr.cpp:162
int root
Definition AnimSmr.cpp:119
std::string output
std::string descriptor
OwnedAudio audio
const std::string & s
std::vector< QuestEvent > pending
struct SQVM * HSQUIRRELVM
std::string title
eve::Value condition
HSQUIRRELVM vm
Definition ECS.cpp:20
std::uint16_t method
Versioned JSON facade shared by scripting, MCP and scenario tooling.
std::string message
DiagnosticCode code
const GltfImportRequest & request
std::uint32_t key
wgpu::PopErrorScopeStatus status
glm::vec4 tint
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
double r
float v
std::int32_t c
std::int32_t first
int h
std::string text
std::array< float, 3 > position
std::uint32_t bone
Capability-backed domain tool families for the embedded MCP server.
std::uint64_t bytes
bool exists
std::string name
Project-script → MCP export registry.
bool valid
double maxError
std::vector< std::string > mismatches
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< BvhNode > nodes
int level
const std::string * tag
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
std::string error
Definition Package.cpp:60
float f
std::string action
Definition PlayHost.cpp:117
std::string script
Definition PlayHost.cpp:116
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
Versioned Play Host protocol for Agent pause / step / observe / capture.
std::string uri
std::uint32_t generation
std::weak_ptr< PrimitiveScene > scene
float d
int detail
glm::vec3 eye
const RoadNode * node
bool found
std::string string
std::uint32_t count
int limit
Definition TreeMesh.cpp:164
int children
Definition TreeMesh.cpp:295
float size
Definition TreeMesh.cpp:156
V3 dir
Definition TreeMesh.cpp:150
UIHostHandle host
uint32_t index
const UnitySourceAsset & source
Audio control surface (provided by the audio module).
Definition AudioQuery.h:8
Module-neutral bridge from DevTools/MCP to the editor command protocol.
Declarative editor-host control (provided by the ui module).
Definition EditorHost.h:17
Particle system query surface (provided by the particles module).
2D physics world query surface (provided by the physics module).
Module-neutral JSON bridge from DevTools/MCP to PixelWorld tooling.
Procedural generation query surface (provided by the procgen module).
Frame capture + camera + visible-entity inspection (graphics).
virtual bool savePng(const std::string &path, int *outWidth, int *outHeight, std::string *err)=0
Capture the last presented frame and write a PNG.
virtual Result< Renderable3DInfo > inspectRenderable3D(std::uint32_t entityId, std::uint32_t generation)=0
Inspect one live Renderable3D using its complete ECS identity.
virtual RenderStatusInfo status() const =0
Status.
Scene graph query/mutation surface (provided by the scene module).
Definition SceneQuery.h:30
Semantic retained-UI automation provided by the ui module.
void addNote(std::string text)
Definition AiPanel.cpp:95
std::string formatLog(size_t max=64) const
Definition AiPanel.cpp:106
void setMcpPort(int port)
Definition AiPanel.cpp:49
void addLog(std::string kind, std::string title, std::string detail={})
Definition AiPanel.cpp:84
static AiPanel & instance()
Definition AiPanel.cpp:15
void setMcpConnected(bool on)
Definition AiPanel.cpp:59
void setClientName(std::string name)
Definition AiPanel.cpp:69
static DebugAdapter & instance()
static Debugger & instance()
Definition Debugger.cpp:387
static DevTool & instance()
Definition DevTool.cpp:132
Embedded Model Context Protocol (MCP) server for AI-assisted game development.
Definition McpServer.hpp:35
void setGameRoot(std::string root)
Game / project directory hint for agents.
int listen(uint16_t port)
Bind TCP listen port (0 = ephemeral). Returns bound port or 0 on failure.
bool listenStdio(std::istream &in=std::cin, std::ostream &out=std::cout)
Switch to stdio transport (MCP stdio server). A reader thread pulls newline-delimited JSON from in in...
static McpServer & instance()
static RenderVision & instance()
void pollPending(eve::IRenderCapture *cap, const std::string &renderDataJson)
McpServer::poll hook: performs one pending dump when safe, clears flag.
std::string describe(eve::IRenderCapture *cap, const std::string &renderDataJson, bool fresh, const std::string &reason={})
Main-thread capture + vision describe. Returns the description text, or a string starting with "error...
std::string currentPoseJson()
当前激活相机位姿 JSON(eye/target/fov/viewport)。
std::string visibleEntitiesJson(const glm::vec3 *eye=nullptr, const glm::vec3 *target=nullptr, float fov=0.f)
生成当前视锥内可见实体的结构化 JSON: { camera, viewport, entities:[ { id, asset, world_aabb:{min,...
InspectCapture capture(const std::string &outDir={}, const std::string &tag="frame", const std::vector< std::string > &buffers={})
锁定当前相机 → 捕获渲染帧 PNG → 立刻导出配套可见实体几何 JSON。 两者共享同一相机位姿,杜绝图片与实体数据错位。 outDir 为空时使用缓存目录。返回文件路径与图像尺寸。 buffers...
static SceneInspect & instance()
bool setCameraPose(const glm::vec3 &eye, const glm::vec3 &rotYawPitch, float fov=0.f)
以 pos + 欧拉角 rot 设置相机位姿。 rot = [yawDeg, pitchDeg](与 firstperson 约定一致,Y-up)。 fov <= 0 时保持当前 FOV。
static std::vector< InspectView > generateViews(const glm::vec3 &center, float fov=60.f)
围绕 center 自动生成一组标准化巡检机位:
std::string capture(HSQUIRRELVM vm, std::string *error=nullptr) const
Capture marked roots, or heuristic roots when none marked.
Definition Snapshot.cpp:438
static Snapshot & instance()
Definition Snapshot.cpp:290
std::vector< BindingSearchHit > search(std::string_view query, std::string_view moduleFilter={}, std::string_view classFilter={}, size_t limit=20) const
Ranked substring/token search over keys, classes, methods and documentation ids.
std::vector< ParamSpec > params
I * query()
Queries .
Definition Capability.h:88
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 isMcpDomainTool(std::string_view name)
Whether name belongs to a capability-backed domain tool family.
int scriptToolCount()
Number of exported tools (for eve_status).
std::size_t mcpCallgraphStackDepth()
Definition DevTool.cpp:868
bool isMcpRuntimeTool(std::string_view name)
Whether name belongs to the MCP runtime console / frame-capture surface.
float getArgFloat(Poco::JSON::Object::Ptr args, const char *key, float def=0.f)
Floating-point argument, or def when absent or not convertible.
Definition McpArgs.hpp:55
Result< std::string > executePlayJson(std::string_view requestJson)
Parse, execute and serialize one Play JSON request against the live engine.
Definition PlayHost.cpp:866
std::string mcpJsonEscape(const std::string &value)
Escape a raw string for embedding inside a JSON string literal.
Definition McpJson.hpp:30
Poco::Dynamic::Var getArgVar(Poco::JSON::Object::Ptr args, const char *key)
Raw JSON value of key, or an empty Var when absent.
Definition McpArgs.hpp:15
std::string callScriptTool(const std::string &name, Poco::JSON::Object::Ptr args)
Invoke one exported tool.
std::size_t mcpCallgraphEvents()
Definition DevTool.cpp:866
std::string_view mcpDomainToolSchemas()
Comma-free MCP tool schema fragments for the domain families.
bool isAgentDevelopmentTool(std::string_view name)
Whether a tool name belongs to the Agent development-session protocol.
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 mcpFormatError(const std::string &message)
Definition DevTool.cpp:874
const std::string & mcpLastReport()
Definition DevTool.cpp:872
std::string_view agentDevelopmentToolSchemas()
Comma-separated MCP tool schema fragments without surrounding array brackets.
bool mcpDevAttached()
Thin hooks so McpServer.cpp need not include DevTool.hpp (avoids a cycle).
Definition DevTool.cpp:864
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.
std::string scriptToolSchemas()
Comma-free MCP tool schema fragments for every exported tool.
std::string_view mcpRuntimeToolSchemas()
Comma-separated MCP tool schema fragments without surrounding array brackets.
std::string sqValueToJson(HSQUIRRELVM vm, SQInteger idx, int depth=0)
Forward declaration: the key serializer falls back to the value serializer.
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
bool isScriptTool(const std::string &name)
Whether name is exported by the project script registry.
std::string callMcpDomainTool(std::string_view name, Poco::JSON::Object::Ptr args)
Execute one domain tool.
std::string callAgentDevelopmentTool(std::string_view name, Poco::JSON::Object::Ptr args)
Execute one Agent development-session tool and return compact JSON.
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
@ Snapshot
Script state was restored while the debugger was already paused.
Result< int > publish(Heightmap &target, std::vector< float > candidate)
Publish.
Result< int > invalid(std::string message)
Invalid.
std::string formatScriptError(const ScriptErrorContext &ctx)
Formats a context into a human-readable multi-line report.
ScriptErrorContext takeLastScriptError(HSQUIRRELVM vm)
Consumes and clears the last recorded error for a VM.
ScriptErrorContext captureCompileError(HSQUIRRELVM vm)
Captures the last compilation error recorded by the VM.
const BindingContractRegistry & generatedBindingContracts()
Process-wide generated Binding Contracts for this linked engine build.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
Result< std::string > executeGameplayControlJson(std::string_view requestJson)
Parse, execute and serialize one gameplay-control JSON request.
const char * scene_director_content
Ray-cast result (value type).
Render surface / frame state snapshot (value type).
Serializable snapshot of one scene node (value type, no scene types).
Definition SceneQuery.h:17
static T & get()
Returns the process-lifetime instance.
Definition Immortal.hpp:21
Complete script-facing contract for a native function or method.
std::string toJson() const
Compact JSON object for one contract (eve.binding-api item).
Structured snapshot of a script error: message, throw site and stack.
Definition ScriptError.h:25
bool empty() const noexcept
True when no error payload was captured.
Definition ScriptError.h:36
glm::uvec4 info