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 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 fill the body into the compose window (subject title-marker + coordinate AX hit-test, then AXValue). 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).
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 Xenova/Transformers
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 [Xenova/Transformers](https://github.com/xenova/transformers.js)
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,5 @@
1
+ {
2
+ "indexInterval": "5m",
3
+ "httpHost": "127.0.0.1",
4
+ "httpPort": 8421
5
+ }
@@ -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.