arc-control-mcp 0.3.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.
- package/CHANGELOG.md +255 -0
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/package.json +52 -0
- package/src/index.js +123 -0
- package/src/jxa.js +243 -0
- package/src/page-lib.js +219 -0
- package/src/registry.js +99 -0
- package/src/state.js +216 -0
- package/src/tools/content.js +217 -0
- package/src/tools/interact.js +321 -0
- package/src/tools/navigation.js +302 -0
- package/src/tools/schema.js +48 -0
- package/src/tools/scripting.js +217 -0
- package/src/tools/shared.js +100 -0
- package/src/tools/spaces.js +66 -0
- package/src/tools/tabs.js +189 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-08-31
|
|
9
|
+
|
|
10
|
+
This release is mostly about one class of bug: the server used to report success
|
|
11
|
+
for things that had not happened. If you have been running 0.1.x, assume any
|
|
12
|
+
`ok: true` you saw from a page-touching tool was unverified. The findings came
|
|
13
|
+
from an agent review of the running server, kept at
|
|
14
|
+
[docs/agent-review-2026-08-31.md](docs/agent-review-2026-08-31.md).
|
|
15
|
+
|
|
16
|
+
The second thread is protocol conformance. The tools themselves worked. How
|
|
17
|
+
results, failures, timeouts and cancellations were reported over MCP did not
|
|
18
|
+
match what a client, or a model reading the response, has any right to expect.
|
|
19
|
+
|
|
20
|
+
### Changed: a tool that changes a tab will not touch the tab you are looking at
|
|
21
|
+
|
|
22
|
+
A call with no `tab_id` used to resolve to this agent's own tab and then, failing
|
|
23
|
+
that, to whatever tab was active in Arc. That second fallback applied to every
|
|
24
|
+
tool, so an agent that called `go_back` or `reload_tab` without a `tab_id`
|
|
25
|
+
navigated and reloaded a tab a human was reading. That happened.
|
|
26
|
+
|
|
27
|
+
Resolution is now split by whether the tool changes anything:
|
|
28
|
+
|
|
29
|
+
- An explicit `tab_id` always wins.
|
|
30
|
+
- Otherwise, a tab this agent opened.
|
|
31
|
+
- Otherwise, for a **read-only** tool, the tab you are looking at. Reading the
|
|
32
|
+
page you already have open is useful and harmless.
|
|
33
|
+
- Otherwise the call is **refused**, with an error naming what to pass instead.
|
|
34
|
+
|
|
35
|
+
`tab_id` was deliberately not made mandatory everywhere. That would force a
|
|
36
|
+
`list_tabs` round trip before every call, and each `osascript` spawn costs a few
|
|
37
|
+
hundred milliseconds. It would also break the implicit targeting that lets
|
|
38
|
+
`open_url`, then `click`, then `fill` run without ids.
|
|
39
|
+
|
|
40
|
+
`arc_status` now reports both branches, as `resolvesTo.readOnly` and
|
|
41
|
+
`resolvesTo.mutating`, since one string can no longer describe both.
|
|
42
|
+
|
|
43
|
+
Also fixed in the same area: a state file written by the pre-0.3.0 flat format is
|
|
44
|
+
no longer treated as a finished run whose tabs can be reaped. It carries no
|
|
45
|
+
session identity, so it may equally belong to a server still running the old
|
|
46
|
+
code, and treating it as dead once marked two live user tabs for closing.
|
|
47
|
+
|
|
48
|
+
### Added: arguments are validated before a handler runs
|
|
49
|
+
|
|
50
|
+
Nothing validated tool arguments at all, so a wrong type surfaced as a confusing
|
|
51
|
+
error from inside the page. Every tool now declares its arguments as a Zod
|
|
52
|
+
schema, the JSON Schema advertised over MCP is generated from it, and the same
|
|
53
|
+
schema validates the call. A bad argument comes back as an `isError` result
|
|
54
|
+
naming it:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Invalid arguments for click. selector: Invalid input: expected string, received number
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Because `batch` calls its peers through the registry, batch steps are validated
|
|
61
|
+
too. Declared defaults are now applied by the parser rather than by a fallback in
|
|
62
|
+
each handler. `zod` becomes a declared dependency: it was already present via the
|
|
63
|
+
MCP SDK, so it deduplicates to a single copy and adds nothing to an install.
|
|
64
|
+
|
|
65
|
+
The server stays on the SDK's low-level `Server` rather than moving to
|
|
66
|
+
`McpServer.registerTool`, which rejects generated JSON Schema and would mean
|
|
67
|
+
rewriting every schema again. It also does not declare `outputSchema` or return
|
|
68
|
+
`structuredContent`: the low-level `Server` does not validate them, so declaring
|
|
69
|
+
them would commit the project to a specification MUST with nothing enforcing it,
|
|
70
|
+
and `execute_javascript` and `batch` have genuinely unschematisable outputs.
|
|
71
|
+
|
|
72
|
+
### MCP protocol conformance
|
|
73
|
+
|
|
74
|
+
- **A tool that ran and failed now says so where the model can see it.** A
|
|
75
|
+
failed `click` used to arrive as an ordinary successful MCP result whose text
|
|
76
|
+
happened to contain `"ok": false` somewhere inside it. A model reads a
|
|
77
|
+
successful result as "the call worked" and carries on building on something
|
|
78
|
+
that never happened. Failures now set `isError` on the result, so a failed
|
|
79
|
+
click is visible as a failure and the model can retry or pick a different
|
|
80
|
+
selector.
|
|
81
|
+
- **An unknown tool name is now a JSON-RPC error.** It used to come back as a
|
|
82
|
+
tool result carrying `isError`, which is the shape reserved for a tool that
|
|
83
|
+
ran and failed. The spec lists an unknown tool under protocol errors, so it
|
|
84
|
+
now raises `InvalidParams`, with the list of real tool names in the message.
|
|
85
|
+
- **A call can no longer outlive the client's deadline.** An MCP client gives up
|
|
86
|
+
on a request after 60 seconds by default, and once it does, the server's
|
|
87
|
+
answer is thrown away: you see `RequestTimeout` instead of the `timedOut`
|
|
88
|
+
payload that says what the page was actually doing. `open_url`'s worst case
|
|
89
|
+
was a 45s open budget plus a 15s load wait, exactly 60000 ms, so a slow page
|
|
90
|
+
reliably produced a client timeout rather than the server's own honest "still
|
|
91
|
+
loading" answer. A caller-supplied `timeout_ms` had no cap at all. The open
|
|
92
|
+
budget is now 25s, every `timeout_ms` is clamped to 30s, and the ceiling is
|
|
93
|
+
stated both as a schema `maximum` and in the description, together with what
|
|
94
|
+
to do instead: call `wait_for_load` or `wait_for_selector` again. Repeated
|
|
95
|
+
short waits each hand back a real `readyState`, `url` or element count, which
|
|
96
|
+
one long wait does not.
|
|
97
|
+
- **Cancelling a call now stops it.** The polling loops behind
|
|
98
|
+
`wait_for_load`, `wait_for_selector`, `open_url`, `go_back`, `go_forward`,
|
|
99
|
+
`reload_tab` and `batch` check the client's cancellation signal between polls.
|
|
100
|
+
Before, a cancelled call kept spawning `osascript` processes against your
|
|
101
|
+
browser for a result nobody was going to read. A cancelled `batch` reports how
|
|
102
|
+
many of its steps had already run, because whatever those steps did to the
|
|
103
|
+
page stands.
|
|
104
|
+
- **`execute_javascript` and `batch` no longer claim `destructiveHint: false`.**
|
|
105
|
+
That is not defensible for two tools that can do anything a page can do,
|
|
106
|
+
including submitting a form or clicking a delete button. `openWorldHint` is
|
|
107
|
+
now true for every tool that touches page content, and false only for the
|
|
108
|
+
tools that read or move Arc's own tab and space bookkeeping.
|
|
109
|
+
- **`close_tab` now says that a bare call can close the tab you are looking
|
|
110
|
+
at.** Its description was "Close a tab." A call with no `tab_id` resolves to
|
|
111
|
+
this agent's current tab or, when it has none yet, whatever tab is active in
|
|
112
|
+
Arc, which may well be yours. The description says so now, and points at
|
|
113
|
+
`close_own_tabs` for cleanup.
|
|
114
|
+
- **`batch` has an aggregate response cap.** The individual read tools capped
|
|
115
|
+
their own output, but nothing capped the total, so a batch of several reads
|
|
116
|
+
could return a response large enough for a client to clip mid-JSON. There is
|
|
117
|
+
now a 60000 character budget across all steps. Past it the batch stops early
|
|
118
|
+
and reports `truncated`, with a note telling you to pass `max_chars` to the
|
|
119
|
+
reading steps or split the sequence across two calls.
|
|
120
|
+
- **The server introduces itself at initialize.** It now sends MCP
|
|
121
|
+
`instructions`, plus a display `title` and a `websiteUrl`. The instructions
|
|
122
|
+
carry the facts a model otherwise wastes a call discovering: call `arc_status`
|
|
123
|
+
first, background tabs are fully scriptable so prefer passing a `tab_id` over
|
|
124
|
+
switching what the user is looking at, `text=` is substring matching, batch a
|
|
125
|
+
known sequence, and page content is untrusted data rather than instructions.
|
|
126
|
+
|
|
127
|
+
### Fixed
|
|
128
|
+
|
|
129
|
+
- **A page script that threw was reported as success.** The JXA layer collapsed
|
|
130
|
+
Arc's empty response into `null`, and every handler spread that `null` into a
|
|
131
|
+
cheerful `{ok: true}`. In the worst observed case, a four-step `batch` in
|
|
132
|
+
which nothing at all happened returned `ok: true` on every step and at the top
|
|
133
|
+
level. Every injected script now returns an explicit envelope, so a failure
|
|
134
|
+
comes back as a real error carrying the page's own message, for example
|
|
135
|
+
`The page script failed: SyntaxError: ...`. An invalid selector and a missing
|
|
136
|
+
element are now distinguishable.
|
|
137
|
+
- **`execute_javascript` silently returned `null` for any statement body.** Code
|
|
138
|
+
without the literal word `return` was wrapped as `return (code)`, which is a
|
|
139
|
+
syntax error for anything that is not a single expression. A thrown error, a
|
|
140
|
+
syntax error and a legitimate null result were all indistinguishable. Code is
|
|
141
|
+
now validated in Node before it is injected: the expression form is tried
|
|
142
|
+
first, then the statement form, and if neither parses you get the actual
|
|
143
|
+
`SyntaxError` message without Arc being contacted. The response reports which
|
|
144
|
+
form was used, so a real null is no longer ambiguous.
|
|
145
|
+
|
|
146
|
+
Note the limits of this. A statement body still yields a value only through
|
|
147
|
+
`return`, because giving `let n = 2; n * 3` the completion value `6` would
|
|
148
|
+
need `eval` inside the page, and that breaks on any site with a strict
|
|
149
|
+
Content-Security-Policy. Instead of a bare `null` you now get an explanatory
|
|
150
|
+
note telling you to add a `return`.
|
|
151
|
+
- **`go_back` was a silent no-op that reported success.** `Arc.goBack` does
|
|
152
|
+
nothing on a background tab (reproduced on a tab with a real back entry,
|
|
153
|
+
polled out to 4 seconds), and none of the history tools checked that the tab
|
|
154
|
+
had moved. `go_back` and `go_forward` now go through the page's own history
|
|
155
|
+
API and verify the URL changed before reporting success. The tab snapshot in
|
|
156
|
+
the response is taken after the navigation, not 400ms into it.
|
|
157
|
+
- **Two agents sharing a label corrupted each other's tab ownership.** The
|
|
158
|
+
in-process set of owned tabs was seeded from a shared state file, and whichever
|
|
159
|
+
process wrote second dropped the other's tabs. That meant `close_own_tabs`
|
|
160
|
+
from one agent could close another agent's tabs. Ownership is now tracked per
|
|
161
|
+
session, and stale entries from previous runs are reaped deliberately at
|
|
162
|
+
cleanup time rather than by accident on the next write.
|
|
163
|
+
- **`get_page_content` with a selector returned only the first match.** Asking
|
|
164
|
+
for `#mw-content-text p` on a Wikipedia article with 19 paragraphs returned the
|
|
165
|
+
lead paragraph and `truncated: false`, with nothing in the response hinting at
|
|
166
|
+
the other 18. It now returns all matches and reports how many there were.
|
|
167
|
+
- **`fill` reported success on elements that cannot be filled.** Filling an
|
|
168
|
+
`<h1>` returned `ok: true`. It now returns a descriptive error naming the tag.
|
|
169
|
+
- **`text=` did not prefer exact matches.** `text=Save` could click "Save and
|
|
170
|
+
close" because matching is by substring and the first DOM-order hit won. Exact
|
|
171
|
+
matches are now ranked ahead of substring matches.
|
|
172
|
+
- **1x1 screen-reader elements were reported as visible.** The visibility check
|
|
173
|
+
only rejected elements whose width and height were both zero, so the standard
|
|
174
|
+
clipped 1x1 accessibility span passed as a plausible click target. It is now
|
|
175
|
+
rejected.
|
|
176
|
+
- **Non-serializable script results came back as a bare `{}`.** `return window`
|
|
177
|
+
looked like an empty object with no explanation. The response now carries a
|
|
178
|
+
note saying the value could not be serialized.
|
|
179
|
+
|
|
180
|
+
### Changed
|
|
181
|
+
|
|
182
|
+
- **Responses are smaller.** `batch` used to repeat the full tab object,
|
|
183
|
+
including a 300-character URL, on every step; the tab is now emitted once per
|
|
184
|
+
batch and repeated only when it changes mid-batch. Element descriptions no
|
|
185
|
+
longer include `rect` and long attribute values by default, which are behind a
|
|
186
|
+
new `verbose` option on the tools that describe elements.
|
|
187
|
+
- **`press_key` no longer returns page text.** It used to include several hundred
|
|
188
|
+
characters of the body's `innerText` as its `target`.
|
|
189
|
+
- **Error messages distinguish an Arc or page problem from a bug in this
|
|
190
|
+
server.** Anything that is not a mapped Arc error is now labelled as internal.
|
|
191
|
+
- **Selector documentation states that `text=` is substring matching**, in the
|
|
192
|
+
tool schemas as well as the README, so an agent reading the schema knows.
|
|
193
|
+
|
|
194
|
+
### Added
|
|
195
|
+
|
|
196
|
+
- `execute_javascript` now reports the wrapper it chose as `form`
|
|
197
|
+
(`"expression"` or `"statement"`), which is what makes a legitimate `null`
|
|
198
|
+
result distinguishable from a script that failed.
|
|
199
|
+
|
|
200
|
+
- `list_spaces` reports `topAppCount`, so its per-space tab counts reconcile
|
|
201
|
+
with `list_tabs`. Arc excludes topApp favourites from `space.tabs`, which made
|
|
202
|
+
9 reported tabs against 21 real ones look like a bug.
|
|
203
|
+
- An `exact: true` option on the tools that take a selector, so a `text=`
|
|
204
|
+
selector can require the whole text instead of a substring. They also report
|
|
205
|
+
how many elements matched, so a label that hits 37 elements says so.
|
|
206
|
+
- `close_own_tabs` takes `include_stale`, and `arc_status` reports
|
|
207
|
+
`staleTabCount`, so tabs leaked by a dead earlier run of the same label can be
|
|
208
|
+
cleaned up on purpose instead of being swept up by a restart.
|
|
209
|
+
- `get_html` takes `nth` and reports how many elements matched, so a
|
|
210
|
+
multi-match selector is visible there too.
|
|
211
|
+
- `ARC_MCP_STATE_DIR` to override where tab ownership is recorded.
|
|
212
|
+
- `--version` and `--help` on the server binary, so an install can be checked
|
|
213
|
+
without an MCP client. `--help` prints the tool count and the environment
|
|
214
|
+
variables.
|
|
215
|
+
- Documentation of a genuine limitation that is not a bug: synthetic events
|
|
216
|
+
cannot drive widgets gated on trusted events. Wikipedia's search suggestions
|
|
217
|
+
never open from `fill`, and hand-dispatched per-character
|
|
218
|
+
`keydown`/`input`/`keyup` does not help, because Arc's
|
|
219
|
+
`execute javascript` gives no CDP access. The workaround does work: `fill`
|
|
220
|
+
with `submit: true`, or navigate straight to the search URL.
|
|
221
|
+
- Public project files: MIT license, contributing guide, security policy, code
|
|
222
|
+
of conduct, issue and pull request templates, and CI that runs the unit tests
|
|
223
|
+
on Node 20, 22 and 24 plus a syntax check over every file in `src/`.
|
|
224
|
+
- Distribution. This is the first version published to npm, as
|
|
225
|
+
`arc-control-mcp`, and listed in the MCP Registry as
|
|
226
|
+
`io.github.DB-25/arc-control-mcp`. Installing is now a config block running
|
|
227
|
+
`npx -y arc-control-mcp@latest` in any MCP client, instead of a clone plus a
|
|
228
|
+
hand-written absolute path, and upgrading is a restart rather than a `git
|
|
229
|
+
pull`. The npm package declares `"os": ["darwin"]`, so a Linux or Windows
|
|
230
|
+
install fails at once with `EBADPLATFORM` rather than succeeding and then
|
|
231
|
+
failing at the first Apple Event. There is no Docker image and there cannot
|
|
232
|
+
be one: Apple Events do not cross a container boundary.
|
|
233
|
+
|
|
234
|
+
## [0.1.0] - 2026-08-27
|
|
235
|
+
|
|
236
|
+
Initial version.
|
|
237
|
+
|
|
238
|
+
- MCP server over stdio that drives Arc on macOS through JXA
|
|
239
|
+
(`osascript -l JavaScript`), with tool arguments injected as a JSON literal
|
|
240
|
+
rather than concatenated into script source.
|
|
241
|
+
- 26 tools in six modules: tabs, navigation, content, interaction, spaces and
|
|
242
|
+
scripting, composed by a registry that validates tool and handler parity at
|
|
243
|
+
load.
|
|
244
|
+
- Implicit tab targeting, per-agent tab ownership with a `mine` flag on every
|
|
245
|
+
tab, and `close_own_tabs` for cleanup.
|
|
246
|
+
- No focus stealing: `open_url` restores the user's previous tab selection when
|
|
247
|
+
Arc auto-selects a new tab, and only when Arc actually took it.
|
|
248
|
+
- Selectors accept CSS or `text=Some label`.
|
|
249
|
+
- `batch` for running several tools in one round trip, which matters because
|
|
250
|
+
each `osascript` spawn costs a few hundred milliseconds.
|
|
251
|
+
- Arc's raw AppleScript error codes mapped to messages that name the remedy,
|
|
252
|
+
including both required macOS permissions.
|
|
253
|
+
|
|
254
|
+
[0.3.0]: https://github.com/DB-25/arc-control-mcp/releases/tag/v0.3.0
|
|
255
|
+
[0.1.0]: https://github.com/DB-25/arc-control-mcp/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dhruv Baradiya
|
|
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.
|