Repository navigation
Description
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_listalready reportsbooted: falsefor 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 anydevice_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
- Boot a spare iOS simulator (here
Agent 1, UDIDB79B260C-B24B-4082-A303-F01F75AA4C09). - In a thread with device access, call
device_screenshotwith{}before opening any device. - Call
device_open {"deviceId":"B79B260C-…","hostId":"local"}. It succeeds. - Call
device_screenshot {"deviceId":"B79B260C-…","hostId":"local"}. It succeeds (1206×2622 PNG). - Call
device_screenshot {"deviceId":"00000000-0000-0000-0000-000000000000","hostId":"local"}. - Shut down the simulator outside T3 (
xcrun simctl shutdown B79B260C-…). - Call
device_screenshot {"deviceId":"B79B260C-…","hostId":"local"}again. - 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:
_tagandreason(already partly there instructuredContent, but not in the text most clients show)- the error's own
message, whichDeviceToolUnavailableErrorandDeviceOperationErroralready define - the underlying cause: the hub's HTTP status and its JSON
errorbody, 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.
-
src/mcp/McpHttpServer.ts,registerDeviceScreenshot(bundle line ~337812). It passes a constant string asfailureText:yield* registerImageTool(DeviceScreenshotTool, ..., "screenshot", "Device screenshot failed.");
registerHtmlPreview, a few lines below, passes a function that returnserror.message. -
src/mcp/McpHttpServer.ts,imageToolFailure(bundle line ~337732). WhenfailureTextis a string,messageis 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
logWarningthat follows records onlyoperation,errorTagandfailureCount, so the reason is also missing from the warning log line. -
src/device/DeviceHubOperations.ts,screenshot(bundle line ~314433).filterStatusOkdiscards the hub's response body, and the error becomesDeviceOperationError{reason:"request_failed"}. Itsmessage(defined inpackages/contracts/src/device.ts) ignorescause, 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 thedevice_open/device_closeerrors "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
- [Bug]: preview_click reports generic execution errors for missing targets #3714:
preview_clickreports generic execution errors for missing targets (same pattern on the preview tools) - [Bug]: preview_snapshot and preview_navigate fail with "Preview automation … failed on client" #19912:
preview_snapshot/preview_navigateopaque failures - [Bug]: Android device panel shows "No video received" instead of the hub's "ffmpeg not found" error #14278 / [Bug]: Device hub install shows "Check this host's network connection" when npm install actually failed (EACCES in ~/.npm/_prebuilds) #17431: earlier cases where a device-hub error was replaced by a generic message
Activity
EndersonPro commentedon Oct 31, 2026
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_opensucceeds. ADB transport, the app process, and the local development-backend port forwards were available.device_screenshotreturnsDevice screenshot failed: Could not communicate with device support. Try refreshing devices.Redacted underlying errors from the existing traces:
The original 503 JSON
errorbody 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.juliusmarminge commentedon Nov 12, 2026
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.