Skip to content

[Bug]: device_screenshot returns bare "Device screenshot failed." and drops the reason (device shut down, not open, hub error) #24033

Closed as duplicate

Description

@noyex

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Summary

Every failure of the device_screenshot MCP tool returns the same text, Device screenshot failed., with no reason. This includes failures where the server already has a specific, actionable message. The caller cannot tell these cases apart:

  • no device is open in this thread (the handler's own message, "No device is open in this thread. Call device_open first.", is thrown away)
  • the device ID is unknown or not open in this thread
  • the simulator or emulator was shut down after device_open (device_list already reports booted: false for it)
  • the hub returned an error, or the request timed out

Automated tooling driving the device hit this often. Over 7 days on one machine there were 120 device_screenshot calls. 27 of them (22%) returned only Device screenshot failed.. From the surrounding calls, the real causes were:

  • a simulator shut down moments earlier
  • an Android emulator (emulator-5554 … emulator-5560) that had already stopped or was still booting
  • calls with no arguments ({}) before any device_open

Each case had an obvious fix (device_open again, wait for boot, pass the device ID), but the caller could not see which one applied. In most cases it retried blindly or gave up.

Steps to reproduce

  1. Boot a spare iOS simulator (here Agent 1, UDID B79B260C-B24B-4082-A303-F01F75AA4C09).
  2. In a thread with device access, call device_screenshot with {} before opening any device.
  3. Call device_open {"deviceId":"B79B260C-…","hostId":"local"}. It succeeds.
  4. Call device_screenshot {"deviceId":"B79B260C-…","hostId":"local"}. It succeeds (1206×2622 PNG).
  5. Call device_screenshot {"deviceId":"00000000-0000-0000-0000-000000000000","hostId":"local"}.
  6. Shut down the simulator outside T3 (xcrun simctl shutdown B79B260C-…).
  7. Call device_screenshot {"deviceId":"B79B260C-…","hostId":"local"} again.
  8. Call device_list.

Expected behavior

The tool result explains why the call failed and what to do next. For example, for step 7:

Device screenshot failed: Agent 1 (8091) (B79B260C-…, local) is not booted (state: Shutdown).
Call device_open to boot it again.

and for steps 2 and 5, the handler's existing message ("No device is open in this thread. Call device_open first." / "Device … on host local is not open in this thread. Call device_open first.").

A good error payload would include, in both the text content and structuredContent.error:

  • _tag and reason (already partly there in structuredContent, but not in the text most clients show)
  • the error's own message, which DeviceToolUnavailableError and DeviceOperationError already define
  • the underlying cause: the hub's HTTP status and its JSON error body, or the simctl/adb stderr / exit code
  • device state from the server's own device list (booted: false, still booting, gone)
  • a suggested next step (device_open, wait for boot, refresh devices)
  • a correlation ID (trace/span ID) that matches the entry in server.trace.ndjson

Actual behavior

Steps 2, 5 and 7 all return exactly this, with nothing else in the text content:

Device screenshot failed.

Step 8 shows the server already knows the cause. The device it cannot capture is still listed as open in this thread and as not booted:

{"hostId":"local","id":"B79B260C-B24B-4082-A303-F01F75AA4C09","platform":"ios","name":"Agent 1 (8091)","version":"iOS 27.0","booted":false,"physical":false}
...
"open":[{"hostId":"local","deviceId":"B79B260C-B24B-4082-A303-F01F75AA4C09"}]

Where the reason is dropped

The line numbers below refer to the shipped bundle apps/server/dist/binCli-Bm8tVhP8.mjs in 0.1.1-nightly.20261020.3411 (commit d36b7da2485b). Source modules are taken from the bundle's //#region markers.

  1. src/mcp/McpHttpServer.ts, registerDeviceScreenshot (bundle line ~337812). It passes a constant string as failureText:

    yield* registerImageTool(DeviceScreenshotTool, ..., "screenshot", "Device screenshot failed.");

    registerHtmlPreview, a few lines below, passes a function that returns error.message.

  2. src/mcp/McpHttpServer.ts, imageToolFailure (bundle line ~337732). When failureText is a string, message is never computed, and the text content is always the constant:

    const message = typeof failureText === "string" || failures[0] === void 0 ? void 0 : failureText(failures[0].error);
    ...
    text: typeof failureText === "string" ? failureText : message ?? `${toolName} failed.`

    The logWarning that follows records only operation, errorTag and failureCount, so the reason is also missing from the warning log line.

  3. src/device/DeviceHubOperations.ts, screenshot (bundle line ~314433). filterStatusOk discards the hub's response body, and the error becomes DeviceOperationError{reason:"request_failed"}. Its message (defined in packages/contracts/src/device.ts) ignores cause, so even with fix 1 the text would read "Could not communicate with device support. Try refreshing devices." That message is misleading here: the hub was reachable and answered with HTTP 500 and a JSON error. The same applies to the device_open / device_close errors "Device list failed: …" and "Device shutdown failed: Could not communicate with device support." seen in the same period.

The trace for step 7 shows what is available and then lost (~/.t3/userdata/logs/server.trace.ndjson):

DeviceHubOperations.screenshot  durationMs: 5014.7  exit: Failure
DeviceOperationError: Device screenshot failed: Could not communicate with device support. Try refreshing devices.
  [cause]: HttpClientError: StatusCode: non 2xx status code (500 POST http://127.0.0.1:58547/vendor/serve-sim/api/screenshot?device=B79B260C-B24B-4082-A303-F01F75AA4C09)

McpServer tools/call device_screenshot  event (WARN):
["device_screenshot failed", {"operation":"screenshot","errorTag":"DeviceOperationError","failureCount":1}]

For steps 2 and 5, the same WARN line shows "errorTag":"DeviceToolUnavailableError", the error that carries the "Call device_open first." text.

Contributing factor in the hub (expo-device-hub 0.15.3, vendored serve-sim)

The hub's 500 body for a shut-down simulator does not contain the real error either:

$ curl -X POST 'http://127.0.0.1:58547/vendor/serve-sim/api/screenshot?device=B79B260C-…'
{"ok":false,"error":"Detected file type from extension: PNG\nNote: No display specified. Defaulting to display: 8B880525-… (screenID: 1, name: LCD)"}
HTTP 500

serve-sim runs xcrun simctl io <udid> screenshot with execFile(..., {timeout: 5000}) and does not check the device state first. On a shut-down simulator, simctl takes more than 5 s to fail, so it is killed and only its informational stderr lines are returned. Run directly, simctl ends with the real reason:

An error was encountered processing the command (domain=NSPOSIXErrorDomain, code=60):
The operation couldn't be completed. Timeout waiting for screen surfaces
exit=60

T3 can avoid depending on this, because it already knows booted: false before it calls the hub. Checking device state in DeviceService.screenshot (or mapping a hub failure against the cached state) would give a precise message without waiting 5 s.

Impact

Major degradation or frequent failure. A fifth of screenshot calls in normal use failed with no actionable information, and callers retried or stopped instead of calling device_open.

Version or commit

T3 Code (Nightly) 0.1.1-nightly.20261020.3411, commit d36b7da2485b; expo-device-hub 0.15.3; agent-device 0.21.25

Environment

macOS 27.0.1, Xcode 27.0 (27A266a), Apple Silicon, local device host

Workaround

Call device_list after a failure and check booted for the device, then call device_open again if it is shut down. For {} calls, always pass deviceId.

Related

Activity

  1. EndersonPro commented on Oct 31, 2026

    @EndersonPro

    Additional occurrence of the diagnostic-loss behavior on a physical Android device. This is evidence for the error-masking problem, not a claim that the underlying Android failure matches the iOS example above.

    Environment: T3 Code desktop 0.1.5 on macOS, T3 CLI 0.1.2, expo-device-hub 0.19.0-canary-20261023-1357114, agent-device 0.21.31; USB-connected Xiaomi Redmi Note 8 running Android 15 (arm64).

    Observed:

    • Device discovery reports the local host ready and the phone booted; device_open succeeds. ADB transport, the app process, and the local development-backend port forwards were available.
    • device_screenshot returns Device screenshot failed: Could not communicate with device support. Try refreshing devices.
    • Retained server traces show three separate screenshot requests failing at approximately 20 seconds with the timeout below, plus another screenshot request receiving HTTP 503 from the local hub after approximately 6.09 seconds.
    • Refreshing the device inventory and subsequently toggling device support/agent access did not recover screenshots. The subsequent bounded screenshot retry returned the same generic message.

    Redacted underlying errors from the existing traces:

    TimeoutError: Operation timed out after '20s'
    HttpClientError: StatusCode: non 2xx status code
      (503 POST <local-hub>/vendor/serve-emu/api/screenshot?device=<physical-android-device>)
    

    The original 503 JSON error body and contemporaneous capture-process stderr were not available in the retained diagnostics, so the internal Android failure remains unproven. In particular, the generic message is not evidence that USB disconnected. Returning the underlying status/body or stderr with a correlation identifier would distinguish timeout, capture initialization, and transport failures instead of sending the caller through repeated inventory refreshes.

  2. juliusmarminge commented on Nov 12, 2026

    @juliusmarminge
    Member

    This is the same problem as #25216, so I'm closing it in favour of that one. If it is different, say how and it will be reopened.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions