apple-tools-mcp 2.1.4 → 3.0.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/README.md +80 -3
- package/bin/apple-tools-http-proxy.js +15 -0
- package/bin/apple-tools-http.js +16 -0
- package/examples/com.apple-tools-http.plist +35 -0
- package/examples/config.json.example +5 -0
- package/examples/mcp-client.md +98 -0
- package/index.js +535 -469
- package/indexer.js +2 -2
- package/lib/config.js +29 -1
- package/lib/httpAuth.js +106 -0
- package/lib/httpStdioProxy.js +45 -0
- package/lib/httpTransport.js +71 -0
- package/lib/mailWrite.js +233 -225
- package/lib/processMode.js +42 -1
- package/lib/writeTools.js +2 -2
- package/package.json +16 -8
package/README.md
CHANGED
|
@@ -374,6 +374,81 @@ launchctl load ~/Library/LaunchAgents/com.apple-tools-mcp.indexer.plist
|
|
|
374
374
|
|
|
375
375
|
KeepAlive belongs on this indexer job only — not on the MCP stdio process.
|
|
376
376
|
|
|
377
|
+
## Remote HTTP transport (v3.0.0)
|
|
378
|
+
|
|
379
|
+
Every tool the stdio server exposes — including the write tools — is also
|
|
380
|
+
available over **Streamable HTTP**, so it's reachable from other machines,
|
|
381
|
+
not just whatever process spawned it locally. This is additive: stdio still
|
|
382
|
+
works exactly as before for local same-machine MCP clients that spawn it
|
|
383
|
+
directly. The intended MacBook clients are Claude Desktop, Claude Code,
|
|
384
|
+
Codex Desktop, and ChatGPT Desktop. Claude Desktop uses the included local
|
|
385
|
+
HTTP-to-stdio proxy; Claude Code and Codex connect to the Mini's HTTP
|
|
386
|
+
endpoint directly. ChatGPT Desktop does not currently support this private
|
|
387
|
+
MCP connection; see the client guide for its provider-supported options.
|
|
388
|
+
|
|
389
|
+
Because HTTP reaches beyond "whoever spawned this process," which stdio's
|
|
390
|
+
trust model relies on, **every HTTP request needs a bearer token.** stdio
|
|
391
|
+
has no equivalent check — a locally spawned child process is already
|
|
392
|
+
trusted by whoever spawned it.
|
|
393
|
+
|
|
394
|
+
**Entrypoint:** `node index.js --transport=http`
|
|
395
|
+
**Convenience bin:** `apple-tools-http` (`bin/apple-tools-http.js`; npm global install provides it)
|
|
396
|
+
**npm script (clone only):** `npm run http`
|
|
397
|
+
|
|
398
|
+
### Auth token
|
|
399
|
+
|
|
400
|
+
The token is a random secret generated on first run and stored in the
|
|
401
|
+
macOS Keychain (service `apple-tools-mcp-http`) — never in a config file,
|
|
402
|
+
the server's config file, or logs after the one-time generation banner.
|
|
403
|
+
There's no external account and no cost; it's a self-issued password,
|
|
404
|
+
like an SSH key.
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
apple-tools-mcp http-token # prints the token, generating one first if needed
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Clients send it as `Authorization: Bearer <token>`. Rotate it by deleting
|
|
411
|
+
the Keychain item and restarting the server (it generates a fresh one on
|
|
412
|
+
the next run), then update every client using the old value.
|
|
413
|
+
|
|
414
|
+
### Config
|
|
415
|
+
|
|
416
|
+
`~/.apple-tools-mcp/config.json` (same file the index interval uses):
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
{
|
|
420
|
+
"httpHost": "127.0.0.1",
|
|
421
|
+
"httpPort": 8421
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Env overrides: `APPLE_TOOLS_HTTP_HOST`, `APPLE_TOOLS_HTTP_PORT`. The default
|
|
426
|
+
host is `127.0.0.1`, so HTTP is local-only until you explicitly set a network
|
|
427
|
+
address such as `0.0.0.0`. Default port is 8421 (the indexer's stdio process
|
|
428
|
+
needs no port; this is only for `--transport=http`). For remote access, use a
|
|
429
|
+
Tailscale address for clients that connect directly from your tailnet, or
|
|
430
|
+
put the server behind a TLS-terminating reverse proxy. Do not send the
|
|
431
|
+
bearer token over an untrusted LAN. Cloud-hosted connectors need a separate
|
|
432
|
+
way to reach the server; a private Tailscale address alone is insufficient.
|
|
433
|
+
|
|
434
|
+
### LaunchAgent
|
|
435
|
+
|
|
436
|
+
See `examples/com.apple-tools-http.plist` (copy to
|
|
437
|
+
`~/Library/LaunchAgents/`, replace the `REPLACE_ME*` placeholders the same
|
|
438
|
+
way as the indexer plist above, then `launchctl load` it). Runs alongside
|
|
439
|
+
the indexer LaunchAgent, not instead of it — the HTTP server is a second,
|
|
440
|
+
independent process.
|
|
441
|
+
|
|
442
|
+
### Wiring up clients
|
|
443
|
+
|
|
444
|
+
See `examples/mcp-client.md` for the MacBook setup. Short version:
|
|
445
|
+
|
|
446
|
+
- **On the Mini:** set `httpHost` to its LAN or Tailscale IP and restart the HTTP server. Retrieve the bearer token with `apple-tools-mcp http-token`.
|
|
447
|
+
- **Claude Desktop on the MacBook:** configure the included `apple-tools-http-proxy` as a local MCP server. It forwards tool calls to the Mini over HTTP.
|
|
448
|
+
- **Claude Code and Codex Desktop on the MacBook:** add `http://<mini-address>:8421/mcp` and the token. The client guide has commands for both.
|
|
449
|
+
- **ChatGPT Desktop on the MacBook:** current custom MCP support does not provide a direct private HTTP connection in the desktop app. The client guide describes the documented ChatGPT web route.
|
|
450
|
+
- **Claude / ChatGPT cloud connectors:** a connector does not connect from your device's tailnet interface. [Claude requires a publicly reachable server](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp); [ChatGPT directs private-network servers to Secure MCP Tunnel and currently supports MCP apps on web only](https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt). Use the provider's supported network setup before adding this server as a cloud connector.
|
|
451
|
+
|
|
377
452
|
## Available Tools
|
|
378
453
|
|
|
379
454
|
Once configured, your MCP client can use these tools:
|
|
@@ -464,7 +539,7 @@ Two arguments are available on **every** write tool:
|
|
|
464
539
|
|
|
465
540
|
`body_format: "html"` uses the same compose path with a tag-stripped body pasted into Mail's native editor. It does **not** set Mail's `content` or `html content` — those setters cite-wrap the whole message (`>` prefixes and `<blockquote type="cite">`, same iOS purple bar). Mail may still generate its own HTML alternative from the native compose. The default is plain text.
|
|
466
541
|
|
|
467
|
-
**Compose is not quoted.** `mail_send` / `mail_draft` (plain and html) call `make new outgoing message` **without** AppleScript `content:` so `newMessage` is a real Mail object, then
|
|
542
|
+
**Compose is not quoted.** `mail_send` / `mail_draft` (plain and html) call `make new outgoing message` **without** AppleScript `content:` so `newMessage` is a real Mail object, then paste the body into the compose window (exact subject title, role `AXWebArea`, proven focus, Cmd-A + clipboard). Mail's `mailto` command was shipped in 2.0.4 and **fails on Mini/MacBook Mail**: it does not return an outgoing message (`newMessage` undefined, AppleScript **-2753**). On current Mail (Ventura+, FB11734014) `content` / `html content` store the body as a citation: every plain-text line prefixed with `>`, plus a `multipart/alternative` HTML part wrapped in `<blockquote type="cite">`. Desktop Mail often hides the bar with inline styles; iOS Mail paints the whole body purple with a left quote bar — even when the subject is not `Re:`/`Fwd:` and there is no `In-Reply-To`. Reply and forward still quote the original, which is expected (those paths still prepend via `content`; fixing reply/forward cite-wrap is out of scope for 2.1.0).
|
|
468
543
|
|
|
469
544
|
Keystrokes use System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) **and** Automation → Mail **and** Automation → System Events. Run `apple-tools-mcp permissions` after first install or upgrade so **node → System Events** pops **Allow** in the same sitting as Mail / Messages / Contacts / Calendar. An Accessibility / System Events deny is not a Mail Automation deny (`-1743` / `-10004`).
|
|
470
545
|
|
|
@@ -486,6 +561,8 @@ Keystrokes use System Events, so **node needs Accessibility** (Privacy & Securit
|
|
|
486
561
|
|
|
487
562
|
**Compile prove (2.1.4 on the Mini host):** Unpatched `buildComposeScript` / `buildMailBodyPasteHandler` `osacompile` **-2741** on those bare AXTitle reads; patched AXTitle-inside-System-Events tell `osacompile` exit 0. Report: `results-214/SOLUTION-PROBE-REPORT.md`. Global install untouched. Live Sent is the 2.1.3 ship prove after npm.
|
|
488
563
|
|
|
564
|
+
**Probe 3 body delivery (2.1.5).** Mini Probe 3 (`atm-p3-deliver.applescript`, classic AppleScript / System Events, not JXA) proved a click-free path: exact subject window title, first `AXWebArea` from `entire contents` of that window, `set focused of` that element to true, then an independent `AXFocusedUIElement` re-read that must match role `AXWebArea` and position/size before any keystroke. Delivery is Cmd-A + clipboard paste. No Mail `content` / `html content`. No `AXValue` write (`AXUIElementSetAttributeValue` on content is a silent no-op; 2.1.4 `atmAxHitFill` returned `OK` when that value was empty, so the keystroke recovery never ran). No coordinate `click at` (Probe 3: a click can land on another Space or app). Focus mismatch is `BODY_FOCUS_UNPROVEN`: the draft is deleted and nothing is sent. The post-paste body needle runs only when the focused element is still that proven body, so a To: recipient chip (`U+FFFC`) is not `BODY_PASTE_MISDIRECTED`. Sent/Outbox verify, `dry_run`, and confirm are unchanged.
|
|
565
|
+
|
|
489
566
|
**Manual prove (2.1.3 on the Mini host):** After `apple-tools-mcp permissions` reports System Events granted, `mail_send` a short **multiline** plain message and a short `body_format: "html"` message whose body looks like ordinary paragraphs (for example `<p>Quick note</p>`), neither with a `Re:`/`Fwd:` subject. The body must land via title-marker + hit-test (not Tab / focused-role): no `BODY_FOCUS_FAILED` / `BODY_PASTE_MISDIRECTED`. Inspect each Sent `.emlx`: the text/plain part must contain the intended text (newlines preserved) and must not prefix every body line with `>`, and any HTML alternative must not wrap the whole body in `<blockquote type="cite">`. Compose must succeed (no `-2753` / undefined `newMessage`). Prove the production path (write-bridge with node → System Events allowed, **or** the documented in-process fallback). That quote-prefix Mini Sent prove is a separate bar from the 2.0.7 verify contract.
|
|
490
567
|
|
|
491
568
|
Emails are addressed by their RFC822 **Message-ID**. Pass `message_id`, or pass the `file_path` from `mail_search` / `mail_recent` and the server reads the Message-ID out of the `.emlx` headers for you. `mail_archive` moves the message to its account's Archive (or All Mail) mailbox; `mail_trash` moves it to that account's Trash.
|
|
@@ -591,7 +668,7 @@ Ask your MCP client things like:
|
|
|
591
668
|
|
|
592
669
|
## Privacy & Security
|
|
593
670
|
|
|
594
|
-
- **Local Processing**: All embeddings are generated locally using
|
|
671
|
+
- **Local Processing**: All embeddings are generated locally using Hugging Face Transformers.js
|
|
595
672
|
- **No Cloud Services**: No data is sent to external servers; the write bridge is a local unix socket, never a network port
|
|
596
673
|
- **Reads are read-only**: Search and lookup tools never modify your data. The [write tools](#write-tools-200) are the only ones that change anything, and they are opt-in per call, with `confirm` required for deletes and multi-recipient sends
|
|
597
674
|
- **No credentials**: The server holds no tokens or passwords. It uses the Mail, Messages, Calendar, and Contacts apps you are already signed into
|
|
@@ -720,4 +797,4 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|
|
720
797
|
|
|
721
798
|
- Built with the [Model Context Protocol SDK](https://github.com/modelcontextprotocol/sdk)
|
|
722
799
|
- Vector search powered by [LanceDB](https://lancedb.com/)
|
|
723
|
-
- Local embeddings via [
|
|
800
|
+
- Local embeddings via [Hugging Face Transformers.js](https://github.com/huggingface/transformers.js)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runHttpStdioProxy } from "../lib/httpStdioProxy.js";
|
|
3
|
+
|
|
4
|
+
const url = process.argv[2];
|
|
5
|
+
if (!url || process.argv.length !== 3) {
|
|
6
|
+
console.error("Usage: apple-tools-http-proxy <http://mini-address:8421/mcp>");
|
|
7
|
+
process.exitCode = 2;
|
|
8
|
+
} else {
|
|
9
|
+
try {
|
|
10
|
+
await runHttpStdioProxy({ url, token: process.env.APPLE_TOOLS_MCP_TOKEN });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(`Apple Tools HTTP proxy: ${error.message}`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm bin for the HTTP transport.
|
|
4
|
+
* Dedicated wrapper so npm 12 pack/publish keeps the CLI — `index.js` as a
|
|
5
|
+
* bin target is rewritten with "script name index.js was invalid and removed".
|
|
6
|
+
* Injects `--transport=http` so a realpath to this file still starts the
|
|
7
|
+
* HTTP server. `permissions` / `http-token` still win (same contract as the
|
|
8
|
+
* apple-tools-indexer bin name for `--mode=indexer`).
|
|
9
|
+
*/
|
|
10
|
+
if (!process.argv.includes('--transport=http')) {
|
|
11
|
+
const transportIdx = process.argv.indexOf('--transport');
|
|
12
|
+
if (transportIdx === -1 || process.argv[transportIdx + 1] !== 'http') {
|
|
13
|
+
process.argv.splice(2, 0, '--transport=http');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
await import('../index.js');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.REPLACE_ME.apple-tools-http</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>/usr/bin/env</string>
|
|
10
|
+
<string>node</string>
|
|
11
|
+
<string>REPLACE_ME_WITH_REPO_PATH/bin/apple-tools-http.js</string>
|
|
12
|
+
</array>
|
|
13
|
+
<key>RunAtLoad</key>
|
|
14
|
+
<true/>
|
|
15
|
+
<key>KeepAlive</key>
|
|
16
|
+
<true/>
|
|
17
|
+
<key>ProcessType</key>
|
|
18
|
+
<string>Background</string>
|
|
19
|
+
<key>WorkingDirectory</key>
|
|
20
|
+
<string>/Users/REPLACE_ME</string>
|
|
21
|
+
<key>EnvironmentVariables</key>
|
|
22
|
+
<dict>
|
|
23
|
+
<key>PATH</key>
|
|
24
|
+
<string>REPLACE_ME_WITH_NODE_BIN_DIR:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
25
|
+
<key>NODE_OPTIONS</key>
|
|
26
|
+
<string>--max-old-space-size=4096</string>
|
|
27
|
+
<key>HOME</key>
|
|
28
|
+
<string>/Users/REPLACE_ME</string>
|
|
29
|
+
</dict>
|
|
30
|
+
<key>StandardOutPath</key>
|
|
31
|
+
<string>/Users/REPLACE_ME/Library/Logs/apple-tools-http.out.log</string>
|
|
32
|
+
<key>StandardErrorPath</key>
|
|
33
|
+
<string>/Users/REPLACE_ME/Library/Logs/apple-tools-http.err.log</string>
|
|
34
|
+
</dict>
|
|
35
|
+
</plist>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# MacBook MCP client setup (Streamable HTTP, v3.0.0+)
|
|
2
|
+
|
|
3
|
+
`--transport=http` exposes the same tools as stdio mode (including writes:
|
|
4
|
+
sending iMessage, editing contacts/calendar) over the network, so every
|
|
5
|
+
request must carry the bearer token from `apple-tools-mcp http-token`.
|
|
6
|
+
This HTTP transport is for connecting the MacBook's Claude Desktop,
|
|
7
|
+
Claude Code, and Codex Desktop to the MCP server on the Mac Mini. ChatGPT
|
|
8
|
+
Desktop is also a target, but its current custom MCP support does not
|
|
9
|
+
accept a private server directly in the desktop app.
|
|
10
|
+
|
|
11
|
+
## Server on the Mac Mini
|
|
12
|
+
|
|
13
|
+
Set `httpHost` in `~/.apple-tools-mcp/config.json` to the Mini's LAN or
|
|
14
|
+
Tailscale IP (or set `APPLE_TOOLS_HTTP_HOST` to that address), then restart
|
|
15
|
+
the HTTP server. The default `127.0.0.1` bind only accepts clients on the
|
|
16
|
+
Mini. Retrieve the token on the Mini with `apple-tools-mcp http-token`.
|
|
17
|
+
|
|
18
|
+
## Claude Desktop on the MacBook
|
|
19
|
+
|
|
20
|
+
Install this package on the MacBook and find the absolute path of
|
|
21
|
+
`apple-tools-http-proxy` with `command -v apple-tools-http-proxy`. In
|
|
22
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`, add a
|
|
23
|
+
local MCP server entry using that path:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"apple-tools": {
|
|
29
|
+
"command": "/absolute/path/to/apple-tools-http-proxy",
|
|
30
|
+
"args": ["http://<mini-address>:8421/mcp"],
|
|
31
|
+
"env": { "APPLE_TOOLS_MCP_TOKEN": "<token-from-mini>" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Merge this entry into any existing `mcpServers` object, then fully quit and
|
|
38
|
+
reopen Claude Desktop. This local proxy makes the private-network request
|
|
39
|
+
from the MacBook. Claude Desktop's **custom connector** instead connects
|
|
40
|
+
from Anthropic's cloud and cannot use the Mini's private LAN or Tailscale IP.
|
|
41
|
+
[Anthropic documents the distinction](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp).
|
|
42
|
+
The example puts the token in Claude Desktop's local configuration; restrict
|
|
43
|
+
that file to your account.
|
|
44
|
+
|
|
45
|
+
## Claude Code on the MacBook
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
claude mcp add --transport http apple-tools http://<mini-address>:8421/mcp \
|
|
49
|
+
-H "Authorization: Bearer <token-from-mini>" \
|
|
50
|
+
-s user
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`<mini-address>` is the Mini's LAN or Tailscale IP, matching the address
|
|
54
|
+
on which the server listens.
|
|
55
|
+
|
|
56
|
+
## Codex Desktop on the MacBook
|
|
57
|
+
|
|
58
|
+
Make the token from the Mini available as `APPLE_TOOLS_MCP_TOKEN` in the
|
|
59
|
+
MacBook environment that runs Codex, then add the server:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
codex mcp add apple-tools --url http://<mini-address>:8421/mcp \
|
|
63
|
+
--bearer-token-env-var APPLE_TOOLS_MCP_TOKEN
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The Codex CLI and desktop app share MCP configuration, but the token
|
|
67
|
+
environment variable must be available to whichever Codex process connects.
|
|
68
|
+
[OpenAI's Codex MCP setup guide](https://developers.openai.com/learn/docs-mcp)
|
|
69
|
+
describes their shared configuration.
|
|
70
|
+
|
|
71
|
+
## Network access
|
|
72
|
+
|
|
73
|
+
For access away from the LAN, run Tailscale on both the Mini and MacBook and
|
|
74
|
+
use the Mini's Tailscale IP. Use TLS or a trusted private network before
|
|
75
|
+
sending the bearer token across the network. Keep the Mini's HTTP server
|
|
76
|
+
running while either client uses its tools.
|
|
77
|
+
|
|
78
|
+
## ChatGPT Desktop on the MacBook
|
|
79
|
+
|
|
80
|
+
[OpenAI currently documents custom MCP apps for ChatGPT web](https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt),
|
|
81
|
+
not ChatGPT Desktop. ChatGPT also cannot connect directly to the Mini's
|
|
82
|
+
private address. If you use ChatGPT web, OpenAI's
|
|
83
|
+
[Secure MCP Tunnel](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels)
|
|
84
|
+
is the documented way to reach a private MCP server, subject to the
|
|
85
|
+
account and workspace access described there. This repository's HTTP
|
|
86
|
+
server does not by itself enable the requested ChatGPT Desktop connection.
|
|
87
|
+
|
|
88
|
+
## Getting/rotating the token
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
apple-tools-mcp http-token # print the current token (generates one if missing)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
On the Mini, the token lives in the macOS Keychain (service
|
|
95
|
+
`apple-tools-mcp-http`), never in the server's config file or the repo. To
|
|
96
|
+
rotate it, delete the Keychain item and restart the HTTP server — it'll
|
|
97
|
+
generate a new one on the next run —
|
|
98
|
+
then update every client that was using the old value.
|