9#include <ser20/external/simdjson/simdjson.h>
24constexpr int k_parse_error = -32700;
25constexpr int k_invalid_request = -32600;
26constexpr int k_method_not_found = -32601;
27constexpr int k_invalid_params = -32602;
28constexpr int k_internal_error = -32603;
44 log_activity(
"lifecycle",
45 fmt::format(
"Registered {} tools", registry_.tools().size()));
68 server_ = std::make_unique<httplib::Server>();
69 server_->Get(
"/health",
70 [
this](
const httplib::Request&, httplib::Response& res)
73 res.set_content(R
"({"ok":true,"service":"unravel-editor-mcp"})", "application/json");
77 [
this](
const httplib::Request& req, httplib::Response& res)
79 res.set_header(
"Access-Control-Allow-Origin",
"*");
80 auto response = handle_http_request(req.body);
82 res.set_content(response,
"application/json");
85 server_->Options(
"/mcp",
86 [](
const httplib::Request&, httplib::Response& res)
88 res.set_header(
"Access-Control-Allow-Origin",
"*");
89 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
90 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type");
95 server_thread_ = std::thread(
99 APPLOG_INFO(
"MCP server listening on {}", endpoint);
100 log_activity(
"lifecycle",
"Listening on " + endpoint);
101 if(!server_->listen(
k_host, port_))
104 log_activity(
"lifecycle", fmt::format(
"Failed to listen on {}:{}",
k_host, port_),
true);
105 running_.store(
false);
112 if(!running_.load() && !server_)
117 running_.store(
false);
122 if(server_thread_.joinable())
124 server_thread_.join();
127 log_activity(
"lifecycle",
"Server stopped");
133 return running_.load();
153 return fmt::format(
"http://{}:{}/mcp",
k_host, port_);
158 return fmt::format(
"http://{}:{}/health",
k_host, port_);
163 return registry_.
tools().size();
168 return request_count_.load();
173 return tool_call_count_.load();
178 return error_count_.load();
183 std::lock_guard<std::mutex> lock(activity_mutex_);
184 return {activity_.begin(), activity_.end()};
189 std::lock_guard<std::mutex> lock(activity_mutex_);
193void mcp_manager::log_activity(std::string category, std::string message,
bool is_error)
196 entry.timestamp = std::chrono::system_clock::now();
197 entry.category = std::move(category);
198 entry.message = std::move(message);
199 entry.is_error = is_error;
201 std::lock_guard<std::mutex> lock(activity_mutex_);
202 activity_.push_back(std::move(
entry));
205 activity_.pop_front();
209auto mcp_manager::run_on_main_thread(job_fn work, std::chrono::milliseconds timeout) -> std::string
211 tpp::this_thread::register_this_thread();
212 auto future = tpp::async(tpp::main_thread::get_id(), std::move(work));
213 if(future.wait_for(timeout) != std::future_status::ready)
215 error_count_.fetch_add(1);
216 log_activity(
"rpc",
"Timed out waiting for main thread",
true);
224 catch(
const std::exception&
ex)
226 error_count_.fetch_add(1);
227 log_activity(
"rpc", std::string(
"Exception on main thread: ") +
ex.what(),
true);
232 error_count_.fetch_add(1);
233 log_activity(
"rpc",
"Exception on main thread",
true);
238auto mcp_manager::handle_http_request(
const std::string& body) -> std::string
240 request_count_.fetch_add(1);
241 return dispatch_json_rpc(body);
244auto mcp_manager::dispatch_json_rpc(
const std::string& body) -> std::string
246 mcp::json_rpc_request request;
247 std::string parse_error;
250 error_count_.fetch_add(1);
251 log_activity(
"rpc",
"Parse error: " + parse_error,
true);
255 std::optional<std::string>
id = request.has_id ? request.id_json : std::nullopt;
257 if(request.method ==
"initialize")
259 log_activity(
"rpc",
"initialize");
260 const auto result = fmt::format(
261 R
"({{"protocolVersion":"2024-11-05","capabilities":{{"tools":{{}}}},"serverInfo":{{"name":"unravel-editor","version":{}}}}})",
266 if(request.method ==
"notifications/initialized" || request.method ==
"initialized")
268 log_activity(
"rpc",
"initialized");
272 if(request.method ==
"ping")
277 if(request.method ==
"tools/list")
279 log_activity(
"rpc", fmt::format(
"tools/list ({} tools)", registry_.tools().size()));
283 if(request.method ==
"tools/call")
287 error_count_.fetch_add(1);
288 log_activity(
"tool",
"tools/call failed: no context",
true);
292 simdjson::dom::parser parser;
293 simdjson::dom::element params_root;
294 if(parser.parse(request.params_json).get(params_root))
296 error_count_.fetch_add(1);
297 log_activity(
"tool",
"tools/call invalid params",
true);
301 simdjson::dom::object params;
302 if(params_root.get(params))
304 error_count_.fetch_add(1);
305 log_activity(
"tool",
"tools/call params must be an object",
true);
309 std::string_view tool_name_view;
310 if(params[
"name"].get(tool_name_view))
312 error_count_.fetch_add(1);
313 log_activity(
"tool",
"tools/call missing name",
true);
316 const std::string tool_name(tool_name_view);
318 const auto* tool = registry_.find(tool_name);
321 error_count_.fetch_add(1);
322 log_activity(
"tool",
"Unknown tool: " + tool_name,
true);
326 std::string args_json =
"{}";
327 simdjson::dom::element args_el;
328 if(!params[
"arguments"].get(args_el))
330 args_json = std::string(simdjson::minify(args_el));
333 if(tool->requires_main_thread)
335 return run_on_main_thread(
336 [
this, tool_name, args_json,
id]() -> std::string
338 return execute_tool_call(tool_name, args_json,
id);
340 std::chrono::milliseconds(15000));
344 return execute_tool_call(tool_name, args_json,
id);
347 error_count_.fetch_add(1);
348 log_activity(
"rpc",
"Method not found: " + request.method,
true);
352auto mcp_manager::execute_tool_call(
const std::string& tool_name,
353 const std::string& args_json,
354 const std::optional<std::string>&
id) -> std::string
356 const auto* tool = registry_.find(tool_name);
359 error_count_.fetch_add(1);
363 simdjson::dom::parser args_parser;
364 simdjson::dom::element args_root;
365 simdjson::dom::object args;
366 if(args_parser.parse(args_json).get(args_root) || args_root.get(args))
368 error_count_.fetch_add(1);
369 log_activity(
"tool",
"Invalid arguments for " + tool_name,
true);
373 tool_call_count_.fetch_add(1);
374 auto result = tool->handler(*ctx_, args);
377 error_count_.fetch_add(1);
378 log_activity(
"tool", fmt::format(
"{} -> error: {}", tool_name, result.text),
true);
382 const auto preview = result.text.size() > 120 ? result.text.substr(0, 117) +
"..." : result.text;
383 const auto image_note = result.image_base64.empty() ?
"" :
" [image]";
384 log_activity(
"tool", fmt::format(
"{} -> {}{}", tool_name, preview, image_note));
auto get_request_count() const -> uint64_t
static constexpr const char * k_host
auto get_error_count() const -> uint64_t
auto get_tool_call_count() const -> uint64_t
auto get_health_url() const -> std::string
auto snapshot_activity() const -> std::vector< mcp_activity_entry >
static constexpr size_t k_max_activity_entries
auto get_host() const -> const char *
auto get_endpoint_url() const -> std::string
auto is_enabled() const -> bool
auto deinit(rtti::context &ctx) -> bool
auto get_tool_count() const -> size_t
auto is_running() const -> bool
auto init(rtti::context &ctx) -> bool
auto get_port() const -> int
#define APPLOG_ERROR(...)
Hash specialization for batch_key to enable use in std::unordered_map.
void register_asset_tools(mcp_tool_registry ®istry)
void register_material_tools(mcp_tool_registry ®istry)
auto parse_json_rpc_request(const std::string &body, json_rpc_request &out, std::string &error) -> bool
auto make_json_string(const std::string &value) -> std::string
void register_scene_batch_tools(mcp_tool_registry ®istry)
void register_scene_tools(mcp_tool_registry ®istry)
void register_script_tools(mcp_tool_registry ®istry)
auto make_json_rpc_error(const std::optional< std::string > &id_json, int code, const std::string &message) -> std::string
void register_project_tools(mcp_tool_registry ®istry)
void register_viewport_tools(mcp_tool_registry ®istry)
auto make_tool_result(const std::string &text, bool is_error) -> std::string
auto make_json_rpc_result(const std::optional< std::string > &id_json, const std::string &result_json) -> std::string
void register_ops_batch_tools(mcp_tool_registry ®istry)
void register_editor_tools(mcp_tool_registry ®istry)
auto get_full() -> std::string
std::vector< math::vec3 > start