Repository navigation
Add secure macOS iMessage plugin - #71
maximedegreve wants to merge 4 commits into
Conversation
Add a macOS-only local MCP server with deny-by-default access controls, read-only Messages history, safe AppleScript sends, marketplace registration, documentation, and focused tests.\n\nAdapt the security model from Anthropic's Apache-2.0 iMessage plugin with attribution and Copilot-specific state and branding.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a local macOS iMessage MCP plugin with access controls, configuration workflows, documentation, and tests.
Changes:
- Implements local message reading, search, and replies.
- Adds secure policy configuration and Copilot skills.
- Registers, documents, licenses, and tests the plugin.
Show a summary per file
| File | Description |
|---|---|
README.md |
Documents MCP support and licensing. |
.github/plugin/marketplace.json |
Registers the iMessage plugin. |
plugins/imessage/.gitignore |
Excludes Python cache files. |
plugins/imessage/.mcp.json |
Configures the MCP server. |
plugins/imessage/LICENSE |
Adds Apache 2.0 license. |
plugins/imessage/NOTICE |
Records upstream attribution. |
plugins/imessage/README.md |
Documents installation and security. |
plugins/imessage/plugin.json |
Defines plugin metadata. |
plugins/imessage/server/__init__.py |
Initializes the server package. |
plugins/imessage/server/imessage.py |
Implements storage, authorization, queries, and sending. |
plugins/imessage/server/imessage_mcp.py |
Implements MCP and configuration CLI. |
plugins/imessage/skills/imessage-configure/SKILL.md |
Guides secure configuration. |
plugins/imessage/skills/imessage-messaging/SKILL.md |
Guides safe messaging workflows. |
plugins/imessage/tests/test_imessage.py |
Tests security and MCP behavior. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Suppressed comments (1)
plugins/imessage/server/imessage.py:503
- Search has the same service-boundary gap: authorized chat IDs scope the query, but matching rows are never filtered by their resolved
m.service/handle service. A spoofable SMS/RCS row joined to an otherwise authorized chat can therefore appear while SMS/RCS is disabled; pass the effective service policy into this query and filter before ordering/limiting.
WHERE cmj.chat_id IN ({placeholders})
AND instr(lower(COALESCE(m.text, '')), lower(?)) > 0
- Files reviewed: 14/14 changed files
- Comments generated: 6
- Review effort level: Balanced
| yield remaining[:split] | ||
| remaining = remaining[split:] | ||
| if remaining.startswith("\n") or remaining.startswith(" "): | ||
| remaining = remaining[1:] |
| if method == "initialize": | ||
| requested = request.get("params", {}).get( | ||
| "protocolVersion", "2025-06-18" | ||
| ) | ||
| result = { | ||
| "protocolVersion": requested, |
There was a problem hiding this comment.
The server echoes any client-supplied protocol version, including unsupported or non-string values, thereby claiming compatibility it does not implement. MCP negotiation requires returning the requested version only when supported and otherwise a version the server supports; advertise the implementation's supported version instead.
| def _policy_update(store: ConfigStore, **changes: Any) -> AccessPolicy: | ||
| policy = replace(store.load(), **changes) | ||
| store.save(policy) |
There was a problem hiding this comment.
load()followed bysave()is not an atomic read-modify-write operation;os.replaceonly prevents partial files. Concurrent helper commands can both read the same policy and the last writer can silently lose—or reintroduce—an allowlist change. Serialize the complete load/update/save transaction with a lock, and compute set/group changes only after acquiring it.
| return [ | ||
| chat.public_dict() | ||
| for chat in self.database.list_chats(scan_limit=None) | ||
| if self.chat_allowed(chat, policy) | ||
| ][:limit] |
There was a problem hiding this comment.
A request for at most 100 chats still loads every chat and runs
_chat's participant query for each one before slicing. Large Messages databases therefore turn a small listing into an unbounded N+1 scan. Iterate recent chats in pages and stop oncelimitauthorized entries have been collected, continuing to another page only when filtering leaves the page short.
| try: | ||
| request = json.loads(raw_line) | ||
| if not isinstance(request, dict): | ||
| raise ValueError("JSON-RPC message must be an object.") | ||
| response = server.dispatch(request) | ||
| except (json.JSONDecodeError, ValueError) as error: | ||
| response = { | ||
| "jsonrpc": "2.0", | ||
| "id": None, | ||
| "error": {"code": -32700, "message": str(error)}, | ||
| } |
| JOIN chat_message_join AS cmj ON cmj.message_id = m.ROWID | ||
| JOIN chat AS c ON c.ROWID = cmj.chat_id | ||
| LEFT JOIN handle AS h ON h.ROWID = m.handle_id | ||
| WHERE cmj.chat_id = ? |
Report privacy-safe authorized and self-chat counts from the status tool, explain the fail-closed empty state, and cover live owner-alias reloads without an MCP restart. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Recognize local destination identities only when they occur on both incoming and outgoing authenticated iMessages for a trusted account. This supports modern Messages self-chats while excluding recipients, one-sided values, short codes, and SMS/RCS data. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Summary
Security and permissions
~/Library/Messages/chat.dbwith SQLite read-only and query-only modes0600file and0700directory permissions under a Copilot-specific Application Support pathUsers must grant Full Disk Access to the application running Copilot CLI for reads and Automation permission to control Messages.app for sends.
Upstream attribution
The security design and typedstream parser are derived from Anthropic's Apache-2.0 iMessage plugin at
anthropics/claude-plugins-officialcommitc54b5608d9be1910de9a5b91c2d15bf6673b9c35. The plugin includes the upstream Apache License 2.0 copyright notice and a modification notice, uses Copilot-specific paths/configuration, and removes upstream outbound branding.Validation
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s plugins/imessage/tests -v(16 tests)initialize,tools/list,imessage_status, andimessage_chatssmoke testscopilot --plugin-dir ./plugins/imessage plugin listgit diff --checkCaveats
message.textcolumn; newer binaryattributedBodycontent is decoded for history on a best-effort basis but is not searchable