载入中...
搜索中...
未找到
McpServer.cpp
浏览该文件的文档.
3
11#include "devtools/Snapshot.hpp"
12
13#include "scripts.h"
14
15#include "common/AudioQuery.h"
16#include "common/Capability.h"
17#include "common/EditorHost.h"
18#include "common/Module.h"
20#include "common/PhysicsQuery.h"
21#include "common/ProcgenQuery.h"
23#include "common/SceneQuery.h"
24#include "common/ScriptError.h"
25
26#include <Poco/Dynamic/Var.h>
27#include <Poco/Exception.h>
28#include <Poco/JSON/Array.h>
29#include <Poco/JSON/Object.h>
30#include <Poco/JSON/Parser.h>
31#include <Poco/JSON/Stringifier.h>
32#include <Poco/Net/NetException.h>
33#include <Poco/Net/ServerSocket.h>
34#include <Poco/Net/SocketAddress.h>
35#include <Poco/Net/StreamSocket.h>
36#include <Poco/Timespan.h>
37
38#include <squirrel.h>
39#include <glm/glm.hpp>
40
41#include <cstdio>
42#include <cstdlib>
43#include <filesystem>
44#include <sstream>
45#include <vector>
46
47namespace eve::dev {
48namespace {
49
50std::string mcpStringify(const Poco::Dynamic::Var& v) {
51 std::ostringstream oss;
52 // indent=0, step=0 => compact single-line JSON (required for newline framing)
53 Poco::JSON::Stringifier::stringify(v, oss, 0, 0);
54 return oss.str();
55}
56
57std::string mcpJsonEscape(const std::string& s) {
58 std::string out;
59 out.reserve(s.size() + 8);
60 for (char c : s) {
61 switch (c) {
62 case '"':
63 out += "\\\"";
64 break;
65 case '\\':
66 out += "\\\\";
67 break;
68 case '\n':
69 out += "\\n";
70 break;
71 case '\r':
72 out += "\\r";
73 break;
74 case '\t':
75 out += "\\t";
76 break;
77 default:
78 if (static_cast<unsigned char>(c) < 0x20) {
79 char buf[8];
80 std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned>(c));
81 out += buf;
82 } else {
83 out += c;
84 }
85 break;
86 }
87 }
88 return out;
89}
90
91std::string idToJson(const Poco::Dynamic::Var& id) {
92 if (id.isEmpty()) return "null";
93 try {
94 if (id.isInteger() || id.isNumeric()) return std::to_string(id.convert<Poco::Int64>());
95 } catch (...) {
96 }
97 try {
98 return std::string("\"") + mcpJsonEscape(id.convert<std::string>()) + "\"";
99 } catch (...) {
100 return "null";
101 }
102}
103
104std::string makeResult(const std::string& idJson, const std::string& resultJson) {
105 return std::string("{\"jsonrpc\":\"2.0\",\"id\":") + idJson + ",\"result\":" + resultJson + "}";
106}
107
108std::string makeError(const std::string& idJson, int code, const std::string& message) {
109 return std::string("{\"jsonrpc\":\"2.0\",\"id\":") + idJson +
110 ",\"error\":{\"code\":" + std::to_string(code) + ",\"message\":\"" + mcpJsonEscape(message) +
111 "\"}}";
112}
113
114std::string textContentResult(const std::string& text, bool isError = false) {
115 Poco::JSON::Object::Ptr result = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
116 Poco::JSON::Array::Ptr content = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
117 Poco::JSON::Object::Ptr item = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
118 item->set("type", "text");
119 item->set("text", text);
120 content->add(item);
121 result->set("content", content);
122 if (isError) result->set("isError", true);
123 return mcpStringify(Poco::Dynamic::Var(result));
124}
125
126std::string pauseReasonName(PauseReason r) {
127 switch (r) {
129 return "breakpoint";
131 return "step";
133 return "exception";
135 return "snapshot";
137 return "pause";
138 default:
139 return "none";
140 }
141}
142
143std::string engineStatusJson(const McpServer& mcp) {
144 auto& dbg = Debugger::instance();
145 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
146 o->set("attached", mcpDevAttached());
147 o->set("paused", dbg.isPaused());
148 o->set("pauseReason", pauseReasonName(dbg.lastPauseReason()));
149 const auto& loc = dbg.pauseLocation();
150 o->set("source", loc.source);
151 o->set("line", loc.line);
152 o->set("function", loc.function);
153 o->set("mcpPort", mcp.port());
154 o->set("mcpConnected", mcp.hasClient());
155 switch (mcp.transport()) {
157 o->set("transport", "stdio");
158 break;
160 o->set("transport", "tcp");
161 break;
162 default:
163 o->set("transport", "none");
164 break;
165 }
166 o->set("dapPort", DebugAdapter::instance().port());
167 o->set("gameRoot", mcp.gameRoot());
169 : std::string("unavailable"));
170 o->set("callgraphEvents", static_cast<int>(mcpCallgraphEvents()));
171 o->set("ai", AiPanel::instance().statusLine());
172 return mcpStringify(Poco::Dynamic::Var(o));
173}
174
175Poco::Dynamic::Var argVar(Poco::JSON::Object::Ptr args, const char* key) {
176 if (!args || !args->has(key)) return Poco::Dynamic::Var();
177 try {
178 return args->get(key);
179 } catch (...) {
180 return Poco::Dynamic::Var();
181 }
182}
183
184std::string argString(Poco::JSON::Object::Ptr args, const char* key, const std::string& def = {}) {
185 if (!args || !args->has(key)) return def;
186 try {
187 return args->get(key).convert<std::string>();
188 } catch (...) {
189 return def;
190 }
191}
192
193int argInt(Poco::JSON::Object::Ptr args, const char* key, int def = 0) {
194 if (!args || !args->has(key)) return def;
195 try {
196 return args->get(key).convert<int>();
197 } catch (...) {
198 return def;
199 }
200}
201
202float argFloat(Poco::JSON::Object::Ptr args, const char* key, float def = 0.f) {
203 if (!args || !args->has(key)) return def;
204 try {
205 return static_cast<float>(args->get(key).convert<double>());
206 } catch (...) {
207 return def;
208 }
209}
210
211glm::vec3 argVec3(Poco::JSON::Object::Ptr args, const char* key, const glm::vec3& def = {}) {
212 if (!args || !args->has(key)) return def;
213 try {
214 auto arr = args->getArray(key);
215 if (arr && arr->size() >= 3) {
216 return glm::vec3(static_cast<float>(arr->get(0).convert<double>()),
217 static_cast<float>(arr->get(1).convert<double>()),
218 static_cast<float>(arr->get(2).convert<double>()));
219 }
220 } catch (...) {
221 }
222 return def;
223}
224
225Poco::JSON::Array::Ptr vec3ToArray(const glm::vec3& v) {
226 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
227 arr->add(v.x);
228 arr->add(v.y);
229 arr->add(v.z);
230 return arr;
231}
232
233bool argBool(Poco::JSON::Object::Ptr args, const char* key, bool def = false) {
234 if (!args || !args->has(key)) return def;
235 try {
236 return args->get(key).convert<bool>();
237 } catch (...) {
238 return def;
239 }
240}
241
242// ============================= Scene Director (AI scene-authoring) =============
243// Thin C++ dispatchers over the Squirrel `scene_director` kit (src/scripts/
244// scene_director.nut, embedded as eve::scene_director_content). Agents build /
245// inspect scenes through `eve_scene_modify` / `eve_scene_info` /
246// `eve_camera_generate` / `eve_scene_reset`; the kit owns the live Renderable3D /
247// Camera3D / lighting state.
248
249// Escape a string as a Squirrel single-line string literal (JSON escapes are
250// a subset Squirrel understands: \n \r \t \\ \" and \uXXXX).
251std::string sqStringLiteralEscape(const std::string& s) { return mcpJsonEscape(s); }
252
253// Encode a Poco JSON value as a Squirrel literal (table/array/scalar/null).
254std::string sqLiteralValue(const Poco::Dynamic::Var& v) {
255 if (v.isEmpty()) return "null";
256 if (v.isBoolean()) return v.convert<bool>() ? "true" : "false";
257 if (v.isInteger()) return std::to_string(v.convert<Poco::Int64>());
258 if (v.isNumeric()) {
259 const double d = v.convert<double>();
260 char buf[64];
261 std::snprintf(buf, sizeof(buf), "%g", d);
262 return buf;
263 }
264 if (v.isString())
265 return std::string("\"") + sqStringLiteralEscape(v.convert<std::string>()) + "\"";
266 if (v.isArray()) {
267 std::string out = "[";
268 try {
269 auto arr = v.extract<Poco::JSON::Array::Ptr>();
270 for (size_t i = 0; i < arr->size(); ++i) {
271 if (i) out += ",";
272 out += sqLiteralValue(arr->get(i));
273 }
274 } catch (...) {
275 }
276 out += "]";
277 return out;
278 }
279 if (v.isStruct()) {
280 std::string out = "{";
281 bool first = true;
282 try {
283 auto obj = v.extract<Poco::JSON::Object::Ptr>();
284 for (const auto& kv : *obj) {
285 if (!first) out += ",";
286 first = false;
287 out += "\"" + sqStringLiteralEscape(kv.first) + "\"=" + sqLiteralValue(kv.second);
288 }
289 } catch (...) {
290 }
291 out += "}";
292 return out;
293 }
294 return "null";
295}
296
297std::string snippetErrorText(HSQUIRRELVM vm, bool compile) {
299 : eve::script::takeLastScriptError(vm);
300 if (!ctx.empty()) return eve::script::formatScriptError(ctx);
301 return compile ? "compile failed" : "runtime failed";
302}
303
304// Compile + run a snippet against the live VM (no return value captured).
305bool runVmSnippet(HSQUIRRELVM vm, const std::string& source, std::string* err) {
306 const SQInteger top = sq_gettop(vm);
307 if (SQ_FAILED(sq_compilebuffer(vm, source.c_str(), static_cast<SQInteger>(source.size()),
308 _SC("mcp_snippet.nut"), SQTrue))) {
309 sq_settop(vm, top);
310 if (err) *err = snippetErrorText(vm, true);
311 return false;
312 }
313 sq_pushroottable(vm);
314 if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
315 sq_settop(vm, top);
316 if (err) *err = snippetErrorText(vm, false);
317 return false;
318 }
319 sq_settop(vm, top);
320 return true;
321}
322
323// Serialize a Squirrel value (at stack idx) to compact JSON.
324std::string sqValueToJson(HSQUIRRELVM vm, SQInteger idx) {
325 if (idx < 0) idx = sq_gettop(vm) + idx + 1; // normalize relative -> absolute
326 switch (sq_gettype(vm, idx)) {
327 case OT_NULL:
328 return "null";
329 case OT_BOOL: {
330 SQBool b = SQFalse;
331 sq_getbool(vm, idx, &b);
332 return b ? "true" : "false";
333 }
334 case OT_INTEGER: {
335 SQInteger i = 0;
336 sq_getinteger(vm, idx, &i);
337 return std::to_string(i);
338 }
339 case OT_FLOAT: {
340 SQFloat f = 0;
341 sq_getfloat(vm, idx, &f);
342 char buf[64];
343 std::snprintf(buf, sizeof(buf), "%g", static_cast<double>(f));
344 return buf;
345 }
346 case OT_STRING: {
347 const SQChar* s = nullptr;
348 sq_getstring(vm, idx, &s);
349 return std::string("\"") + mcpJsonEscape(s ? s : "") + "\"";
350 }
351 case OT_ARRAY: {
352 std::string out = "[";
353 bool first = true;
354 sq_pushnull(vm);
355 while (SQ_SUCCEEDED(sq_next(vm, idx))) {
356 if (!first) out += ",";
357 first = false;
358 out += sqValueToJson(vm, -1);
359 sq_pop(vm, 2);
360 }
361 sq_pop(vm, 1);
362 out += "]";
363 return out;
364 }
365 case OT_TABLE: {
366 std::string out = "{";
367 bool first = true;
368 sq_pushnull(vm);
369 while (SQ_SUCCEEDED(sq_next(vm, idx))) {
370 if (!first) out += ",";
371 first = false;
372 out += sqValueToJson(vm, -2);
373 out += ":";
374 out += sqValueToJson(vm, -1);
375 sq_pop(vm, 2);
376 }
377 sq_pop(vm, 1);
378 out += "}";
379 return out;
380 }
381 default:
382 return "\"<unserializable>\"";
383 }
384}
385
386// Compile a snippet that returns a value, run it, and return the JSON of the
387// return value (e.g. `return ::scene_director.info();`).
388std::string callSceneDirectorReturn(HSQUIRRELVM vm, const std::string& snippet,
389 std::string* err) {
390 const SQInteger top = sq_gettop(vm);
391 if (SQ_FAILED(sq_compilebuffer(vm, snippet.c_str(), static_cast<SQInteger>(snippet.size()),
392 _SC("mcp_scene_director.nut"), SQTrue))) {
393 sq_settop(vm, top);
394 if (err) *err = snippetErrorText(vm, true);
395 return {};
396 }
397 sq_pushroottable(vm);
398 if (SQ_FAILED(sq_call(vm, 1, SQTrue, SQTrue))) {
399 sq_settop(vm, top);
400 std::string text = snippetErrorText(vm, false);
401 if (err)
402 *err = text == "runtime failed"
403 ? "runtime failed (is the scene_director kit installed?)"
404 : std::move(text);
405 return {};
406 }
407 std::string json = sqValueToJson(vm, -1);
408 sq_settop(vm, top);
409 return json;
410}
411
412// Install the scene_director kit into the live VM (idempotent).
413bool ensureSceneDirectorInstalled(HSQUIRRELVM vm, std::string* err) {
414 const std::string check =
415 "return (\"scene_director\" in getroottable()) ? (scene_director != null) : false;";
416 const std::string out = callSceneDirectorReturn(vm, check, nullptr);
417 if (!out.empty() && out.find("true") != std::string::npos) return true;
418 const char* kit = eve::scene_director_content;
419 if (!kit || !*kit) {
420 if (err) *err = "scene_director.nut not embedded (rebuild EVScripts)";
421 return false;
422 }
423 return runVmSnippet(vm, kit, err);
424}
425
426std::string sceneDirectorToolError(const std::string& name, const std::string& err) {
427 return "error: " + name + ": " + (err.empty() ? "unknown" : err);
428}
429
430std::string renderStatusText(eve::IRenderCapture* cap) {
431 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
432 if (!cap) {
433 o->set("error", "Graphics module not available");
434 return mcpStringify(Poco::Dynamic::Var(o));
435 }
436 const eve::RenderStatusInfo s = cap->status();
437 o->set("width", s.width);
438 o->set("height", s.height);
439 o->set("pixelWidth", s.pixelWidth);
440 o->set("pixelHeight", s.pixelHeight);
441 o->set("had3DThisFrame", s.had3DThisFrame);
442 o->set("readbackEnabled", s.readbackEnabled);
443 o->set("backend", s.backend);
444 o->set("renderFlowEvents", static_cast<int>(DevTool::instance().renderFlow().eventCount()));
445 return mcpStringify(Poco::Dynamic::Var(o));
446}
447
455
456std::string callTool(McpServer& mcp, const std::string& name, Poco::JSON::Object::Ptr args) {
457 auto& dbg = Debugger::instance();
458 auto& dap = DebugAdapter::instance();
459
460 if (name == "eve_status") return engineStatusJson(mcp);
461
462 // ============================= Scene / Entity =============================
463 if (name == "eve_scene_status") {
464 auto* scene = mcpScene();
465 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
466 if (!scene) {
467 o->set("error", "Scene module not available");
468 return mcpStringify(Poco::Dynamic::Var(o));
469 }
470 const std::string host = scene->activeHost();
471 if (host.empty()) {
472 o->set("activeHost", Poco::Dynamic::Var());
473 o->set("nodeCount", 0);
474 return mcpStringify(Poco::Dynamic::Var(o));
475 }
476 o->set("activeHost", host);
477 o->set("nodeCount", scene->nodeCount());
478 o->set("rootId", scene->rootId());
479 return mcpStringify(Poco::Dynamic::Var(o));
480 }
481
482 if (name == "eve_scene_nodes") {
483 auto* scene = mcpScene();
484 if (!scene || scene->activeHost().empty()) return "error: no active scene host";
485 const int limit = argInt(args, "limit", 500);
486 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
487 for (const auto& n : scene->nodes(limit)) {
488 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
489 o->set("id", n.id);
490 o->set("name", n.name);
491 o->set("path", n.path);
492 o->set("visible", n.visible);
493 arr->add(o);
494 }
495 return mcpStringify(Poco::Dynamic::Var(arr));
496 }
497
498 if (name == "eve_scene_node_get") {
499 auto* scene = mcpScene();
500 if (!scene || scene->activeHost().empty()) return "error: no active scene host";
501 const std::string id = argString(args, "id");
503 if (id.empty() || !scene->getNode(id, &n)) return "error: node not found: " + id;
504 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
505 o->set("id", n.id);
506 o->set("name", n.name);
507 o->set("path", n.path);
508 o->set("visible", n.visible);
509 o->set("x", n.x);
510 o->set("y", n.y);
511 o->set("z", n.z);
512 o->set("yaw", n.yaw);
513 o->set("pitch", n.pitch);
514 o->set("roll", n.roll);
515 o->set("sx", n.sx);
516 o->set("sy", n.sy);
517 o->set("sz", n.sz);
518 o->set("parent", n.parent);
519 Poco::JSON::Array::Ptr kids = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
520 for (const auto& c : n.children) kids->add(c);
521 o->set("children", kids);
522 return mcpStringify(Poco::Dynamic::Var(o));
523 }
524
525 if (name == "eve_scene_node_set") {
526 auto* scene = mcpScene();
527 if (!scene || scene->activeHost().empty()) return "error: no active scene host";
528 const std::string id = argString(args, "id");
529 if (id.empty()) return "error: node not found: " + id;
530 bool changed = false;
531 if (args && args->has("x") && args->has("y") && args->has("z")) {
532 changed = scene->setNodeTransform(id, argFloat(args, "x"), argFloat(args, "y"), argFloat(args, "z"));
533 }
534 if (args && args->has("visible")) {
535 changed = scene->setNodeVisible(id, argBool(args, "visible")) || changed;
536 }
537 return changed ? "ok" : "error: node not found: " + id;
538 }
539
540 // ============================= Procgen =============================
541 if (name == "eve_procgen_recipes") {
542 auto* pg = mcpProcgen();
543 if (!pg) return "error: Procgen module not available";
544 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
545 auto addList = [&](const char* key, const std::vector<std::string>& items) {
546 Poco::JSON::Array::Ptr a = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
547 for (const auto& it : items) a->add(it);
548 o->set(key, a);
549 };
550 addList("algorithms", pg->algorithms());
551 addList("meshRecipes", pg->meshRecipes());
552 addList("textureRecipes", pg->textureRecipes());
553 addList("pbrRecipes", pg->pbrRecipes());
554 return mcpStringify(Poco::Dynamic::Var(o));
555 }
556
557 if (name == "eve_procgen_map") {
558 auto* pg = mcpProcgen();
559 if (!pg) return "error: Procgen module not available";
560 const std::string algorithm = argString(args, "algorithm");
561 if (algorithm.empty()) return "error: missing algorithm";
562 std::vector<std::pair<std::string, std::string>> params;
563 for (const auto& key : {"roomCount", "roomMin", "roomMax", "corridorWidth", "autotile",
564 "scale", "octaves"}) {
565 if (args && args->has(key)) params.emplace_back(key, std::to_string(argInt(args, key)));
566 }
567 if (args && args->has("corridorStyle")) params.emplace_back("corridorStyle", argString(args, "corridorStyle"));
568 std::string err;
569 std::string json = pg->generateMap(algorithm, argInt(args, "width", 32), argInt(args, "height", 32),
570 static_cast<uint32_t>(argInt(args, "seed", 0)), params, &err);
571 if (json.empty()) return "error: " + (err.empty() ? std::string("empty grid") : err);
572 return json;
573 }
574
575 if (name == "eve_procgen_mesh") {
576 auto* pg = mcpProcgen();
577 if (!pg) return "error: Procgen module not available";
578 const std::string recipe = argString(args, "recipe");
579 if (recipe.empty()) return "error: missing recipe";
580 std::string err;
581 std::string json =
582 pg->buildMesh(recipe, static_cast<uint32_t>(argInt(args, "seed", 0)), argInt(args, "width", -1),
583 argInt(args, "height", -1), argInt(args, "depth", -1), &err);
584 if (json.empty()) return "error: " + (err.empty() ? std::string("build failed") : err);
585 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
586 o->set("recipe", recipe);
587 try {
588 Poco::JSON::Parser parser;
589 Poco::Dynamic::Var parsed = parser.parse(json);
590 Poco::JSON::Object::Ptr jo = parsed.extract<Poco::JSON::Object::Ptr>();
591 o->set("vertices", jo->get("vertices"));
592 o->set("triangles", jo->get("triangles"));
593 } catch (...) {
594 return json;
595 }
596 return mcpStringify(Poco::Dynamic::Var(o));
597 }
598
599 // ============================= Physics =============================
600 if (name == "eve_physics_new_world") {
601 auto* ph = mcpPhysics();
602 if (!ph) return "error: Physics module not available";
603 const int id = ph->newWorld(argFloat(args, "gravityX", 0.f), argFloat(args, "gravityY", 900.f));
604 if (id < 0) return "error: failed to create world";
605 float gx = 0.f, gy = 0.f;
606 ph->worldGravity(id, &gx, &gy);
607 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
608 o->set("id", id);
609 o->set("gravityX", gx);
610 o->set("gravityY", gy);
611 return mcpStringify(Poco::Dynamic::Var(o));
612 }
613
614 if (name == "eve_physics_list_worlds") {
615 auto* ph = mcpPhysics();
616 if (!ph) return "error: Physics module not available";
617 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
618 for (int i = 0; i < ph->worldCount(); ++i) {
619 float gx = 0.f, gy = 0.f;
620 if (!ph->worldGravity(i, &gx, &gy)) continue;
621 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
622 o->set("id", i);
623 o->set("gravityX", gx);
624 o->set("gravityY", gy);
625 arr->add(o);
626 }
627 return mcpStringify(Poco::Dynamic::Var(arr));
628 }
629
630 if (name == "eve_physics_raycast") {
631 auto* ph = mcpPhysics();
632 if (!ph) return "error: Physics module not available";
634 if (!ph->rayCast(argInt(args, "world", 0), argFloat(args, "x1"), argFloat(args, "y1"), argFloat(args, "x2"),
635 argFloat(args, "y2"), &h))
636 return "error: unknown physics world id";
637 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
638 o->set("hit", h.hit);
639 if (h.hit) {
640 o->set("bodyId", h.bodyId);
641 o->set("x", h.x);
642 o->set("y", h.y);
643 o->set("normalX", h.normalX);
644 o->set("normalY", h.normalY);
645 o->set("fraction", h.fraction);
646 }
647 return mcpStringify(Poco::Dynamic::Var(o));
648 }
649
650 if (name == "eve_physics_remove_world") {
651 auto* ph = mcpPhysics();
652 if (!ph) return "error: Physics module not available";
653 return ph->removeWorld(argInt(args, "world", -1)) ? "ok" : "error: unknown physics world id";
654 }
655
656 // ============================= Render =============================
657 if (name == "eve_render_status") {
658 return renderStatusText(mcpCapture());
659 }
660
661 if (name == "eve_render_describe") {
662 const bool fresh = argBool(args, "fresh", false);
663 const std::string reason = argString(args, "reason");
664 return RenderVision::instance().describe(mcpCapture(), renderStatusText(mcpCapture()), fresh, reason);
665 }
666
667 if (name == "eve_render_vision_config") {
668 auto& rv = RenderVision::instance();
669 if (args && args->has("baseUrl")) rv.setBaseUrl(argString(args, "baseUrl"));
670 if (args && args->has("apiKey")) rv.setApiKey(argString(args, "apiKey"));
671 if (args && args->has("model")) rv.setModel(argString(args, "model"));
672 if (args && args->has("path")) rv.setPath(argString(args, "path"));
673 if (args && args->has("timeoutMs")) rv.setTimeoutMs(argInt(args, "timeoutMs", 20000));
674 return rv.configJson();
675 }
676
677 if (name == "eve_screenshot") {
678 auto* cap = mcpCapture();
679 if (!cap) return "error: Graphics module not available";
680 std::string path = argString(args, "path");
681 if (path.empty()) path = "mcp_screenshot.png";
682 int w = 0, h = 0;
683 std::string err;
684 if (!cap->savePng(path, &w, &h, &err)) return "error: " + err;
685 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
686 o->set("path", path);
687 o->set("width", w);
688 o->set("height", h);
689 return mcpStringify(Poco::Dynamic::Var(o));
690 }
691
692 // ============================= Particles / Weather =============================
693 if (name == "eve_particles_status") {
694 auto* part = mcpParticles();
695 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
696 if (!part) {
697 o->set("error", "Particles module not available");
698 return mcpStringify(Poco::Dynamic::Var(o));
699 }
700 o->set("emitterCount", part->emitterCount());
701 return mcpStringify(Poco::Dynamic::Var(o));
702 }
703
704 if (name == "eve_particles_emit") {
705 auto* part = mcpParticles();
706 if (!part) return "error: Particles module not available";
707 float ex = 0.f, ey = 0.f;
708 int cnt = 0;
709 if (!part->createEmitter(argInt(args, "buffer", 1000), argFloat(args, "x"), argFloat(args, "y"),
710 argString(args, "preset"), argInt(args, "count", 100), &ex, &ey, &cnt))
711 return "error: failed to create emitter";
712 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
713 o->set("x", ex);
714 o->set("y", ey);
715 o->set("count", cnt);
716 return mcpStringify(Poco::Dynamic::Var(o));
717 }
718
719 // ============================= Audio =============================
720 if (name == "eve_audio_status") {
721 auto* audio = mcpAudio();
722 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
723 if (!audio) {
724 o->set("error", "Audio module not available");
725 return mcpStringify(Poco::Dynamic::Var(o));
726 }
727 o->set("volume", audio->volume());
728 return mcpStringify(Poco::Dynamic::Var(o));
729 }
730
731 if (name == "eve_audio_set_volume") {
732 auto* audio = mcpAudio();
733 if (!audio) return "error: Audio module not available";
734 audio->setVolume(argFloat(args, "volume", 1.f));
735 return "ok";
736 }
737
738 if (name == "eve_audio_stop_all") {
739 auto* audio = mcpAudio();
740 if (!audio) return "error: Audio module not available";
741 audio->stopAll();
742 return "ok";
743 }
744
745 // ============================= Host UI (headless MCP editor host) =====
746 if (name == "eve_host_status") {
747 return mcpHost() ? mcpHost()->status() : "error: ui module not available";
748 }
749 if (name == "eve_host_window_open") {
750 const std::string title = argString(args, "title", "EVEngine AI Host");
751 return mcpHost() ? mcpHost()->openWindow(title, argInt(args, "width", 1280), argInt(args, "height", 800))
752 : "error: ui module not available";
753 }
754 if (name == "eve_host_window_close") {
755 return mcpHost() ? mcpHost()->closeWindow() : "error: ui module not available";
756 }
757 if (name == "eve_host_window_state") {
758 return mcpHost() ? mcpHost()->windowState() : "error: ui module not available";
759 }
760 if (name == "eve_host_editor_apply") {
761 std::string json;
762 if (args && args->has("editor")) {
763 try {
764 Poco::JSON::Object::Ptr o = args->getObject("editor");
765 if (o)
766 json = mcpStringify(Poco::Dynamic::Var(o));
767 else
768 json = argString(args, "editor");
769 } catch (...) {
770 json = argString(args, "editor");
771 }
772 }
773 if (json.empty()) return "error: missing editor";
774 return mcpHost() ? mcpHost()->applyEditor(json) : "error: ui module not available";
775 }
776 if (name == "eve_host_editor_remove") {
777 return mcpHost() ? mcpHost()->removeEditor(argString(args, "id")) : "error: ui module not available";
778 }
779 if (name == "eve_host_editor_list") {
780 return mcpHost() ? mcpHost()->listEditors() : "error: ui module not available";
781 }
782 if (name == "eve_host_editor_state") {
783 return mcpHost() ? mcpHost()->editorState(argString(args, "id")) : "error: ui module not available";
784 }
785 if (name == "eve_host_editor_set_value") {
786 if (!args || !args->has("value")) return "error: missing value";
787 return mcpHost() ? mcpHost()->setEditorValue(argString(args, "editor"), argString(args, "widget"),
788 mcpStringify(argVar(args, "value")))
789 : "error: ui module not available";
790 }
791 if (name == "eve_host_editor_save") {
792 return mcpHost() ? mcpHost()->saveEditor(argString(args, "id")) : "error: ui module not available";
793 }
794 if (name == "eve_host_editor_unload") {
795 return mcpHost() ? mcpHost()->unloadEditor(argString(args, "id")) : "error: ui module not available";
796 }
797 if (name == "eve_host_vm_register") {
798 return mcpHost() ? mcpHost()->registerVM(argString(args, "name"), argString(args, "source"))
799 : "error: ui module not available";
800 }
801 if (name == "eve_host_vm_unregister") {
802 return mcpHost() ? mcpHost()->unregisterVM(argString(args, "name")) : "error: ui module not available";
803 }
804 if (name == "eve_host_events") {
805 return mcpHost() ? mcpHost()->consumeEvents(argString(args, "editor")) : "error: ui module not available";
806 }
807 if (name == "eve_host_widget_rect") {
808 return mcpHost() ? mcpHost()->widgetRect(argString(args, "editor"), argString(args, "widget"))
809 : "error: ui module not available";
810 }
811 if (name == "eve_host_capture") {
812 return mcpHost() ? mcpHost()->capture(argString(args, "path")) : "error: ui module not available";
813 }
814 if (name == "eve_host_script") {
815 return mcpHost() ? mcpHost()->runScript(argString(args, "source")) : "error: ui module not available";
816 }
817 if (name == "eve_host_shutdown") {
818 if (mcpHost()) mcpHost()->requestExit();
819 return "ok";
820 }
821
822 if (name == "eve_eval") {
823 const std::string expr = argString(args, "expression");
824 if (expr.empty()) return "error: missing expression";
825 auto info = dbg.evaluate(expr);
826 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
827 o->set("expression", expr);
828 o->set("name", info.name);
829 o->set("value", info.value);
830 o->set("type", info.type);
831 o->set("ok", !info.value.empty() || info.type == "null" || !info.name.empty());
832 return mcpStringify(Poco::Dynamic::Var(o));
833 }
834
835 if (name == "eve_pause") {
836 dbg.pause(PauseReason::PauseKey);
837 dap.notifyStopped(PauseReason::PauseKey, dbg.pauseLocation());
838 return "paused";
839 }
840 if (name == "eve_continue") {
841 dbg.resume();
842 dap.notifyContinued();
843 return "continued";
844 }
845 if (name == "eve_step_over") {
846 dbg.stepOver();
847 dap.notifyContinued();
848 return "step_over";
849 }
850 if (name == "eve_step_into") {
851 dbg.stepInto();
852 dap.notifyContinued();
853 return "step_into";
854 }
855 if (name == "eve_step_out") {
856 dbg.stepOut();
857 dap.notifyContinued();
858 return "step_out";
859 }
860 if (name == "eve_step_frame") {
861 dbg.stepFrame();
862 dap.notifyContinued();
863 return "step_frame";
864 }
865
866 if (name == "eve_stack") {
867 auto frames = dbg.stackTrace();
868 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
869 for (const auto& f : frames) {
870 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
871 o->set("id", f.id);
872 o->set("name", f.name);
873 o->set("source", f.loc.source);
874 o->set("line", f.loc.line);
875 o->set("function", f.loc.function);
876 arr->add(o);
877 }
878 return mcpStringify(Poco::Dynamic::Var(arr));
879 }
880
881 if (name == "eve_locals") {
882 const int level = argInt(args, "level", 1);
883 auto vars = dbg.locals(level);
884 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
885 for (const auto& v : vars) {
886 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
887 o->set("name", v.name);
888 o->set("value", v.value);
889 o->set("type", v.type);
890 arr->add(o);
891 }
892 return mcpStringify(Poco::Dynamic::Var(arr));
893 }
894
895 if (name == "eve_set_breakpoint") {
896 const std::string source = argString(args, "source");
897 const int line = argInt(args, "line");
898 if (source.empty() || line <= 0) return "error: need source and line";
899 const int id = dbg.setBreakpoint(source, line, true);
900 return "ok id=" + std::to_string(id);
901 }
902 if (name == "eve_clear_breakpoint") {
903 const std::string source = argString(args, "source");
904 const int line = argInt(args, "line");
905 if (source.empty() || line <= 0) return "error: need source and line";
906 return dbg.clearBreakpoint(source, line) ? "cleared" : "not_found";
907 }
908 if (name == "eve_list_breakpoints") {
909 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
910 for (const auto& bp : dbg.breakpoints()) {
911 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
912 o->set("id", bp.id);
913 o->set("source", bp.source);
914 o->set("line", bp.line);
915 o->set("enabled", bp.enabled);
916 arr->add(o);
917 }
918 return mcpStringify(Poco::Dynamic::Var(arr));
919 }
920
921 if (name == "eve_watch_add") {
922 const std::string expr = argString(args, "expression");
923 if (expr.empty()) return "error: missing expression";
924 dbg.addWatch(expr);
925 dbg.refreshWatches();
926 return "ok";
927 }
928 if (name == "eve_watch_list") {
929 dbg.refreshWatches();
930 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
931 for (const auto& w : dbg.watches()) {
932 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
933 o->set("expression", w.expression);
934 o->set("value", w.value);
935 o->set("ok", w.ok);
936 arr->add(o);
937 }
938 return mcpStringify(Poco::Dynamic::Var(arr));
939 }
940
941 if (name == "eve_snapshot_capture") {
942 HSQUIRRELVM vm = dbg.vm();
943 if (!vm) return "error: no VM";
944 std::string err;
945 std::string json = Snapshot::instance().capture(vm, &err);
946 if (!err.empty() && json.empty()) return "error: " + err;
947 return json;
948 }
949 if (name == "eve_snapshot_restore") {
950 HSQUIRRELVM vm = dbg.vm();
951 if (!vm) return "error: no VM";
952 const std::string json = argString(args, "json");
953 std::string err;
954 if (!Snapshot::instance().restore(vm, json, &err)) return "error: " + err;
955 return "ok";
956 }
957 if (name == "eve_snapshot_save") {
958 HSQUIRRELVM vm = dbg.vm();
959 if (!vm) return "error: no VM";
960 const std::string path = argString(args, "path");
961 std::string err;
962 if (!Snapshot::instance().saveFile(vm, path, &err)) return "error: " + err;
963 return "ok";
964 }
965 if (name == "eve_snapshot_load") {
966 HSQUIRRELVM vm = dbg.vm();
967 if (!vm) return "error: no VM";
968 const std::string path = argString(args, "path");
969 std::string err;
970 if (!Snapshot::instance().loadFile(vm, path, &err)) return "error: " + err;
971 return "ok";
972 }
973
974 if (name == "eve_error_slice") {
975 const std::string& last = mcpLastReport();
976 if (!last.empty()) return last;
977 return mcpFormatError("no prior error; callgraph slice at latest site");
978 }
979
980 if (name == "eve_run_script") {
981 HSQUIRRELVM vm = dbg.vm();
982 if (!vm) return "error: no VM";
983 const std::string source = argString(args, "source");
984 if (source.empty()) return "error: missing source";
985 const SQInteger top = sq_gettop(vm);
986 if (SQ_FAILED(sq_compilebuffer(vm, source.c_str(),
987 static_cast<SQInteger>(source.size()),
988 _SC("mcp_snippet.nut"), SQTrue))) {
989 sq_settop(vm, top);
990 return "error: " + snippetErrorText(vm, true);
991 }
992 sq_pushroottable(vm);
993 if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
994 sq_settop(vm, top);
995 return "error: " + snippetErrorText(vm, false);
996 }
997 sq_settop(vm, top);
998 return "ok";
999 }
1000
1001 if (name == "eve_ai_note") {
1002 const std::string text = argString(args, "text");
1003 if (text.empty()) return "error: missing text";
1004 AiPanel::instance().addNote(text);
1005 return "ok";
1006 }
1007 if (name == "eve_ai_log") return AiPanel::instance().formatLog(100);
1008
1009 // ===================== Scene Director (AI scene-authoring) =====================
1010 // Agent-drivable scene construction backed by the `scene_director` script kit.
1011 if (name == "eve_scene_director_install") {
1012 HSQUIRRELVM vm = dbg.vm();
1013 if (!vm) return "error: no VM";
1014 std::string err;
1015 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1016 return "ok";
1017 }
1018
1019 if (name == "eve_scene_director_status") {
1020 HSQUIRRELVM vm = dbg.vm();
1021 if (!vm) return "error: no VM";
1022 std::string err;
1023 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1024 return callSceneDirectorReturn(vm, "return ::scene_director.status();", &err);
1025 }
1026
1027 if (name == "eve_scene_reset") {
1028 HSQUIRRELVM vm = dbg.vm();
1029 if (!vm) return "error: no VM";
1030 std::string err;
1031 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1032 const std::string out = callSceneDirectorReturn(vm, "return ::scene_director.reset();", &err);
1033 if (!err.empty()) return sceneDirectorToolError(name, err);
1034 return out;
1035 }
1036
1037 if (name == "eve_scene_modify") {
1038 HSQUIRRELVM vm = dbg.vm();
1039 if (!vm) return "error: no VM";
1040 std::string err;
1041 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1042 const std::string action = argString(args, "action");
1043 const std::string target = argString(args, "target");
1044 Poco::Dynamic::Var paramsVar;
1045 if (args && args->has("params")) {
1046 try {
1047 paramsVar = Poco::Dynamic::Var(args->getObject("params"));
1048 } catch (...) {
1049 }
1050 }
1051 const std::string snippet =
1052 "return ::scene_director.modify(" + sqLiteralValue(Poco::Dynamic::Var(action)) + "," +
1053 sqLiteralValue(Poco::Dynamic::Var(target)) + "," + sqLiteralValue(paramsVar) + ");";
1054 const std::string out = callSceneDirectorReturn(vm, snippet, &err);
1055 if (!err.empty()) return sceneDirectorToolError(name, err);
1056 return out;
1057 }
1058
1059 if (name == "eve_camera_generate") {
1060 HSQUIRRELVM vm = dbg.vm();
1061 if (!vm) return "error: no VM";
1062 std::string err;
1063 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1064 const int count = argInt(args, "count", 6);
1065 const std::string snippet =
1066 "return ::scene_director.cameras(" + std::to_string(count) + ");";
1067 const std::string out = callSceneDirectorReturn(vm, snippet, &err);
1068 if (!err.empty()) return sceneDirectorToolError(name, err);
1069 return out;
1070 }
1071
1072 if (name == "eve_scene_info") {
1073 HSQUIRRELVM vm = dbg.vm();
1074 if (!vm) return "error: no VM";
1075 std::string err;
1076 if (!ensureSceneDirectorInstalled(vm, &err)) return sceneDirectorToolError(name, err);
1077 const std::string out = callSceneDirectorReturn(vm, "return ::scene_director.info();", &err);
1078 if (!err.empty()) return sceneDirectorToolError(name, err);
1079 return out;
1080 }
1081
1082 // ---- 场景巡检工具集(图像与 3D 几何数据严格同步) ----
1083 if (name == "inspect_generate_scene_camera_views") {
1084 const glm::vec3 center = argVec3(args, "center");
1085 const float fov = argFloat(args, "fov", 60.f);
1086 auto views = SceneInspect::instance().generateViews(center, fov);
1087 Poco::JSON::Object::Ptr root = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1088 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1089 for (const auto& v : views) {
1090 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1091 o->set("name", v.name);
1092 o->set("kind", v.kind);
1093 o->set("eye", vec3ToArray(v.eye));
1094 o->set("target", vec3ToArray(v.target));
1095 o->set("fov", static_cast<double>(v.fovYDeg));
1096 arr->add(o);
1097 }
1098 root->set("views", arr);
1099 return mcpStringify(Poco::Dynamic::Var(root));
1100 }
1101
1102 if (name == "set_camera_pose") {
1103 const glm::vec3 pos = argVec3(args, "pos", glm::vec3(0.f, 1.8f, 0.f));
1104 const glm::vec3 rot = argVec3(args, "rot", glm::vec3(0.f));
1105 const float fov = argFloat(args, "fov", 0.f);
1106 const bool ok = SceneInspect::instance().setCameraPose(pos, rot, fov);
1107 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1108 o->set("ok", ok);
1109 if (!ok) {
1110 o->set("error", "failed to set camera pose (no Graphics/Camera3D)");
1111 } else {
1112 try {
1113 Poco::JSON::Parser parser;
1114 o->set("pose", parser.parse(SceneInspect::instance().currentPoseJson()));
1115 } catch (...) {
1116 }
1117 }
1118 return mcpStringify(Poco::Dynamic::Var(o));
1119 }
1120
1121 if (name == "capture_render_frame") {
1122 const std::string dir = argString(args, "dir");
1123 const std::string tag = argString(args, "tag", "frame");
1124 std::vector<std::string> buffers;
1125 if (args && args->has("buffers")) {
1126 try {
1127 auto arr = args->getArray("buffers");
1128 if (arr) {
1129 for (size_t i = 0; i < arr->size(); ++i)
1130 buffers.push_back(arr->get(i).convert<std::string>());
1131 }
1132 } catch (...) {
1133 }
1134 }
1135 const auto res = SceneInspect::instance().capture(dir, tag, buffers);
1136 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1137 o->set("ok", res.ok);
1138 if (res.ok) {
1139 o->set("png", res.pngPath);
1140 o->set("json", res.jsonPath);
1141 o->set("width", res.width);
1142 o->set("height", res.height);
1143 o->set("entityCount", res.entityCount);
1144 if (!res.depthPngPath.empty()) o->set("depthPng", res.depthPngPath);
1145 if (!res.normalPngPath.empty()) o->set("normalPng", res.normalPngPath);
1146 if (!res.idPngPath.empty()) o->set("idPng", res.idPngPath);
1147 if (!res.idJsonPath.empty()) o->set("idJson", res.idJsonPath);
1148 if (!res.unsupported.empty()) {
1149 Poco::JSON::Array::Ptr uns = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1150 for (const auto& u : res.unsupported) uns->add(u);
1151 o->set("unsupported", uns);
1152 }
1153 } else {
1154 o->set("error", res.error);
1155 }
1156 return mcpStringify(Poco::Dynamic::Var(o));
1157 }
1158
1159 if (name == "get_visible_entities_screen_bbox") {
1160 const bool hasPos = args && args->has("pos");
1161 const bool hasTarget = args && args->has("target");
1162 if (hasPos && hasTarget) {
1163 const glm::vec3 eye = argVec3(args, "pos");
1164 const glm::vec3 tgt = argVec3(args, "target");
1165 const float fov = argFloat(args, "fov", 0.f);
1166 return SceneInspect::instance().visibleEntitiesJson(&eye, &tgt, fov);
1167 }
1168 const float fov = argFloat(args, "fov", 0.f);
1169 return SceneInspect::instance().visibleEntitiesJson(nullptr, nullptr, fov);
1170 }
1171
1172 return "error: unknown tool " + name;
1173}
1174
1175std::string handleInitialize(McpServer& mcp, const std::string& idJson,
1176 Poco::JSON::Object::Ptr params) {
1177 std::string clientName = "mcp-client";
1178 std::string protocol = "2025-06-18";
1179 if (params) {
1180 try {
1181 if (params->has("clientInfo")) {
1182 auto info = params->getObject("clientInfo");
1183 if (info && info->has("name"))
1184 clientName = info->get("name").convert<std::string>();
1185 }
1186 if (params->has("protocolVersion"))
1187 protocol = params->get("protocolVersion").convert<std::string>();
1188 } catch (...) {
1189 }
1190 }
1191 (void)mcp;
1192 AiPanel::instance().setClientName(clientName);
1193 AiPanel::instance().addLog("system", "mcp.initialize", clientName);
1194
1195 // Hand-built JSON keeps initialize compact (newline framing) without a Poco Object tree.
1196 const std::string resultJson =
1197 std::string("{\"protocolVersion\":\"") + mcpJsonEscape(protocol) +
1198 "\",\"capabilities\":{\"tools\":{},\"resources\":{},\"prompts\":{}},"
1199 "\"serverInfo\":{\"name\":\"evengine\",\"title\":\"EVEngine MCP\",\"version\":\"0.1.0\"},"
1200 "\"instructions\":\"EVEngine MCP for AI-assisted game development. eve_host_* tools create JSON-defined editor windows bound to Squirrel ViewModels (MVVM) for AI-crafted terrain/material/event editors.\"}";
1201 return makeResult(idJson, resultJson);
1202}
1203
1204std::string handleToolsList(const std::string& idJson) {
1205 static const char* const kToolsParts[] = {
1206 "{\"tools\":["
1207 "{\"name\":\"eve_status\",\"description\":\"Runtime + debugger + MCP/DAP status JSON.\","
1208 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1209 "{\"name\":\"eve_eval\",\"description\":\"Evaluate a Squirrel expression (local or roottable).\","
1210 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"expression\":{\"type\":\"string\"}},\"required\":[\"expression\"]}},"
1211 "{\"name\":\"eve_pause\",\"description\":\"Pause the game / script.\","
1212 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1213 "{\"name\":\"eve_continue\",\"description\":\"Continue from pause.\","
1214 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1215 "{\"name\":\"eve_step_over\",\"description\":\"Step over next statement.\","
1216 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1217 "{\"name\":\"eve_step_into\",\"description\":\"Step into next statement.\","
1218 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1219 "{\"name\":\"eve_step_out\",\"description\":\"Step out of current function.\","
1220 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1221 "{\"name\":\"eve_step_frame\",\"description\":\"Run one game frame then pause.\","
1222 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1223 "{\"name\":\"eve_stack\",\"description\":\"Return current script stack frames.\","
1224 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1225 "{\"name\":\"eve_locals\",\"description\":\"List locals at a stack level (default 1).\","
1226 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1227 "{\"name\":\"eve_set_breakpoint\",\"description\":\"Set a script breakpoint.\","
1228 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"},\"line\":{\"type\":\"integer\"}},\"required\":[\"source\",\"line\"]}},"
1229 "{\"name\":\"eve_clear_breakpoint\",\"description\":\"Clear a script breakpoint.\","
1230 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"},\"line\":{\"type\":\"integer\"}},\"required\":[\"source\",\"line\"]}},"
1231 "{\"name\":\"eve_list_breakpoints\",\"description\":\"List breakpoints.\","
1232 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1233 "{\"name\":\"eve_watch_add\",\"description\":\"Add a watch expression.\","
1234 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"expression\":{\"type\":\"string\"}},\"required\":[\"expression\"]}},"
1235 "{\"name\":\"eve_watch_list\",\"description\":\"List watches with last values.\","
1236 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1237 "{\"name\":\"eve_snapshot_capture\",\"description\":\"Capture script-state snapshot JSON.\","
1238 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1239 "{\"name\":\"eve_snapshot_restore\",\"description\":\"Restore script-state from snapshot JSON.\","
1240 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"json\":{\"type\":\"string\"}},\"required\":[\"json\"]}},"
1241 "{\"name\":\"eve_snapshot_save\",\"description\":\"Save snapshot to a file path.\","
1242 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}},"
1243 "{\"name\":\"eve_snapshot_load\",\"description\":\"Load snapshot from a file path.\","
1244 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}},"
1245 "{\"name\":\"eve_error_slice\",\"description\":\"Return last error report / backward slice (script + render).\","
1246 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1247 "{\"name\":\"eve_run_script\",\"description\":\"Compile and run a short Squirrel snippet in the live VM.\","
1248 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"}},\"required\":[\"source\"]}},"
1249 "{\"name\":\"eve_ai_note\",\"description\":\"Append a note to the DevTools AI session log.\","
1250 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"required\":[\"text\"]}},"
1251 "{\"name\":\"eve_ai_log\",\"description\":\"Read the DevTools AI session log.\","
1252 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1253 "{\"name\":\"eve_scene_director_install\",\"description\":\"Install the scene-director authoring kit into the live VM (idempotent).\","
1254 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1255 "{\"name\":\"eve_scene_director_status\",\"description\":\"Scene-director kit status (installed / propCount / hasCamera).\","
1256 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1257 "{\"name\":\"eve_scene_reset\",\"description\":\"Clear all staged props, camera and reset lighting to defaults.\","
1258 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1259 "{\"name\":\"eve_scene_modify\",\"description\":\"Agent scene action: action in add_object|spawn|place|move_object|move|scale|rotate|rotation|remove_object|remove|visibility|material|lighting|set_lighting|camera|cameras|info|list|reset. spawn/move params: {id,kind,x,y,z,sx,sy,sz,yaw_deg,scale,pos,tint,seed,mesh_params,...}; lighting params: {timeOfDay,atmosphere,intensity,background}; camera params: {eye,target,fov}. Returns JSON.\","
1260 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\"},\"target\":{\"type\":\"string\"},\"params\":{\"type\":\"object\"}},\"required\":[\"action\"]}},"
1261 "{\"name\":\"eve_camera_generate\",\"description\":\"Generate standardized QC camera rigs (eye/target/fov) orbiting the staged scene.\","
1262 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"count\":{\"type\":\"integer\"}}}},"
1263 "{\"name\":\"eve_scene_info\",\"description\":\"Authoritative staged-scene truth JSON: props (id/kind/pos/scale/yaw_deg/tint) + count.\","
1264 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1265 "{\"name\":\"inspect_generate_scene_camera_views\",\"description\":\"Generate a standard set of inspection camera views (road-level, bird's-eye, corner close-up, vista) around a center point.\","
1266 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"center\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},\"description\":\"[x,y,z] center point to orbit (default [0,0,0])\"},\"fov\":{\"type\":\"number\",\"description\":\"base vertical FOV in degrees (default 60)\"}}}}"
1267 ","
1268 "{\"name\":\"set_camera_pose\",\"description\":\"Set the active camera pose from position + Euler rotation + optional FOV.\","
1269 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"pos\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},\"description\":\"[x,y,z] camera position\"},\"rot\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},\"description\":\"[yawDeg,pitchDeg] facing\"},\"fov\":{\"type\":\"number\",\"description\":\"vertical FOV in degrees (0=keep current)\"}},\"required\":[\"pos\",\"rot\"]}}"
1270 ","
1271 "{\"name\":\"capture_render_frame\",\"description\":\"Atomically capture the current view and export matching buffers. PNG color frame + geometry JSON always; 'buffers' may add depth/normal (GBuffer), id (per-pixel render ID mask with JSON mapping) — shadow is unsupported on current backend.\","
1272 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"dir\":{\"type\":\"string\",\"description\":\"output directory (default: cache dir)\"},\"tag\":{\"type\":\"string\",\"description\":\"file name tag (default 'frame')\"},\"buffers\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"optional: 'color'|'depth'|'normal'|'id' (default ['color'])\"}}}}"
1273 ","
1274 "{\"name\":\"get_visible_entities_screen_bbox\",\"description\":\"Return visible scene entities in the frustum with their screen-space bbox, world AABB, id and asset label.\","
1275 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"pos\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},\"description\":\"optional [x,y,z] camera eye override\"},\"target\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},\"description\":\"optional [x,y,z] look target override\"},\"fov\":{\"type\":\"number\",\"description\":\"optional FOV override (degrees)\"}}}}"
1276 ","
1277 "{\"name\":\"eve_scene_status\",\"description\":\"Active scene host name, node count and root id.\","
1278 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1279 "{\"name\":\"eve_scene_nodes\",\"description\":\"List nodes of the active scene host (id/name/path/visible).\","
1280 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"limit\":{\"type\":\"integer\"}}}},"
1281 "{\"name\":\"eve_scene_node_get\",\"description\":\"Read transform/visibility/parent/children of a scene node by id.\","
1282 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},"
1283 "{\"name\":\"eve_scene_node_set\",\"description\":\"Set position (x,y,z) or visibility of a scene node by id.\","
1284 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"},\"visible\":{\"type\":\"boolean\"}},\"required\":[\"id\"]}},"
1285 "{\"name\":\"eve_procgen_recipes\",\"description\":\"List available procgen map algorithms and mesh/texture/PBR recipes.\","
1286 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1287 "{\"name\":\"eve_procgen_map\",\"description\":\"Generate a semantic tile grid with a procgen map algorithm.\","
1288 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"algorithm\":{\"type\":\"string\"},\"width\":{\"type\":\"integer\"},\"height\":{\"type\":\"integer\"},\"seed\":{\"type\":\"integer\"}},\"required\":[\"algorithm\"]}},"
1289 "{\"name\":\"eve_procgen_mesh\",\"description\":\"Build a procedural CPU mesh (mesh.* recipe) and return stats.\","
1290 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"recipe\":{\"type\":\"string\"},\"seed\":{\"type\":\"integer\"}},\"required\":[\"recipe\"]}},"
1291 "{\"name\":\"eve_physics_new_world\",\"description\":\"Create a 2D physics world and return its id.\","
1292 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"gravityX\":{\"type\":\"number\"},\"gravityY\":{\"type\":\"number\"}}}},"
1293 "{\"name\":\"eve_physics_list_worlds\",\"description\":\"List live 2D physics worlds with gravity.\","
1294 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1295 "{\"name\":\"eve_physics_raycast\",\"description\":\"Raycast a segment in a physics world and report the closest hit.\","
1296 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},\"x1\":{\"type\":\"number\"},\"y1\":{\"type\":\"number\"},\"x2\":{\"type\":\"number\"},\"y2\":{\"type\":\"number\"}},\"required\":[\"world\",\"x1\",\"y1\",\"x2\",\"y2\"]}},"
1297 "{\"name\":\"eve_physics_remove_world\",\"description\":\"Destroy a 2D physics world by id.\","
1298 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"}},\"required\":[\"world\"]}},"
1299 "{\"name\":\"eve_render_status\",\"description\":\"Render window size, 3D frame flag, readback state and RenderFlow event count.\","
1300 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1301 "{\"name\":\"eve_screenshot\",\"description\":\"Capture the current frame to a PNG file (enables readback).\","
1302 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}},"
1303 "{\"name\":\"eve_render_describe\",\"description\":\"Capture the current frame and ask the configured vision model to describe it and relate it to render parameters. Cached unless fresh=true.\","
1304 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"fresh\":{\"type\":\"boolean\"},\"reason\":{\"type\":\"string\"}}}},"
1305 "{\"name\":\"eve_render_vision_config\",\"description\":\"Set/read the vision model config (baseUrl/apiKey/model/path/timeoutMs). No args returns current config (key masked).\","
1306 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"baseUrl\":{\"type\":\"string\"},\"apiKey\":{\"type\":\"string\"},\"model\":{\"type\":\"string\"},\"path\":{\"type\":\"string\"},\"timeoutMs\":{\"type\":\"integer\"}}}},"
1307 "{\"name\":\"eve_particles_status\",\"description\":\"Report live particle emitter count.\","
1308 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1309 "{\"name\":\"eve_particles_emit\",\"description\":\"Spawn a particle emitter at a position (optionally a preset) and emit particles.\","
1310 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"preset\":{\"type\":\"string\"},\"count\":{\"type\":\"integer\"}}}},"
1311 "{\"name\":\"eve_audio_status\",\"description\":\"Report master audio volume.\","
1312 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1313 "{\"name\":\"eve_audio_set_volume\",\"description\":\"Set master audio volume (0..1).\","
1314 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"volume\":{\"type\":\"number\"}},\"required\":[\"volume\"]}},"
1315 "{\"name\":\"eve_audio_stop_all\",\"description\":\"Stop all playing audio sources.\","
1316 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1317 "{\"name\":\"eve_host_status\",\"description\":\"Headless editor host status: window, editors, registered ViewModels, project root.\","
1318 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1319 "{\"name\":\"eve_host_window_open\",\"description\":\"Create the host OS window (lazy; editors can also auto-open it).\","
1320 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"width\":{\"type\":\"integer\"},\"height\":{\"type\":\"integer\"}}}},"
1321 "{\"name\":\"eve_host_window_close\",\"description\":\"Close the host OS window (MCP server stays alive).\","
1322 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1323 "{\"name\":\"eve_host_window_state\",\"description\":\"Host window open state + size.\","
1324 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1325 "{\"name\":\"eve_host_editor_apply\",\"description\":\"Apply an editor View (JSON widget tree). Auto-opens the host window on first use.\","
1326 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"editor\":{\"type\":\"object\"}},\"required\":[\"editor\"]}},",
1327 "{\"name\":\"eve_host_editor_remove\",\"description\":\"Remove an editor panel from the session.\","
1328 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},",
1329 "{\"name\":\"eve_host_editor_list\",\"description\":\"List editors (id/title/vm).\","
1330 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},",
1331 "{\"name\":\"eve_host_editor_state\",\"description\":\"Editor values + pending events (id omitted = all editors).\","
1332 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}}}},"
1333 "{\"name\":\"eve_host_editor_set_value\",\"description\":\"Set a widget value (JSON value). Writes the bound ViewModel and emits a change event.\","
1334 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"editor\":{\"type\":\"string\"},\"widget\":{\"type\":\"string\"},\"value\":{}},\"required\":[\"editor\",\"widget\",\"value\"]}},",
1335 "{\"name\":\"eve_host_editor_save\",\"description\":\"Persist editor as editors/<id>.editor.json + <id>.vm.nut in the project.\","
1336 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},",
1337 "{\"name\":\"eve_host_editor_unload\",\"description\":\"Remove an editor from the session (files stay on disk).\","
1338 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}},",
1339 "{\"name\":\"eve_host_vm_register\",\"description\":\"Compile a Squirrel ViewModel and register it by table name.\","
1340 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"source\":{\"type\":\"string\"}},\"required\":[\"name\",\"source\"]}},",
1341 "{\"name\":\"eve_host_vm_unregister\",\"description\":\"Unregister a ViewModel table.\","
1342 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}},",
1343 "{\"name\":\"eve_host_events\",\"description\":\"Read and clear interaction events (human clicks/sliders or AI set_value).\","
1344 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"editor\":{\"type\":\"string\"}}}},"
1345 "{\"name\":\"eve_host_widget_rect\",\"description\":\"Last-frame screen rect of a widget (for script drawing in viewports).\","
1346 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"editor\":{\"type\":\"string\"},\"widget\":{\"type\":\"string\"}},\"required\":[\"editor\",\"widget\"]}},",
1347 "{\"name\":\"eve_host_capture\",\"description\":\"Capture the host window to a PNG and return path/size.\","
1348 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}},"
1349 "{\"name\":\"eve_host_script\",\"description\":\"Run a Squirrel snippet in the host VM (full engine API + eve.host).\","
1350 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"}},\"required\":[\"source\"]}},",
1351 "{\"name\":\"eve_host_shutdown\",\"description\":\"Exit the headless MCP host process.\","
1352 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}",
1353 "]}"
1354 };
1355 static const std::string kToolsJson = [] {
1356 std::string out;
1357 for (const char* p : kToolsParts) out += p;
1358 return out;
1359 }();
1360 return makeResult(idJson, kToolsJson);
1361}
1362
1363std::string handleToolsCall(McpServer& mcp, const std::string& idJson,
1364 Poco::JSON::Object::Ptr params) {
1365 if (!params || !params->has("name"))
1366 return makeError(idJson, -32602, "tools/call requires params.name");
1367 std::string name;
1368 try {
1369 name = params->get("name").convert<std::string>();
1370 } catch (...) {
1371 return makeError(idJson, -32602, "tools/call params.name must be a string");
1372 }
1373 Poco::JSON::Object::Ptr args = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1374 if (params->has("arguments")) {
1375 try {
1376 args = params->getObject("arguments");
1377 } catch (...) {
1378 }
1379 }
1380
1381 std::string detail;
1382 try {
1383 if (args) detail = mcpStringify(Poco::Dynamic::Var(args));
1384 } catch (...) {
1385 }
1386 AiPanel::instance().addLog("tool", name, detail);
1387
1388 try {
1389 const std::string out = callTool(mcp, name, args);
1390 const bool isErr = out.rfind("error:", 0) == 0;
1391 return makeResult(idJson, textContentResult(out, isErr));
1392 } catch (const std::exception& e) {
1393 AiPanel::instance().addLog("error", name, e.what());
1394 return makeResult(idJson, textContentResult(std::string("error: ") + e.what(), true));
1395 }
1396}
1397
1398std::string handleResourcesList(const std::string& idJson) {
1399 return makeResult(idJson,
1400 "{\"resources\":["
1401 "{\"uri\":\"eve://status\",\"name\":\"status\",\"description\":\"Debugger / MCP / DAP status\",\"mimeType\":\"application/json\"},"
1402 "{\"uri\":\"eve://error-report\",\"name\":\"error-report\",\"description\":\"Last DevTools error slice report\",\"mimeType\":\"text/plain\"},"
1403 "{\"uri\":\"eve://ai-session\",\"name\":\"ai-session\",\"description\":\"AI / MCP session log\",\"mimeType\":\"text/plain\"},"
1404 "{\"uri\":\"eve://callgraph\",\"name\":\"callgraph\",\"description\":\"CallGraph event summary\",\"mimeType\":\"application/json\"}"
1405 "]}");
1406}
1407
1408std::string handleResourcesRead(const std::string& idJson, Poco::JSON::Object::Ptr params) {
1409 if (!params || !params->has("uri"))
1410 return makeError(idJson, -32602, "resources/read requires params.uri");
1411 std::string uri;
1412 try {
1413 uri = params->get("uri").convert<std::string>();
1414 } catch (...) {
1415 return makeError(idJson, -32602, "resources/read params.uri must be a string");
1416 }
1417 std::string text;
1418 std::string mime = "text/plain";
1419 if (uri == "eve://status") {
1420 text = engineStatusJson(McpServer::instance());
1421 mime = "application/json";
1422 } else if (uri == "eve://error-report") {
1423 text = mcpLastReport();
1424 if (text.empty()) text = "(no error report yet)\n";
1425 } else if (uri == "eve://ai-session") {
1427 if (text.empty()) text = "(empty)\n";
1428 } else if (uri == "eve://callgraph") {
1429 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1430 o->set("events", static_cast<int>(mcpCallgraphEvents()));
1431 o->set("stackDepth", static_cast<int>(mcpCallgraphStackDepth()));
1432 text = mcpStringify(Poco::Dynamic::Var(o));
1433 mime = "application/json";
1434 } else {
1435 return makeError(idJson, -32002, "Unknown resource: " + uri);
1436 }
1437
1438 Poco::JSON::Object::Ptr contentsItem = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1439 contentsItem->set("uri", uri);
1440 contentsItem->set("mimeType", mime);
1441 contentsItem->set("text", text);
1442 Poco::JSON::Array::Ptr contents = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1443 contents->add(contentsItem);
1444 Poco::JSON::Object::Ptr result = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1445 result->set("contents", contents);
1446 return makeResult(idJson, mcpStringify(Poco::Dynamic::Var(result)));
1447}
1448
1449std::string handlePromptsList(const std::string& idJson) {
1450 return makeResult(idJson,
1451 "{\"prompts\":["
1452 "{\"name\":\"debug_failure\",\"description\":\"Investigate the latest script/render error using MCP tools and the error slice.\"},"
1453 "{\"name\":\"test_scenario\",\"description\":\"Drive a reproducible in-engine test: pause, snapshot, eval assertions, continue.\"},"
1454 "{\"name\":\"ai_game_review\",\"description\":\"Review live game state for AI-generated content issues.\"}"
1455 "]}");
1456}
1457
1458std::string handlePromptsGet(const std::string& idJson, Poco::JSON::Object::Ptr params) {
1459 if (!params || !params->has("name"))
1460 return makeError(idJson, -32602, "prompts/get requires params.name");
1461 std::string name;
1462 try {
1463 name = params->get("name").convert<std::string>();
1464 } catch (...) {
1465 return makeError(idJson, -32602, "prompts/get params.name must be a string");
1466 }
1467 std::string text;
1468 if (name == "debug_failure") {
1469 text =
1470 "You are debugging an EVEngine game via MCP.\n"
1471 "1) Call eve_status and read resource eve://error-report.\n"
1472 "2) Use eve_stack / eve_locals / eve_eval to inspect state.\n"
1473 "3) Use eve_error_slice to narrow script/render causes.\n"
1474 "4) Propose a minimal fix; verify with eve_run_script or snapshot restore.";
1475 } else if (name == "test_scenario") {
1476 text =
1477 "Design a short automated test against the live EVEngine session:\n"
1478 "1) eve_pause then eve_snapshot_capture as baseline.\n"
1479 "2) Mutate or advance with eve_step_frame / eve_run_script.\n"
1480 "3) Assert with eve_eval / eve_watch_list.\n"
1481 "4) eve_snapshot_restore to reset; record notes via eve_ai_note.";
1482 } else if (name == "ai_game_review") {
1483 text =
1484 "Review this AI-generated or AI-assisted game build:\n"
1485 "1) eve_status + eve_ai_log for recent agent actions.\n"
1486 "2) Spot-check critical roots with eve_eval.\n"
1487 "3) If unstable, pause and capture a snapshot.\n"
1488 "4) Summarize risks and suggested script fixes.";
1489 } else {
1490 return makeError(idJson, -32602, "Unknown prompt: " + name);
1491 }
1492
1493 Poco::JSON::Object::Ptr msg = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1494 msg->set("role", "user");
1495 Poco::JSON::Array::Ptr content = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1496 Poco::JSON::Object::Ptr part = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1497 part->set("type", "text");
1498 part->set("text", text);
1499 content->add(part);
1500 msg->set("content", content);
1501 Poco::JSON::Array::Ptr messages = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
1502 messages->add(msg);
1503 Poco::JSON::Object::Ptr result = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1504 result->set("description", name);
1505 result->set("messages", messages);
1506 return makeResult(idJson, mcpStringify(Poco::Dynamic::Var(result)));
1507}
1508
1509} // namespace
1510
1512 // Process-immortal singleton: the stdio reader thread (detached when
1513 // reading stdin) and cross-singleton calls make destruction unsafe; see
1514 // devtools/Immortal.hpp.
1515 return Immortal<McpServer>::get();
1516}
1517
1518McpServer::McpServer() = default;
1519
1520McpServer::~McpServer() {
1521 // Only reached if someone deletes the instance; keep sockets tidy without
1522 // touching AiPanel (may already be torn down in other lifetime models).
1523 std::lock_guard<std::mutex> lock(ioMu_);
1524 if (client_) {
1525 try {
1526 client_->close();
1527 } catch (...) {
1528 }
1529 client_.reset();
1530 }
1531 if (server_) {
1532 try {
1533 server_->close();
1534 } catch (...) {
1535 }
1536 server_.reset();
1537 }
1538 stdioQueue_.clear();
1539 listening_.store(false);
1540 hasClient_.store(false);
1541 port_.store(0);
1542 transport_.store(Transport::None);
1543}
1544
1545void McpServer::setGameRoot(std::string root) {
1546 for (char& c : root) {
1547 if (c == '\\') c = '/';
1548 }
1549 while (!root.empty() && (root.back() == '/' || root.back() == '\\')) root.pop_back();
1550 gameRoot_ = std::move(root);
1551}
1552
1553int McpServer::listen(uint16_t port) {
1554 stop();
1555 try {
1556 Poco::Net::SocketAddress addr("127.0.0.1", port);
1557 server_ = std::make_unique<Poco::Net::ServerSocket>(addr);
1558 server_->setBlocking(false);
1559 const int bound = static_cast<int>(server_->address().port());
1560 port_.store(bound);
1561 listening_.store(true);
1562 initialized_ = false;
1563 try {
1564 if (gameRoot_.empty()) setGameRoot(std::filesystem::current_path().string());
1565 } catch (...) {
1566 }
1569 AiPanel::instance().addLog("system", "mcp.listen",
1570 "listening on 127.0.0.1:" + std::to_string(bound));
1571 transport_.store(Transport::Tcp);
1572 return bound;
1573 } catch (...) {
1574 server_.reset();
1575 listening_.store(false);
1576 port_.store(0);
1577 transport_.store(Transport::None);
1579 return 0;
1580 }
1581}
1582
1583bool McpServer::listenStdio(std::istream& in, std::ostream& out) {
1584 stop();
1585 {
1586 std::lock_guard<std::mutex> lock(ioMu_);
1587 stdioQueue_.clear();
1588 }
1589 transport_.store(Transport::Stdio);
1590 listening_.store(true);
1591 port_.store(0);
1592 initialized_ = false;
1593 stdinClosed_.store(false);
1594 hasClient_.store(true);
1595 stdioIn_ = &in;
1596 stdioOut_ = &out;
1597 joinReader_ = (&in != &std::cin);
1598 try {
1599 stdioReader_ = std::thread([this, &in]() {
1600 std::string line;
1601 while (listening_.load() && std::getline(in, line)) {
1602 if (!line.empty() && line.back() == '\r') line.pop_back();
1603 if (line.empty()) continue;
1604 {
1605 std::lock_guard<std::mutex> lock(ioMu_);
1606 stdioQueue_.push_back(std::move(line));
1607 }
1608 }
1609 stdinClosed_.store(true);
1610 hasClient_.store(false);
1611 });
1612 } catch (...) {
1613 transport_.store(Transport::None);
1614 listening_.store(false);
1615 hasClient_.store(false);
1616 return false;
1617 }
1620 AiPanel::instance().addLog("system", "mcp.listen",
1621 "stdio transport ready (newline JSON-RPC)");
1622 return true;
1623}
1624
1625void McpServer::stop() {
1626 listening_.store(false);
1627 hasClient_.store(false);
1628 if (stdioReader_.joinable()) {
1629 if (joinReader_) {
1630 stdioReader_.join();
1631 } else {
1632 stdioReader_.detach();
1633 }
1634 }
1635 std::lock_guard<std::mutex> lock(ioMu_);
1636 if (client_) {
1637 try {
1638 client_->close();
1639 } catch (...) {
1640 }
1641 client_.reset();
1642 }
1643 if (server_) {
1644 try {
1645 server_->close();
1646 } catch (...) {
1647 }
1648 server_.reset();
1649 }
1650 stdioQueue_.clear();
1651 recvBuf_.clear();
1652 port_.store(0);
1653 transport_.store(Transport::None);
1654 initialized_ = false;
1655 stdioIn_ = nullptr;
1656 stdioOut_ = nullptr;
1657 AiPanel::instance().setMcpPort(0);
1658 AiPanel::instance().setMcpConnected(false);
1659 AiPanel::instance().setClientName({});
1660}
1661
1662void McpServer::poll() {
1663 if (!listening_.load()) return;
1664 if (transport_.load() == Transport::Stdio) {
1665 std::vector<std::string> batch;
1666 {
1667 std::lock_guard<std::mutex> lock(ioMu_);
1668 batch.swap(stdioQueue_);
1669 }
1670 for (const auto& line : batch) handleMessage(line);
1671 return;
1672 }
1673 acceptNonBlocking();
1674 readAndDispatch();
1675 // Main-thread hook: run a pending breakpoint/error vision dump (if any) on
1676 // the render thread where Graphics readback is safe.
1677 if (RenderVision::instance().pending()) {
1678 auto* cap = mcpCapture();
1679 if (cap) RenderVision::instance().pollPending(cap, renderStatusText(cap));
1680 }
1681}
1682
1683void McpServer::acceptNonBlocking() {
1684 if (!server_ || client_) return;
1685 try {
1686 Poco::Net::SocketAddress clientAddr;
1687 Poco::Net::StreamSocket ss = server_->acceptConnection(clientAddr);
1688 ss.setBlocking(true);
1689 ss.setReceiveTimeout(Poco::Timespan(0, 1000));
1690 client_ = std::make_unique<Poco::Net::StreamSocket>(ss);
1691 hasClient_.store(true);
1692 recvBuf_.clear();
1693 initialized_ = false;
1694 AiPanel::instance().setMcpConnected(true);
1695 AiPanel::instance().addLog("system", "mcp.accept", "client connected");
1696 } catch (const Poco::TimeoutException&) {
1697 } catch (const Poco::Net::NetException&) {
1698 } catch (...) {
1699 }
1700}
1701
1702bool McpServer::sendLine(const std::string& json) {
1703 if (transport_.load() == Transport::Stdio) {
1704 std::lock_guard<std::mutex> lock(ioMu_);
1705 if (!stdioOut_) return false;
1706 try {
1707 const std::string frame = json + "\n";
1708 (*stdioOut_) << frame;
1709 stdioOut_->flush();
1710 return true;
1711 } catch (...) {
1712 return false;
1713 }
1714 }
1715 if (!client_) return false;
1716 const std::string frame = json + "\n";
1717 try {
1718 const int sent = client_->sendBytes(frame.data(), static_cast<int>(frame.size()));
1719 return sent == static_cast<int>(frame.size());
1720 } catch (...) {
1721 client_.reset();
1722 hasClient_.store(false);
1723 AiPanel::instance().setMcpConnected(false);
1724 return false;
1725 }
1726}
1727
1728void McpServer::readAndDispatch() {
1729 if (!client_) return;
1730 char buf[8192];
1731 try {
1732 const int n = client_->receiveBytes(buf, sizeof(buf));
1733 if (n <= 0) {
1734 if (n == 0) {
1735 client_.reset();
1736 hasClient_.store(false);
1737 AiPanel::instance().setMcpConnected(false);
1738 AiPanel::instance().addLog("system", "mcp.disconnect", "client closed");
1739 }
1740 return;
1741 }
1742 recvBuf_.append(buf, static_cast<size_t>(n));
1743 } catch (const Poco::TimeoutException&) {
1744 return;
1745 } catch (const Poco::Net::NetException&) {
1746 return;
1747 } catch (...) {
1748 client_.reset();
1749 hasClient_.store(false);
1750 AiPanel::instance().setMcpConnected(false);
1751 return;
1752 }
1753
1754 while (true) {
1755 const auto nl = recvBuf_.find('\n');
1756 if (nl == std::string::npos) break;
1757 std::string line = recvBuf_.substr(0, nl);
1758 recvBuf_.erase(0, nl + 1);
1759 if (!line.empty() && line.back() == '\r') line.pop_back();
1760 if (line.empty()) continue;
1761 handleMessage(line);
1762 }
1763}
1764
1765void McpServer::handleMessage(const std::string& json) {
1766 try {
1767 Poco::JSON::Parser parser;
1768 auto var = parser.parse(json);
1769 auto obj = var.extract<Poco::JSON::Object::Ptr>();
1770 if (!obj) return;
1771
1772 std::string method;
1773 if (obj->has("method")) {
1774 try {
1775 method = obj->get("method").convert<std::string>();
1776 } catch (const Poco::Exception& e) {
1777 sendLine(makeError("null", -32600, std::string("bad method: ") + e.displayText()));
1778 return;
1779 }
1780 }
1781 const bool hasId = obj->has("id");
1782 Poco::Dynamic::Var idVar;
1783 if (hasId) idVar = obj->get("id");
1784 std::string idJson = "null";
1785 try {
1786 if (hasId) idJson = idToJson(idVar);
1787 } catch (const Poco::Exception& e) {
1788 sendLine(makeError("null", -32600, std::string("bad id: ") + e.displayText()));
1789 return;
1790 }
1791
1792 if (!hasId) {
1793 if (method == "notifications/initialized" || method == "initialized") {
1794 initialized_ = true;
1795 AiPanel::instance().addLog("system", "mcp.initialized", "handshake complete");
1796 }
1797 return;
1798 }
1799
1800 auto params = [&]() -> Poco::JSON::Object::Ptr {
1801 if (!obj->has("params")) return Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1802 try {
1803 return obj->getObject("params");
1804 } catch (...) {
1805 return Poco::JSON::Object::Ptr(new Poco::JSON::Object());
1806 }
1807 };
1808
1809 if (method == "initialize") {
1810 sendLine(handleInitialize(*this, idJson, params()));
1811 return;
1812 }
1813 if (method == "ping") {
1814 sendLine(makeResult(idJson, "{}"));
1815 return;
1816 }
1817 if (method == "tools/list") {
1818 sendLine(handleToolsList(idJson));
1819 return;
1820 }
1821 if (method == "tools/call") {
1822 sendLine(handleToolsCall(*this, idJson, params()));
1823 return;
1824 }
1825 if (method == "resources/list") {
1826 sendLine(handleResourcesList(idJson));
1827 return;
1828 }
1829 if (method == "resources/read") {
1830 sendLine(handleResourcesRead(idJson, params()));
1831 return;
1832 }
1833 if (method == "prompts/list") {
1834 sendLine(handlePromptsList(idJson));
1835 return;
1836 }
1837 if (method == "prompts/get") {
1838 sendLine(handlePromptsGet(idJson, params()));
1839 return;
1840 }
1841
1842 sendLine(makeError(idJson, -32601, "Method not found: " + method));
1843 } catch (const Poco::Exception& e) {
1844 sendLine(makeError("null", -32700, std::string("Parse error: ") + e.displayText()));
1845 } catch (const std::exception& e) {
1846 sendLine(makeError("null", -32603, e.what()));
1847 } catch (...) {
1848 sendLine(makeError("null", -32603, "Internal error"));
1849 }
1850}
1851
1852} // namespace eve::dev
struct SQVM * HSQUIRRELVM
int line
HSQUIRRELVM vm
Definition ECS.cpp:20
std::string title
std::string id
float u
Definition Grass.cpp:234
glm::vec3 n
Definition Grass.cpp:64
int h
int w
JobStatus status
std::vector< std::string > vars
uint32_t a
uint32_t b
uint32_t c
int idx
float f
glm::vec3 eye
glm::vec4 p[6]
const char * name
Definition RockMesh.cpp:21
int d
int v
int children
Definition TreeMesh.cpp:177
V3 dir
Definition TreeMesh.cpp:121
uint32_t s
Definition Weather.cpp:28
Audio control surface (provided by the audio module).
Definition AudioQuery.h:8
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).
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 RenderStatusInfo status() const =0
Scene graph query/mutation surface (provided by the scene module).
Definition SceneQuery.h:30
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:304
static DevTool & instance()
Definition DevTool.cpp:118
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()
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:401
static Snapshot & instance()
Definition Snapshot.cpp:285
I * query()
Definition Capability.h:77
std::size_t mcpCallgraphStackDepth()
Definition DevTool.cpp:676
std::size_t mcpCallgraphEvents()
Definition DevTool.cpp:674
std::string mcpFormatError(const std::string &message)
Definition DevTool.cpp:682
const std::string & mcpLastReport()
Definition DevTool.cpp:680
bool mcpDevAttached()
Thin hooks so McpServer.cpp need not include DevTool.hpp (avoids a cycle).
Definition DevTool.cpp:672
ScriptErrorContext captureCompileError(HSQUIRRELVM vm)
Captures the last compilation error recorded by the VM.
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.
WidgetDesc text(std::string content, std::string id)
Static text label.
Definition Widget.cpp:235
Definition Build.cpp:11
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
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