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>
68constexpr const SQChar* kMcpSnippetSource = _SC(
"memory://mcp/snippet");
69constexpr const SQChar* kMcpSceneDirectorSource = _SC(
"memory://mcp/scene-director");
71std::string idToJson(
const Poco::Dynamic::Var&
id) {
72 if (
id.isEmpty())
return "null";
74 if (
id.isInteger() ||
id.isNumeric())
return std::to_string(
id.convert<Poco::Int64>());
78 return std::string(
"\"") +
mcpJsonEscape(
id.convert<std::string>()) +
"\"";
84std::string makeResult(
const std::string& idJson,
const std::string& resultJson) {
85 return std::string(
"{\"jsonrpc\":\"2.0\",\"id\":") + idJson +
",\"result\":" + resultJson +
"}";
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) +
100 default:
return "none";
104std::string engineStatusJson(
const McpServer& mcp) {
106 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
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()) {
119 default: o->set(
"transport",
"none");
break;
122 o->set(
"gameRoot", mcp.gameRoot());
123 o->set(
"host", eve::cap::query<eve::IEditorHost>() ? eve::cap::query<eve::IEditorHost>()->
status()
134glm::vec3 argVec3(Poco::JSON::Object::Ptr args,
const char*
key,
const glm::vec3& def = {}) {
135 if (!args || !args->has(
key))
return def;
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>()));
148Poco::JSON::Array::Ptr vec3ToArray(
const glm::vec3&
v) {
149 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
165std::string sqStringLiteralEscape(
const std::string&
s) {
return mcpJsonEscape(
s); }
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>());
173 const double d =
v.convert<
double>();
175 std::snprintf(buf,
sizeof(buf),
"%g",
d);
178 if (
v.isString())
return std::string(
"\"") + sqStringLiteralEscape(
v.convert<std::string>()) +
"\"";
180 std::string out =
"[";
182 auto arr =
v.extract<Poco::JSON::Array::Ptr>();
183 for (
size_t i = 0; i < arr->size(); ++i) {
185 out += sqLiteralValue(arr->get(
static_cast<unsigned int>(i)));
193 std::string out =
"{";
196 auto obj =
v.extract<Poco::JSON::Object::Ptr>();
197 for (
const auto& kv : *obj) {
198 if (!
first) out +=
",";
200 out +=
"\"" + sqStringLiteralEscape(kv.first) +
"\"=" + sqLiteralValue(kv.second);
214 return compile ?
"compile failed" :
"runtime failed";
219 const SQInteger
top = sq_gettop(
vm);
224 sq_compilebuffer(
vm,
source.c_str(),
static_cast<SQInteger
>(
source.size()), kMcpSnippetSource, SQTrue))) {
226 if (err) *err = snippetErrorText(
vm,
true);
229 sq_pushroottable(
vm);
230 if (SQ_FAILED(sq_call(
vm, 1, SQFalse, SQTrue))) {
232 if (err) *err = snippetErrorText(
vm,
false);
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,
250 if (err) *err = snippetErrorText(
vm,
true);
253 sq_pushroottable(
vm);
254 if (SQ_FAILED(sq_call(
vm, 1, SQTrue, SQTrue))) {
256 std::string
text = snippetErrorText(
vm,
false);
258 *err =
text ==
"runtime failed" ?
"runtime failed (is the scene_director kit installed?)" : std::move(
text);
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;
273 if (err) *err =
"scene_director.nut not embedded (rebuild EVScripts)";
276 return runVmSnippet(
vm, kit, err);
279std::string sceneDirectorToolError(
const std::string&
name,
const std::string& err) {
280 return "error: " +
name +
": " + (err.empty() ?
"unknown" : err);
284 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
286 o->set(
"error",
"Graphics module not available");
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()));
311 return eve::cap::query<eve::IPixelWorldAutomation>();
320void runPendingVisionDump() {
322 auto* cap = mcpCapture();
327 Poco::JSON::Object::Ptr
output = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
331 output->set(
"status",
"stale");
332 output->set(
"entityId", entityId);
334 output->set(
"error", inspected.status().describe());
337 const auto&
info = inspected.value();
338 output->set(
"status",
"ok");
341 Poco::JSON::Array::Ptr
position = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
346 Poco::JSON::Array::Ptr
tint = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
354 output->set(
"parallaxScale",
info.parallaxScale);
356 output->set(
"receiveLight",
info.receiveLight);
358 output->set(
"receiveShadow",
info.receiveShadow);
359 output->set(
"hasPackedMaterial",
info.hasPackedMaterial);
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");
374 output->set(
"status",
"not-found");
375 output->set(
"error",
"Scene node is missing from the observed host");
378 output->set(
"status",
"ok");
393 Poco::JSON::Array::Ptr
children = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
399Poco::JSON::Object::Ptr runtimeObservation(Poco::JSON::Object::Ptr args, std::string*
error) {
402 auto* capture = mcpCapture();
404 if (
error) *
error =
"graphics module not available";
407 const int entityId =
getArgInt(args,
"entityId", -1);
410 if (
error) *
error =
"entityId and generation are required";
418 auto*
scene = mcpScene();
420 if (
error) *
error =
"scene module not available";
429struct ExpectationMatch {
436bool isJsonObject(
const Poco::Dynamic::Var&
value) {
437 return value.type() ==
typeid(Poco::JSON::Object::Ptr);
440bool isJsonArray(
const Poco::Dynamic::Var&
value) {
441 return value.type() ==
typeid(Poco::JSON::Array::Ptr);
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;
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;
462 compareExpectation(actualObject->get(item.first), item.second, childPath, tolerance, match);
466 if (isJsonArray(expected)) {
467 if (!isJsonArray(actual)) {
468 match->valid =
false;
469 match->error =
"Expected array at " +
path;
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;
480 compareExpectation(actualArray->get(
static_cast<unsigned>(
index)),
481 expectedArray->get(
static_cast<unsigned>(
index)),
482 path +
"[" + std::to_string(
index) +
"]", tolerance, match);
486 if (expected.isNumeric()) {
487 if (!actual.isNumeric()) {
488 match->valid =
false;
489 match->error =
"Expected numeric value at " +
path;
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);
497 if (expected.isBoolean()) {
498 if (!actual.isBoolean()) {
499 match->valid =
false;
500 match->error =
"Expected boolean value at " +
path;
503 if (actual.convert<
bool>() != expected.convert<
bool>()) match->mismatches.push_back(
path);
506 if (expected.isString()) {
507 if (!actual.isString()) {
508 match->valid =
false;
509 match->error =
"Expected string value at " +
path;
512 if (actual.convert<std::string>() != expected.convert<std::string>()) match->mismatches.push_back(
path);
515 if (expected.isEmpty()) {
516 if (!actual.isEmpty()) match->mismatches.push_back(
path);
519 match->valid =
false;
520 match->error =
"Unsupported expectation value at " +
path;
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());
531 if (!match.error.empty())
output->set(
"error", match.error);
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);
547 double tolerance = 0.001;
550 tolerance =
descriptor->get(
"tolerance").convert<
double>();
555 if (!std::isfinite(tolerance) || tolerance < 0.0) {
556 if (
error) *
error =
"tolerance must be a finite non-negative number";
559 const Poco::Dynamic::Var expected =
descriptor->get(
"expect");
560 if (!isJsonObject(expected)) {
561 if (
error) *
error =
"expect must be a JSON object";
564 ExpectationMatch match;
565 compareExpectation(Poco::Dynamic::Var(observation), expected,
"", tolerance, &match);
570 event->set(
"expectation", expectationValue(match));
571 event->set(
"converged", match.mismatches.empty());
572 event->set(
"maxError", match.maxError);
576std::string callTool(McpServer& mcp,
const std::string&
name, Poco::JSON::Object::Ptr args) {
582 if (
name ==
"eve_status")
return engineStatusJson(mcp);
584 if (
name ==
"eve_api_search") {
586 const std::string
module = getArgString(args, "module");
587 const std::string scriptClass =
getArgString(args,
"class");
592 std::ostringstream json;
593 json <<
"{\"ok\":true,\"schema\":\"eve.binding-api\",\"schemaVersion\":1,\"query\":\"" <<
mcpJsonEscape(query)
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() <<
'}';
604 if (
name ==
"eve_api_get") {
606 const std::string scriptClass =
getArgString(args,
"class");
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);
617 return "{\"ok\":false,\"error\":\"missing key or method\"}";
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\":") +
624 if (
name ==
"eve_gameplay") {
625 if (!args || !args->has(
"request"))
return "error: missing request";
629 }
catch (
const std::exception&
error) {
630 return std::string(
"error: invalid request: ") +
error.what();
633 return response ? std::move(response).takeValue()
634 : std::string(
"error: ") + response.status().describe();
637 if (
name ==
"eve_play") {
638 if (!args || !args->has(
"request"))
return "error: missing request";
642 }
catch (
const std::exception&
error) {
643 return std::string(
"error: invalid request: ") +
error.what();
646 return response ? std::move(response).takeValue()
647 : std::string(
"error: ") + response.status().describe();
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()),
658 if (
name ==
"eve_ui_tree") {
659 return mcpUI() ? mcpUI()->tree(
getArgString(args,
"host")) :
"error: ui module not available";
661 if (
name ==
"eve_ui_get") {
663 :
"error: ui module not available";
665 if (
name ==
"eve_ui_click") {
667 :
"error: ui module not available";
671 const auto editorInvoke = [&](
const char*
operation) {
673 : std::string(
"error: editor module not available");
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";
687 Poco::JSON::Object::Ptr observation = runtimeObservation(args, &
error);
688 if (!observation)
return "error: " +
error;
689 if (observation->getValue<std::string>(
"status") !=
"ok")
691 Poco::JSON::Object::Ptr
event = observationEvent(args, observation, &
error);
693 Poco::JSON::Object::Ptr
invalid = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
694 invalid->set(
"status",
"invalid-expectation");
696 invalid->set(
"observation", observation);
699 Poco::JSON::Object::Ptr
request = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
700 request->set(
"descriptor", args);
704 if (
name ==
"eve_editor_observe_poll") {
705 auto* editor = mcpEditor();
706 if (!editor)
return "error: editor module not available";
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")
714 auto descriptor = describedObject->getObject(
"descriptor");
716 Poco::JSON::Object::Ptr observation = runtimeObservation(
descriptor, &
error);
718 observation = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
719 observation->set(
"status",
"unavailable");
720 observation->set(
"error",
error);
722 Poco::JSON::Object::Ptr
event = observationEvent(
descriptor, observation, &
error);
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);
729 Poco::JSON::Object::Ptr
publish = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
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")
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();
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";
748 Poco::JSON::Object::Ptr before = runtimeObservation(args, &
error);
749 if (!before)
return "error: " +
error;
750 if (before->getValue<std::string>(
"status") !=
"ok")
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")) {
759 tolerance = args->get(
"tolerance").convert<
double>();
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");
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");
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);
786 bool transactionAccepted =
false;
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());
799 Poco::JSON::Object::Ptr after = runtimeObservation(args, &
error);
801 after = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
802 after->set(
"status",
"unavailable");
803 after->set(
"error",
error);
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);
815 output->set(
"status",
"expectation-observation-failed");
816 output->set(
"error", match.error);
818 }
else if (hasExpectation) {
819 output->set(
"converged",
false);
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());
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");
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);
840 if (entityId < 0 ||
generation < 0)
return "error: entityId and generation are required";
845 if (
name ==
"eve_scene_status") {
846 auto*
scene = mcpScene();
847 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
849 o->set(
"error",
"Scene module not available");
852 const std::string
host =
scene->activeHost();
854 o->set(
"activeHost", Poco::Dynamic::Var());
855 o->set(
"nodeCount", 0);
858 o->set(
"activeHost",
host);
859 o->set(
"nodeCount",
scene->nodeCount());
860 o->set(
"rootId",
scene->rootId());
864 if (
name ==
"eve_scene_nodes") {
865 auto*
scene = mcpScene();
866 if (!scene ||
scene->activeHost().empty())
return "error: no active scene host";
868 Poco::JSON::Array::Ptr arr = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
870 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
872 o->set(
"name",
n.name);
873 o->set(
"path",
n.path);
874 o->set(
"visible",
n.visible);
880 if (
name ==
"eve_scene_node_get") {
881 auto*
scene = mcpScene();
882 if (!scene ||
scene->activeHost().empty())
return "error: no active scene host";
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());
888 o->set(
"name",
n.name);
889 o->set(
"path",
n.path);
890 o->set(
"visible",
n.visible);
894 o->set(
"yaw",
n.yaw);
895 o->set(
"pitch",
n.pitch);
896 o->set(
"roll",
n.roll);
900 o->set(
"parent",
n.parent);
901 Poco::JSON::Array::Ptr kids = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
903 o->set(
"children", kids);
907 if (
name ==
"eve_scene_node_set") {
908 auto*
scene = mcpScene();
909 if (!scene ||
scene->activeHost().empty())
return "error: no active scene host";
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")) {
917 if (args && args->has(
"visible")) {
918 changed =
scene->setNodeVisible(
id,
getArgBool(args,
"visible")) || changed;
920 return changed ?
"ok" :
"error: node not found: " +
id;
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);
933 addList(
"algorithms", pg->algorithms());
934 addList(
"meshRecipes", pg->meshRecipes());
935 addList(
"textureRecipes", pg->textureRecipes());
936 addList(
"pbrRecipes", pg->pbrRecipes());
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"}) {
949 if (args && args->has(
"corridorStyle"))
952 std::string json = pg->generateMap(algorithm,
getArgInt(args,
"width", 32),
getArgInt(args,
"height", 32),
954 if (json.empty())
return "error: " + (err.empty() ? std::string(
"empty grid") : err);
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";
965 pg->buildMesh(recipe,
static_cast<uint32_t
>(
getArgInt(args,
"seed", 0)),
getArgInt(args,
"width", -1),
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);
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"));
983 if (
name ==
"eve_physics_new_world") {
984 auto* ph = mcpPhysics();
985 if (!ph)
return "error: Physics module not available";
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());
992 o->set(
"gravityX", gx);
993 o->set(
"gravityY", gy);
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());
1006 o->set(
"gravityX", gx);
1007 o->set(
"gravityY", gy);
1013 if (
name ==
"eve_physics_raycast") {
1014 auto* ph = mcpPhysics();
1015 if (!ph)
return "error: Physics module not available";
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);
1023 o->set(
"bodyId",
h.bodyId);
1026 o->set(
"normalX",
h.normalX);
1027 o->set(
"normalY",
h.normalY);
1028 o->set(
"fraction",
h.fraction);
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";
1040 if (
name ==
"eve_render_status") {
1041 return renderStatusText(mcpCapture());
1044 if (
name ==
"eve_render_describe") {
1045 const bool fresh =
getArgBool(args,
"fresh",
false);
1046 const std::string reason =
getArgString(args,
"reason");
1050 if (
name ==
"eve_render_vision_config") {
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();
1060 if (
name ==
"eve_screenshot") {
1061 auto* cap = mcpCapture();
1062 if (!cap)
return "error: Graphics module not available";
1064 if (
path.empty())
path =
"mcp_screenshot.png";
1068 if (std::filesystem::path(
path).is_relative() && !mcp.gameRoot().empty())
1069 path = (std::filesystem::path(mcp.gameRoot()) /
path).
string();
1072 for (
char&
c :
path) {
1073 if (
c ==
'\\')
c =
'/';
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);
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());
1100 o->set(
"path",
path);
1101 o->set(
"bytes", sizeError ? Poco::Int64(0) : static_cast<
Poco::Int64>(
bytes));
1103 o->set(
"height",
h);
1108 if (
name ==
"eve_particles_status") {
1109 auto* part = mcpParticles();
1110 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
1112 o->set(
"error",
"Particles module not available");
1115 o->set(
"emitterCount", part->emitterCount());
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;
1126 return "error: failed to create emitter";
1127 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
1130 o->set(
"count", cnt);
1135 if (
name ==
"eve_audio_status") {
1136 auto*
audio = mcpAudio();
1137 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
1139 o->set(
"error",
"Audio module not available");
1142 o->set(
"volume",
audio->volume());
1146 if (
name ==
"eve_audio_set_volume") {
1147 auto*
audio = mcpAudio();
1148 if (!audio)
return "error: Audio module not available";
1153 if (
name ==
"eve_audio_stop_all") {
1154 auto*
audio = mcpAudio();
1155 if (!audio)
return "error: Audio module not available";
1161 if (
name ==
"eve_host_status") {
1162 return mcpHost() ? mcpHost()->status() :
"error: ui module not available";
1164 if (
name ==
"eve_host_window_open") {
1166 return mcpHost() ? mcpHost()->openWindow(
title,
getArgInt(args,
"width", 1280),
getArgInt(args,
"height", 800))
1167 :
"error: ui module not available";
1169 if (
name ==
"eve_host_window_close") {
1170 return mcpHost() ? mcpHost()->closeWindow() :
"error: ui module not available";
1172 if (
name ==
"eve_host_window_state") {
1173 return mcpHost() ? mcpHost()->windowState() :
"error: ui module not available";
1175 if (
name ==
"eve_host_editor_apply") {
1177 if (args && args->has(
"editor")) {
1179 Poco::JSON::Object::Ptr o = args->getObject(
"editor");
1188 if (json.empty())
return "error: missing editor";
1189 return mcpHost() ? mcpHost()->applyEditor(json) :
"error: ui module not available";
1191 if (
name ==
"eve_host_editor_remove") {
1192 return mcpHost() ? mcpHost()->removeEditor(
getArgString(args,
"id")) :
"error: ui module not available";
1194 if (
name ==
"eve_host_editor_list") {
1195 return mcpHost() ? mcpHost()->listEditors() :
"error: ui module not available";
1197 if (
name ==
"eve_host_editor_state") {
1198 return mcpHost() ? mcpHost()->editorState(
getArgString(args,
"id")) :
"error: ui module not available";
1200 if (
name ==
"eve_host_editor_set_value") {
1201 if (!args || !args->has(
"value"))
return "error: missing value";
1204 :
"error: ui module not available";
1206 if (
name ==
"eve_host_editor_save") {
1207 return mcpHost() ? mcpHost()->saveEditor(
getArgString(args,
"id")) :
"error: ui module not available";
1209 if (
name ==
"eve_host_editor_unload") {
1210 return mcpHost() ? mcpHost()->unloadEditor(
getArgString(args,
"id")) :
"error: ui module not available";
1212 if (
name ==
"eve_host_vm_register") {
1214 :
"error: ui module not available";
1216 if (
name ==
"eve_host_vm_unregister") {
1217 return mcpHost() ? mcpHost()->unregisterVM(
getArgString(args,
"name")) :
"error: ui module not available";
1219 if (
name ==
"eve_host_events") {
1220 return mcpHost() ? mcpHost()->consumeEvents(
getArgString(args,
"editor")) :
"error: ui module not available";
1222 if (
name ==
"eve_host_widget_rect") {
1224 :
"error: ui module not available";
1226 if (
name ==
"eve_host_capture") {
1227 return mcpHost() ? mcpHost()->capture(
getArgString(args,
"path")) :
"error: ui module not available";
1229 if (
name ==
"eve_host_script") {
1230 return mcpHost() ? mcpHost()->runScript(
getArgString(args,
"source")) :
"error: ui module not available";
1232 if (
name ==
"eve_host_resource_reload") {
1233 return mcpHost() ? mcpHost()->reloadResource(
getArgString(args,
"path")) :
"error: ui module not available";
1235 if (
name ==
"eve_host_hot_reload_status") {
1236 return mcpHost() ? mcpHost()->hotReloadStatus() :
"error: ui module not available";
1238 if (
name ==
"eve_host_shutdown") {
1239 if (mcpHost()) mcpHost()->requestExit();
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());
1256 if (
name ==
"eve_skeleton_inspect") {
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";
1266 if (
info.value.size() >= 2 &&
info.value.front() ==
'"' &&
info.value.back() ==
'"')
1267 return info.value.substr(1,
info.value.size() - 2);
1271 if (
name ==
"eve_pause") {
1276 if (
name ==
"eve_continue") {
1278 dap.notifyContinued();
1281 if (
name ==
"eve_step_over") {
1283 dap.notifyContinued();
1286 if (
name ==
"eve_step_into") {
1288 dap.notifyContinued();
1291 if (
name ==
"eve_step_out") {
1293 dap.notifyContinued();
1296 if (
name ==
"eve_step_frame") {
1298 dap.notifyContinued();
1299 return "step_frame";
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());
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);
1317 if (
name ==
"eve_locals") {
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);
1331 if (
name ==
"eve_set_breakpoint") {
1334 if (
source.empty() ||
line <= 0)
return "error: need source and line";
1337 return "ok id=" + std::to_string(
id);
1339 if (
name ==
"eve_clear_breakpoint") {
1342 if (
source.empty() ||
line <= 0)
return "error: need source and line";
1343 return dbg.clearBreakpoint(
source,
line) ?
"cleared" :
"not_found";
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);
1359 if (
name ==
"eve_watch_add") {
1360 const std::string expr =
getArgString(args,
"expression");
1361 if (expr.empty())
return "error: missing expression";
1363 dbg.refreshWatches();
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);
1379 if (
name ==
"eve_snapshot_capture") {
1381 if (!
vm)
return "error: no VM";
1384 if (!err.empty() && json.empty())
return "error: " + err;
1387 if (
name ==
"eve_snapshot_restore") {
1389 if (!
vm)
return "error: no VM";
1395 if (
name ==
"eve_snapshot_save") {
1397 if (!
vm)
return "error: no VM";
1403 if (
name ==
"eve_snapshot_load") {
1405 if (!
vm)
return "error: no VM";
1412 if (
name ==
"eve_error_slice") {
1414 if (!last.empty())
return last;
1415 return mcpFormatError(
"no prior error; callgraph slice at latest site");
1418 if (
name ==
"eve_run_script") {
1420 if (!
vm)
return "error: no VM";
1422 if (
source.empty())
return "error: missing source";
1428 if (
name ==
"eve_ai_note") {
1430 if (
text.empty())
return "error: missing text";
1438 if (
name ==
"eve_scene_director_install") {
1440 if (!
vm)
return "error: no VM";
1442 if (!ensureSceneDirectorInstalled(
vm, &err))
return sceneDirectorToolError(
name, err);
1446 if (
name ==
"eve_scene_director_status") {
1448 if (!
vm)
return "error: no VM";
1450 if (!ensureSceneDirectorInstalled(
vm, &err))
return sceneDirectorToolError(
name, err);
1451 return callSceneDirectorReturn(
vm,
"return ::scene_director.status();", &err);
1454 if (
name ==
"eve_scene_reset") {
1456 if (!
vm)
return "error: no VM";
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);
1464 if (
name ==
"eve_scene_modify") {
1466 if (!
vm)
return "error: no VM";
1468 if (!ensureSceneDirectorInstalled(
vm, &err))
return sceneDirectorToolError(
name, err);
1471 Poco::Dynamic::Var paramsVar;
1472 if (args && args->has(
"params")) {
1474 paramsVar = Poco::Dynamic::Var(args->getObject(
"params"));
1478 const std::string snippet =
"return ::scene_director.modify(" + sqLiteralValue(Poco::Dynamic::Var(action)) +
1479 "," + sqLiteralValue(Poco::Dynamic::Var(
target)) +
"," + sqLiteralValue(paramsVar) +
1481 const std::string out = callSceneDirectorReturn(
vm, snippet, &err);
1482 if (!err.empty())
return sceneDirectorToolError(
name, err);
1486 if (
name ==
"eve_camera_generate") {
1488 if (!
vm)
return "error: no VM";
1490 if (!ensureSceneDirectorInstalled(
vm, &err))
return sceneDirectorToolError(
name, err);
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);
1498 if (
name ==
"eve_scene_info") {
1500 if (!
vm)
return "error: no VM";
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);
1509 if (
name ==
"inspect_generate_scene_camera_views") {
1510 const glm::vec3
center = argVec3(args,
"center");
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));
1524 root->set(
"views", arr);
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));
1533 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
1536 o->set(
"error",
"failed to set camera pose (no Graphics/Camera3D)");
1539 Poco::JSON::Parser parser;
1547 if (
name ==
"capture_render_frame") {
1550 std::vector<std::string> buffers;
1551 if (args && args->has(
"buffers")) {
1553 auto arr = args->getArray(
"buffers");
1555 for (
size_t i = 0; i < arr->size(); ++i)
1556 buffers.push_back(arr->get(
static_cast<unsigned int>(i)).convert<std::string>());
1562 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
1563 o->set(
"ok", 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);
1580 o->set(
"error", res.error);
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");
1598 return "error: unknown tool " +
name;
1604constexpr const char* kMcpProtocolLatest =
"2025-06-18";
1606bool isSupportedProtocol(
const std::string& requested) {
1607 return requested ==
"2025-06-18" || requested ==
"2025-03-26" || requested ==
"2024-11-05";
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;
1615 if (
params->has(
"clientInfo")) {
1617 if (
info &&
info->has(
"name")) clientName =
info->get(
"name").convert<std::string>();
1619 if (
params->has(
"protocolVersion")) requested =
params->get(
"protocolVersion").convert<std::string>();
1630 const std::string protocol = isSupportedProtocol(requested) ? requested : kMcpProtocolLatest;
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);
1651std::string handleToolsList(
const std::string& idJson) {
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# "
1660 "\"inputSchema\":{\"type\":\"object\",\"properties\":{"
1661 "\"query\":{\"type\":\"string\",\"description\":\"case-insensitive method, class, key, or space-separated "
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\":["
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\":["
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 "
1701 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"},"
1702 "\"sequence\":{\"type\":\"integer\"},\"kind\":{\"type\":\"string\",\"enum\":[\"paint\",\"heat\",\"explosion\"]}"
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 "
1718 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"world\":{\"type\":\"integer\"}},\"required\":["
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 "
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 "
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\","
1751 "{\"name\":\"eve_editor_target_close\",\"description\":\"Unregister and destroy an Editor-owned automation "
1753 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"target\":{\"type\":\"string\"}},\"required\":["
1755 "{\"name\":\"eve_editor_inspect\",\"description\":\"Inspect authoritative structured state for an editor "
1757 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"target\":{\"type\":\"string\"}},\"required\":["
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\":["
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 "
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\":["
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\":["
1795 "{\"name\":\"eve_editor_cancel\",\"description\":\"Discard a retained editor command plan without side "
1797 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"planId\":{\"type\":\"string\"}},\"required\":["
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\"]}}"
1852 "{\"name\":\"eve_snapshot_save\",\"description\":\"Save snapshot to a file path.\","
1853 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}}"
1855 "{\"name\":\"eve_snapshot_load\",\"description\":\"Load snapshot from a file path.\","
1856 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}}"
1858 "{\"name\":\"eve_error_slice\",\"description\":\"Return last error report / backward slice (script + "
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\":["
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\"]}}"
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 / "
1874 "\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},"
1875 "{\"name\":\"eve_scene_reset\",\"description\":\"Clear all staged props, camera and reset lighting to "
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)\"}}}}"
1897 "{\"name\":\"set_camera_pose\",\"description\":\"Set the active camera pose from position + Euler rotation + "
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\"]}}"
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'])\"}}}}"
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)\"}}}}"
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 "
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 "
1929 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"x\":{\"type\":\"number\"},"
1930 "\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"},\"visible\":{\"type\":\"boolean\"}},\"required\":["
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\"]}}"
1939 "{\"name\":\"eve_procgen_mesh\",\"description\":\"Build a procedural CPU mesh (mesh.* recipe) and return "
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 "
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\":["
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\":"
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\":"
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\":["
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 "
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\":["
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 "
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 "
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 "
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\"]}}"
2025 "{\"name\":\"eve_host_events\",\"description\":\"Read and clear interaction events (human clicks/sliders or AI "
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 "
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 + "
2036 "\"inputSchema\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\"}},\"required\":["
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\"]}}"
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\":{}}}",
2048 static const std::string kToolsBody = [] {
2054 for (
const char* part : kToolsParts) out += part;
2055 for (std::string_view family :
2057 if (family.empty())
continue;
2066 std::string tools =
"{\"tools\":[";
2067 tools += kToolsBody;
2069 if (!scripted.empty()) {
2074 return makeResult(idJson, tools);
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");
2081 name =
params->get(
"name").convert<std::string>();
2083 return makeError(idJson, -32602,
"tools/call params.name must be a string");
2085 Poco::JSON::Object::Ptr args = Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
2086 if (
params->has(
"arguments")) {
2088 args =
params->getObject(
"arguments");
2105 }
catch (
const std::exception& e) {
2107 return makeResult(idJson,
textContentResult(std::string(
"error: ") + e.what(),
true));
2116 }
catch (
const std::exception& e) {
2118 return makeResult(idJson,
textContentResult(std::string(
"error: ") + e.what(),
true));
2127 }
catch (
const std::exception& e) {
2129 return makeResult(idJson,
textContentResult(std::string(
"error: ") + e.what(),
true));
2134 const std::string out = callTool(mcp,
name, args);
2135 const bool isErr = out.rfind(
"error:", 0) == 0;
2137 }
catch (
const std::exception& e) {
2139 return makeResult(idJson,
textContentResult(std::string(
"error: ") + e.what(),
true));
2143std::string handleResourcesList(
const std::string& idJson) {
2144 return makeResult(idJson,
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\"}"
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");
2163 uri =
params->get(
"uri").convert<std::string>();
2165 return makeError(idJson, -32602,
"resources/read params.uri must be a string");
2168 std::string mime =
"text/plain";
2169 if (
uri ==
"eve://status") {
2171 mime =
"application/json";
2172 }
else if (
uri ==
"eve://error-report") {
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());
2183 mime =
"application/json";
2184 }
else if (
uri ==
"eve://crash-log") {
2188 constexpr std::streamoff kCrashResourceBytes = 64 * 1024;
2189 text =
"(no crash log yet)\n";
2191 std::error_code
exists = std::error_code();
2193 std::ifstream in(
path, std::ios::binary);
2195 in.seekg(0, std::ios::end);
2196 const std::streamoff
size = in.tellg();
2197 const std::streamoff
start =
size > kCrashResourceBytes ?
size - kCrashResourceBytes : 0;
2199 std::ostringstream
buffer;
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;
2207 if (
text.empty())
text =
"(crash log is empty)\n";
2211 return makeError(idJson, -32002,
"Unknown resource: " +
uri);
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)));
2225std::string handlePromptsList(
const std::string& idJson) {
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.\"}"
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");
2241 name =
params->get(
"name").convert<std::string>();
2243 return makeError(idJson, -32602,
"prompts/get params.name must be a string");
2246 if (
name ==
"debug_failure") {
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") {
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") {
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.";
2268 return makeError(idJson, -32602,
"Unknown prompt: " +
name);
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);
2278 msg->set(
"content", content);
2279 Poco::JSON::Array::Ptr messages = Poco::JSON::Array::Ptr(
new Poco::JSON::Array());
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)));
2296McpServer::McpServer() =
default;
2298McpServer::~McpServer() {
2301 std::lock_guard<std::mutex> lock(ioMu_);
2316 stdioQueue_.clear();
2317 listening_.store(
false);
2318 hasClient_.store(
false);
2324 for (
char&
c :
root) {
2325 if (
c ==
'\\')
c =
'/';
2327 while (!
root.empty() && (
root.back() ==
'/' ||
root.back() ==
'\\'))
root.pop_back();
2328 gameRoot_ = std::move(
root);
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());
2339 listening_.store(
true);
2340 initialized_ =
false;
2342 if (gameRoot_.empty())
setGameRoot(std::filesystem::current_path().string());
2352 listening_.store(
false);
2363 std::lock_guard<std::mutex> lock(ioMu_);
2364 stdioQueue_.clear();
2367 listening_.store(
true);
2369 initialized_ =
false;
2370 stdinClosed_.store(
false);
2371 hasClient_.store(
true);
2374 joinReader_ = (&in != &std::cin);
2376 stdioReader_ = std::thread([
this, &in]() {
2378 while (listening_.load() && std::getline(in,
line)) {
2379 if (!line.empty() && line.back() ==
'\r') line.pop_back();
2380 if (line.empty()) continue;
2382 std::lock_guard<std::mutex> lock(ioMu_);
2383 stdioQueue_.push_back(std::move(line));
2386 stdinClosed_.store(
true);
2387 hasClient_.store(
false);
2390 transport_.store(Transport::None);
2391 listening_.store(
false);
2392 hasClient_.store(
false);
2401void McpServer::stop() {
2402 listening_.store(
false);
2403 hasClient_.store(
false);
2404 if (stdioReader_.joinable()) {
2406 stdioReader_.join();
2408 stdioReader_.detach();
2411 std::lock_guard<std::mutex> lock(ioMu_);
2426 stdioQueue_.clear();
2429 transport_.store(Transport::None);
2430 initialized_ =
false;
2432 stdioOut_ =
nullptr;
2433 AiPanel::instance().setMcpPort(0);
2434 AiPanel::instance().setMcpConnected(
false);
2435 AiPanel::instance().setClientName({});
2438void McpServer::poll() {
2439 if (!listening_.load())
return;
2440 if (transport_.load() == Transport::Stdio) {
2441 std::vector<std::string> batch;
2443 std::lock_guard<std::mutex> lock(ioMu_);
2444 batch.swap(stdioQueue_);
2446 for (
const auto&
line : batch) handleMessage(
line);
2447 runPendingVisionDump();
2450 acceptNonBlocking();
2452 runPendingVisionDump();
2455void McpServer::acceptNonBlocking() {
2456 if (!server_ || client_)
return;
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);
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&) {
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;
2479 const std::string frame = json +
"\n";
2480 (*stdioOut_) << frame;
2487 if (!client_)
return false;
2488 const std::string frame = json +
"\n";
2490 const int sent = client_->sendBytes(frame.data(),
static_cast<int>(frame.size()));
2491 return sent ==
static_cast<int>(frame.size());
2494 hasClient_.store(
false);
2495 AiPanel::instance().setMcpConnected(
false);
2500void McpServer::readAndDispatch() {
2501 if (!client_)
return;
2504 const int n = client_->receiveBytes(buf,
sizeof(buf));
2508 hasClient_.store(
false);
2509 AiPanel::instance().setMcpConnected(
false);
2510 AiPanel::instance().addLog(
"system",
"mcp.disconnect",
"client closed");
2514 recvBuf_.append(buf,
static_cast<size_t>(
n));
2515 }
catch (
const Poco::TimeoutException&) {
2517 }
catch (
const Poco::Net::NetException&) {
2521 hasClient_.store(
false);
2522 AiPanel::instance().setMcpConnected(
false);
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);
2537void McpServer::handleMessage(
const std::string& json) {
2539 Poco::JSON::Parser parser;
2540 auto var = parser.parse(json);
2541 auto obj = var.extract<Poco::JSON::Object::Ptr>();
2545 if (obj->has(
"method")) {
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()));
2553 const bool hasId = obj->has(
"id");
2554 Poco::Dynamic::Var idVar;
2555 if (hasId) idVar = obj->get(
"id");
2556 std::string idJson =
"null";
2558 if (hasId) idJson = idToJson(idVar);
2559 }
catch (
const Poco::Exception& e) {
2560 sendLine(makeError(
"null", -32600, std::string(
"bad id: ") + e.displayText()));
2565 if (
method ==
"notifications/initialized" ||
method ==
"initialized") {
2566 initialized_ =
true;
2567 AiPanel::instance().addLog(
"system",
"mcp.initialized",
"handshake complete");
2572 auto params = [&]() -> Poco::JSON::Object::Ptr {
2573 if (!obj->has(
"params"))
return Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
2575 return obj->getObject(
"params");
2577 return Poco::JSON::Object::Ptr(
new Poco::JSON::Object());
2581 if (
method ==
"initialize") {
2582 sendLine(handleInitialize(*
this, idJson,
params()));
2586 sendLine(makeResult(idJson,
"{}"));
2589 if (
method ==
"tools/list") {
2590 sendLine(handleToolsList(idJson));
2593 if (
method ==
"tools/call") {
2594 sendLine(handleToolsCall(*
this, idJson,
params()));
2597 if (
method ==
"resources/list") {
2598 sendLine(handleResourcesList(idJson));
2601 if (
method ==
"resources/read") {
2602 sendLine(handleResourcesRead(idJson,
params()));
2605 if (
method ==
"prompts/list") {
2606 sendLine(handlePromptsList(idJson));
2609 if (
method ==
"prompts/get") {
2610 sendLine(handlePromptsGet(idJson,
params()));
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()));
2620 sendLine(makeError(
"null", -32603,
"Internal error"));
ActionParameterOperation operation
std::vector< QuestEvent > pending
struct SQVM * HSQUIRRELVM
Versioned JSON facade shared by scripting, MCP and scenario tooling.
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
std::array< float, 3 > position
Capability-backed domain tool families for the embedded MCP server.
std::vector< std::string > mismatches
std::unique_ptr< gpgpu::GpuBuffer > buffer
Versioned Play Host protocol for Agent pause / step / observe / capture.
std::weak_ptr< PrimitiveScene > scene
const UnitySourceAsset & source
Audio control surface (provided by the audio module).
Module-neutral bridge from DevTools/MCP to the editor command protocol.
Declarative editor-host control (provided by the ui module).
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).
Semantic retained-UI automation provided by the ui module.
void addNote(std::string text)
std::string formatLog(size_t max=64) const
void setMcpPort(int port)
void addLog(std::string kind, std::string title, std::string detail={})
static AiPanel & instance()
void setMcpConnected(bool on)
void setClientName(std::string name)
static DebugAdapter & instance()
static Debugger & instance()
Embedded Model Context Protocol (MCP) server for AI-assisted game development.
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 ¢er, float fov=60.f)
围绕 center 自动生成一组标准化巡检机位:
std::string capture(HSQUIRRELVM vm, std::string *error=nullptr) const
Capture marked roots, or heuristic roots when none marked.
static Snapshot & instance()
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
bool getArgBool(Poco::JSON::Object::Ptr args, const char *key, bool def=false)
Boolean argument, or def when absent or not convertible.
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()
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.
Result< std::string > executePlayJson(std::string_view requestJson)
Parse, execute and serialize one Play JSON request against the live engine.
std::string mcpJsonEscape(const std::string &value)
Escape a raw string for embedding inside a JSON string literal.
Poco::Dynamic::Var getArgVar(Poco::JSON::Object::Ptr args, const char *key)
Raw JSON value of key, or an empty Var when absent.
std::string callScriptTool(const std::string &name, Poco::JSON::Object::Ptr args)
Invoke one exported tool.
std::size_t mcpCallgraphEvents()
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.
std::string textContentResult(const std::string &text, bool isError=false)
MCP tools/call result object carrying one text content item.
std::string mcpFormatError(const std::string &message)
const std::string & mcpLastReport()
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).
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.
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.
@ 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.
Build metadata (engine git commit, build time, third-party version).
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).
static T & get()
Returns the process-lifetime instance.
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.
bool empty() const noexcept
True when no error payload was captured.