ios-mcp 0.1.0__tar.gz
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.
- ios_mcp-0.1.0/.env.example +102 -0
- ios_mcp-0.1.0/.gitignore +23 -0
- ios_mcp-0.1.0/ARCHITECTURE.md +212 -0
- ios_mcp-0.1.0/LICENSE +21 -0
- ios_mcp-0.1.0/PKG-INFO +336 -0
- ios_mcp-0.1.0/README.md +302 -0
- ios_mcp-0.1.0/SAFETY.md +110 -0
- ios_mcp-0.1.0/SECURITY.md +47 -0
- ios_mcp-0.1.0/agent/README.md +93 -0
- ios_mcp-0.1.0/agent/pyproject.toml +35 -0
- ios_mcp-0.1.0/docs/adr/0001-the-agent-is-a-peer-of-the-server.md +71 -0
- ios_mcp-0.1.0/docs/adr/0002-no-explicit-planner.md +98 -0
- ios_mcp-0.1.0/docs/adr/0003-no-cross-session-memory.md +107 -0
- ios_mcp-0.1.0/docs/adr/0004-no-subagents.md +107 -0
- ios_mcp-0.1.0/docs/adr/0005-langgraph-core-not-deepagents.md +72 -0
- ios_mcp-0.1.0/docs/adr/0006-the-agent-calls-iossession-directly.md +81 -0
- ios_mcp-0.1.0/docs/adr/0007-report-unreachable-content-rather-than-reaching-it.md +86 -0
- ios_mcp-0.1.0/docs/adr/0008-the-front-end-is-a-third-distribution.md +101 -0
- ios_mcp-0.1.0/docs/adr/0009-the-eval-trend-is-committed.md +81 -0
- ios_mcp-0.1.0/docs/adr/README.md +25 -0
- ios_mcp-0.1.0/docs/real-device-setup.md +194 -0
- ios_mcp-0.1.0/docs/tool-reference.md +96 -0
- ios_mcp-0.1.0/ios_mcp/__init__.py +0 -0
- ios_mcp-0.1.0/ios_mcp/__main__.py +96 -0
- ios_mcp-0.1.0/ios_mcp/actions/__init__.py +0 -0
- ios_mcp-0.1.0/ios_mcp/actions/idempotency.py +58 -0
- ios_mcp-0.1.0/ios_mcp/actions/result.py +128 -0
- ios_mcp-0.1.0/ios_mcp/actions/stabilize.py +88 -0
- ios_mcp-0.1.0/ios_mcp/config.py +278 -0
- ios_mcp-0.1.0/ios_mcp/devices/__init__.py +0 -0
- ios_mcp-0.1.0/ios_mcp/devices/base.py +143 -0
- ios_mcp-0.1.0/ios_mcp/devices/devicectl.py +126 -0
- ios_mcp-0.1.0/ios_mcp/devices/discovery.py +253 -0
- ios_mcp-0.1.0/ios_mcp/devices/doctor.py +587 -0
- ios_mcp-0.1.0/ios_mcp/devices/pool.py +215 -0
- ios_mcp-0.1.0/ios_mcp/devices/ports.py +45 -0
- ios_mcp-0.1.0/ios_mcp/devices/real_device.py +468 -0
- ios_mcp-0.1.0/ios_mcp/devices/shell.py +79 -0
- ios_mcp-0.1.0/ios_mcp/devices/simulator.py +348 -0
- ios_mcp-0.1.0/ios_mcp/devices/tunnel.py +75 -0
- ios_mcp-0.1.0/ios_mcp/errors.py +211 -0
- ios_mcp-0.1.0/ios_mcp/perception/__init__.py +0 -0
- ios_mcp-0.1.0/ios_mcp/perception/digest.py +897 -0
- ios_mcp-0.1.0/ios_mcp/perception/refs.py +92 -0
- ios_mcp-0.1.0/ios_mcp/perception/resolve.py +280 -0
- ios_mcp-0.1.0/ios_mcp/perception/roles.py +204 -0
- ios_mcp-0.1.0/ios_mcp/perception/vision.py +85 -0
- ios_mcp-0.1.0/ios_mcp/policy/__init__.py +0 -0
- ios_mcp-0.1.0/ios_mcp/policy/audit.py +110 -0
- ios_mcp-0.1.0/ios_mcp/policy/faults.py +126 -0
- ios_mcp-0.1.0/ios_mcp/policy/gate.py +169 -0
- ios_mcp-0.1.0/ios_mcp/policy/redact.py +55 -0
- ios_mcp-0.1.0/ios_mcp/policy/secrets.py +56 -0
- ios_mcp-0.1.0/ios_mcp/py.typed +0 -0
- ios_mcp-0.1.0/ios_mcp/server/__init__.py +0 -0
- ios_mcp-0.1.0/ios_mcp/server/annotations.py +39 -0
- ios_mcp-0.1.0/ios_mcp/server/app.py +67 -0
- ios_mcp-0.1.0/ios_mcp/server/context.py +88 -0
- ios_mcp-0.1.0/ios_mcp/server/errors.py +37 -0
- ios_mcp-0.1.0/ios_mcp/server/resources.py +124 -0
- ios_mcp-0.1.0/ios_mcp/server/tools_act.py +252 -0
- ios_mcp-0.1.0/ios_mcp/server/tools_app.py +139 -0
- ios_mcp-0.1.0/ios_mcp/server/tools_env.py +100 -0
- ios_mcp-0.1.0/ios_mcp/server/tools_perceive.py +152 -0
- ios_mcp-0.1.0/ios_mcp/server/tools_session.py +54 -0
- ios_mcp-0.1.0/ios_mcp/session.py +949 -0
- ios_mcp-0.1.0/ios_mcp/wda/__init__.py +0 -0
- ios_mcp-0.1.0/ios_mcp/wda/client.py +271 -0
- ios_mcp-0.1.0/ios_mcp/wda/models.py +153 -0
- ios_mcp-0.1.0/ios_mcp/wda/session.py +393 -0
- ios_mcp-0.1.0/pyproject.toml +149 -0
- ios_mcp-0.1.0/scripts/prepare_wda.sh +205 -0
- ios_mcp-0.1.0/scripts/start_tunnel.sh +38 -0
- ios_mcp-0.1.0/server.json +46 -0
- ios_mcp-0.1.0/tui/README.md +152 -0
- ios_mcp-0.1.0/tui/pyproject.toml +33 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copy to `.env` and edit. `.env` is gitignored; this file is not.
|
|
2
|
+
#
|
|
3
|
+
# Precedence, highest first: a real environment variable, then `.env`, then
|
|
4
|
+
# `ios-mcp.toml`, then the defaults in code. So a shell export or CI setting
|
|
5
|
+
# overrides this file without anyone editing it.
|
|
6
|
+
#
|
|
7
|
+
# Nested settings use a double underscore: IOS_MCP_<SECTION>__<FIELD>.
|
|
8
|
+
# Every value below is the default unless noted, so an untouched copy of this
|
|
9
|
+
# file changes nothing.
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Model provider (agent only). The base install commits to no vendor; install
|
|
13
|
+
# the matching extra, e.g. `uv sync --extra openai`.
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
IOS_AGENT_PROVIDER=anthropic
|
|
16
|
+
IOS_AGENT_MODEL=claude-opus-5
|
|
17
|
+
IOS_AGENT_MAX_TOKENS=16000
|
|
18
|
+
# Anthropic only, sent as output_config. Skipped for every other provider.
|
|
19
|
+
IOS_AGENT_EFFORT=medium
|
|
20
|
+
# Deliberately unset. Claude Opus 5, Opus 4.8, Opus 4.7 and Sonnet 5 reject it
|
|
21
|
+
# with a 400; set it only on a provider that accepts it.
|
|
22
|
+
# IOS_AGENT_TEMPERATURE=0.7
|
|
23
|
+
# Turns before the loop gives up on a goal.
|
|
24
|
+
IOS_AGENT_MAX_STEPS=24
|
|
25
|
+
# Anything this project has not heard of, passed to the provider untouched.
|
|
26
|
+
# Applied last, so it overrides everything above. JSON.
|
|
27
|
+
# IOS_AGENT_EXTRA={"reasoning_effort": "high"}
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Provider API keys are NOT read by this project. Each vendor SDK resolves its
|
|
31
|
+
# own, and it does so better than we could: the Anthropic SDK also accepts an
|
|
32
|
+
# auth token or an `ant auth login` profile. Listed here only so the full set
|
|
33
|
+
# of knobs is in one place.
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# ANTHROPIC_API_KEY=
|
|
36
|
+
# OPENAI_API_KEY=
|
|
37
|
+
# GOOGLE_API_KEY=
|
|
38
|
+
# GROQ_API_KEY=
|
|
39
|
+
# (ollama runs locally and needs no key)
|
|
40
|
+
|
|
41
|
+
# What the eval suite prices tokens at, per million. Defaults are Claude
|
|
42
|
+
# Opus 5's rates; nothing here can know what another vendor charges.
|
|
43
|
+
# IOS_AGENT_USD_PER_MTOK_IN=5.0
|
|
44
|
+
# IOS_AGENT_USD_PER_MTOK_OUT=25.0
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Safety. Read SAFETY.md before turning any of this off. Doing so is a
|
|
48
|
+
# reasonable choice for a simulator running tests, and is not a reasonable one
|
|
49
|
+
# for a device carrying someone's real accounts.
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
IOS_MCP_POLICY__ENABLED=true
|
|
52
|
+
IOS_MCP_POLICY__CONFIRM_DESTRUCTIVE=true
|
|
53
|
+
IOS_MCP_POLICY__MAX_CONSECUTIVE_FAILURES=5
|
|
54
|
+
IOS_MCP_POLICY__LOOP_DETECTION_WINDOW=6
|
|
55
|
+
# Empty means "anything not blocked". Set it to restrict the agent to one app.
|
|
56
|
+
# IOS_MCP_POLICY__APP_ALLOWLIST=["com.apple.Preferences"]
|
|
57
|
+
|
|
58
|
+
# Secrets are referenced, never inlined. `ios_type_secret` resolves a reference
|
|
59
|
+
# from the macOS keychain first, falling back to IOS_MCP_SECRET_<REF_UPPER>.
|
|
60
|
+
# The value never reaches a prompt, a tool result, or the audit trail.
|
|
61
|
+
# security add-generic-password -s ios-mcp -a icloud-password -w
|
|
62
|
+
# IOS_MCP_SECRET_ICLOUD_PASSWORD=
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Device and WebDriverAgent
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# UDID or a name substring. Unset picks the best ready device, preferring a
|
|
68
|
+
# simulator: acting on a real phone should be deliberate.
|
|
69
|
+
# IOS_MCP_DEFAULT_DEVICE=
|
|
70
|
+
# Point at a WebDriverAgent someone else is running (a device farm, a phone on
|
|
71
|
+
# Wi-Fi, a runner started by hand). When set, nothing is launched or torn down.
|
|
72
|
+
# IOS_MCP_WDA__BASE_URL=
|
|
73
|
+
# Free Apple accounts cannot sign com.facebook.*, so the runner bundle id is
|
|
74
|
+
# configurable for exactly that reason.
|
|
75
|
+
# IOS_MCP_WDA__BUNDLE_ID=com.facebook.WebDriverAgentRunner.xctrunner
|
|
76
|
+
# A physical device needs a longer startup budget than a simulator.
|
|
77
|
+
# IOS_MCP_WDA__STARTUP_TIMEOUT_S=90
|
|
78
|
+
|
|
79
|
+
# `simctl boot` starts the runtime, not the window: without this a simulator
|
|
80
|
+
# runs headlessly and nothing appears on the Mac. Turn it off for CI, where a
|
|
81
|
+
# window is at best pointless.
|
|
82
|
+
IOS_MCP_SIMULATOR__SHOW_WINDOW=true
|
|
83
|
+
|
|
84
|
+
# A device snapshot costs about 3.7s against well under a second on a
|
|
85
|
+
# simulator, so max_wait_s must exceed stable_samples snapshots or a real
|
|
86
|
+
# device times out on every action.
|
|
87
|
+
# IOS_MCP_STABILIZE__MAX_WAIT_S=20
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Perception. These decide what an agent loop costs.
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
IOS_MCP_DIGEST__TOKEN_BUDGET=1500
|
|
93
|
+
# Depth 20 silently loses a third of a Settings screen; past 30 is free,
|
|
94
|
+
# because the round trip dominates rather than the traversal.
|
|
95
|
+
IOS_MCP_SNAPSHOT__MAX_DEPTH=50
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Server transport
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
IOS_MCP_SERVER__TRANSPORT=stdio
|
|
101
|
+
# IOS_MCP_SERVER__PORT=8765
|
|
102
|
+
IOS_MCP_LOG_LEVEL=INFO
|
ios_mcp-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.coverage
|
|
11
|
+
htmlcov/
|
|
12
|
+
.env
|
|
13
|
+
.artifacts/
|
|
14
|
+
vendor/wda/*.app/
|
|
15
|
+
vendor/wda/*.ipa
|
|
16
|
+
|
|
17
|
+
# WebDriverAgent checkout and build products (rebuild with scripts/prepare_wda.sh)
|
|
18
|
+
vendor/wda/WebDriverAgent/
|
|
19
|
+
vendor/wda/DerivedData/
|
|
20
|
+
vendor/wda/*.xctestrun
|
|
21
|
+
|
|
22
|
+
# Personal, machine-local Claude Code settings
|
|
23
|
+
.claude/settings.local.json
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Six layers. Layers 1 to 4 are a plain async library with no MCP imports, so an
|
|
4
|
+
agent framework can import `IosSession` directly and skip the protocol
|
|
5
|
+
round-trip on latency-critical steps. The MCP server is one consumer of that
|
|
6
|
+
library, not the thing itself, and `ios_agent` is a second one: a peer of the
|
|
7
|
+
server rather than a layer above it, since the policy gate lives inside
|
|
8
|
+
`IosSession` and both pass through it identically. `ios_tui` is a third
|
|
9
|
+
distribution on top of the agent, and it is separate for a concrete reason
|
|
10
|
+
rather than a tidy one: the agent is held to a seven-module public surface, and
|
|
11
|
+
a front end needs device discovery and the doctor, which are outside it. See
|
|
12
|
+
`docs/adr/0008-the-front-end-is-a-third-distribution.md`.
|
|
13
|
+
`tests/unit/test_layering.py` enforces the no-MCP-imports rule, the agent's
|
|
14
|
+
public surface, and the front end's Textual-free event path statically.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
ios_tui <- terminal front end
|
|
18
|
+
|
|
|
19
|
+
MCP clients (Claude Code, Claude Desktop, an iOS app) ios_agent
|
|
20
|
+
| |
|
|
21
|
+
v |
|
|
22
|
+
5 mcp FastMCP: tools, resources, prompts, transports | <- only package importing MCP
|
|
23
|
+
| |
|
|
24
|
+
+---------------------+--------------------+
|
|
25
|
+
v
|
|
26
|
+
4 actions act, stabilize, re-observe; idempotency keys (IosSession)
|
|
27
|
+
3 perception accessibility tree -> UI Digest, stable refs, resolution <- most of the value
|
|
28
|
+
2 wda typed WebDriverAgent client, session settings, auto-heal
|
|
29
|
+
1 devices SimulatorAdapter | RealDeviceAdapter, device pool
|
|
30
|
+
|
|
|
31
|
+
iOS Simulator iPhone (USB / Wi-Fi)
|
|
32
|
+
|
|
33
|
+
6 policy approval gate, secret injection, redaction, audit, kill switch
|
|
34
|
+
constructed inside IosSession, so every action passes through
|
|
35
|
+
it whichever consumer started the call
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 1. Device fabric (`ios_mcp/devices/`)
|
|
39
|
+
|
|
40
|
+
One `DeviceAdapter` protocol, two implementations, so nothing above branches on
|
|
41
|
+
device kind.
|
|
42
|
+
|
|
43
|
+
`SimulatorAdapter` uses `simctl` for what WebDriverAgent cannot do: privacy
|
|
44
|
+
permissions, location, appearance, freezing the status bar for reproducible
|
|
45
|
+
screenshots. `RealDeviceAdapter` uses `go-ios`, including the RemoteXPC tunnel
|
|
46
|
+
that iOS 17+ requires.
|
|
47
|
+
|
|
48
|
+
Starting WebDriverAgent differs fundamentally between the two, and this is not
|
|
49
|
+
a detail that can be abstracted away:
|
|
50
|
+
|
|
51
|
+
- **Simulator**: must go through `xcodebuild test-without-building` against a
|
|
52
|
+
prebuilt `.xctestrun`. `simctl launch` on the `.xctrunner` app cannot work,
|
|
53
|
+
because the app aborts immediately without an XCTestConfiguration that only
|
|
54
|
+
`testmanagerd` supplies.
|
|
55
|
+
- **Device**: `go-ios` drives `testmanagerd` directly, so a signed prebuilt
|
|
56
|
+
runner app can be launched without Xcode.
|
|
57
|
+
|
|
58
|
+
`DevicePool` keeps one live session per device, allocates ports, and reclaims
|
|
59
|
+
idle leases. It is where a remote or device-farm adapter would drop in.
|
|
60
|
+
|
|
61
|
+
## 2. WebDriverAgent client (`ios_mcp/wda/`)
|
|
62
|
+
|
|
63
|
+
A typed `httpx` client. No Appium.
|
|
64
|
+
|
|
65
|
+
Two things happen here that everything above depends on. **Session settings**
|
|
66
|
+
are pushed on every new session: WebDriverAgent's default snapshot depth
|
|
67
|
+
produces trees far too large and slow for an agent loop. **Auto-heal** catches
|
|
68
|
+
a dead session or a crashed runner, rebuilds it, restores the foreground app,
|
|
69
|
+
and retries once, reporting `recovered` rather than raising. An agent driving a
|
|
70
|
+
phone for minutes will outlive at least one runner crash, and making it reason
|
|
71
|
+
about that wastes its turns.
|
|
72
|
+
|
|
73
|
+
## 3. Perception (`ios_mcp/perception/`)
|
|
74
|
+
|
|
75
|
+
Where the cost of the whole system is decided. Raw page source for a 200-row
|
|
76
|
+
list is roughly 37,000 tokens; the digest is 329.
|
|
77
|
+
|
|
78
|
+
The pipeline is prune, deduplicate, collapse repeats, score, budget, assign
|
|
79
|
+
refs. Two rules do most of the work:
|
|
80
|
+
|
|
81
|
+
- **Wrapper collapse.** Containers that exist only to hold other nodes are
|
|
82
|
+
dropped, as are StaticText children echoing their row's own label. iOS emits
|
|
83
|
+
that echo for nearly every list row.
|
|
84
|
+
- **Coincidence merge.** iOS reports one control several times: a row-wide
|
|
85
|
+
element carrying the label, and the control itself at the trailing edge.
|
|
86
|
+
These merge into one node that takes *semantics from the labelled node and
|
|
87
|
+
geometry from the tighter one*. That split is not cosmetic. Keeping the row's
|
|
88
|
+
rect aims taps at the label, where a switch ignores them, so `set_value`
|
|
89
|
+
reports success while changing nothing.
|
|
90
|
+
|
|
91
|
+
Overlap is proportional rather than strict containment, because real iOS rects
|
|
92
|
+
do not nest: a Settings toggle reports `x=305 w=63` inside a row at `x=36
|
|
93
|
+
w=330`, overhanging its own parent by two points.
|
|
94
|
+
|
|
95
|
+
**Resolution** runs a six-tier chain server-side, so a retry costs zero model
|
|
96
|
+
tokens where bouncing back to the model costs a whole turn:
|
|
97
|
+
|
|
98
|
+
| Tier | How |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `exact` | ref from the last digest, verified to still denote the same element |
|
|
101
|
+
| `id` | stale ref, re-found by accessibility identifier |
|
|
102
|
+
| `label+role` | stale ref, re-found by label and role |
|
|
103
|
+
| `proximity` | stale ref, nearest same-role element |
|
|
104
|
+
| `text-exact` / `id-exact` | plain-language target, exact match |
|
|
105
|
+
| `text-partial` / `text-fuzzy` | substring, then edit distance |
|
|
106
|
+
|
|
107
|
+
Refs are positional, so inserting a row shifts every ref below it. Tier 1
|
|
108
|
+
therefore verifies identity before trusting position; acting on the wrong
|
|
109
|
+
control is the worst failure this system can have. `RefTable` records only what
|
|
110
|
+
the agent was actually shown, which is what makes that check possible.
|
|
111
|
+
|
|
112
|
+
Ambiguity is refused, not guessed: two identically labelled Delete buttons
|
|
113
|
+
raise with both candidates listed.
|
|
114
|
+
|
|
115
|
+
**Fingerprints** hash structure, state and title. Positions round to 4px so
|
|
116
|
+
animation jitter does not register while real shifts do. The title is included
|
|
117
|
+
because a split view keeps most of the screen identical during navigation.
|
|
118
|
+
|
|
119
|
+
## 4. Actions (`ios_mcp/actions/`, `ios_mcp/session.py`)
|
|
120
|
+
|
|
121
|
+
Every action is act, stabilize, re-observe in one call. An observe/act/observe
|
|
122
|
+
loop costs two round-trips per step; folding the observation in halves that.
|
|
123
|
+
|
|
124
|
+
- **Fresh geometry.** Every action re-reads the screen before acting rather
|
|
125
|
+
than trusting the last observation. Seconds of model latency sit between
|
|
126
|
+
observe and act, and anything that moved in between would send the tap to
|
|
127
|
+
whatever now occupies those coordinates.
|
|
128
|
+
- **Stabilization** polls the fingerprint until it repeats, rather than
|
|
129
|
+
sleeping a fixed interval. An optional baseline keeps it polling while the
|
|
130
|
+
screen still matches its pre-action state, so a slow transition is not
|
|
131
|
+
mistaken for an action that did nothing.
|
|
132
|
+
- **Deltas.** When the screen is structurally similar the result is a diff
|
|
133
|
+
(`~ e2 switch "Bold Text" =1 (was 0)`); a genuine navigation returns the
|
|
134
|
+
whole screen.
|
|
135
|
+
- **Idempotency keys** make a repeat a no-op returning the original result.
|
|
136
|
+
This exists from the first action because agent frameworks re-run the node an
|
|
137
|
+
interrupt was raised from, and retrofitting act-once semantics after callers
|
|
138
|
+
exist is not safely possible.
|
|
139
|
+
|
|
140
|
+
## 5. MCP surface (`ios_mcp/server/`)
|
|
141
|
+
|
|
142
|
+
Roughly 30 semantic tools, not a mirror of WebDriverAgent's HTTP routes: a
|
|
143
|
+
large set of confusable tools measurably degrades tool selection. A test
|
|
144
|
+
asserts the count stays bounded.
|
|
145
|
+
|
|
146
|
+
Digest payloads omit the structured element list by default. It duplicates the
|
|
147
|
+
rendered text at roughly twice the tokens, and both would be pushed into the
|
|
148
|
+
model's context, so sending both means paying twice for one screen.
|
|
149
|
+
|
|
150
|
+
Errors reach the client as structured JSON carrying a code, a hint, and
|
|
151
|
+
candidate elements, so a failed resolution tells the agent what it could have
|
|
152
|
+
picked instead.
|
|
153
|
+
|
|
154
|
+
## 6. Policy (`ios_mcp/policy/`)
|
|
155
|
+
|
|
156
|
+
See [SAFETY.md](SAFETY.md).
|
|
157
|
+
|
|
158
|
+
## The agent (`agent/ios_agent/`)
|
|
159
|
+
|
|
160
|
+
A separate distribution in the uv workspace, depending on `ios-mcp` and never
|
|
161
|
+
the reverse. It is a second consumer of `IosSession`, not a seventh layer: the
|
|
162
|
+
policy gate is constructed inside the session, so an agent passes through it on
|
|
163
|
+
the same code path the server does.
|
|
164
|
+
|
|
165
|
+
The loop is one model node, one tool node, and an edge back. Eight tools rather
|
|
166
|
+
than the server's thirty, because a large confusable set degrades tool
|
|
167
|
+
selection. The provider is chosen through `init_chat_model`, so the package
|
|
168
|
+
commits to no vendor.
|
|
169
|
+
|
|
170
|
+
A destructive action **pauses the graph** with a LangGraph `interrupt()` rather
|
|
171
|
+
than being decided on the phone owner's behalf. The gate classifies before
|
|
172
|
+
acting, so nothing has touched the device at the moment of the pause, and with
|
|
173
|
+
no approver attached everything destructive is refused: an unanswerable
|
|
174
|
+
question is not consent.
|
|
175
|
+
|
|
176
|
+
Resuming re-runs the entire node, which is why every action's idempotency key
|
|
177
|
+
is its **tool call id** rather than a step counter. A counter yields a
|
|
178
|
+
different key on the re-run, misses the cache, and taps the device twice; a
|
|
179
|
+
test asserts this by putting a harmless tap in the same turn as one that needs
|
|
180
|
+
approval and counting what reaches the device.
|
|
181
|
+
|
|
182
|
+
What is *not* in it is the interesting part. Planning, subagents and
|
|
183
|
+
cross-session memory were each specified, measured, and rejected with numbers;
|
|
184
|
+
see [docs/adr/](docs/adr/). The one mechanism that survived is verification,
|
|
185
|
+
and it exists because actions already return the screen they produced: it reads
|
|
186
|
+
`screen_changed` and the delta to notice an action the device ignored, and
|
|
187
|
+
costs zero additional observations. That cut actions across the task set by 38%.
|
|
188
|
+
|
|
189
|
+
The reason it can cost nothing is layer 4's design. Because an action folds its
|
|
190
|
+
resulting screen into the response, the agent spends exactly one observation
|
|
191
|
+
per task, which is the same number a hand-written oracle needs. The
|
|
192
|
+
observation-per-step cost that this whole stack was optimised against turned
|
|
193
|
+
out to be already at its floor before any agent pillar was built.
|
|
194
|
+
|
|
195
|
+
## Testing
|
|
196
|
+
|
|
197
|
+
- `tests/unit` (326) runs against an in-process fake WebDriverAgent, so it
|
|
198
|
+
needs no Xcode, simulator, or phone. It also enforces the layering statically:
|
|
199
|
+
only `ios_mcp/server` may import MCP, and `ios_agent` may touch only a listed
|
|
200
|
+
public surface.
|
|
201
|
+
- `tests/integration` (13) runs against a real simulator, including a chaos
|
|
202
|
+
test that kills the runner mid-flow.
|
|
203
|
+
- `tests/evals` (11 flows) measures tokens, wall time, action count, and
|
|
204
|
+
resolution-tier distribution. These four numbers are the product metrics.
|
|
205
|
+
- `tests/evals/agent` measures the agent itself: 10 goal-directed tasks against
|
|
206
|
+
a scripted device, each declaring the action count a hand-written oracle
|
|
207
|
+
needs. Three tasks inject failures taken from real hardware. A `model` tier
|
|
208
|
+
puts a real model in the loop, and a `simulator` tier runs it against real
|
|
209
|
+
iOS.
|
|
210
|
+
|
|
211
|
+
The fakes cannot catch everything. Every bug in the Phase 5 commit was found
|
|
212
|
+
only by running against a real device, which is why the eval suite exists.
|
ios_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ehsan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|