feat(mcp): add get_network_log + UI automation tools#9437
Draft
eordano wants to merge 73 commits into
Draft
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t/explorer-mcp-server
…eat/explorer-mcp-server Resolved conflict in DynamicWorldContainer.cs: dropped the unused 'using CommunicationData.URLHelpers;' brought by the remote side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs
…n't drift The path rename /mcp → /unity-explorer-mcp reached the HttpListener prefix but not the two "listening on …" announce strings (McpHttpServer and McpServerPlugin) nor the docs, so the client logged and copy-pasted a dead address. Introduce McpHttpServer.EndpointUrl, derived from one ENDPOINT_PATH constant, and route the listener prefix and both announces through it — the logged address is now computed from the same string it actually binds. Update every external copy that can't reference the constant (docs/mcp-automation.md, docs/app-arguments.md, the mcp-scene-iteration skill incl. the functional screenshot.sh) and anchor them with a sync comment on the constant.
# Conflicts: # Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset
…stion The Content-Length guard in HandlePostAsync was bypassed by Transfer-Encoding: chunked requests (ContentLength64 == -1), letting ReadToEndAsync read an unbounded body into memory. Read into a fixed MAX_BODY_BYTES buffer and reject oversized bodies with 413.
…s teardown
- McpHttpServer: cap POST body read into a fixed buffer so the 1MB limit
can't be bypassed via Transfer-Encoding: chunked (ContentLength64 == -1)
- McpServerPlugin: store DebugMenuConsoleLogEntryBus as a field and
unsubscribe logBuffer.Append in Dispose()
- McpHttpServer: cap POST body read via ReadBlockAsync so chunked requests
can't bypass the Content-Length limit and exhaust memory
- McpHttpServer: capture the listener into a local in the accept loop to
avoid an NRE race with a concurrent Dispose()
- McpHttpServer: document the deliberate stateless single-session design
(unconditional, unvalidated Mcp-Session-Id)
- McpToolsRegistry: hand out a DeepClone of the tools/list payload so
concurrent dispatches don't race on the shared JObject
- McpJsonRpcDispatcher: return -32600 (Invalid Request) for well-formed
JSON that isn't an object, keeping -32700 for unparseable input
- McpJsonRpcDispatcher: log a warning when the client's initialize
protocolVersion differs from the server's
- McpServerPlugin: store the log entry bus in a field and unsubscribe in
Dispose()
…eat/explorer-mcp-server
…hed payload The implicit JObject conversion on McpToolsRegistry hid a DeepClone behind `Result(id, tools)` in the dispatcher — a surprising conversion with a per-request allocation. Serialize the tools/list payload once in Build() and hand it out via an explicit ToolsListPayload() that wraps the immutable JSON string in a fresh JRaw, so concurrent dispatches share it without cloning or re-parenting a JToken tree. - Drop `implicit operator JObject` and the per-request DeepClone - Store the sealed payload as a serialized string, built once in Build() - Update the dispatcher call site to tools.ToolsListPayload() - Re-parse the raw payload in registry tests via a Payload() helper
Replace repeated routable.Value.X access with a single tuple deconstruction and a property-pattern guard in DispatchAsync.
Centralize Formatting.Indented serialization in McpToolResult so tools can't drift the structured payload from its text mirror or forget the formatting. Convert the eight callsites and drop their now-unused Newtonsoft.Json import.
The builder produces both input and output JSON Schemas (OutputSchema, VectorSchema), so the "Input" name misrepresented it. Pure rename of the type and its file/test.
The three structured tools declare OutputSchema and build the payload by hand, so a field added to one and forgotten in the other would silently misdescribe the result. Add McpSchemaAssert.KeysMatch, a recursive schema-vs-payload key check, and wire it into the Get*/List* tool tests (populating a camera entity and a scene facade so nested objects are covered too).
… Build Dedup nullable type-token logic in McpJsonSchema via a private TypeToken(type, nullable) helper, dropping the (JToken)type cast in Property and sharing it with Object. Add symmetric OutputSchema validation in McpToolsRegistry.Build so a malformed output schema fails fast at registration instead of reaching the client.
Change the extension to take HttpStatusCode instead of int, casting once inside the method. Drops the (int) cast and statusCode: label from all seven call sites and makes the status intent type-safe.
Cap each tools/call at TOOL_CALL_TIMEOUT with a CancellationTokenSource linked to the server-lifetime token. A hung tool now returns a timeout error result instead of holding its HttpListenerContext open until shutdown. Distinguish shutdown cancellation (rethrow) from timeout (error result) via a when (ct.IsCancellationRequested) filter.
Tools declared "switch to the main thread yourself" and 15 of 16 opened with an identical `await UniTask.SwitchToMainThread(ct)`. Hop once in McpJsonRpcDispatcher.CallToolAsync instead and flip the IMcpTool contract to "invoked on the main thread". Readers with no other await become non-async (UniTask.FromResult); tools that offload to the thread pool keep their return-trip hops.
Bring the embedded MCP server to parity with chrome-devtool-protocol-unity
by adding the two capability sets CDP had that MCP lacked.
Network (get_network_log):
- McpNetworkLogBuffer: thread-safe ring buffer (512) of finished requests with
monotonic seq numbers, mirroring SceneLogBuffer (pure, unit-tested).
- McpNetworkAnalyticsHandler: an IWebRequestAnalyticsHandler taps the exact seam
the Chrome DevTools Network domain uses (ChromeDevToolHandler), so coverage is
identical: everything through the shared WebRequestController. Records
{url, method, status, mimeType, sizeBytes, durationMs, failed, reason} on
finish/exception. Wired in WebRequestsContainer, exposed as NetworkLogBuffer,
passed to McpServerPlugin.
- get_network_log tool: limit/sinceSeq/failedOnly/status filters.
Client UI automation (list_ui_elements / get_ui_state / click_ui):
- UiAutomation walks both UI systems: uGUI Selectables via
Selectable.allSelectablesArray, and UI-Toolkit focusable elements under each
UIDocument.rootVisualElement. Real clicks: pointer down/up/click (uGUI) or
navigation-submit (UI-Toolkit), the way AltTester drives live UI.
- UiElementPath: pure path build/match rules (unit-tested).
Tests (folded into DCL.EditMode.Tests): McpNetworkLogBufferShould,
UiElementPathShould. UI/engine walk + click are integration-only.
Docs: new sections in docs/mcp-automation.md (tool catalog + coverage +
implementation map).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bring capabilities to debug the client, not just scenes, to the embedded MCP server.
Network (get_network_log):
Client UI automation (list_ui_elements / get_ui_state / click_ui):
Tests (folded into DCL.EditMode.Tests): McpNetworkLogBufferShould, UiElementPathShould. UI/engine walk + click are integration-only.
Docs: new sections in docs/mcp-automation.md (tool catalog + coverage + implementation map).