9#include <Poco/Dynamic/Var.h>
10#include <Poco/JSON/Array.h>
11#include <Poco/JSON/Object.h>
13#include <simplesquirrel/simplesquirrel.hpp>
28constexpr std::size_t kMaxTools = 128;
29constexpr std::size_t kMaxNameLength = 64;
30constexpr std::size_t kMinNameLength = 3;
31constexpr std::size_t kMaxDescription = 512;
32constexpr std::size_t kMaxSchemaBytes = 16 * 1024;
33constexpr std::size_t kMaxResultBytes = 256 * 1024;
34constexpr int kMaxArgumentDepth = 16;
37const char*
const kReservedPrefixes[] = {
"eve_",
"inspect_",
"set_",
"capture_",
"get_"};
48std::vector<ScriptTool> g_tools;
50bool hasReservedPrefix(
const std::string&
name) {
51 for (
const char* raw : kReservedPrefixes) {
52 const std::string prefix(raw);
53 if (
name.size() > prefix.size() &&
name.compare(0, prefix.size(), prefix) == 0)
return true;
63 const SQInteger
top = sq_gettop(
vm);
64 sq_pushobject(
vm, table);
65 sq_pushstring(
vm,
key, -1);
67 if (SQ_SUCCEEDED(sq_get(
vm, -2)) && sq_gettype(
vm, -1) == OT_STRING) {
68 const SQChar*
text =
nullptr;
76std::string tableFieldJson(
HSQUIRRELVM vm, HSQOBJECT table,
const char* field) {
77 const SQInteger
top = sq_gettop(
vm);
78 sq_pushobject(
vm, table);
79 sq_pushstring(
vm, field, -1);
81 if (SQ_SUCCEEDED(sq_get(
vm, -2)) && sq_gettype(
vm, -1) == OT_TABLE) json =
sqValueToJson(
vm, -1);
88 if (
depth > kMaxArgumentDepth ||
value.isEmpty()) {
92 if (
value.isBoolean()) {
93 sq_pushbool(
vm,
value.convert<
bool>() ? SQTrue : SQFalse);
96 if (
value.isInteger()) {
97 sq_pushinteger(
vm,
static_cast<SQInteger
>(
value.convert<Poco::Int64>()));
100 if (
value.isNumeric()) {
101 sq_pushfloat(
vm,
static_cast<SQFloat
>(
value.convert<
double>()));
104 if (
value.isString()) {
105 const std::string
text =
value.convert<std::string>();
106 sq_pushstring(
vm,
text.c_str(),
static_cast<SQInteger
>(
text.size()));
109 if (
value.type() ==
typeid(Poco::JSON::Array::Ptr)) {
110 auto array =
value.extract<Poco::JSON::Array::Ptr>();
112 const SQInteger
target = sq_gettop(
vm);
119 if (
value.type() ==
typeid(Poco::JSON::Object::Ptr)) {
120 auto object =
value.extract<Poco::JSON::Object::Ptr>();
122 const SQInteger
target = sq_gettop(
vm);
123 for (
const auto& entry : *
object) {
124 sq_pushstring(
vm, entry.first.c_str(),
static_cast<SQInteger
>(entry.first.size()));
125 pushJsonValue(
vm, entry.second,
depth + 1);
136 return "tool handler failed";
139void reportRejection(
const std::string&
name,
const std::string& reason) {
149void releaseEntry(ScriptTool& tool,
HSQUIRRELVM owner) {
150 if (tool.vm != owner || owner ==
nullptr || sq_isnull(tool.handler))
return;
151 sq_release(owner, &tool.handler);
159 reportRejection(
name,
160 "name must match [a-z][a-z0-9_]{2,63} and must not use a built-in prefix "
161 "(eve_ / inspect_ / set_ / capture_ / get_)");
164 if (spec._type != OT_TABLE) {
165 reportRejection(
name,
"spec must be a table with a handler closure");
169 const std::string
description = tableString(
vm, spec,
"description");
171 reportRejection(
name,
"description is longer than " + std::to_string(kMaxDescription) +
" characters");
175 const std::string schema = tableFieldJson(
vm, spec,
"inputSchema");
176 if (schema.size() > kMaxSchemaBytes) {
177 reportRejection(
name,
"inputSchema serializes to more than " + std::to_string(kMaxSchemaBytes) +
" bytes");
180 if (!schema.empty() && schema.front() !=
'{') {
181 reportRejection(
name,
"inputSchema must be a table describing a JSON object");
185 const SQInteger
top = sq_gettop(
vm);
186 sq_pushobject(
vm, spec);
187 sq_pushstring(
vm,
"handler", -1);
188 const bool hasHandler =
189 SQ_SUCCEEDED(sq_get(
vm, -2)) && (sq_gettype(
vm, -1) == OT_CLOSURE || sq_gettype(
vm, -1) == OT_NATIVECLOSURE);
192 reportRejection(
name,
"spec.handler must be a function");
203 tool.inputSchemaJson = schema;
209 std::lock_guard<std::mutex> lock(g_mutex);
210 const auto existing =
211 std::find_if(g_tools.begin(), g_tools.end(), [&](
const ScriptTool& entry) { return entry.name == name; });
212 if (existing != g_tools.end()) {
215 releaseEntry(*existing,
vm);
216 *existing = std::move(tool);
217 }
else if (g_tools.size() >= kMaxTools) {
220 g_tools.push_back(std::move(tool));
225 releaseEntry(tool,
vm);
226 reportRejection(
name,
"registry is full (" + std::to_string(kMaxTools) +
" tools)");
236 if (
name.size() < kMinNameLength ||
name.size() > kMaxNameLength)
return false;
237 if (
name.front() <
'a' ||
name.front() >
'z')
return false;
238 for (
char c :
name) {
239 const bool ok = (
c >=
'a' &&
c <=
'z') || (
c >=
'0' &&
c <=
'9') ||
c ==
'_';
240 if (!
ok)
return false;
242 return !hasReservedPrefix(
name);
246 std::lock_guard<std::mutex> lock(g_mutex);
247 return std::any_of(g_tools.begin(), g_tools.end(), [&](
const ScriptTool& tool) { return tool.name == name; });
251 std::lock_guard<std::mutex> lock(g_mutex);
252 std::vector<std::string> names;
253 names.reserve(g_tools.size());
254 for (
const auto& tool : g_tools) names.push_back(tool.name);
259 std::lock_guard<std::mutex> lock(g_mutex);
260 return static_cast<int>(g_tools.size());
264 std::lock_guard<std::mutex> lock(g_mutex);
266 for (
const auto& tool : g_tools) {
267 if (!out.empty()) out +=
',';
269 "\",\"inputSchema\":";
270 out += tool.inputSchemaJson.empty() ? std::string(
"{\"type\":\"object\"}") : tool.inputSchemaJson;
280 std::lock_guard<std::mutex> lock(g_mutex);
282 std::find_if(g_tools.begin(), g_tools.end(), [&](
const ScriptTool& tool) { return tool.name == name; });
283 if (
found == g_tools.end())
return resultEnvelope(
"error: unknown tool " +
name,
true);
292 return resultEnvelope(
"error: tool '" +
name +
"' has no handler in this VM",
true);
295 const SQInteger
top = sq_gettop(
vm);
297 sq_pushroottable(
vm);
299 pushJsonValue(
vm, Poco::Dynamic::Var(args), 0);
305 bool isError =
false;
306 if (SQ_FAILED(sq_call(
vm, 2, SQTrue, SQTrue))) {
307 text =
"error: " + scriptErrorText(
vm);
311 if (
text.size() > kMaxResultBytes) {
312 text =
"error: tool '" +
name +
"' returned " + std::to_string(
text.size()) +
" bytes of JSON, above the " +
313 std::to_string(kMaxResultBytes) +
" byte limit";
319 return resultEnvelope(
text, isError);
323 std::vector<ScriptTool>
dropped;
325 std::lock_guard<std::mutex> lock(g_mutex);
328 for (
auto& tool :
dropped) releaseEntry(tool, owner);
336 ssq::Table eveTable =
vm.find(
"eve").toTable();
337 ssq::Table mcp = eveTable.addTable(
"mcp");
339 mcp.addFunc(
"tool", [raw](
const std::string&
name, ssq::Object spec) ->
bool {
340 return registerTool(raw,
name, spec.getRaw());
343 mcp.addFunc(
"remove", [raw](
const std::string&
name) ->
bool {
346 std::lock_guard<std::mutex> lock(g_mutex);
347 const auto found = std::find_if(g_tools.begin(), g_tools.end(),
348 [&](
const ScriptTool& entry) { return entry.name == name; });
349 if (
found != g_tools.end()) {
350 releaseEntry(*
found, raw);
351 g_tools.erase(
found);
361 mcp.addFunc(
"clear", [raw]() ->
int {
struct SQVM * HSQUIRRELVM
const VegetationPresetContext & context
void addLog(std::string level, std::string text)
Append a leveled log line (thread-safe).
static ConsolePanel & instance()
const char * defaultValue
bool isValidScriptToolName(const std::string &name)
Validate an exported tool name.
int scriptToolCount()
Number of exported tools (for eve_status).
std::string mcpJsonEscape(const std::string &value)
Escape a raw string for embedding inside a JSON string literal.
std::string callScriptTool(const std::string &name, Poco::JSON::Object::Ptr args)
Invoke one exported tool.
void clearScriptTools(HSQUIRRELVM owner)
Drop registry entries owned by owner.
std::vector< std::string > scriptToolNames()
Exported tool names in registration order.
std::string textContentResult(const std::string &text, bool isError=false)
MCP tools/call result object carrying one text content item.
std::string scriptToolSchemas()
Comma-free MCP tool schema fragments for every exported tool.
void exposeMcpScriptApi(ssq::VM &vm)
Expose eve.mcp.tool/remove/tools/clear on a VM.
std::string sqValueToJson(HSQUIRRELVM vm, SQInteger idx, int depth=0)
Forward declaration: the key serializer falls back to the value serializer.
bool isScriptTool(const std::string &name)
Whether name is exported by the project script registry.
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.
Structured snapshot of a script error: message, throw site and stack.