Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_manager.cpp
Go to the documentation of this file.
1#include "mcp_manager.h"
2
3#include "mcp/mcp_protocol.h"
4
5#include <logging/logging.h>
6#include <version/version.h>
7
8#include <httplib.h>
9#include <ser20/external/simdjson/simdjson.h>
10
11#include <utility>
12
13namespace unravel
14{
15
16mcp_manager::mcp_manager() = default;
21
22namespace
23{
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;
29} // namespace
30
32{
33 ctx_ = &ctx;
43
44 log_activity("lifecycle",
45 fmt::format("Registered {} tools", registry_.tools().size()));
46
47 if(enabled_)
48 {
49 start();
50 }
51 return true;
52}
53
55{
56 stop();
57 ctx_ = nullptr;
58 return true;
59}
60
62{
63 if(running_.load())
64 {
65 return;
66 }
67
68 server_ = std::make_unique<httplib::Server>();
69 server_->Get("/health",
70 [this](const httplib::Request&, httplib::Response& res)
71 {
72 res.status = 200;
73 res.set_content(R"({"ok":true,"service":"unravel-editor-mcp"})", "application/json");
74 });
75
76 server_->Post("/mcp",
77 [this](const httplib::Request& req, httplib::Response& res)
78 {
79 res.set_header("Access-Control-Allow-Origin", "*");
80 auto response = handle_http_request(req.body);
81 res.status = 200;
82 res.set_content(response, "application/json");
83 });
84
85 server_->Options("/mcp",
86 [](const httplib::Request&, httplib::Response& res)
87 {
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");
91 res.status = 204;
92 });
93
94 running_.store(true);
95 server_thread_ = std::thread(
96 [this]()
97 {
98 const auto endpoint = get_endpoint_url();
99 APPLOG_INFO("MCP server listening on {}", endpoint);
100 log_activity("lifecycle", "Listening on " + endpoint);
101 if(!server_->listen(k_host, port_))
102 {
103 APPLOG_ERROR("MCP server failed to listen on {}:{}", k_host, port_);
104 log_activity("lifecycle", fmt::format("Failed to listen on {}:{}", k_host, port_), true);
105 running_.store(false);
106 }
107 });
108}
109
111{
112 if(!running_.load() && !server_)
113 {
114 return;
115 }
116
117 running_.store(false);
118 if(server_)
119 {
120 server_->stop();
121 }
122 if(server_thread_.joinable())
123 {
124 server_thread_.join();
125 }
126 server_.reset();
127 log_activity("lifecycle", "Server stopped");
128 APPLOG_INFO("MCP server stopped");
129}
130
131auto mcp_manager::is_running() const -> bool
132{
133 return running_.load();
134}
135
136auto mcp_manager::is_enabled() const -> bool
137{
138 return enabled_;
139}
140
141auto mcp_manager::get_host() const -> const char*
142{
143 return k_host;
144}
145
146auto mcp_manager::get_port() const -> int
147{
148 return port_;
149}
150
151auto mcp_manager::get_endpoint_url() const -> std::string
152{
153 return fmt::format("http://{}:{}/mcp", k_host, port_);
154}
155
156auto mcp_manager::get_health_url() const -> std::string
157{
158 return fmt::format("http://{}:{}/health", k_host, port_);
159}
160
161auto mcp_manager::get_tool_count() const -> size_t
162{
163 return registry_.tools().size();
164}
165
166auto mcp_manager::get_request_count() const -> uint64_t
167{
168 return request_count_.load();
169}
170
171auto mcp_manager::get_tool_call_count() const -> uint64_t
172{
173 return tool_call_count_.load();
174}
175
176auto mcp_manager::get_error_count() const -> uint64_t
177{
178 return error_count_.load();
179}
180
182{
183 std::lock_guard<std::mutex> lock(activity_mutex_);
184 return {activity_.begin(), activity_.end()};
185}
186
188{
189 std::lock_guard<std::mutex> lock(activity_mutex_);
190 activity_.clear();
191}
192
193void mcp_manager::log_activity(std::string category, std::string message, bool is_error)
194{
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;
200
201 std::lock_guard<std::mutex> lock(activity_mutex_);
202 activity_.push_back(std::move(entry));
203 while(activity_.size() > k_max_activity_entries)
204 {
205 activity_.pop_front();
206 }
207}
208
209auto mcp_manager::run_on_main_thread(job_fn work, std::chrono::milliseconds timeout) -> std::string
210{
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)
214 {
215 error_count_.fetch_add(1);
216 log_activity("rpc", "Timed out waiting for main thread", true);
217 return mcp::make_json_rpc_error(std::nullopt, k_internal_error, "Timed out waiting for main thread");
218 }
219
220 try
221 {
222 return future.get();
223 }
224 catch(const std::exception& ex)
225 {
226 error_count_.fetch_add(1);
227 log_activity("rpc", std::string("Exception on main thread: ") + ex.what(), true);
228 return mcp::make_json_rpc_error(std::nullopt, k_internal_error, ex.what());
229 }
230 catch(...)
231 {
232 error_count_.fetch_add(1);
233 log_activity("rpc", "Exception on main thread", true);
234 return mcp::make_json_rpc_error(std::nullopt, k_internal_error, "Unknown MCP job error");
235 }
236}
237
238auto mcp_manager::handle_http_request(const std::string& body) -> std::string
239{
240 request_count_.fetch_add(1);
241 return dispatch_json_rpc(body);
242}
243
244auto mcp_manager::dispatch_json_rpc(const std::string& body) -> std::string
245{
246 mcp::json_rpc_request request;
247 std::string parse_error;
248 if(!mcp::parse_json_rpc_request(body, request, parse_error))
249 {
250 error_count_.fetch_add(1);
251 log_activity("rpc", "Parse error: " + parse_error, true);
252 return mcp::make_json_rpc_error(std::nullopt, k_parse_error, parse_error);
253 }
254
255 std::optional<std::string> id = request.has_id ? request.id_json : std::nullopt;
256
257 if(request.method == "initialize")
258 {
259 log_activity("rpc", "initialize");
260 const auto result = fmt::format(
261 R"({{"protocolVersion":"2024-11-05","capabilities":{{"tools":{{}}}},"serverInfo":{{"name":"unravel-editor","version":{}}}}})",
263 return mcp::make_json_rpc_result(id, result);
264 }
265
266 if(request.method == "notifications/initialized" || request.method == "initialized")
267 {
268 log_activity("rpc", "initialized");
269 return mcp::make_json_rpc_result(id, "null");
270 }
271
272 if(request.method == "ping")
273 {
274 return mcp::make_json_rpc_result(id, "{}");
275 }
276
277 if(request.method == "tools/list")
278 {
279 log_activity("rpc", fmt::format("tools/list ({} tools)", registry_.tools().size()));
280 return mcp::make_json_rpc_result(id, fmt::format(R"({{"tools":{}}})", registry_.list_tools_json()));
281 }
282
283 if(request.method == "tools/call")
284 {
285 if(!ctx_)
286 {
287 error_count_.fetch_add(1);
288 log_activity("tool", "tools/call failed: no context", true);
289 return mcp::make_json_rpc_error(id, k_internal_error, "MCP manager has no context");
290 }
291
292 simdjson::dom::parser parser;
293 simdjson::dom::element params_root;
294 if(parser.parse(request.params_json).get(params_root))
295 {
296 error_count_.fetch_add(1);
297 log_activity("tool", "tools/call invalid params", true);
298 return mcp::make_json_rpc_error(id, k_invalid_params, "Invalid tools/call params");
299 }
300
301 simdjson::dom::object params;
302 if(params_root.get(params))
303 {
304 error_count_.fetch_add(1);
305 log_activity("tool", "tools/call params must be an object", true);
306 return mcp::make_json_rpc_error(id, k_invalid_params, "tools/call params must be an object");
307 }
308
309 std::string_view tool_name_view;
310 if(params["name"].get(tool_name_view))
311 {
312 error_count_.fetch_add(1);
313 log_activity("tool", "tools/call missing name", true);
314 return mcp::make_json_rpc_error(id, k_invalid_params, "tools/call missing name");
315 }
316 const std::string tool_name(tool_name_view);
317
318 const auto* tool = registry_.find(tool_name);
319 if(!tool)
320 {
321 error_count_.fetch_add(1);
322 log_activity("tool", "Unknown tool: " + tool_name, true);
323 return mcp::make_json_rpc_error(id, k_method_not_found, "Unknown tool: " + tool_name);
324 }
325
326 std::string args_json = "{}";
327 simdjson::dom::element args_el;
328 if(!params["arguments"].get(args_el))
329 {
330 args_json = std::string(simdjson::minify(args_el));
331 }
332
333 if(tool->requires_main_thread)
334 {
335 return run_on_main_thread(
336 [this, tool_name, args_json, id]() -> std::string
337 {
338 return execute_tool_call(tool_name, args_json, id);
339 },
340 std::chrono::milliseconds(15000));
341 }
342
343 // Action+wait tools: run on HTTP worker so waiting does not stall the frame loop.
344 return execute_tool_call(tool_name, args_json, id);
345 }
346
347 error_count_.fetch_add(1);
348 log_activity("rpc", "Method not found: " + request.method, true);
349 return mcp::make_json_rpc_error(id, k_method_not_found, "Method not found: " + request.method);
350}
351
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
355{
356 const auto* tool = registry_.find(tool_name);
357 if(!tool || !ctx_)
358 {
359 error_count_.fetch_add(1);
360 return mcp::make_json_rpc_error(id, k_method_not_found, "Unknown tool: " + tool_name);
361 }
362
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))
367 {
368 error_count_.fetch_add(1);
369 log_activity("tool", "Invalid arguments for " + tool_name, true);
370 return mcp::make_json_rpc_error(id, k_invalid_params, "Invalid tool arguments");
371 }
372
373 tool_call_count_.fetch_add(1);
374 auto result = tool->handler(*ctx_, args);
375 if(result.is_error)
376 {
377 error_count_.fetch_add(1);
378 log_activity("tool", fmt::format("{} -> error: {}", tool_name, result.text), true);
379 }
380 else
381 {
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));
385 }
387}
388
389} // namespace unravel
auto tools() const -> const std::vector< mcp_tool > &
auto get_request_count() const -> uint64_t
static constexpr const char * k_host
Definition mcp_manager.h:42
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
Definition mcp_manager.h:44
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(...)
Definition logging.h:20
#define APPLOG_INFO(...)
Definition logging.h:18
Hash specialization for batch_key to enable use in std::unordered_map.
void register_asset_tools(mcp_tool_registry &registry)
void register_material_tools(mcp_tool_registry &registry)
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 &registry)
void register_scene_tools(mcp_tool_registry &registry)
void register_script_tools(mcp_tool_registry &registry)
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 &registry)
void register_viewport_tools(mcp_tool_registry &registry)
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 &registry)
void register_editor_tools(mcp_tool_registry &registry)
auto get_full() -> std::string
Definition version.cpp:226
std::vector< math::vec3 > start
Definition mcp_manager.h:32