Skip to content

feat(mcp): drive the client UI, gated out of release builds - #9437

Draft
eordano wants to merge 1 commit into
devfrom
feat/mcp-cdpu-parity
Draft

feat(mcp): drive the client UI, gated out of release builds#9437
eordano wants to merge 1 commit into
devfrom
feat/mcp-cdpu-parity

Conversation

@eordano

@eordano eordano commented Jul 20, 2026

Copy link
Copy Markdown
Member

Bring capabilities to debug the client, not just scenes, to the embedded MCP server.

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).

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

badge

New build in progress, come back later!

Base automatically changed from feat/explorer-mcp-server to dev July 24, 2026 10:16
eordano added a commit that referenced this pull request Jul 27, 2026
Rebases PR #9437's parity work onto dev and finishes it: the embedded MCP server
can now drive the explorer-automation suite end to end, with no instrumented
AltTester build.

Rebase. #9339 was squash-merged into dev after this work branched, so the four
tools that came over (get_network_log, list_ui_elements, get_ui_state, click_ui)
are ported from the pre-review contract to dev's: the abstract McpTool base,
DescribeInput(McpJsonSchema) instead of an InputSchema property, and the
McpPointerEvent* naming. Nothing of dev's post-review version is reverted.

Finding elements. The first pass could only enumerate uGUI Selectables, so about
half of what the suite locates was invisible: its 118 By.PATH and 61 By.NAME
locators are largely plain GameObjects, and presence/absence waits over them are
the bulk of a UI suite. UiAutomation now walks the full active Transform tree
under every root Canvas (plus the UI-Toolkit trees), iteratively, with a node cap
and the nameFilter fast path. UiElementPath gained AltTester's By.PATH dialect —
// descendant, / child, * wildcard, the Name[i] sibling indexer — matched against
the whole candidate path so a query identifies the element and not an ancestor.
An unmatched get_ui_state is an error result, which is how absence is expressed.

Acting on them. click_ui and scroll hit-test through the live EventSystem at the
element's screen position and dispatch at the real top hit, so a control covered
by a modal returns clicked:false with the blocker instead of the false success a
direct ExecuteEvents call produced; only an element with no raycastable graphic
falls back to direct dispatch, and says so. set_ui_text covers uGUI InputField,
TMP_InputField and UI-Toolkit TextField (the 9 SetText sites gate the whole auth
path). press_key queues keyboard state onto the Input System device the client
reads — the only seam reaching every DCLInput action, with the real action-map
gating intact. Text reads now cover TextMeshPro, which is what the Explorer's
labels actually are.

Not in release builds. The MCP server itself keeps shipping everywhere — Creator
Tools drives the installed client through it, so the transport and the
scene-iteration tools cannot be compiled out. The surface added here to replace
AltTester can be, and is: the six client-UI automation tools, the three
reflection tools, the four Utils types behind them and their EditMode tests sit
behind a new MCP_TEST_AUTOMATION define, on by default and removed for release by
CloudBuild.PreExport. tools/list therefore differs by build, which the doc states.

This matters now rather than in the abstract: sdk-commands only ever sets mcp
into the decentraland:// link and DeepLinkAllowlist drops it, so the creator flow
is broken today, and the natural fix is a loopback-gated allowlist entry for mcp.
At that point a deep link can start the server on a shipped client and a
command-line-only runtime flag stops being sufficient on its own. The compile gate
is a property of the release binary that no allowlist edit can undo. That
allowlist change is deliberately not made here.

Reflection takes a second gate inside the builds that keep it:
get/set_component_property and call_static_method are arbitrary in-process reads,
writes and invocation, so --mcp-reflection (FeatureId.McpReflection) must also be
passed, and they are absent from tools/list otherwise rather than refused when
called. The suite passes both flags.

docs/mcp-automation.md documents the tools, the path dialect, the AltTester
coverage table and that reasoning. It also corrects a standing error: the doc
claimed --mcp is accepted from a deep link, but the allowlist drops it, so the
documented `npm run start -- --mcp` flow is silently a no-op — the fix belongs in
sdk-commands, not in the allowlist.

EditMode tests cover the pure parts (path building/matching, key-name parsing,
the reflected property reads/writes, the network-log ring buffer). The hierarchy
walk, hit-test and event dispatch are integration-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@eordano
eordano force-pushed the feat/mcp-cdpu-parity branch from 194b843 to b5b09a1 Compare July 27, 2026 22:48
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

badge

Lint in progress, come back later!

@eordano eordano added the force-build Used to trigger a build on draft PR label Jul 27, 2026
@eordano eordano changed the title feat(mcp): add get_network_log + UI automation tools feat(mcp): drive the client UI, so the MCP server can replace AltTester Jul 27, 2026
@eordano eordano changed the title feat(mcp): drive the client UI, so the MCP server can replace AltTester feat(mcp): drive the client UI, gated out of release builds Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Tests: 24507 passed, 0 failed

eordano added a commit that referenced this pull request Jul 28, 2026
Rebases PR #9437's parity work onto dev and finishes it: the embedded MCP server
can now drive the client's own UI end to end, so a UI test suite needs no
instrumented build.

Rebase. #9339 was squash-merged into dev after this work branched, so the four
tools that came over (get_network_log, list_ui_elements, get_ui_state, click_ui)
are ported from the pre-review contract to dev's: the abstract McpTool base,
DescribeInput(McpJsonSchema) instead of an InputSchema property, and the
McpPointerEvent* naming. Nothing of dev's post-review version is reverted.

Finding elements. The first pass could only enumerate uGUI Selectables, so about
half of what a UI suite locates was invisible: real locators are largely plain
GameObjects, and presence/absence waits over them are the bulk of such a suite.
UiAutomation now walks the full active Transform tree under every root Canvas
(plus the UI-Toolkit trees), iteratively, with a node cap and the nameFilter fast
path. UiElementPath gained a path-expression dialect — // descendant, / child,
* wildcard, the Name[i] sibling indexer — matched against the whole candidate
path so a query identifies the element and not an ancestor. An unmatched
get_ui_state is an error result, which is how absence is expressed.

Acting on them. click_ui and scroll hit-test through the live EventSystem at the
element's screen position and dispatch at the real top hit, so a control covered
by a modal returns clicked:false with the blocker instead of the false success a
direct ExecuteEvents call produced; only an element with no raycastable graphic
falls back to direct dispatch, and says so. set_ui_text covers uGUI InputField,
TMP_InputField and UI-Toolkit TextField, which is what the login email, OTP code,
username and search fields need. press_key queues keyboard state onto the Input
System device the client reads — the only seam reaching every DCLInput action,
with the real action-map gating intact. Text reads now cover TextMeshPro, which
is what the Explorer's labels actually are.

Not in release builds. The MCP server itself keeps shipping everywhere — Creator
Tools drives the installed client through it, so the transport and the
scene-iteration tools cannot be compiled out. The surface added here can be, and
is: the six client-UI automation tools, the three reflection tools, the four
Utils types behind them and their EditMode tests sit behind a new
MCP_TEST_AUTOMATION define, on by default and removed for release by
CloudBuild.PreExport. tools/list therefore differs by build, which the doc states.

This matters now rather than in the abstract: sdk-commands only ever sets mcp
into the decentraland:// link and DeepLinkAllowlist drops it, so the creator flow
is broken today, and the natural fix is a loopback-gated allowlist entry for mcp.
At that point a deep link can start the server on a shipped client and a
command-line-only runtime flag stops being sufficient on its own. The compile gate
is a property of the release binary that no allowlist edit can undo. That
allowlist change is deliberately not made here.

Reflection takes a second gate inside the builds that keep it:
get/set_component_property and call_static_method are arbitrary in-process reads,
writes and invocation, so --mcp-reflection (FeatureId.McpReflection) must also be
passed, and they are absent from tools/list otherwise rather than refused when
called.

McpServerGatingShould guards both gates: it asserts the tool count per build
(17 shipped, +9 behind the define) so a tool cannot be added without deciding
which side it belongs on, and drives the real FeaturesRegistry from synthetic app
args to pin the flag matrix — including that --mcp-reflection is inert without
--mcp. It sits outside the define, because asserting what the gate removes is only
meaningful from outside it.

docs/mcp-automation.md documents the tools, the path-expression syntax, what a UI
suite can do with the surface, and the security reasoning. It also corrects a
standing error: the doc claimed --mcp is accepted from a deep link, but the
allowlist drops it, so the documented `npm run start -- --mcp` flow is silently a
no-op — the fix belongs in sdk-commands, not in the allowlist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@eordano
eordano force-pushed the feat/mcp-cdpu-parity branch from b5b09a1 to b2924a2 Compare July 28, 2026 00:03
eordano added a commit that referenced this pull request Jul 28, 2026
Rebases PR #9437's parity work onto dev and finishes it: the embedded MCP server
can now drive the client's own UI end to end, so a UI test suite needs no
instrumented build.

Rebase. #9339 was squash-merged into dev after this work branched, so the four
tools that came over (get_network_log, list_ui_elements, get_ui_state, click_ui)
are ported from the pre-review contract to dev's: the abstract McpTool base,
DescribeInput(McpJsonSchema) instead of an InputSchema property, and the
McpPointerEvent* naming. Nothing of dev's post-review version is reverted.

Finding elements. The first pass could only enumerate uGUI Selectables, so about
half of what a UI suite locates was invisible: real locators are largely plain
GameObjects, and presence/absence waits over them are the bulk of such a suite.
UiAutomation now walks the full active Transform tree under every root Canvas
(plus the UI-Toolkit trees), iteratively, with a node cap and the nameFilter fast
path. UiElementPath gained a path-expression dialect — // descendant, / child,
* wildcard, the Name[i] sibling indexer — matched against the whole candidate
path so a query identifies the element and not an ancestor. An unmatched
get_ui_state is an error result, which is how absence is expressed.

Acting on them. click_ui and scroll hit-test through the live EventSystem at the
element's screen position and dispatch at the real top hit, so a control covered
by a modal returns clicked:false with the blocker instead of the false success a
direct ExecuteEvents call produced; only an element with no raycastable graphic
falls back to direct dispatch, and says so. set_ui_text covers uGUI InputField,
TMP_InputField and UI-Toolkit TextField, which is what the login email, OTP code,
username and search fields need. press_key queues keyboard state onto the Input
System device the client reads — the only seam reaching every DCLInput action,
with the real action-map gating intact. Text reads now cover TextMeshPro, which
is what the Explorer's labels actually are.

Not in release builds. The MCP server itself keeps shipping everywhere — Creator
Tools drives the installed client through it, so the transport and the
scene-iteration tools cannot be compiled out. The surface added here can be, and
is: the six client-UI automation tools, the three reflection tools, the four
Utils types behind them and their EditMode tests sit behind a new
MCP_TEST_AUTOMATION define, on by default and removed for release by
CloudBuild.PreExport. tools/list therefore differs by build, which the doc states.

This matters now rather than in the abstract: sdk-commands only ever sets mcp
into the decentraland:// link and DeepLinkAllowlist drops it, so the creator flow
is broken today, and the natural fix is a loopback-gated allowlist entry for mcp.
At that point a deep link can start the server on a shipped client and a
command-line-only runtime flag stops being sufficient on its own. The compile gate
is a property of the release binary that no allowlist edit can undo. That
allowlist change is deliberately not made here.

Reflection takes a second gate inside the builds that keep it:
get/set_component_property and call_static_method are arbitrary in-process reads,
writes and invocation, so --mcp-reflection (FeatureId.McpReflection) must also be
passed, and they are absent from tools/list otherwise rather than refused when
called.

McpServerGatingShould guards both gates: it asserts the tool count per build
(17 shipped, +9 behind the define) so a tool cannot be added without deciding
which side it belongs on, and drives the real FeaturesRegistry from synthetic app
args to pin the flag matrix — including that --mcp-reflection is inert without
--mcp. It sits outside the define, because asserting what the gate removes is only
meaningful from outside it.

docs/mcp-automation.md documents the tools, the path-expression syntax, what a UI
suite can do with the surface, and the security reasoning. It also corrects a
standing error: the doc claimed --mcp is accepted from a deep link, but the
allowlist drops it, so the documented `npm run start -- --mcp` flow is silently a
no-op — the fix belongs in sdk-commands, not in the allowlist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@eordano
eordano force-pushed the feat/mcp-cdpu-parity branch from b2924a2 to 300c108 Compare July 28, 2026 01:11
Rebases PR #9437's parity work onto dev and finishes it: the embedded MCP server
can now drive the client's own UI end to end, so a UI test suite needs no
instrumented build.

Rebase. #9339 was squash-merged into dev after this work branched, so the four
tools that came over (get_network_log, list_ui_elements, get_ui_state, click_ui)
are ported from the pre-review contract to dev's: the abstract McpTool base,
DescribeInput(McpJsonSchema) instead of an InputSchema property, and the
McpPointerEvent* naming. Nothing of dev's post-review version is reverted.

Finding elements. The first pass could only enumerate uGUI Selectables, so about
half of what a UI suite locates was invisible: real locators are largely plain
GameObjects, and presence/absence waits over them are the bulk of such a suite.
UiAutomation now walks the full active Transform tree under every root Canvas
(plus the UI-Toolkit trees), iteratively, with a node cap and the nameFilter fast
path. UiElementPath gained a path-expression dialect — // descendant, / child,
* wildcard, the Name[i] sibling indexer — matched against the whole candidate
path so a query identifies the element and not an ancestor. An unmatched
get_ui_state is an error result, which is how absence is expressed.

Acting on them. click_ui and scroll hit-test through the live EventSystem at the
element's screen position and dispatch at the real top hit, so a control covered
by a modal returns clicked:false with the blocker instead of the false success a
direct ExecuteEvents call produced; only an element with no raycastable graphic
falls back to direct dispatch, and says so. set_ui_text covers uGUI InputField,
TMP_InputField and UI-Toolkit TextField, which is what the login email, OTP code,
username and search fields need. press_key queues keyboard state onto the Input
System device the client reads — the only seam reaching every DCLInput action,
with the real action-map gating intact. Text reads now cover TextMeshPro, which
is what the Explorer's labels actually are.

Not in release builds. The MCP server itself keeps shipping everywhere — Creator
Tools drives the installed client through it, so the transport and the
scene-iteration tools cannot be compiled out. The surface added here can be, and
is: the seven client-UI automation tools, the three reflection tools, the four
Utils types behind them and their EditMode tests sit behind a new
MCP_TEST_AUTOMATION define, on by default and removed for release by
CloudBuild.PreExport. tools/list therefore differs by build, which the doc states.

This matters now rather than in the abstract: sdk-commands only ever sets mcp
into the decentraland:// link and DeepLinkAllowlist drops it, so the creator flow
is broken today, and the natural fix is a loopback-gated allowlist entry for mcp.
At that point a deep link can start the server on a shipped client and a
command-line-only runtime flag stops being sufficient on its own. The compile gate
is a property of the release binary that no allowlist edit can undo. That
allowlist change is deliberately not made here.

Reflection takes a second gate inside the builds that keep it:
get/set_component_property and call_static_method are arbitrary in-process reads,
writes and invocation, so --mcp-reflection (FeatureId.McpReflection) must also be
passed, and they are absent from tools/list otherwise rather than refused when
called.

McpServerGatingShould guards both gates: it asserts the tool count per build
(17 shipped, +10 behind the define) so a tool cannot be added without deciding
which side it belongs on, and drives the real FeaturesRegistry from synthetic app
args to pin the flag matrix — including that --mcp-reflection is inert without
--mcp. It sits outside the define, because asserting what the gate removes is only
meaningful from outside it.

docs/mcp-automation.md documents the tools, the path-expression syntax, what a UI
suite can do with the surface, and the security reasoning. It also corrects a
standing error: the doc claimed --mcp is accepted from a deep link, but the
allowlist drops it, so the documented `npm run start -- --mcp` flow is silently a
no-op — the fix belongs in sdk-commands, not in the allowlist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@eordano
eordano force-pushed the feat/mcp-cdpu-parity branch from 300c108 to 91c6618 Compare July 28, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

force-build Used to trigger a build on draft PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant