ApplesauceOS
Read Markdown ↗Download .md

Device HTTP API

These routes belong to the device HTTP server, not the public MCP URL.

The device server uses port 8080. Use the device's reachable local-network address or a direct USB port forward from your computer. Send Content-Type: application/json for JSON POST bodies and URL-encode query parameters. Each route below lists its HTTP method.

Connection and capability discovery

Browse the HTTP endpoint reference for an individual page per method/path, with parameters, responses and curl examples.

Use a user-provided base URL, such as http://DEVICE_LAN_IP:8080. For direct USB access, forward the device's HTTP port to a computer port; for example, iproxy 18080:8080, then use http://127.0.0.1:18080. This does not require SSH. Keep the forward running. Device pairing/trust and a working USB connection are required. A computer's localhost is not the device unless a forward exists.

GET /automation returns apiVersion: 1, supported features, and size limits. Use GET /endpoints to discover the installed endpoint set. The following endpoints support the HTTP-only MCP and any client that can send HTTP requests.

Apps, buttons, gestures, and full screenshots

MethodPathInput / result
GET/apps/list?query=...&limit=100Installed {apps:[{bundleId,name}],limit}; limit 1..300
POST/apps/open{bundleId,restart:false}; returns {opened,front,restarted}
POST/device/button{button:"home"}; also power, volume_up, volume_down
POST/device/touch{x,y} for a tap; add endX,endY,duration for a swipe
GET/device/screenshotFull-device PNG, including overlays; available independently of an app's element tree

A restart explicitly terminates an app and may discard unsaved state. Opening an app does not prove its contents have loaded; verify the returned foreground identity and wait for a meaningful element. Gesture coordinates use native interface pixels from a fresh screenshot. duration is 0.05..3 seconds, default 0.35, and both swipe end coordinates are required. Buttons and gestures do not bypass a passcode. Inspect the resulting screen after every action.

Files and project directories

All file paths are relative to the user's Scripts folder. Absolute paths, parent traversal, symbolic-link traversal, and managed execution files are rejected. Do not use a device-specific absolute filesystem prefix. Parent folders are created when writing a file, so writing Demo.aso/lib/helper.js can create an organized project without a separate upload step.

MethodPathInput / result
GET/files/list?path=&limit=100&offset=0{directory,entries,total,nextOffset}; entries include path, name, directory, size, modifiedAt
GET/files/read?path=Demo.aso/main.js{path,content,encoding,bytes,sha256,offset,truncated,nextOffset}
GET/files/download?path=...Raw file bytes
POST/files/write{path,content,encoding:"utf8",overwrite:false,expectedSha256?}; returns {path,bytes,sha256}
POST/files/mkdir{path}; create a directory or project folder
POST/files/move{path,destination,expectedSha256?}; destination must not exist
POST/files/delete{path,expectedSha256?}; delete one file or an empty directory

Files are limited to 8 MiB. Read/write encodings are utf8 and base64; binary assets should use Base64 or the download endpoint. Reads accept byte offset and maxBytes (1..524288; default 262144). Follow nextOffset when truncated is true. For UTF-8 text, use the returned offset rather than adding a character count. Listing accepts limit 1..500 and offset 0..10000.

Creating a file never replaces an existing one. To replace it, first read its current sha256, then send overwrite:true and that value as expectedSha256. A changed or missing file returns HTTP 409. Deleting or moving a regular file also requires its current hash. Empty directory deletion needs no hash and is never recursive. Active script/project paths cannot be changed. Stop related runs first, and delete retained automated runs before moving their project folder. Preserve unrelated files and unsaved editor work.

json
{
  "path": "Demo.aso/main.js",
  "content": "console.log('Ready');\n",
  "encoding": "utf8",
  "overwrite": false
}

JavaScript execution with results

MethodPathInput / result
POST/automation/run{id?,source?,path?,args?}; returns {runId,nativeRunId,state,sourcePath}
GET/automation/runs?id=...One run's state, running, result, logs and metadata
GET/automation/runs?limit=20Retained run IDs, newest first; limit 1..64
POST/automation/stop{runId}; request cancellation of that run only
POST/automation/delete{runId}; remove a finished run and its execution artifacts, preserving the original script

Provide either an async JavaScript function body in source, or a saved .js path. Bodies may use return and await. Saved scripts keep their original relative-import base and are not overwritten. Use /scripts/run for the normal .js or .aso/.cso launcher, including repeats; use /automation/run for a structured returned value, per-run console output and an error stack.

Source is limited to 256 KiB. Results must be JSON-serializable and fit within 128 KiB of serialized text. Convert native proxies with .toJS() and exact large integers to strings. Console output retains up to 200 events and 64 KiB of text. The result contains ok, value or error, runId, timestamps, logs, and droppedLogEntries. Errors include name, code, message, stack, line, and source URL when available. Logs are also available while a run is active.

json
{
  "source": "console.log('Test started'); return {foreground: frontMostAppId()};",
  "args": {"label": "example"}
}

Arguments use the same immutable string args / at.args contract as the normal launcher. A submission acknowledges acceptance, not completion. Poll its own runId until running is false. States are running, finishing, succeeded, failed, cancelled, or interrupted; an initial acknowledgement uses submitted. Cancellation is cooperative and does not undo completed work. A run that finishes before cancellation may still report success.

Choose a UUID id before submission when recovering from a lost connection matters. Both standard UUID notation and 32 hexadecimal characters are accepted; the returned runId is canonical lowercase hexadecimal. A retained ID cannot be submitted twice. After an uncertain submission, inspect that ID and the actual screen before deciding what to do. Do not replay a mutation blindly.

Up to 64 execution records are retained until explicitly deleted. Records remain available after reconnecting the MCP or restarting the device automation service. An active run interrupted by a device/service restart may have no completion record and reports interrupted; that is not success. Collect error evidence before deleting a record. An active run cannot be deleted.

Crash reports

MethodPathResult
GET/diagnostics/crashes?limit=20{reports:[{reportId,name,modifiedAt,bytes}]}; limit 1..100
GET/diagnostics/crash?id=...&offset=0&maxBytes=65536{reportId,content,offset,truncated,nextOffset}

Use the opaque reportId returned by the list; arbitrary system paths and report deletion are not supported. Only reports accessible to ApplesauceOS are listed. Report pages accept 1024..524288 bytes and offsets up to 64 MiB. Follow nextOffset for large reports. Check the process name and timestamp before attributing an existing report to a test. A JavaScript exception is not itself an iOS process crash.

HTTP failures

Check the HTTP status and JSON error response. Invalid inputs return 400, unavailable/forbidden files 403 or 404, conflicts and active-file changes 409, and oversized files 413. UI errors may include a specific error code and message; file/execution errors include an error message. Re-observe after a stale reference and re-read after a hash conflict. Fix invalid inputs rather than repeatedly sending them.

Status & Info

EndpointDescriptionExample Response
GET /Browser landing page with endpoint linksHTML
GET /endpointsAll registered method/path pairs, read directly from the router{port:8080, routes:[{method,path}], options}
GET /isBusyScript status; busy/retV exclude daemon scripts{"retV":false,"busy":false,"count":0,"scripts":[]}
GET /runningScriptsList running script paths["MyAutomation.aso"]
GET /lastErrorLast JS error from engine{"lastError":"..."}
GET /deviceInfoSame object as getDeviceInfo()serialNumber, systemVersion, screen, battery, etc.
GET /SNSerial number (plain text)DEVICE_SERIAL
GET /IPLocal WiFi IP (plain text)192.168.1.42
GET /tweakVersionInstalled ApplesauceOS versionPlain text
GET /appInfo?bundleId=...Same object as appInfo(bundleId)App metadata or installed:false
GET /temperatureDevice temperature; negative value if unavailable{"celsius":32.5}

Each /isBusy entry contains path, fullPath, entryPath, daemon, stopRequested, and runningSeconds. count includes daemon scripts. /runningScripts returns the original path strings used to start them.

Script Control

EndpointJSON body / queryDescription
POST /scripts/run{path,args?,id?,repeat?,interval?}Start a relative .js file or .aso/.cso directory under Scripts; returns {id,path,fullPath,entryPath}
GET /scripts/runsOptional id queryWithout id: {runs:[...]}; with id: one execution journal, including output and errors
POST /scripts/stop{id}Request cancellation of that execution; returns {stopRequested}
GET /stopAll—Request cancellation of all non-daemon scripts
sh
curl 'http://127.0.0.1:8080/scripts/run' \
  -H 'Content-Type: application/json' \
  -d '{"path":"MyAutomation.aso","args":{"name":"Alex","count":5,"enabled":false},"repeat":1}'

curl 'http://127.0.0.1:8080/scripts/runs?id=RUN_UUID'

curl 'http://127.0.0.1:8080/scripts/stop' \
  -H 'Content-Type: application/json' \
  -d '{"id":"RUN_UUID"}'

path must be relative, remain inside Scripts, and end in .js, .aso, or .cso. An .aso or .cso path is a directory: its entry is main.js, then index.js. A missing entry fails instead of executing an arbitrary helper. Starting a project and its entry concurrently is rejected as a duplicate. HTTP execution reads saved files; App Run saves project drafts before submitting.

info.plist RepeatInterval may supply its default; otherwise 0.

to uppercase UUID notation. A retained ID cannot be submitted again: inspect its journal instead of replaying an action after a lost response.

in the script's reserved, immutable args namespace. at.args is the same object.

Arguments in memory

Every run receives its own read-only argument object, available to main and all required modules. Nothing automatically creates or reads an argument file. Argument names do not become global variables, so they cannot replace automation functions or launcher options. Use bracket notation for names with spaces.

Submitted valueValue exposed to JavaScript
"hello", "", "007"Unchanged strings
5, false, null"5", "false", "null"
JSON object / arrayCompact JSON string; parse explicitly when needed
Missing keyundefined
js
console.log(args.name);             // "Alex"
const count = Number(args.count);   // 5
const enabled = args.enabled === "true"; // false
console.log(at.args === args);      // true

Do not use Boolean(args.enabled) to parse a boolean: "false" is a nonempty string. JSON.parse(args.config) can reconstruct a structured argument. Assigning, replacing, deleting, or extending the argument namespace is unsupported; strict JavaScript throws for attempted mutation. A module can read args directly or accept explicit function parameters. Concurrent runs have independent values.

Submission acknowledges startup, not successful completion. Invalid arguments return HTTP 400. Missing entries, duplicate submissions, or launch failure return

  1. Query the returned ID to determine the outcome. Stop is cooperative; a pure

busy loop without cancellation checks can delay stopping.

Journals retain 30 finished runs plus active runs in memory and reset when SpringBoard restarts. Fields include id, path, entryPath, state (running, completed, failed, stopped), startedAt, finishedAt after completion, logs, and droppedLogs. Failures include scriptError, errorSource, and errorLine. List summaries omit logs. At most eight journaled runs may be active. The shared log is available through /logs; use a run ID for attributed results.

Logs

EndpointParamsDescription
GET /logslines=N (default 200, 0=all)Tail of script log (plain text)
GET /logs/clear—Clear the log

Logs are shared by scripts. Use a unique run ID in your own completion/error messages, poll that run's path, and verify the intended UI outcome. /lastError is historical engine state, not a per-run success result. Clearing logs does not clear that field.

Settings

Synthetic touch indicators are always active and cannot be disabled through settings or JavaScript.

The installed ApplesauceOS app provides these controls without requiring a computer. Its Blacklist disables the helper for selected bundle identifiers. Fully close and reopen an app after changing its blacklist entry. UIHelper and RemoteObjC/RemoteC calls are unavailable for blocked apps. Coordinate touch and visual automation remain available. Obtain fresh elements and reconnect native clients after enabling the helper again.

Avoid safe mode and Show IP in status bar are disabled by default. Recovery changes apply after restarting SpringBoard. Other switches update running processes through notifications and cached preferences.

Keep screen awake prevents automatic display sleep while enabled; it does not unlock a device or prevent manually locking it. It is disabled by default.

GET /settings returns the preference snapshot; POST /settings applies a validated partial update. Boolean keys are avoidSafeMode, keepAwake, debugLog, helperEnabled, serverEnabled, inspectorShortcut, volumeStop, showIPInStatusBar, errorAlerts, and scriptToasts. disabledHelperBundleIDs is an array of bundle identifiers. Writes preserve unrelated settings and are serialized across processes. Turning serverEnabled off disconnects new HTTP/MCP requests; it can be enabled again from the app.

GET /settings/logs reads at most the last 128 KiB of the script log. POST /settings/action accepts {action:"inspector"}, {action:"stopScripts"}, or {action:"respring",confirm:true}. Restarting SpringBoard ends active scripts.

EndpointParamsDescription
GET /setDebugLogon=0|1Toggle verbose [ApplesauceOS] system log
GET /setErrorAlerton=0|1Toggle error alert popups
GET /setScriptToastson=0|1Toggle script start/stop toasts
GET /setKeepAwakeon=0|1Persist ApplesauceOS's keep-awake preference
GET /setSafeModeon=0|1Set avoidSafeMode for the next SpringBoard launch; on=1 enables avoidance, not safe mode

Omitting on reads the current value for every setting above except /setErrorAlert, which requires it. Temporary scripts should restore the preferences they change. These settings are not performance or crash guarantees.

---

UIHelper inspection and actions

Press Volume Up + Volume Down together to open or close the on-device UIHelper inspector. A chord toggles once until both buttons are released. Holding either button alone for 0.2 seconds still stops scripts. The HID monitor observes the buttons; normal system volume behavior is not suppressed.

The inspector captures the foreground app and displays a frozen screenshot at the available screen width. Drag to pan; pinch or double-tap to zoom. UIHelper/OCR allow up to 4x; Coordinates allows substantially higher zoom, reaching up to 32 interface points per source pixel (capped at 128x). Fit shows the entire image. These gestures manipulate the captured image, not the underlying app. Zoom uses gestures; there are no +/− buttons. Tap the image to select an element without shrinking the screenshot. Picking prioritizes named controls and accessibility elements over decorative views, including SwiftUI drawing layers. Within each priority, smaller and deeper elements come first. Previous / Next cycles through overlapping elements and containers. Details opens a separate panel with larger text; Back to screen captures the app again and clears the old selection. Zoom and position are preserved when the image and viewport dimensions are unchanged; select an element from the refreshed image. Refresh also captures again and resets the viewport. The details show UIHelper attributes, supported operations, candidate selectors and their match counts at capture time. Test queries the selected suggestion against the live app; Read retrieves current text; Copy JS copies a UIHelper snippet using a unique selector. When none is unique, it copies an explanatory comment instead of selecting an arbitrary match. Tap hides the inspector, acts through UIHelper and refreshes the snapshot. Refresh captures again; Close dismisses it. Test only counts matches; it does not tap. The selected name is followed by its center x/y in native touch pixels. Copy XY copies x, y; zoom and panning do not alter those coordinates. In OCR mode, the primary button is Copy Rect and the displayed geometry is x, y, width, height. Copy Rect produces a JSON region in native pixels, with edges rounded outward and clipped to the screen. The highlighted box uses those same bounds. Pass the copied object as region with ocr({coordinates:"pixels", region: ...}). A separate Copy XY button between Previous and Next copies the text rectangle's center for touches. The mode switch offers:

touch. Enlarged pixels use nearest-neighbor sampling, a grid appears at high zoom, and the selected pixel is outlined at its actual size. The four arrows replace Previous/Next and move exactly one native pixel per press. Double-tap jumps to pixel-level zoom. Refreshing captures only the screen, without a helper query. Coordinates identify image pixels: taps inside [x,x+1) and [y,y+1) select (x,y), independently of the displayed magnification.

then select a text box. Copy its text or code that calls ocr({level:0, coordinates:"pixels"}), requires one exact text match, and taps its center. Results are reused until the next capture; errors are shown without disabling coordinate picking.

In Coordinates Details, Copy XY replaces Test. In OCR Details, Copy Rect replaces Test and Copy text replaces Read. The separate Copy XY center button remains available. Tap sends a simulated touch rather than a UIHelper operation. Coordinate/OCR modes also work with a blacklisted app or no accessible elements. Insert code into script offers coordinate taps for every selection, OCR lookup/tap for recognized text, and UIHelper actions when a unique selector is available. Inserted snippets have their own block scope. Fixed coordinates depend on device size, orientation, and layout. OCR code searches the live screen when executed; it does not reuse a saved text position. Recognition locates text, not the tappable area of its associated control. Test the center or choose the actual target in Coordinates mode (for example, an icon instead of its label).

External UIHelper mutations are rejected while the inspector is visible, to avoid sending injected taps into the overlay. Scripts may still read state. The open inspector retains its current observation independently of the HTTP cache, releasing it on refresh or close. It does not expire after 60 seconds or disappear when HTTP clients request other observations. Element and app identity checks still apply; refresh when the underlying UI changes or its native reference is no longer available. The inspector window is excluded from UIHelper's own hierarchy.

For script-side observation:

js
const ui = UIHelper.forApp("com.apple.Preferences");
const observation = ui.observe({limit: 300, includeContainers: false});
console.log(JSON.stringify(observation));

For an AI or another runtime client, use the existing ApplesauceOS HTTP server:

Omit bundleId to use the foreground app, or provide it to require that app to be foreground. /ui/query and /ui/wait require an object selector; use {name:"..."} instead of the JavaScript string shorthand. Observation flags default to false; screenshots are requested explicitly.

RouteBehavior
GET /uiDiscover the UIHelper endpoint contract.
GET /ui/observe or POST /ui/observeCapture structured foreground-app elements. Options: bundleId, limit (1..1000, default 300), includeContainers, includeHidden, screenshot. GET boolean flags accept 1 or true; POST uses JSON booleans.
GET /ui/screenshot?observationId=...PNG for an observation requested with screenshot: true.
POST /ui/query{bundleId, selector}; return live match count, uniqueness and up to 50 match summaries. Uses exactly UIHelper selector semantics.
GET /ui/element?observationId=...&ref=e1Read fresh attributes for an observed element.
POST /ui/action{observationId, ref, action, text?, direction?}; actions: read, tap, setText, typeText, clear, scroll, scrollIntoView. Mutations are never retried automatically.
POST /ui/wait{bundleId, selector, condition, timeoutMs, intervalMs}; condition is exists or absent. Defaults: 5000/200 ms; limits: timeout 0..10000, interval 100..2000 ms.
POST /ui/inspector{action: "open"} or {action: "close"} schedules the overlay operation.
sh
curl 'http://localhost:8080/ui/observe?includeContainers=0&screenshot=1'
curl -X POST 'http://localhost:8080/ui/query' \
  -H 'Content-Type: application/json' \
  -d '{"bundleId":"com.example.app","selector":{"id":"login"}}'
curl -X POST 'http://localhost:8080/ui/action' \
  -H 'Content-Type: application/json' \
  -d '{"observationId":"COPY_FROM_OBSERVE","ref":"e3","action":"tap"}'

Observations contain schemaVersion, observationId, bundleId, pid, processSessionId, timestamps, screen geometry and flat elements with short ref values, parentRef/depth, attributes, rectangles, action names and selector suggestions. HTTP and script-side observations share the same server cache and omit internal element IDs. A script can pass its observation ID and references to an HTTP client. Short references are meaningful only together with their observation ID. Copy a suggested selector into findElement or waitForElement for a reusable script.

Observation fieldMeaning
backend, schemaVersion"UIHelper", currently 1
observationId, bundleId, pid, processSessionIdSnapshot and process identity
timestamp, screenshotTimestampUnix seconds; screenshot timestamp is present only when requested
captureDurationMs, expiresInSecondsCapture duration and nominal cache lifetime
screen{width, height, scale, coordinateSpace:"nativePixels"}
scanned, eligible, truncated, completeTraversalTraversal/output coverage
screenshotURLRelative PNG endpoint, present only when requested
elementsFlat element records; parentRef is omitted when no included ancestor exists

Each element includes ref, depth, identifier, type, className, label, text, value, rect, visible, enabled, accessible, selected, focused, index, actions, and selectors. Selector suggestions have {selector, matches, unique, verified, stability}; stability is identifier, label, or class. Use suggestion.selector, not the entire suggestion object, as the query selector.

/ui/element and /ui/action return {backend, observationId, ref, action, value}. Reads put fresh attributes in value; successful mutations put true there. This acknowledges the action, not completion of subsequent navigation. /ui/wait success returns {matched:true, condition, count, elapsedMs}. A screenshot request returns PNG bytes, or 404 if it expired or was not included in the observation.

The server retains observations for up to 60 seconds, at most eight entries; screenshots also have a 12-million-pixel aggregate cache budget (one screenshot may exceed that budget). Old entries may be evicted earlier. References retain UIHelper's process and detached-view checks. Before a reference operation, the helper checks the element's identifier, class, type, label and rectangle in its main-thread action handler. A changed/recycled element, changed foreground app or expired observation returns a stale-reference error; observe again.

Snapshots scan at most 5000 nodes and return at most the requested limit, within the response size limit. truncated reports omitted output; completeTraversal indicates whether selector counts covered the traversed tree completely. Suggestions are marked unique only after a complete traversal. Match counts are capture-time evidence, not a guarantee across navigation or app updates. Visibility follows UIHelper's frame/clipping checks, not full occlusion testing. Secure text field text/value are omitted from inspection responses.

Screenshots are opt-in. Screenshot and tree captures occur close together but are not atomic; timestamps and capture duration are included. Foreground-app or screen-geometry changes during capture reject the observation. Close the inspector before requesting an HTTP screenshot. Rectangles use native pixels; the inspector maps those coordinates through the screenshot's zoom and pan.

The endpoints do not launch apps, continuously capture screenshots, or poll the full UI tree in the background. /ui/wait polls only its selector; an individual helper request can outlast the polling deadline. Errors return JSON with error and message, with HTTP 400 for invalid requests, 409 for stale/non-interactable observations, 408 for timeouts, and 503 for an unavailable helper. A failed wait includes its last query for diagnosis.