lane-mac-mcp
by lanemiles
README.md
# lane-mac-mcp
A read-only iMessage MCP server + CLI for macOS. One auditable file, no send capability.
Claude (via [MCP](https://modelcontextprotocol.io)) or you (via the CLI) can list conversations, read a chat, and search message history — and nothing else. **Read-only by construction: v1 contains no send code at all.** All data access shells out to Apple's own `/usr/bin/sqlite3` with `-json -readonly` against `~/Library/Messages/chat.db`; contact names come from read-only queries against the local AddressBook databases.
## Design constraints
The entire server is `server.ts` (~600 lines, comments included) so it can be audited end-to-end — the audited file is the running file, since Node ≥ 22.18 executes TypeScript directly with native type-stripping (no build step, no compiled artifacts). There are exactly two dependencies, pinned via `package-lock.json`: `@modelcontextprotocol/sdk` and `zod`. No network imports, no open ports — stdio JSON-RPC only, spawned as a child process by Claude Desktop; CLI runs are one-shot processes. There is no daemon: Claude Desktop starts and stops the server with the app (restart the app to reload code).
## Requirements
macOS with Messages set up, Node.js ≥ 22.18, and **Full Disk Access** for whichever process runs the server: Terminal (or your terminal app) for CLI use, and Claude Desktop for MCP use. Grant it in System Settings → Privacy & Security → Full Disk Access, then restart the app. `node server.ts doctor` checks all of this and tells you what's missing.
## Setup
```sh
git clone <this repo> && cd MacMCPServer
npm install # installs the two pinned dependencies
node server.ts doctor
```
Then add the server to Claude Desktop's `claude_desktop_config.json` (Settings → Developer → Edit Config):
```json
{
"mcpServers": {
"lane-mac": {
"command": "node",
"args": ["/Users/you/Desktop/MacMCPServer/server.ts"]
}
}
}
```
Restart Claude Desktop. Three tools appear: `list_chats`, `read_chat`, `search_messages`.
## CLI usage
```sh
node server.ts chats # recent chats, names resolved
node server.ts chats --search "mom" --limit 5
node server.ts chats --awaiting-reply-only # threads where the last word wasn't yours
node server.ts read "Martha" --limit 100 # fuzzy name; ambiguous → candidates
node server.ts read 42 --start-date 2024-01-01 --end-date 2024-06-30 --include-reactions
node server.ts search "dinner" --from "martha" # last 30 days by default
node server.ts search "dinner" --start-date 2015-01-01 --deep # older history + rich-text decode
node server.ts doctor
```
Output is JSON. `search` covers `message.text` with SQL `LIKE`; `--deep` additionally decodes messages whose text lives only in the `attributedBody` blob (bounded scan, slower). Tapback reactions are excluded everywhere unless `--include-reactions`.
## Apple quirks handled
Timestamps are nanoseconds since 2001-01-01 (seconds on very old rows) — both are converted. Message text often lives in the `attributedBody` typedstream blob rather than `message.text`; it's decoded in JS with the known `NSString` heuristic. Contact names are matched by normalized last-10-digit phone or lowercased email across all AddressBook sources, degrading gracefully to raw numbers when contacts are unreadable.
## Privacy & safety posture
The server can only ever read. It opens the databases with sqlite3's `-readonly` flag, contains no code path that writes to any database or sends any message, makes no network connections, and listens on no ports. User input is escaped into SQL string literals (`''` doubling) and numerics are validated. `doctor` prints row counts only, never message content. Env overrides (`CHAT_DB_PATH`, `SQLITE_BIN`, `ADDRESSBOOK_DIR`) exist so tests never touch real data.
## Testing
```sh
npm test
```
Black-box tests generate a synthetic `chat.db` and AddressBook fixture (including an attributedBody-only message, a tapback, and an unreplied incoming thread) and drive the real CLI and the real MCP stdio interface as child processes. Development never touches real data.
## Roadmap (later, not v1)
Contact enrichment polish → Notes read via `osascript` → sends gated behind a `SEND_ENABLED` env var plus per-call approval.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues