swipium 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +32 -20
  3. package/THREAT_MODEL.md +109 -21
  4. package/dist/automationGen/run.js.map +1 -1
  5. package/dist/cli/init.js +101 -11
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/cli/verify.js +2 -2
  8. package/dist/cli/verify.js.map +1 -1
  9. package/dist/consent/consent.js +365 -17
  10. package/dist/consent/consent.js.map +1 -1
  11. package/dist/context/projectRoot.js +9 -4
  12. package/dist/context/projectRoot.js.map +1 -1
  13. package/dist/context/protocolEra.js +15 -0
  14. package/dist/context/protocolEra.js.map +1 -0
  15. package/dist/drivers/DirectDriver.js +5 -2
  16. package/dist/drivers/DirectDriver.js.map +1 -1
  17. package/dist/featureTesting/executionBootstrap.js +85 -77
  18. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  19. package/dist/flows/run.js +9 -5
  20. package/dist/flows/run.js.map +1 -1
  21. package/dist/lib/abortScope.js +36 -3
  22. package/dist/lib/abortScope.js.map +1 -1
  23. package/dist/lib/android.js +15 -6
  24. package/dist/lib/android.js.map +1 -1
  25. package/dist/lib/codexEnv.js +112 -0
  26. package/dist/lib/codexEnv.js.map +1 -0
  27. package/dist/lib/logger.js +18 -0
  28. package/dist/lib/logger.js.map +1 -1
  29. package/dist/lib/result.js +188 -10
  30. package/dist/lib/result.js.map +1 -1
  31. package/dist/lib/schemaHash.js +20 -31
  32. package/dist/lib/schemaHash.js.map +1 -1
  33. package/dist/lib/simctl.js +102 -3
  34. package/dist/lib/simctl.js.map +1 -1
  35. package/dist/lib/toolSchema.js +143 -0
  36. package/dist/lib/toolSchema.js.map +1 -0
  37. package/dist/lib/wda.js +5 -2
  38. package/dist/lib/wda.js.map +1 -1
  39. package/dist/mobileAudit/runner.js +12 -4
  40. package/dist/mobileAudit/runner.js.map +1 -1
  41. package/dist/oracle/failures.js +2 -2
  42. package/dist/oracle/failures.js.map +1 -1
  43. package/dist/orchestration/testThis/execute.js +21 -4
  44. package/dist/orchestration/testThis/execute.js.map +1 -1
  45. package/dist/orchestration/testThis/pipeline.js +2 -0
  46. package/dist/orchestration/testThis/pipeline.js.map +1 -1
  47. package/dist/orchestration/testThis/plan.js.map +1 -1
  48. package/dist/server.js +564 -139
  49. package/dist/server.js.map +1 -1
  50. package/dist/services/prepareAndroid.js +3 -2
  51. package/dist/services/prepareAndroid.js.map +1 -1
  52. package/dist/services/prepareIos.js +31 -4
  53. package/dist/services/prepareIos.js.map +1 -1
  54. package/dist/services/smoke.js +38 -4
  55. package/dist/services/smoke.js.map +1 -1
  56. package/dist/session/processRegistry.js +12 -1
  57. package/dist/session/processRegistry.js.map +1 -1
  58. package/dist/snapshot/parse.js +64 -9
  59. package/dist/snapshot/parse.js.map +1 -1
  60. package/dist/snapshot/present.js +14 -4
  61. package/dist/snapshot/present.js.map +1 -1
  62. package/dist/snapshot/settle.js +14 -3
  63. package/dist/snapshot/settle.js.map +1 -1
  64. package/dist/tools/act.js +694 -670
  65. package/dist/tools/act.js.map +1 -1
  66. package/dist/tools/agent.js +15 -16
  67. package/dist/tools/agent.js.map +1 -1
  68. package/dist/tools/appControl.js +4 -4
  69. package/dist/tools/appControl.js.map +1 -1
  70. package/dist/tools/appMap.js +11 -13
  71. package/dist/tools/appMap.js.map +1 -1
  72. package/dist/tools/build.js +4 -6
  73. package/dist/tools/build.js.map +1 -1
  74. package/dist/tools/bundletool.js +3 -3
  75. package/dist/tools/bundletool.js.map +1 -1
  76. package/dist/tools/clearOverlay.js +2 -3
  77. package/dist/tools/clearOverlay.js.map +1 -1
  78. package/dist/tools/device.js +4 -3
  79. package/dist/tools/device.js.map +1 -1
  80. package/dist/tools/doctor.js +23 -11
  81. package/dist/tools/doctor.js.map +1 -1
  82. package/dist/tools/explore.js +15 -21
  83. package/dist/tools/explore.js.map +1 -1
  84. package/dist/tools/featureTesting.js +22 -6
  85. package/dist/tools/featureTesting.js.map +1 -1
  86. package/dist/tools/firstRun.js +4 -5
  87. package/dist/tools/firstRun.js.map +1 -1
  88. package/dist/tools/flow.js +13 -17
  89. package/dist/tools/flow.js.map +1 -1
  90. package/dist/tools/flowRepair.js +3 -5
  91. package/dist/tools/flowRepair.js.map +1 -1
  92. package/dist/tools/generate.js +10 -11
  93. package/dist/tools/generate.js.map +1 -1
  94. package/dist/tools/getArtifact.js +114 -10
  95. package/dist/tools/getArtifact.js.map +1 -1
  96. package/dist/tools/health.js +2 -1
  97. package/dist/tools/health.js.map +1 -1
  98. package/dist/tools/ios.js +35 -13
  99. package/dist/tools/ios.js.map +1 -1
  100. package/dist/tools/issues.js +8 -8
  101. package/dist/tools/issues.js.map +1 -1
  102. package/dist/tools/jobs.js +26 -10
  103. package/dist/tools/jobs.js.map +1 -1
  104. package/dist/tools/metro.js +4 -5
  105. package/dist/tools/metro.js.map +1 -1
  106. package/dist/tools/mobileAudit.js +11 -10
  107. package/dist/tools/mobileAudit.js.map +1 -1
  108. package/dist/tools/note.js +2 -3
  109. package/dist/tools/note.js.map +1 -1
  110. package/dist/tools/prepareIosTarget.js +102 -31
  111. package/dist/tools/prepareIosTarget.js.map +1 -1
  112. package/dist/tools/prepareTarget.js +3 -5
  113. package/dist/tools/prepareTarget.js.map +1 -1
  114. package/dist/tools/report.js +4 -5
  115. package/dist/tools/report.js.map +1 -1
  116. package/dist/tools/resolveArtifact.js +2 -3
  117. package/dist/tools/resolveArtifact.js.map +1 -1
  118. package/dist/tools/resolveTarget.js +3 -6
  119. package/dist/tools/resolveTarget.js.map +1 -1
  120. package/dist/tools/screenRecord.js +2 -3
  121. package/dist/tools/screenRecord.js.map +1 -1
  122. package/dist/tools/screenshot.js +2 -2
  123. package/dist/tools/screenshot.js.map +1 -1
  124. package/dist/tools/smoke.js +4 -2
  125. package/dist/tools/smoke.js.map +1 -1
  126. package/dist/tools/snapshot.js +6 -7
  127. package/dist/tools/snapshot.js.map +1 -1
  128. package/dist/tools/startSession.js +12 -16
  129. package/dist/tools/startSession.js.map +1 -1
  130. package/dist/tools/suite.js +2 -4
  131. package/dist/tools/suite.js.map +1 -1
  132. package/dist/tools/testSuite.js +13 -19
  133. package/dist/tools/testSuite.js.map +1 -1
  134. package/dist/tools/testThis.js +28 -13
  135. package/dist/tools/testThis.js.map +1 -1
  136. package/dist/tools/visual.js +7 -9
  137. package/dist/tools/visual.js.map +1 -1
  138. package/dist/tools/wait.js +130 -21
  139. package/dist/tools/wait.js.map +1 -1
  140. package/dist/tools/wda.js +199 -60
  141. package/dist/tools/wda.js.map +1 -1
  142. package/dist/version.js +1 -1
  143. package/docs/README.md +4 -4
  144. package/docs/ci-reports.md +39 -7
  145. package/docs/concepts.md +37 -25
  146. package/docs/flows.md +1 -1
  147. package/docs/mcp-server.md +281 -76
  148. package/docs/physical-devices.md +4 -4
  149. package/docs/tools.md +59 -30
  150. package/package.json +4 -3
@@ -3,6 +3,16 @@
3
3
  Swipium is a local stdio MCP server. Your MCP client starts it as a child process and talks
4
4
  JSON-RPC over its stdin and stdout. Nothing listens on the network.
5
5
 
6
+ This page covers client setup, the protocol, and how the server behaves. Sessions, consent and the
7
+ project root are explained in [concepts.md](concepts.md); every tool is in [tools.md](tools.md);
8
+ environment variables are in the
9
+ [README](../README.md#configuration--environment-variables).
10
+
11
+ **Contents**: [Requirements](#requirements) · [Server command](#server-command) ·
12
+ [Client setup](#client-setup) · [Protocol versions](#protocol-versions) ·
13
+ [What the server exposes](#what-the-server-exposes) · [Server behavior](#server-behavior) ·
14
+ [Verification](#verification) · [Debugging](#debugging) · [Troubleshooting](#troubleshooting)
15
+
6
16
  ## Requirements
7
17
 
8
18
  - Node.js 20 or newer.
@@ -15,6 +25,9 @@ JSON-RPC over its stdin and stdout. Nothing listens on the network.
15
25
  the structured UI tree, taps, typing and swipes. Visual-only checks work through `simctl` without
16
26
  it: `qa_visual` baselines and diffs, OCR `find_text`, and template `find_image`.
17
27
 
28
+ Swipium only drives Android Emulators and iOS Simulators, never a physical device. See
29
+ [physical-devices.md](physical-devices.md).
30
+
18
31
  ### How Android tools are found
19
32
 
20
33
  Swipium looks for an Android SDK in `$ANDROID_HOME`, then `$ANDROID_SDK_ROOT`, then the default
@@ -47,15 +60,6 @@ instead. From a source checkout, run `npm run build` and use
47
60
  | `swipium init`, `verify`, `scan`, `suite`, `report`, `gc` | CLI subcommands. See `swipium --help`. |
48
61
  | `swipium <unknown word>` | Prints usage and exits with status 2 instead of starting a server. |
49
62
 
50
- ## Project root
51
-
52
- Every tool call works in one app repository. Swipium takes it from the `projectRoot` argument, then
53
- the client's MCP roots, then `SWIPIUM_PROJECT_ROOT`, then `CLAUDE_PROJECT_DIR`, then the server's
54
- working directory when that looks like an app. The full rules are in
55
- [concepts.md](concepts.md#project-root). For client setup, the practical rule is: if your client
56
- neither sends MCP roots nor lets you set a `cwd` (Claude Desktop, Windsurf), set
57
- `SWIPIUM_PROJECT_ROOT` in the server `env`.
58
-
59
63
  ## Client setup
60
64
 
61
65
  `swipium init <client>` prints the exact registration and changes nothing. Add `--apply` to
@@ -65,7 +69,7 @@ perform it. After a successful apply it runs `swipium verify`. Options: `--scope
65
69
  | Client | `swipium init` does | Where it lands |
66
70
  | --- | --- | --- |
67
71
  | Claude Code | Runs `claude mcp add swipium [--scope …] -- <command>` | `local` / `user`: `~/.claude.json`; `project`: `.mcp.json` |
68
- | Codex | Appends a `[mcp_servers.swipium]` block with `cwd` and timeouts (refuses if `--cwd` doesn't exist; leaves an existing block alone) | `~/.codex/config.toml` |
72
+ | Codex | Appends a `[mcp_servers.swipium]` block with `cwd`, timeouts and `env_vars` (see [Codex](#codex)) | `~/.codex/config.toml` (`$CODEX_HOME/config.toml` when set) |
69
73
  | Gemini CLI | Runs `gemini mcp add --scope project\|user swipium …`; prints a manual block if that fails | `.gemini/settings.json` (project, the default), `~/.gemini/settings.json` (`--scope user`) |
70
74
  | Cursor | Merges a `swipium` entry under `mcpServers` | `.cursor/mcp.json` (for all projects, add the same entry to `~/.cursor/mcp.json` yourself) |
71
75
  | VS Code | Merges a `swipium` entry under `servers`; prints a `code --add-mcp …` line for the user profile | `.vscode/mcp.json` |
@@ -77,24 +81,42 @@ Which command gets written:
77
81
  - **Team-shared files** get the portable `npx -y swipium`. That covers Claude `--scope project`,
78
82
  Gemini project scope, `.cursor/mcp.json` and `.vscode/mcp.json`.
79
83
  - **Machine-local registrations** get this machine's `node` and the absolute path of the installed
80
- `dist/index.js`. That covers Claude local and user scope, Gemini user scope, and Codex. The
81
- exception is when Swipium itself runs from the npx cache; that path would disappear, so these
82
- also get `npx -y swipium`.
84
+ `dist/index.js`. That covers Claude local and user scope, Gemini user scope, and Codex. The node
85
+ path is one that survives upgrades: a Homebrew `node` is written as its `opt` path (for example
86
+ `/opt/homebrew/opt/node@20/bin/node`), not the versioned `Cellar` path that `brew upgrade`
87
+ removes; otherwise `init` prefers the `node` on your `PATH` that resolves to the running binary.
88
+ If Swipium itself runs from the npx cache, that path would disappear, so these get
89
+ `npx -y swipium` too.
83
90
 
84
91
  For Cursor and VS Code, `init` refuses to edit a file that isn't plain JSON (for example JSONC with
85
92
  comments), prints the entry to add by hand, and exits with status 2. An existing `swipium` entry
86
93
  is left unchanged.
87
94
 
95
+ ### Project root
96
+
97
+ Every tool call works in one app repository. The full resolution order is in
98
+ [concepts.md](concepts.md#project-root). For setup, the practical rule: if your client neither
99
+ sends MCP roots nor lets you set a `cwd` (Claude Desktop, Windsurf), set `SWIPIUM_PROJECT_ROOT` in
100
+ the server `env`. Clients on MCP 2026-07-28 never send roots (see
101
+ [Protocol versions](#protocol-versions)); Claude Code covers that by setting `CLAUDE_PROJECT_DIR`.
102
+
88
103
  ### Manual configuration
89
104
 
90
- Claude Code:
105
+ #### Claude Code
91
106
 
92
107
  ```bash
93
108
  claude mcp add swipium --scope project -- npx -y swipium
94
109
  ```
95
110
 
96
- Codex (`~/.codex/config.toml`). Codex's defaults of 10 s to start and 60 s per tool call are too
97
- short for a first `npx` run and for builds and boots:
111
+ Claude Code sets `CLAUDE_PROJECT_DIR` for the servers it launches, so Swipium picks up the open
112
+ project without extra config. Claude Code 2.1.285 and later (verified on 2.1.289) connect on MCP 2026-07-28, so consent
113
+ prompts arrive as an `InputRequiredResult` (see [Protocol versions](#protocol-versions)); the user
114
+ sees the same one-checkbox prompt either way.
115
+
116
+ #### Codex
117
+
118
+ Add this to `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`). Codex's defaults of 10 s to
119
+ start and 60 s per tool call are too short for a first `npx` run and for simulator boots:
98
120
 
99
121
  ```toml
100
122
  [mcp_servers.swipium]
@@ -103,13 +125,51 @@ args = ["-y", "swipium"]
103
125
  cwd = "/absolute/path/to/your/mobile-app"
104
126
  startup_timeout_sec = 30
105
127
  tool_timeout_sec = 600
128
+ env_vars = ["SWIPIUM_TEST_EMAIL", "SWIPIUM_TEST_USERNAME", "SWIPIUM_TEST_PASSWORD", "SWIPIUM_TEST_OTP", "SWIPIUM_TEST_PIN", "SWIPIUM_TEST_TOKEN", "SWIPIUM_TEST_DEEP_LINK", "SWIPIUM_VERIFICATION_CODE", "SWIPIUM_PROJECT_ROOT", "SWIPIUM_REQUIRE_ELICITATION", "SWIPIUM_LOG_LEVEL", "SWIPIUM_OCR_CMD", "SWIPIUM_VISUAL_MASK_CMD", "SWIPIUM_RETENTION_DAYS", "SWIPIUM_RETENTION_KEEP", "ANDROID_HOME", "ANDROID_SDK_ROOT", "ANDROID_SDK_HOME", "ANDROID_USER_HOME", "ANDROID_AVD_HOME", "ANDROID_EMULATOR_HOME", "JAVA_HOME", "JAVA_TOOL_OPTIONS", "GRADLE_USER_HOME", "GRADLE_OPTS", "BUNDLETOOL_JAR", "APPIUM_HOME", "DEVELOPER_DIR", "DEVELOPMENT_TEAM", "XCODE_DEVELOPMENT_TEAM", "WDA_PROJECT_PATH", "WEBDRIVERAGENT_PROJECT", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "CI", "GITHUB_SHA", "GITHUB_REF_NAME", "GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_RUN_ID", "CI_COMMIT_SHA", "CI_COMMIT_REF_NAME", "CI_PIPELINE_URL", "BITBUCKET_COMMIT", "BITBUCKET_BRANCH"]
129
+ # Optional: a smaller tool list (off by default). `swipium init codex` prints the core list here
130
+ # as a comment; it includes every tool the server instructions and qa_status point to.
131
+ # enabled_tools = ["qa_test_this", "qa_status", ...]
106
132
  ```
107
133
 
108
- Known caveat: in the Codex Desktop app, custom stdio MCP tools can show up in `/mcp` without being
109
- available in threads ([openai/codex#19425](https://github.com/openai/codex/issues/19425)). If that
110
- happens, use the Codex CLI.
111
-
112
- Gemini CLI:
134
+ **Codex does not pass your shell environment to MCP servers.** A stdio server only gets a fixed
135
+ whitelist (`HOME`, `LANG`, `LC_ALL`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, `TMPDIR`, `USER`,
136
+ `__CF_USER_TEXT_ENCODING` and the CA certificate variables), plus the names in `env_vars` and the
137
+ `env = { NAME = "value" }` table of `[mcp_servers.swipium]` (see the
138
+ [Codex config reference](https://developers.openai.com/codex/config-reference)). So
139
+ `SWIPIUM_TEST_*`, other `SWIPIUM_*` settings, `ANDROID_HOME` and `JAVA_HOME` exported in your shell
140
+ never reach Swipium unless they are listed.
141
+
142
+ - `env_vars` forwards a name from the environment Codex was started in (unset names are skipped);
143
+ `env` sets a literal value. Add any custom `SWIPIUM_*` flow variables and
144
+ `ORG_GRADLE_PROJECT_*` signing variables you use.
145
+ - The default list leaves out the names that grant approval (`SWIPIUM_CONSENT_PREAPPROVE`,
146
+ `SWIPIUM_CONSENT_PREAPPROVE_RUN_CODE`, `SWIPIUM_ALLOW_REMOTE_WDA`). Forwarding them would let an
147
+ inherited shell export, or a per-directory env tool such as direnv in a cloned repo, pre-approve
148
+ actions. If you want them, set them literally in `env = { ... }`.
149
+ - `env_vars` is config-only: `codex mcp add --env NAME=value` sets literal values but can't forward
150
+ names, so after `codex mcp add`, add the `env_vars` line and the timeouts to `config.toml` by hand.
151
+
152
+ What `swipium init codex --apply` does: it refuses if `--cwd` doesn't exist, and otherwise appends
153
+ the block above (with this machine's `node`, the commented `enabled_tools` line, and a few comment
154
+ lines). If `config.toml` already has a `[mcp_servers.swipium]` (or `[mcp_servers."swipium"]`)
155
+ table, it leaves it alone: when that table has no `env_vars`, it prints just the line to add;
156
+ otherwise it prints the expected block so you can compare.
157
+
158
+ `qa_doctor` adds two rows when the client is Codex (or you pass `qa_doctor { client: "codex" }`):
159
+
160
+ - `codex-env` lists the Swipium variables the server can see and prints the `env_vars` line. It
161
+ warns when no Android SDK is found (no `ANDROID_HOME`/`ANDROID_SDK_ROOT`, no SDK at the default
162
+ location, no `adb` on `PATH`) or `java -version` fails without `JAVA_HOME`: if you installed the
163
+ SDK or JDK in a custom location, forward `ANDROID_HOME` / `JAVA_HOME`; if not, install it first.
164
+ - `codex-tool-timeout` is a reminder only: the server can't read `tool_timeout_sec`, so keep it at
165
+ 600 or more.
166
+
167
+ Codex 0.146 connects on MCP 2025-06-18. Known caveat: in the Codex Desktop app, custom stdio MCP
168
+ tools can show up in `/mcp` without being available in threads
169
+ ([openai/codex#19425](https://github.com/openai/codex/issues/19425)). If that happens, use the
170
+ Codex CLI.
171
+
172
+ #### Gemini CLI
113
173
 
114
174
  ```bash
115
175
  gemini mcp add --scope project swipium npx -- -y swipium
@@ -118,9 +178,11 @@ gemini mcp add --scope project swipium npx -- -y swipium
118
178
  The `--` keeps Gemini from reading `-y` as its own flag. `init gemini` also suggests
119
179
  `"timeout": 600000` in the settings entry.
120
180
 
121
- Cursor (`.cursor/mcp.json`). For VS Code (`.vscode/mcp.json`), use the same entry under a top-level
122
- `"servers"` key instead of `"mcpServers"`. This is the entry `swipium init cursor` and
123
- `swipium init vscode` write:
181
+ #### Cursor and VS Code
182
+
183
+ This is the entry `swipium init cursor` writes to `.cursor/mcp.json`. For VS Code
184
+ (`.vscode/mcp.json`, written by `swipium init vscode`), use the same entry under a top-level
185
+ `"servers"` key instead of `"mcpServers"`:
124
186
 
125
187
  ```json
126
188
  {
@@ -135,13 +197,13 @@ Cursor (`.cursor/mcp.json`). For VS Code (`.vscode/mcp.json`), use the same entr
135
197
  }
136
198
  ```
137
199
 
138
- Why the entry sets `SWIPIUM_PROJECT_ROOT` even though these editors can send MCP roots: roots come
139
- first in the resolution order, so when the editor sends them, Swipium uses them and ignores the
140
- variable. The variable is a fallback for an editor version or window that sends no roots. The editor
141
- replaces `${workspaceFolder}` with the open folder; if it is left unexpanded, the value is not an
142
- absolute path and Swipium skips it.
200
+ Why set `SWIPIUM_PROJECT_ROOT` when these editors can send MCP roots: roots come first in the
201
+ resolution order, so when the editor sends them, Swipium uses them and ignores the variable. The
202
+ variable is a fallback for an editor version or window that sends no roots. The editor replaces
203
+ `${workspaceFolder}` with the open folder; if it's left unexpanded, the value isn't an absolute
204
+ path and Swipium skips it.
143
205
 
144
- Claude Desktop and Windsurf:
206
+ #### Claude Desktop and Windsurf
145
207
 
146
208
  ```json
147
209
  {
@@ -155,20 +217,63 @@ Claude Desktop and Windsurf:
155
217
  }
156
218
  ```
157
219
 
158
- Environment variables (test credentials, OCR provider, remote WDA allowlist, elicitation policy,
159
- retention) are listed in the
160
- [README's configuration section](../README.md#configuration--environment-variables).
220
+ ### Headless runs
221
+
222
+ `codex exec` and `claude -p` advertise elicitation but answer every consent prompt automatically
223
+ (decline or cancel), so boots, installs and builds never run there. To allow specific actions, an
224
+ operator lists them in `SWIPIUM_CONSENT_PREAPPROVE` in the server's environment. The rules (exact
225
+ names only, the `SWIPIUM_CONSENT_PREAPPROVE_RUN_CODE=1` tier for actions that run code, what can
226
+ never be pre-approved) are in [concepts.md](concepts.md#consent). Where to set it:
227
+
228
+ - **Codex**: literally in the `env` table, since `env_vars` never forwards it:
229
+ `env = { SWIPIUM_CONSENT_PREAPPROVE = "prepare_plan,install_app" }`.
230
+ - **`claude -p`**: in the `env` of the server entry you pass with `--mcp-config`. The
231
+ [CI recipe](ci-reports.md#pre-approving-consents-in-ci) shows a complete config.
232
+
233
+ ## Protocol versions
234
+
235
+ Swipium is a dual-era stdio server. The client's first message picks the era for the whole
236
+ connection:
237
+
238
+ | Client opens with | Protocol | Served as |
239
+ | --- | --- | --- |
240
+ | `initialize` (`protocolVersion` 2025-06-18 or 2025-11-25), e.g. Codex | 2025-era | Handshake, session-scoped capabilities, `elicitation/create` and `roots/list` requests to the client. |
241
+ | `server/discover`, or any request with the `io.modelcontextprotocol/*` `_meta` envelope, e.g. Claude Code | 2026-07-28 | No handshake: the version and client capabilities come with every request. |
242
+
243
+ What differs on a 2026-07-28 connection:
244
+
245
+ - `server/discover` returns the supported versions (`2026-07-28`), capabilities, the server
246
+ instructions and `serverInfo`. Every result carries `resultType`.
247
+ - `tools/list` is sorted by tool name (2025-era connections keep the registration order; the schema
248
+ hash doesn't depend on order). List results carry cache hints: `tools/list`, `prompts/list`,
249
+ `resources/templates/list` and `server/discover` are `ttlMs: 3600000`, `cacheScope: "public"`
250
+ (fixed for the life of the process); `resources/list` and `resources/read` are `ttlMs: 0`,
251
+ `cacheScope: "private"` (they change during a run and name local paths).
252
+ - Consent prompts travel inside an `InputRequiredResult`, and the answer comes back on the client's
253
+ retry of the tool call (see [Consent](#consent-prompts) below).
254
+ - There are no server-to-client requests, so MCP roots are never asked for. Pass `projectRoot`, or
255
+ rely on `SWIPIUM_PROJECT_ROOT` / `CLAUDE_PROJECT_DIR`.
256
+ - `ping` and `logging/setLevel` don't exist in 2026-07-28. Swipium logs to stderr either way.
257
+
258
+ The same in both eras: cancellation (`notifications/cancelled`), the `INVALID_ARGUMENT`,
259
+ `STALE_CLIENT` and `CONSENT_*` envelopes, and the `-32602` error for a missing resource.
260
+
261
+ Swipium is built on the MCP TypeScript SDK v2 (`@modelcontextprotocol/server` 2.3.1). For
262
+ 2025-era clients the move from the 1.x SDK is invisible: tool results are byte-identical and the
263
+ schema hash is unchanged. The one `tools/list` difference is that `qa_act`'s `for.selector` schema
264
+ is inlined instead of a `$ref`.
161
265
 
162
266
  ## What the server exposes
163
267
 
164
- - **Tools**: listed in [tools.md](tools.md). Run `swipium verify` to see the exact list and count
165
- your installed version serves. Every tool carries MCP annotations: read-only tools set
268
+ - **Tools**: listed in [tools.md](tools.md). Run `swipium verify` to see the exact list, count and
269
+ schema hash your installed version serves. Every tool carries MCP annotations: read-only tools set
166
270
  `readOnlyHint: true` and `openWorldHint: false`, and the others also set `destructiveHint` and
167
271
  `idempotentHint`.
168
- - **Server instructions**: sent on `initialize`. They give the first call
169
- (`qa_test_this { mode: "execute" }`), the polling loop (`qa_job_status … waitMs`), how to relay
170
- `needs_input`, blockers and consent, and the project-root order. `qa_status` without a
171
- `sessionId` returns the same orientation plus the tool groups.
272
+ - **Server instructions**: sent on `initialize` (2025) or `server/discover` (2026-07-28). They give
273
+ the first call (`qa_test_this { mode: "execute" }`), the polling loop
274
+ (`qa_job_status … waitMs: 45000`), how to relay `needs_input`, blockers and consent, and the
275
+ project-root order. `qa_status` without a `sessionId` returns the same orientation plus the tool
276
+ groups.
172
277
  - **Prompts** (5): `swipium_setup_check`, `swipium_guardrail_validation`, `swipium_full_smoke`,
173
278
  `swipium_bug_repro`, `swipium_convert_run_to_flow`.
174
279
  - **Resources**:
@@ -177,42 +282,123 @@ retention) are listed in the
177
282
  - `swipium://project/{projectId}/app-map`: the full app map.
178
283
  - `swipium://project/{projectId}/app-map/{kind}/{id}`: one feature, screen or test-suite section.
179
284
 
180
- `resources/list` shows only the current client's project roots (its MCP roots plus roots of
181
- sessions in this server process), never lists sensitive-mode sessions, and is capped at 100
182
- entries per template, with the cap stated on the last entry. Anything not listed can still be
183
- read by URI. Clients without resource support use `qa_get_artifact` and `qa_app_map_read`.
285
+ `resources/list` shows only the current client's projects: the client's MCP roots (2025-era
286
+ connections only) plus the roots of sessions used in this server process. It never lists
287
+ sensitive-mode sessions, and it's capped at 100 entries per template, with the cap stated on the
288
+ last entry. Anything not listed can still be read by URI. Clients without resource support use
289
+ `qa_get_artifact` and `qa_app_map_read`.
184
290
 
185
291
  ## Server behavior
186
292
 
187
- - **Response modes.** Pass `responseMode: "compact" | "normal" | "verbose"` on `qa_start_session`
188
- or `qa_test_this`, and every later call in that session uses it. `compact` shortens only the
189
- text channel to a summary plus URIs. `structuredContent` always carries the full payload.
190
- - **Artifacts.** Evidence is stored under `~/.swipium/runs/` and returned as `swipium://` URIs.
191
- `qa_get_artifact` returns metadata for images by default. Pass `mode: "inline"` only when you
192
- need the pixels.
193
- - **Unknown arguments are rejected.** A top-level argument a tool doesn't declare returns
194
- `INVALID_ARGUMENT` with the accepted parameter list, and nothing runs. Swipium doesn't silently
195
- drop it.
196
- - **Stale clients.** A call to a tool removed in 2.0 (`qa_agent_brief`, `qa_capabilities`,
197
- `qa_next_best_action`, `qa_detect_context`, `qa_plan`, `qa_assert_visual`), or a legacy call
198
- shape (`qa_ios` `wda_*` or `screenshot` actions, `qa_wait for:"job_done"`), returns
199
- `STALE_CLIENT` with the replacement call and a hint to restart the client. `qa_doctor` accepts
200
- `expectedVersion`, `expectedToolCount` and `expectedSchemaHash` and reports a mismatch.
201
- - **Consent.** Privileged actions (builds, boots, installs, data wipes and similar) return
202
- `requiresConsent` with a `consentId` instead of running. On clients that support MCP elicitation,
203
- Swipium asks the user directly. How consent works, and what each outcome returns, is in
204
- [concepts.md](concepts.md#consent); the security reasoning is in
205
- [THREAT_MODEL.md](../THREAT_MODEL.md).
206
- - **Startup and shutdown.** The version and tool count are logged to stderr at startup, and
207
- processes left behind by a crashed earlier server are reaped in the background. On shutdown or
208
- client disconnect, Swipium restores changed network state and stops screen recorders and Metro.
209
- Managed WebDriverAgent keeps running so the next server can reuse it; `qa_wda stop` stops it.
210
-
211
- ## Scope
212
-
213
- Swipium supports the Android Emulator and the iOS Simulator, with optional WebDriverAgent for
214
- structured iOS automation. Swipium never acts on a physical device. See
215
- [physical-devices.md](physical-devices.md).
293
+ ### What the model sees
294
+
295
+ Every tool result has a text block and `structuredContent`. Claude Code and Codex give the model
296
+ the `structuredContent` JSON, not the text, for successful results (Codex does it for every result
297
+ that has `structuredContent`; Claude Code shows the text only for errors). So successful results
298
+ carry:
299
+
300
+ - `summary`: the summary's first line (its headline). Results whose later lines say something the
301
+ payload doesn't (for example `qa_job_status` with the job's result text) keep the whole summary.
302
+ - `next`: next-step guidance ("Next: ...", "Call qa_report ..."), a list where each entry starts
303
+ with the tool to call. It's left out when the payload already has `nextBestAction`, `nextAction`,
304
+ `nextRecommendedAction` or `nextSteps`. Budget stops always carry `next: ["qa_report ..."]`.
305
+
306
+ Errors carry `what` and `nextSteps`. The text block stays as a plain-text copy for clients that
307
+ read it.
308
+
309
+ `responseMode` (`compact`, `normal`, `verbose`, set per session) mostly changes the text block, so
310
+ on Claude Code and Codex it makes little difference, with one exception: element lists. Outside
311
+ `verbose`, the `elements` of `qa_snapshot` and `qa_act` are compact one-line strings such as
312
+ `@e3 [button] "Log in" #login_btn [40,200][1040,245]`, about 40% smaller than one JSON object per
313
+ element; `verbose` returns the objects. Details: [Response modes](tools.md#response-modes) and
314
+ [Element lines](tools.md#element-lines).
315
+
316
+ ### Every call returns within about 50 s
317
+
318
+ Client tool timeouts are short (Codex defaults to 60 s), so calls that wait are capped below that,
319
+ and long work runs as a background job:
320
+
321
+ - `qa_job_status` long-polls at most 50 s (`waitMs`, recommended 45000). Larger values are clamped,
322
+ not rejected.
323
+ - `qa_wait`, `qa_act` and `qa_test_this { waitForCompletion: true }` clamp `timeoutMs` to 50000
324
+ with a note (`qa_wait` and `qa_test_this` default to 45000).
325
+ - `qa_wda build` runs as a job. `qa_wda start` waits at most 45 s, then returns
326
+ `status: "starting"`; poll `qa_wait { for: "wda_ready" }` until WebDriverAgent is up.
327
+ - `qa_ios { action: "boot" }` waits at most 40 s for an iOS Simulator. A slower cold boot returns
328
+ `status: "booting"` with the simulator already bound; poll `qa_wait { for: "simulator_booted" }`.
329
+ `qa_prepare_ios_target` waits at most 30 s, then hands the rest (boot, install, launch) to a job
330
+ and returns `status: "booting"` with its `jobId`.
331
+ - Builds, test runs, exploration, Android boot and install steps, and the device preparation of
332
+ `qa_test_feature` are jobs too (see [Jobs](concepts.md#jobs)).
333
+
334
+ A few synchronous steps can still take longer: a large app install (`qa_ios install`, or
335
+ `qa_prepare_ios_target` on a simulator that is already booted), `qa_flow_run` and `qa_smoke` with
336
+ long saved flows, `qa_mobile_audit`, and `qa_generate { target: "appium" }` when it bootstraps a
337
+ device. Keep the client's tool timeout generous (Codex `tool_timeout_sec = 600`).
338
+
339
+ ### Cancellation
340
+
341
+ When the client cancels a call (`notifications/cancelled`), the call's own work stops: waits,
342
+ long-polls, UI settling, boot waits and WebDriverAgent startup all end early, and the result is
343
+ `CANCELLED`. Cancelling a call never cancels a background job (use `qa_job_cancel`). The rules,
344
+ including what is and isn't rolled back, are in [Cancellation](concepts.md#cancellation).
345
+
346
+ ### Resources and artifacts
347
+
348
+ Evidence is stored under `~/.swipium/runs/` and returned as `swipium://` URIs.
349
+
350
+ - `qa_get_artifact` returns metadata (URI, MIME type, size, local path) for images, recordings and
351
+ other non-text artifacts by default. Pass `mode: "inline"` only when you need the bytes.
352
+ - `resources/read` and `qa_get_artifact` share the same size caps. Text over 1 MB returns the first
353
+ 1 MB (the last 1 MB for logs, including `*.log` files such as Metro and WDA logs) with a
354
+ `[swipium: truncated ...]` marker naming the local file to read for the rest. Binaries over 8 MB
355
+ aren't inlined; you get a note with the size and local path.
356
+ - A URI that matches a Swipium template but names nothing that exists (unknown artifact, a project
357
+ without an app map, an unknown section) fails with JSON-RPC error `-32602` and the URI in
358
+ `error.data.uri`. Swipium 2.0 sent a generic `-32603` (internal error); the spec asks clients to
359
+ accept the older `-32002` too.
360
+
361
+ ### Invalid arguments and stale clients
362
+
363
+ - An argument a tool doesn't declare, a missing required argument or a wrong type returns
364
+ `INVALID_ARGUMENT` with the accepted parameter list, and nothing runs. Swipium never silently
365
+ drops an argument. Validation failures carry a per-field `what` (for example `uri: Required`) and
366
+ `invalidArguments`.
367
+ - An unknown tool name returns an `isError` result ("Tool ... not found").
368
+ - Errors never echo caller input at full size: argument names and paths are cut at 100 characters
369
+ (20 listed at most), `what` keeps its first and last part around a marker, every string is
370
+ capped, and an error still over 64 KB drops its extra fields (`extraDropped` names them).
371
+ - A call to a tool removed in 2.0, or a legacy call shape, returns `STALE_CLIENT` with the
372
+ replacement call and a hint to restart the client. `qa_doctor` accepts `expectedVersion`,
373
+ `expectedToolCount` and `expectedSchemaHash` and reports a mismatch. See
374
+ [tools.md](tools.md#unknown-arguments-and-stale-clients).
375
+
376
+ ### Consent prompts
377
+
378
+ Privileged actions (builds, boots, installs, data wipes and similar) need consent. When the client
379
+ supports MCP form elicitation, Swipium asks the user directly and the model never sees a
380
+ `consentId`:
381
+
382
+ - 2025-era: an `elicitation/create` request while the tool call waits.
383
+ - 2026-07-28: an `InputRequiredResult` with the same form and a single-use `requestState`, sent
384
+ only when the request's `_meta` client capabilities declare form elicitation. A `requestState`
385
+ that is forged, reused or presented on another tool call fails with JSON-RPC `-32602`
386
+ (`Invalid or expired requestState`) and nothing runs.
387
+
388
+ Otherwise the tool returns a `requiresConsent` envelope for the agent to relay. Operator
389
+ pre-approval is checked first in both eras. The full model (mechanisms, outcomes, rules) is in
390
+ [concepts.md](concepts.md#consent); the security reasoning is in
391
+ [THREAT_MODEL.md](../THREAT_MODEL.md).
392
+
393
+ ### Startup and shutdown
394
+
395
+ At startup the version and tool count are logged to stderr, and processes left behind by a crashed
396
+ earlier server are reaped in the background.
397
+
398
+ The server shuts down when the client closes stdin (even in the middle of a long call), on
399
+ `SIGINT` or `SIGTERM`, or when the transport closes. It then cancels running jobs, restores network
400
+ state it changed, and stops screen recorders and Metro. Managed WebDriverAgent keeps running so the
401
+ next server can reuse it; `qa_wda stop` stops it.
216
402
 
217
403
  ## Verification
218
404
 
@@ -221,13 +407,30 @@ swipium verify
221
407
  ```
222
408
 
223
409
  This starts a Swipium server over stdio, checks that every tool and prompt this version declares is
224
- listed, prints their names, count and schema hash, and calls `qa_doctor`. It exits with status 1 if a tool is missing
225
- or `qa_doctor` errors. It starts the copy of Swipium you ran it with, not the command your client is
226
- configured with.
410
+ listed, prints their names, count and schema hash, and calls `qa_doctor`. It exits with status 1 if
411
+ a tool is missing or `qa_doctor` errors. It starts the copy of Swipium you ran it with, not the
412
+ command your client is configured with.
227
413
 
228
414
  Inside the client, call `qa_doctor`. It checks both platforms by default on macOS (ready if either
229
415
  one is) and Android elsewhere. Pass `platform: "android" | "ios" | "both"` to be explicit.
230
416
 
417
+ ## Debugging
418
+
419
+ Swipium logs JSON lines to stderr only (stdout carries the MCP stream). Most clients show server
420
+ stderr in their MCP logs. Set `SWIPIUM_LOG_LEVEL` in the server `env` to change how much it writes
421
+ (an unknown value falls back to `info`):
422
+
423
+ | Value | Writes |
424
+ | --- | --- |
425
+ | `error` | Errors only. |
426
+ | `warn` | Errors and warnings, including every operator pre-approved consent. |
427
+ | `info` (default) | Startup, shutdown, and notable events. |
428
+ | `debug` | Everything above, plus one `tool call` line per call and the protocol era of each connection. |
429
+
430
+ A `tool call` line carries `tool`, `sessionId` (when the call has one), `durationMs`, `isError`,
431
+ `failureCode` (on errors) and `cancelled`. Argument values are never logged, since they can hold
432
+ secrets.
433
+
231
434
  ## Troubleshooting
232
435
 
233
436
  | Symptom | Fix |
@@ -235,9 +438,11 @@ one is) and Android elsewhere. Pass `platform: "android" | "ios" | "both"` to be
235
438
  | The client lists fewer or different tools than `swipium verify`, or calls return `STALE_CLIENT` | The client is still running a server it started before the upgrade. Restart the client, or reload its MCP server list. |
236
439
  | `PROJECT_ROOT_UNRESOLVED` | Pass an absolute `projectRoot`, or set `SWIPIUM_PROJECT_ROOT` in the server `env` (needed on Claude Desktop and Windsurf). |
237
440
  | The server times out on first start (Codex, Gemini) | The first `npx` run downloads the package. Raise the startup timeout (Codex `startup_timeout_sec = 30`), or install globally and point the client at `swipium`. |
238
- | Long tool calls time out | Raise the client's tool timeout (Codex `tool_timeout_sec = 600`, Gemini `timeout: 600000`). For builds and runs, prefer `qa_test_this { mode: "execute" }` plus `qa_job_status` polling. |
441
+ | Long tool calls time out | Raise the client's tool timeout (Codex `tool_timeout_sec = 600`, Gemini `timeout: 600000`). For builds and runs, use `qa_test_this { mode: "execute" }` and poll `qa_job_status`. |
442
+ | `CONSENT_DECLINED` or `CONSENT_CANCELLED` with `likelyAutomatic: true` | A headless client (`codex exec`, `claude -p`) answered the prompt. See [Headless runs](#headless-runs). |
239
443
  | `adb` or `emulator` not found from a GUI client | Set `ANDROID_HOME` in the server `env`, or install the SDK in its default location. See [How Android tools are found](#how-android-tools-are-found). |
240
444
  | `INVALID_ARGUMENT` listing accepted parameters | Remove the undeclared argument. If the tool list looks outdated, restart the client. |
445
+ | Codex: test credentials, `ANDROID_HOME` or `JAVA_HOME` ignored | Codex only passes a fixed env whitelist to MCP servers. List the names in `env_vars` under `[mcp_servers.swipium]` (see [Codex](#codex)); `qa_doctor` prints the line. |
241
446
  | Tools missing in Codex Desktop threads | Known Codex Desktop issue ([openai/codex#19425](https://github.com/openai/codex/issues/19425)). Use the Codex CLI. |
242
447
  | `swipium init cursor --apply` or `init vscode --apply` exits with status 2 | The existing file isn't plain JSON. Add the printed entry by hand. |
243
448
  | `PHYSICAL_DEVICE_UNSUPPORTED` | A phone was the only device, or was requested explicitly. Start an emulator or simulator. See [physical-devices.md](physical-devices.md). |
@@ -1,9 +1,9 @@
1
1
  # Physical devices
2
2
 
3
- Status: **not supported in Swipium 2.0.** Swipium runs only on Android Emulators and iOS
4
- Simulators. A physical device is visible to Swipium, but Swipium never installs on it, launches on it
5
- or drives it. The policy is server-side; a client cannot opt in by passing a flag. This page
6
- describes the current behavior and what support would need.
3
+ Status: **not supported.** Swipium runs only on Android Emulators and iOS Simulators. A physical
4
+ device is visible to Swipium, but Swipium never installs on it, launches on it or drives it. The
5
+ policy is server-side; a client can't opt in by passing a flag. This page describes the current
6
+ behavior and what support would need.
7
7
 
8
8
  ## The refusal rule
9
9