airmail-mcp 1.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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +514 -0
- package/dist/index.js.map +1 -0
- package/logo.png +0 -0
- package/manifest.json +432 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Airmail
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Airmail MCP
|
|
2
|
+
|
|
3
|
+
MCP server for [Airmail](https://airmailapp.com) — manage emails, calendars, contacts, and more from Claude.
|
|
4
|
+
|
|
5
|
+
This is a lightweight bridge that connects Claude Desktop and Claude Code to Airmail's built-in MCP server. The bridge connects locally to Airmail on your Mac. Data retrieved by AI tools is processed by your chosen AI provider.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- macOS 13+
|
|
10
|
+
- [Airmail](https://airmailapp.com) installed with MCP enabled (Preferences → MCP)
|
|
11
|
+
- Node.js 18+
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Claude Desktop (MCPB extension)
|
|
16
|
+
|
|
17
|
+
Install from the [Claude MCP Directory](https://claude.ai/mcp) or download the latest `.mcpb` file from [Releases](https://github.com/Airmail/airmail-mcp/releases) and double-click to install.
|
|
18
|
+
|
|
19
|
+
### Claude Desktop (manual)
|
|
20
|
+
|
|
21
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"airmail": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "airmail-mcp"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The auth token is read automatically from the macOS Keychain. If you set `AIRMAIL_MCP_TOKEN`, the Keychain is not accessed:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"airmail": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "airmail-mcp"],
|
|
42
|
+
"env": {
|
|
43
|
+
"AIRMAIL_MCP_TOKEN": "your-token-here"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Claude Code
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp add --transport stdio airmail -- npx -y airmail-mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Authentication
|
|
57
|
+
|
|
58
|
+
The bridge reads the auth token automatically from the macOS Keychain — no configuration needed. When macOS prompts for Keychain access, click **Always Allow** so it won't ask again.
|
|
59
|
+
|
|
60
|
+
If you set `AIRMAIL_MCP_TOKEN`, the Keychain is skipped entirely:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export AIRMAIL_MCP_TOKEN="your-token-here"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To find your token: open Airmail → **Preferences → MCP** → copy the **Auth Token**.
|
|
67
|
+
|
|
68
|
+
## Tools (89)
|
|
69
|
+
|
|
70
|
+
### Email (core)
|
|
71
|
+
`list_accounts` · `list_folders` · `list_messages` · `get_message` · `list_inbox` · `list_starred` · `list_sent` · `list_trash` · `list_spam` · `search_messages` · `fetch_message_body` · `list_attachments` · `get_attachment` · `get_unread_counts` · `search_contacts` · `get_draft` · `delete_draft` · `get_message_thread` · `list_windows` · `export_eml`
|
|
72
|
+
|
|
73
|
+
### Actions
|
|
74
|
+
`mark_messages` · `archive_messages` · `trash_messages` · `move_messages` · `copy_messages` · `snooze_messages` · `add_to_list` · `delete_messages` · `empty_folder` · `refresh_inbox` · `enable_disable_account` · `share_icloud`
|
|
75
|
+
|
|
76
|
+
### Compose
|
|
77
|
+
`compose_email` · `reply_to_message` · `forward_message` · `quick_reply` · `send_email` · `save_as_draft` · `list_drafts`
|
|
78
|
+
|
|
79
|
+
### Folders
|
|
80
|
+
`create_folder` · `rename_folder` · `delete_folder`
|
|
81
|
+
|
|
82
|
+
### Semantic Search
|
|
83
|
+
`semantic_search` · `semantic_index_status`
|
|
84
|
+
|
|
85
|
+
### Profile & Triage
|
|
86
|
+
`analyze_email_history` · `batch_triage_inbox` · `get_user_profile` · `update_user_profile` · `suggest_folder` · `get_behavior_stats`
|
|
87
|
+
|
|
88
|
+
### Calendar & Reminders
|
|
89
|
+
`list_calendars` · `list_events` · `get_event` · `create_event` · `update_event` · `delete_event` · `list_reminders` · `create_reminder` · `complete_reminder` · `delete_reminder`
|
|
90
|
+
|
|
91
|
+
### Contacts
|
|
92
|
+
`list_contacts_book` · `get_contact` · `search_contacts_book` · `create_contact` · `update_contact` · `delete_contact` · `list_contact_groups`
|
|
93
|
+
|
|
94
|
+
### VIP & Blocked Lists
|
|
95
|
+
`list_vips` · `add_vip` · `remove_vip` · `list_blocked` · `add_blocked` · `remove_blocked`
|
|
96
|
+
|
|
97
|
+
### Rules
|
|
98
|
+
`list_rules` · `get_rule` · `create_rule` · `delete_rule` · `toggle_rule`
|
|
99
|
+
|
|
100
|
+
### Signatures
|
|
101
|
+
`list_signatures` · `create_signature` · `update_signature` · `delete_signature`
|
|
102
|
+
|
|
103
|
+
### Smart Folders
|
|
104
|
+
`list_smart_folders` · `create_smart_folder` · `update_smart_folder` · `delete_smart_folder`
|
|
105
|
+
|
|
106
|
+
### Preferences
|
|
107
|
+
`get_preferences` · `set_preferences`
|
|
108
|
+
|
|
109
|
+
### Meta
|
|
110
|
+
`manage_capabilities` — enable/disable tool groups to reduce context usage
|
|
111
|
+
|
|
112
|
+
## How it works
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
Claude ←stdio→ airmail-mcp (Node.js) ←HTTP→ Airmail.app (localhost:9876)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This package is a thin transport bridge. All tool logic runs inside Airmail's native Swift MCP server. The bridge:
|
|
119
|
+
|
|
120
|
+
1. Reads JSON-RPC messages from stdin
|
|
121
|
+
2. Forwards them via HTTP POST to Airmail's local MCP server
|
|
122
|
+
3. Writes responses back to stdout
|
|
123
|
+
|
|
124
|
+
If Airmail is not running, the bridge will attempt to launch it automatically.
|
|
125
|
+
|
|
126
|
+
## Environment variables
|
|
127
|
+
|
|
128
|
+
| Variable | Description | Default |
|
|
129
|
+
|----------|-------------|---------|
|
|
130
|
+
| `AIRMAIL_MCP_TOKEN` | Auth token (optional — automatically read from macOS Keychain if not set) | — |
|
|
131
|
+
| `AIRMAIL_MCP_PORT` | MCP server port | `9876` |
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
git clone https://github.com/Airmail/airmail-mcp.git
|
|
137
|
+
cd airmail-mcp
|
|
138
|
+
npm install
|
|
139
|
+
npm run build
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Sync tools from Airmail source
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run sync-tools # reads Swift source, updates manifest.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Build .mcpb extension
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npx @anthropic-ai/mcpb pack .
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Airmail MCP — Desktop Extension
|
|
4
|
+
*
|
|
5
|
+
* stdio↔HTTP bridge connecting Claude Desktop / Claude Code to Airmail's
|
|
6
|
+
* built-in MCP server on localhost.
|
|
7
|
+
*
|
|
8
|
+
* Uses raw TCP sockets instead of Node.js http module because Airmail's
|
|
9
|
+
* NWListener closes the connection immediately after sending — the http
|
|
10
|
+
* module can miss the response body ("socket hang up"). Raw sockets
|
|
11
|
+
* collect all data before the FIN arrives.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Airmail MCP — Desktop Extension
|
|
4
|
+
*
|
|
5
|
+
* stdio↔HTTP bridge connecting Claude Desktop / Claude Code to Airmail's
|
|
6
|
+
* built-in MCP server on localhost.
|
|
7
|
+
*
|
|
8
|
+
* Uses raw TCP sockets instead of Node.js http module because Airmail's
|
|
9
|
+
* NWListener closes the connection immediately after sending — the http
|
|
10
|
+
* module can miss the response body ("socket hang up"). Raw sockets
|
|
11
|
+
* collect all data before the FIN arrives.
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
14
|
+
import { writeSync } from "fs";
|
|
15
|
+
import * as net from "net";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Configuration
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const AIRMAIL_HOST = "127.0.0.1";
|
|
20
|
+
const AIRMAIL_PORT = (() => {
|
|
21
|
+
const p = parseInt(process.env.AIRMAIL_MCP_PORT ?? "9876", 10);
|
|
22
|
+
if (isNaN(p) || p < 1 || p > 65535) {
|
|
23
|
+
try {
|
|
24
|
+
writeSync(2, `[airmail-mcp] Invalid AIRMAIL_MCP_PORT: "${process.env.AIRMAIL_MCP_PORT}". Must be 1-65535.\n`);
|
|
25
|
+
}
|
|
26
|
+
catch { /* fd closed */ }
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
return p;
|
|
30
|
+
})();
|
|
31
|
+
const AIRMAIL_PATH = "/mcp";
|
|
32
|
+
const VERSION = "1.0.0";
|
|
33
|
+
let currentToken = "";
|
|
34
|
+
const RETRY_DELAY_MS = 2000;
|
|
35
|
+
const MAX_LAUNCH_RETRIES = 5;
|
|
36
|
+
const REQUEST_TIMEOUT_MS = 120_000;
|
|
37
|
+
const MAX_STDIN_BUFFER = 10 * 1024 * 1024; // 10 MB — matches server limit
|
|
38
|
+
/** Resolve parent process code signing Team ID (macOS only). */
|
|
39
|
+
let parentCodeSignTeamID = null;
|
|
40
|
+
function resolveParentCodeSign() {
|
|
41
|
+
try {
|
|
42
|
+
const ppid = process.ppid;
|
|
43
|
+
// Get parent executable path — ppid is always numeric, safe for arg
|
|
44
|
+
const parentPath = execFileSync("ps", ["-p", String(ppid), "-o", "comm="], { encoding: "utf-8" }).trim();
|
|
45
|
+
if (!parentPath)
|
|
46
|
+
return;
|
|
47
|
+
// Walk up to find .app bundle (if any)
|
|
48
|
+
let appPath = parentPath;
|
|
49
|
+
const appIdx = parentPath.indexOf(".app/");
|
|
50
|
+
if (appIdx !== -1) {
|
|
51
|
+
appPath = parentPath.slice(0, appIdx + 4);
|
|
52
|
+
}
|
|
53
|
+
// codesign writes everything to stderr — use spawnSync to capture it
|
|
54
|
+
const result = spawnSync("codesign", ["-dv", "--verbose=2", appPath], {
|
|
55
|
+
encoding: "utf-8",
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
const output = (result.stdout || "") + (result.stderr || "");
|
|
59
|
+
const match = output.match(/TeamIdentifier=(\S+)/);
|
|
60
|
+
if (match && match[1] !== "not" && match[1] !== "not set") {
|
|
61
|
+
parentCodeSignTeamID = match[1];
|
|
62
|
+
log(`Parent code sign: Team ID ${parentCodeSignTeamID}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Not code-signed or codesign not available — leave as null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
function log(msg) {
|
|
73
|
+
const line = `${new Date().toISOString()} [airmail-mcp] ${msg}\n`;
|
|
74
|
+
try {
|
|
75
|
+
writeSync(2, line);
|
|
76
|
+
}
|
|
77
|
+
catch { /* fd closed or unavailable */ }
|
|
78
|
+
}
|
|
79
|
+
/** Sanitize a string for use in an HTTP header value (strip CR/LF). */
|
|
80
|
+
function sanitizeHeaderValue(value) {
|
|
81
|
+
return value.replace(/[\r\n]/g, "");
|
|
82
|
+
}
|
|
83
|
+
function readTokenFromKeychain() {
|
|
84
|
+
try {
|
|
85
|
+
const token = execFileSync("security", [
|
|
86
|
+
"find-generic-password",
|
|
87
|
+
"-s", "com.airmail.mcp",
|
|
88
|
+
"-a", "com.airmail.mcp.token",
|
|
89
|
+
"-w",
|
|
90
|
+
], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
91
|
+
if (token) {
|
|
92
|
+
log("Auth token read from macOS Keychain.");
|
|
93
|
+
}
|
|
94
|
+
return token;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
log("Could not read auth token from macOS Keychain. " +
|
|
98
|
+
"macOS may prompt you to approve Keychain access — click \"Always Allow\" to avoid this next time. " +
|
|
99
|
+
"Alternatively, set the AIRMAIL_MCP_TOKEN environment variable.");
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function sleep(ms) {
|
|
104
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
105
|
+
}
|
|
106
|
+
function ping() {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
let settled = false;
|
|
109
|
+
function settle(v) { if (!settled) {
|
|
110
|
+
settled = true;
|
|
111
|
+
resolve(v);
|
|
112
|
+
} }
|
|
113
|
+
const sock = net.createConnection({ host: AIRMAIL_HOST, port: AIRMAIL_PORT }, () => {
|
|
114
|
+
sock.destroy();
|
|
115
|
+
settle(true);
|
|
116
|
+
});
|
|
117
|
+
sock.setTimeout(3000);
|
|
118
|
+
sock.on("error", () => settle(false));
|
|
119
|
+
sock.on("timeout", () => { sock.destroy(); settle(false); });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function ensureAirmailRunning() {
|
|
123
|
+
if (await ping())
|
|
124
|
+
return;
|
|
125
|
+
log("Airmail MCP server not reachable, launching Airmail...");
|
|
126
|
+
try {
|
|
127
|
+
execFileSync("open", ["-a", "Airmail"], { stdio: "ignore" });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
log("Could not launch Airmail. Is it installed?");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
for (let i = 0; i < MAX_LAUNCH_RETRIES; i++) {
|
|
134
|
+
await sleep(RETRY_DELAY_MS);
|
|
135
|
+
if (await ping()) {
|
|
136
|
+
log("Airmail MCP server is ready.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
log("Airmail launched but MCP server not available. Enable MCP in Airmail Preferences.");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const HEADER_SEPARATOR = Buffer.from("\r\n\r\n");
|
|
144
|
+
const CRLF = Buffer.from("\r\n");
|
|
145
|
+
/** Parse raw HTTP response bytes into status, headers, and body. Handles chunked TE. */
|
|
146
|
+
function parseHttpResponse(raw) {
|
|
147
|
+
const headerEnd = raw.indexOf(HEADER_SEPARATOR);
|
|
148
|
+
if (headerEnd === -1)
|
|
149
|
+
return null;
|
|
150
|
+
// Headers are always ASCII-safe
|
|
151
|
+
const headerPart = raw.subarray(0, headerEnd).toString("utf-8");
|
|
152
|
+
let bodyBuf = raw.subarray(headerEnd + 4);
|
|
153
|
+
// Status line
|
|
154
|
+
const statusMatch = headerPart.match(/^HTTP\/\d\.\d\s+(\d+)/);
|
|
155
|
+
const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
|
156
|
+
// Parse headers
|
|
157
|
+
const headers = {};
|
|
158
|
+
for (const line of headerPart.split("\r\n").slice(1)) {
|
|
159
|
+
const colon = line.indexOf(":");
|
|
160
|
+
if (colon !== -1) {
|
|
161
|
+
headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Decode chunked transfer encoding (byte-level)
|
|
165
|
+
if (headers["transfer-encoding"]?.toLowerCase() === "chunked") {
|
|
166
|
+
bodyBuf = decodeChunked(bodyBuf);
|
|
167
|
+
}
|
|
168
|
+
// Validate Content-Length if present
|
|
169
|
+
const contentLength = headers["content-length"];
|
|
170
|
+
if (contentLength && !headers["transfer-encoding"]) {
|
|
171
|
+
const expected = parseInt(contentLength, 10);
|
|
172
|
+
if (!isNaN(expected) && bodyBuf.length < expected) {
|
|
173
|
+
return null; // Incomplete response
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { statusCode, headers, body: bodyBuf.toString("utf-8").trim() };
|
|
177
|
+
}
|
|
178
|
+
/** Decode a chunked HTTP body. Operates on Buffer for byte-correct slicing. */
|
|
179
|
+
function decodeChunked(raw) {
|
|
180
|
+
const parts = [];
|
|
181
|
+
let pos = 0;
|
|
182
|
+
while (pos < raw.length) {
|
|
183
|
+
const lineEnd = raw.indexOf(CRLF, pos);
|
|
184
|
+
if (lineEnd === -1)
|
|
185
|
+
break;
|
|
186
|
+
// Chunk size line — parseInt stops at non-hex chars (handles chunk extensions per RFC 7230)
|
|
187
|
+
const sizeStr = raw.subarray(pos, lineEnd).toString("ascii").trim();
|
|
188
|
+
const size = parseInt(sizeStr, 16);
|
|
189
|
+
if (isNaN(size) || size === 0)
|
|
190
|
+
break;
|
|
191
|
+
const chunkStart = lineEnd + 2;
|
|
192
|
+
if (chunkStart + size > raw.length)
|
|
193
|
+
break; // Incomplete chunk
|
|
194
|
+
parts.push(raw.subarray(chunkStart, chunkStart + size));
|
|
195
|
+
pos = chunkStart + size + 2; // skip chunk data + \r\n
|
|
196
|
+
}
|
|
197
|
+
return Buffer.concat(parts);
|
|
198
|
+
}
|
|
199
|
+
/** Settle a forward() promise from parsed HTTP response or raw chunks. */
|
|
200
|
+
function settleFromChunks(chunks, hasId, resolve, reject) {
|
|
201
|
+
if (chunks.length === 0) {
|
|
202
|
+
if (hasId) {
|
|
203
|
+
reject(new Error("Empty response from Airmail"));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
resolve("");
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const raw = Buffer.concat(chunks);
|
|
211
|
+
const parsed = parseHttpResponse(raw);
|
|
212
|
+
if (!parsed) {
|
|
213
|
+
if (hasId) {
|
|
214
|
+
reject(new Error("Incomplete or malformed HTTP response from Airmail"));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
resolve("");
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (parsed.statusCode === 202) {
|
|
222
|
+
resolve("");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (parsed.statusCode >= 400) {
|
|
226
|
+
// Truncate body in error message to avoid leaking sensitive data
|
|
227
|
+
const safeBody = parsed.body.length > 200 ? parsed.body.slice(0, 200) + "..." : parsed.body;
|
|
228
|
+
reject(new Error(`Airmail HTTP ${parsed.statusCode}: ${safeBody}`));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
resolve(parsed.body);
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Raw TCP HTTP forwarding
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
/**
|
|
237
|
+
* Send an HTTP POST via raw TCP socket and return the response body.
|
|
238
|
+
*
|
|
239
|
+
* Airmail's NWListener calls connection.cancel() in the send completion handler,
|
|
240
|
+
* which means the TCP FIN can arrive before Node.js http module finishes parsing.
|
|
241
|
+
* Using raw sockets, we collect all data until the connection closes, then parse
|
|
242
|
+
* the HTTP response ourselves.
|
|
243
|
+
*/
|
|
244
|
+
function forward(body, clientName, token, hasId) {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
const chunks = [];
|
|
247
|
+
let settled = false;
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
if (!settled) {
|
|
250
|
+
settled = true;
|
|
251
|
+
sock.destroy();
|
|
252
|
+
reject(new Error("Request timed out"));
|
|
253
|
+
}
|
|
254
|
+
}, REQUEST_TIMEOUT_MS);
|
|
255
|
+
function finish(err) {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
if (settled)
|
|
258
|
+
return;
|
|
259
|
+
settled = true;
|
|
260
|
+
if (err && chunks.length === 0) {
|
|
261
|
+
reject(err);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// If we have an error but also data, the connection may have closed
|
|
265
|
+
// after sending a complete response (expected with NWListener).
|
|
266
|
+
// Only trust the data if it parses as valid HTTP.
|
|
267
|
+
if (err && chunks.length > 0) {
|
|
268
|
+
const raw = Buffer.concat(chunks);
|
|
269
|
+
const parsed = parseHttpResponse(raw);
|
|
270
|
+
if (!parsed) {
|
|
271
|
+
reject(new Error(`Connection error with partial response: ${err.message}`));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
settleFromChunks(chunks, hasId, resolve, reject);
|
|
276
|
+
}
|
|
277
|
+
const sock = net.createConnection({ host: AIRMAIL_HOST, port: AIRMAIL_PORT }, () => {
|
|
278
|
+
// Build and send raw HTTP request as a single write
|
|
279
|
+
const bodyBuf = Buffer.from(body, "utf-8");
|
|
280
|
+
const safeClient = sanitizeHeaderValue(clientName);
|
|
281
|
+
let reqHeaders = `POST ${AIRMAIL_PATH} HTTP/1.1\r\n`;
|
|
282
|
+
reqHeaders += `Host: ${AIRMAIL_HOST}:${AIRMAIL_PORT}\r\n`;
|
|
283
|
+
reqHeaders += `Content-Type: application/json\r\n`;
|
|
284
|
+
reqHeaders += `Content-Length: ${bodyBuf.length}\r\n`;
|
|
285
|
+
reqHeaders += `Accept: application/json\r\n`;
|
|
286
|
+
reqHeaders += `Connection: close\r\n`;
|
|
287
|
+
reqHeaders += `User-Agent: airmail-mcp/1.0\r\n`;
|
|
288
|
+
if (token) {
|
|
289
|
+
reqHeaders += `Authorization: Bearer ${token}\r\n`;
|
|
290
|
+
}
|
|
291
|
+
reqHeaders += `X-MCP-Client: ${safeClient}\r\n`;
|
|
292
|
+
if (parentCodeSignTeamID) {
|
|
293
|
+
reqHeaders += `X-MCP-CodeSign: ${sanitizeHeaderValue(parentCodeSignTeamID)}\r\n`;
|
|
294
|
+
}
|
|
295
|
+
reqHeaders += `\r\n`;
|
|
296
|
+
// Single atomic write to avoid backpressure issues
|
|
297
|
+
sock.write(Buffer.concat([Buffer.from(reqHeaders), bodyBuf]));
|
|
298
|
+
});
|
|
299
|
+
sock.on("data", (chunk) => chunks.push(chunk));
|
|
300
|
+
sock.on("end", () => finish());
|
|
301
|
+
sock.on("error", (err) => finish(err));
|
|
302
|
+
sock.on("close", () => finish());
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// stdio ↔ HTTP bridge
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
/** Client identity for X-MCP-Client header. Updated from initialize clientInfo. */
|
|
309
|
+
let resolvedClientName = "airmail-mcp/1.0";
|
|
310
|
+
/** Whether initialize has been sent and completed. */
|
|
311
|
+
let initialized = false;
|
|
312
|
+
/** Queue of messages waiting for initialize to complete. */
|
|
313
|
+
let pendingAfterInit = [];
|
|
314
|
+
async function processMessage(line) {
|
|
315
|
+
let parsed;
|
|
316
|
+
try {
|
|
317
|
+
parsed = JSON.parse(line);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
log(`Invalid JSON: ${line.slice(0, 200)}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const hasId = parsed.id !== undefined;
|
|
324
|
+
// Extract client identity from initialize for X-MCP-Client header
|
|
325
|
+
if (parsed.method === "initialize" && parsed.params) {
|
|
326
|
+
const ci = parsed.params.clientInfo;
|
|
327
|
+
if (ci?.name) {
|
|
328
|
+
resolvedClientName = ci.version ? `${ci.name}/${ci.version}` : ci.name;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const response = await forward(line, resolvedClientName, currentToken, hasId);
|
|
333
|
+
if (response) {
|
|
334
|
+
process.stdout.write(response + "\n");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
339
|
+
// Re-read token from Keychain on 401 and retry once
|
|
340
|
+
if (msg.includes("HTTP 401")) {
|
|
341
|
+
if (!process.env.AIRMAIL_MCP_TOKEN) {
|
|
342
|
+
const newToken = readTokenFromKeychain();
|
|
343
|
+
if (newToken && newToken !== currentToken) {
|
|
344
|
+
log("Token rotated in Keychain, retrying with new token.");
|
|
345
|
+
currentToken = newToken;
|
|
346
|
+
try {
|
|
347
|
+
const response = await forward(line, resolvedClientName, currentToken, hasId);
|
|
348
|
+
if (response) {
|
|
349
|
+
process.stdout.write(response + "\n");
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// Fall through to error handling
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
log("Authentication failed (HTTP 401). The auth token is missing or invalid.\n" +
|
|
359
|
+
" \u2192 Open Airmail \u2192 Preferences \u2192 MCP and copy the current Auth Token\n" +
|
|
360
|
+
" \u2192 Set it as: export AIRMAIL_MCP_TOKEN=\"your-token-here\"\n" +
|
|
361
|
+
" \u2192 Or approve the macOS Keychain access prompt if it appears.");
|
|
362
|
+
}
|
|
363
|
+
if (hasId) {
|
|
364
|
+
// Sanitize error message — don't forward raw server responses that may contain tokens
|
|
365
|
+
const safeMsg = msg.length > 300 ? msg.slice(0, 300) + "..." : msg;
|
|
366
|
+
process.stdout.write(JSON.stringify({
|
|
367
|
+
jsonrpc: "2.0", id: parsed.id,
|
|
368
|
+
error: { code: -32000, message: safeMsg },
|
|
369
|
+
}) + "\n");
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
log(`Notification error: ${msg}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function main() {
|
|
377
|
+
log(`airmail-mcp v${VERSION} starting (Node.js ${process.version}, pid ${process.pid})`);
|
|
378
|
+
if (process.platform !== "darwin") {
|
|
379
|
+
log("Airmail MCP is macOS-only.");
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
// Resolve auth token — done inside main() so logs are captured
|
|
383
|
+
const envToken = (process.env.AIRMAIL_MCP_TOKEN ?? "").trim();
|
|
384
|
+
// Skip env var if empty or unresolved template placeholder
|
|
385
|
+
const isValidEnvToken = envToken.length > 0
|
|
386
|
+
&& !envToken.startsWith("${")
|
|
387
|
+
&& envToken !== "undefined"
|
|
388
|
+
&& envToken !== "null";
|
|
389
|
+
if (isValidEnvToken) {
|
|
390
|
+
currentToken = envToken;
|
|
391
|
+
log(`Auth token provided via AIRMAIL_MCP_TOKEN (${envToken.length} chars).`);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
if (envToken)
|
|
395
|
+
log(`AIRMAIL_MCP_TOKEN ignored (placeholder: "${envToken.slice(0, 20)}...").`);
|
|
396
|
+
log("Trying macOS Keychain...");
|
|
397
|
+
currentToken = readTokenFromKeychain();
|
|
398
|
+
}
|
|
399
|
+
if (!currentToken) {
|
|
400
|
+
log("WARNING: no auth token found. Requests will fail with 401.\n" +
|
|
401
|
+
" 1. Open Airmail \u2192 Preferences \u2192 MCP and copy the Auth Token\n" +
|
|
402
|
+
" 2. Set it as: export AIRMAIL_MCP_TOKEN=\"your-token-here\"\n" +
|
|
403
|
+
" Or approve the macOS Keychain prompt when it appears.");
|
|
404
|
+
}
|
|
405
|
+
resolveParentCodeSign();
|
|
406
|
+
await ensureAirmailRunning();
|
|
407
|
+
log(`Bridge ready \u2014 Airmail MCP at ${AIRMAIL_HOST}:${AIRMAIL_PORT} (token: ${currentToken ? "present" : "MISSING"})`);
|
|
408
|
+
// Handle stdout errors (broken pipe)
|
|
409
|
+
process.stdout.on("error", (err) => {
|
|
410
|
+
if (err.code === "EPIPE") {
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
log(`stdout error: ${err.message}`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
});
|
|
416
|
+
// Graceful shutdown — drain inflight requests before exiting
|
|
417
|
+
let shuttingDown = false;
|
|
418
|
+
const shutdown = () => {
|
|
419
|
+
if (shuttingDown)
|
|
420
|
+
return;
|
|
421
|
+
shuttingDown = true;
|
|
422
|
+
if (inflight.size === 0) {
|
|
423
|
+
process.exit(0);
|
|
424
|
+
}
|
|
425
|
+
log(`Shutting down, waiting for ${inflight.size} inflight request(s)...`);
|
|
426
|
+
// Force exit after 5 seconds if inflight requests don't complete
|
|
427
|
+
setTimeout(() => { process.exit(0); }, 5000).unref();
|
|
428
|
+
};
|
|
429
|
+
process.on("SIGINT", shutdown);
|
|
430
|
+
process.on("SIGTERM", shutdown);
|
|
431
|
+
process.on("SIGHUP", shutdown);
|
|
432
|
+
let buffer = "";
|
|
433
|
+
let stdinClosed = false;
|
|
434
|
+
const inflight = new Set();
|
|
435
|
+
function enqueue(line) {
|
|
436
|
+
const p = processMessage(line).catch((err) => log(`Error: ${err}`));
|
|
437
|
+
inflight.add(p);
|
|
438
|
+
p.finally(() => {
|
|
439
|
+
inflight.delete(p);
|
|
440
|
+
if ((stdinClosed || shuttingDown) && inflight.size === 0)
|
|
441
|
+
process.exit(0);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
process.stdin.setEncoding("utf-8");
|
|
445
|
+
process.stdin.on("data", (chunk) => {
|
|
446
|
+
if (shuttingDown)
|
|
447
|
+
return;
|
|
448
|
+
buffer += chunk;
|
|
449
|
+
// Protect against unbounded memory growth — drop only the oversized portion,
|
|
450
|
+
// preserving any trailing partial line for framing continuity
|
|
451
|
+
if (buffer.length > MAX_STDIN_BUFFER) {
|
|
452
|
+
log("stdin buffer exceeded 10 MB, dropping accumulated data.");
|
|
453
|
+
const lastNewline = buffer.lastIndexOf("\n");
|
|
454
|
+
if (lastNewline !== -1) {
|
|
455
|
+
// Keep the trailing partial line so framing stays aligned
|
|
456
|
+
buffer = buffer.slice(lastNewline + 1);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
buffer = "";
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
let idx;
|
|
464
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
465
|
+
const line = buffer.slice(0, idx).trim();
|
|
466
|
+
buffer = buffer.slice(idx + 1);
|
|
467
|
+
if (!line)
|
|
468
|
+
continue;
|
|
469
|
+
// Serialize initialize — queue other messages until it completes
|
|
470
|
+
if (!initialized) {
|
|
471
|
+
let parsed;
|
|
472
|
+
try {
|
|
473
|
+
parsed = JSON.parse(line);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (parsed.method === "initialize") {
|
|
479
|
+
const p = processMessage(line)
|
|
480
|
+
.then(() => {
|
|
481
|
+
initialized = true;
|
|
482
|
+
// Flush queued messages
|
|
483
|
+
for (const queued of pendingAfterInit) {
|
|
484
|
+
enqueue(queued);
|
|
485
|
+
}
|
|
486
|
+
pendingAfterInit = [];
|
|
487
|
+
})
|
|
488
|
+
.catch((err) => log(`Initialize error: ${err}`));
|
|
489
|
+
inflight.add(p);
|
|
490
|
+
p.finally(() => {
|
|
491
|
+
inflight.delete(p);
|
|
492
|
+
if ((stdinClosed || shuttingDown) && inflight.size === 0)
|
|
493
|
+
process.exit(0);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// Queue until initialize completes (notifications/initialized are fine to queue)
|
|
498
|
+
pendingAfterInit.push(line);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
enqueue(line);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
process.stdin.on("end", () => {
|
|
507
|
+
stdinClosed = true;
|
|
508
|
+
if (inflight.size === 0) {
|
|
509
|
+
log("stdin closed, exiting.");
|
|
510
|
+
process.exit(0);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
main().catch((err) => { log(`Fatal: ${err}`); process.exit(1); });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAE3B,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E,MAAM,YAAY,GAAG,WAAW,CAAC;AACjC,MAAM,YAAY,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;AAC1E,MAAM,YAAY,GAAG,MAAM,CAAC;AAC5B,IAAI,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,qBAAqB,EAAE,CAAC;AAE5E,MAAM,cAAc,GAAG,IAAI,CAAC;AAC5B,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAC7B,MAAM,kBAAkB,GAAG,OAAO,CAAC;AACnC,MAAM,gBAAgB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,+BAA+B;AAE1E,gEAAgE;AAChE,IAAI,oBAAoB,GAAkB,IAAI,CAAC;AAC/C,SAAS,qBAAqB;IAC5B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC1B,6BAA6B;QAC7B,MAAM,UAAU,GAAG,QAAQ,CAAC,SAAS,IAAI,WAAW,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpF,IAAI,CAAC,UAAU;YAAE,OAAO;QAExB,uCAAuC;QACvC,IAAI,OAAO,GAAG,UAAU,CAAC;QACzB,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;QAC5C,CAAC;QAED,+BAA+B;QAC/B,MAAM,OAAO,GAAG,QAAQ,CAAC,6BAA6B,OAAO,QAAQ,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;QAC9F,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACpD,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;YAC1D,oBAAoB,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAChC,GAAG,CAAC,6BAA6B,oBAAoB,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,4DAA4D;IAC9D,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,GAAG,CAAC,GAAW;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,qBAAqB;IAC5B,IAAI,CAAC;QACH,OAAO,QAAQ,CACb,+FAA+F,EAC/F,EAAE,QAAQ,EAAE,OAAO,EAAE,CACtB,CAAC,IAAI,EAAE,CAAC;IACX,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,IAAI;IACX,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE;YACjF,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QACvC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,oBAAoB;IACjC,IAAI,MAAM,IAAI,EAAE;QAAE,OAAO;IACzB,GAAG,CAAC,wDAAwD,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,QAAQ,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,CAAC,4CAA4C,CAAC,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,kBAAkB,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC;QAC5B,IAAI,MAAM,IAAI,EAAE,EAAE,CAAC;YAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;YAAC,OAAO;QAAC,CAAC;IACpE,CAAC;IACD,GAAG,CAAC,mFAAmF,CAAC,CAAC;IACzF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAYD,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,GAAW;IACpC,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,SAAS,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC3C,IAAI,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAEpC,cAAc;IACd,MAAM,WAAW,GAAG,UAAU,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC9D,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAElE,gBAAgB;IAChB,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC;IAED,mCAAmC;IACnC,IAAI,OAAO,CAAC,mBAAmB,CAAC,EAAE,WAAW,EAAE,KAAK,SAAS,EAAE,CAAC;QAC9D,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;AACpD,CAAC;AAED,kCAAkC;AAClC,SAAS,aAAa,CAAC,GAAW;IAChC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,OAAO,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACzC,IAAI,OAAO,KAAK,CAAC,CAAC;YAAE,MAAM;QAC1B,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACnC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC;YAAE,MAAM;QACrC,MAAM,UAAU,GAAG,OAAO,GAAG,CAAC,CAAC;QAC/B,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC,CAAC;QACnD,GAAG,GAAG,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,yBAAyB;IACxD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,0EAA0E;AAC1E,SAAS,gBAAgB,CACvB,MAAgB,EAChB,OAA4B,EAC5B,MAA0B;IAE1B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IAEjD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAEtC,IAAI,CAAC,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IACrC,IAAI,MAAM,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IACvD,IAAI,MAAM,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC;QAC7B,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB,MAAM,CAAC,UAAU,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACvE,OAAO;IACT,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACvB,CAAC;AAED,8EAA8E;AAC9E,0BAA0B;AAC1B,8EAA8E;AAE9E;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,IAAY,EAAE,UAAkB,EAAE,KAAa;IAC9D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,OAAO,EAAE,CAAC;gBAAC,OAAO,GAAG,IAAI,CAAC;gBAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAAC,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;YAAC,CAAC;QAC3F,CAAC,EAAE,kBAAkB,CAAC,CAAC;QAEvB,SAAS,MAAM,CAAC,GAAW;YACzB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,GAAG,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAAC,OAAO;YAAC,CAAC;YACxD,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAC5C,CAAC;QAED,MAAM,IAAI,GAAG,GAAG,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE;YACjF,oDAAoD;YACpD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC3C,IAAI,UAAU,GAAG,QAAQ,YAAY,eAAe,CAAC;YACrD,UAAU,IAAI,SAAS,YAAY,IAAI,YAAY,MAAM,CAAC;YAC1D,UAAU,IAAI,oCAAoC,CAAC;YACnD,UAAU,IAAI,mBAAmB,OAAO,CAAC,MAAM,MAAM,CAAC;YACtD,UAAU,IAAI,8BAA8B,CAAC;YAC7C,UAAU,IAAI,uBAAuB,CAAC;YACtC,UAAU,IAAI,iCAAiC,CAAC;YAChD,IAAI,KAAK,EAAE,CAAC;gBACV,UAAU,IAAI,yBAAyB,KAAK,MAAM,CAAC;YACrD,CAAC;YACD,UAAU,IAAI,iBAAiB,UAAU,MAAM,CAAC;YAChD,IAAI,oBAAoB,EAAE,CAAC;gBACzB,UAAU,IAAI,mBAAmB,oBAAoB,MAAM,CAAC;YAC9D,CAAC;YACD,UAAU,IAAI,MAAM,CAAC;YAErB,mDAAmD;YACnD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACvC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E,mFAAmF;AACnF,IAAI,kBAAkB,GAAG,iBAAiB,CAAC;AAE3C,KAAK,UAAU,cAAc,CAAC,IAAY;IACxC,IAAI,MAA2E,CAAC;IAChF,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,CAAC,iBAAiB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3C,OAAO;IACT,CAAC;IAED,kEAAkE;IAClE,IAAI,MAAM,CAAC,MAAM,KAAK,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QACpD,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,UAA6D,CAAC;QACvF,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC;YACb,kBAAkB,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC;QACzE,CAAC;IACH,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,kBAAkB,EAAE,YAAY,CAAC,CAAC;QAEvE,qDAAqD;QACrD,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;YACzC,qDAAqD;QACvD,CAAC;QAED,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAE7D,oDAAoD;QACpD,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,CAAC;YAC/D,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAC;YACzC,IAAI,QAAQ,IAAI,QAAQ,KAAK,YAAY,EAAE,CAAC;gBAC1C,GAAG,CAAC,yCAAyC,CAAC,CAAC;gBAC/C,YAAY,GAAG,QAAQ,CAAC;gBACxB,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,kBAAkB,EAAE,YAAY,CAAC,CAAC;oBACvE,IAAI,QAAQ,EAAE,CAAC;wBAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;oBAAC,CAAC;oBACxD,OAAO;gBACT,CAAC;gBAAC,OAAO,QAAQ,EAAE,CAAC;oBAClB,iCAAiC;gBACnC,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,MAAM,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;gBAClC,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE;gBAC7B,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE;aACtC,CAAC,GAAG,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,uBAAuB,GAAG,EAAE,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAClC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,qBAAqB,EAAE,CAAC;IACxB,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,GAAG,CAAC,qFAAqF,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,oBAAoB,EAAE,CAAC;IAC7B,GAAG,CAAC,iCAAiC,YAAY,IAAI,YAAY,EAAE,CAAC,CAAC;IAErE,qCAAqC;IACrC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QACjC,IAAK,GAA6B,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,GAAG,CAAC,iBAAiB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,oBAAoB;IACpB,MAAM,QAAQ,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAE/B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAE1C,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACnC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;QACzC,MAAM,IAAI,KAAK,CAAC;QAEhB,0CAA0C;QAC1C,IAAI,MAAM,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;YACrC,GAAG,CAAC,wCAAwC,CAAC,CAAC;YAC9C,MAAM,GAAG,EAAE,CAAC;YACZ,OAAO;QACT,CAAC;QAED,IAAI,GAAW,CAAC;QAChB,OAAO,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;YACzC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;YAC/B,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,CAAC,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC;gBACpE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAChB,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;oBACb,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;oBACnB,IAAI,WAAW,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;wBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC1D,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;QAC3B,WAAW,GAAG,IAAI,CAAC;QACnB,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,GAAG,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
package/logo.png
ADDED
|
Binary file
|
package/manifest.json
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "0.3",
|
|
3
|
+
"name": "airmail-mcp",
|
|
4
|
+
"display_name": "Airmail",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Manage emails, calendars, contacts, and more from Claude using Airmail's MCP server.",
|
|
7
|
+
"long_description": "Airmail MCP connects Claude to the Airmail email client for macOS. Read, search, compose, and organize emails. Manage calendars and reminders. Search contacts. The bridge connects locally to Airmail on your Mac. Data retrieved by AI tools is processed by your chosen AI provider.",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Airmail",
|
|
10
|
+
"email": "contact@airmailapp.com",
|
|
11
|
+
"url": "https://airmailapp.com"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/Airmail/airmail-mcp.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://airmailapp.com",
|
|
18
|
+
"documentation": "https://github.com/Airmail/airmail-mcp#readme",
|
|
19
|
+
"support": "https://github.com/Airmail/airmail-mcp/issues",
|
|
20
|
+
"icon": "logo.png",
|
|
21
|
+
"server": {
|
|
22
|
+
"type": "node",
|
|
23
|
+
"entry_point": "dist/index.js",
|
|
24
|
+
"mcp_config": {
|
|
25
|
+
"command": "node",
|
|
26
|
+
"args": [
|
|
27
|
+
"${__dirname}/dist/index.js"
|
|
28
|
+
],
|
|
29
|
+
"env": {
|
|
30
|
+
"AIRMAIL_MCP_TOKEN": "${user_config.auth_token}",
|
|
31
|
+
"AIRMAIL_MCP_PORT": "${user_config.port}"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"tools_generated": true,
|
|
36
|
+
"tools": [
|
|
37
|
+
{
|
|
38
|
+
"name": "manage_capabilities",
|
|
39
|
+
"description": "Enable/disable tool groups to manage context."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "list_accounts",
|
|
43
|
+
"description": "List email accounts."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "list_folders",
|
|
47
|
+
"description": "List folders for an account."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "list_messages",
|
|
51
|
+
"description": "List messages in a folder (date desc)."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "get_message",
|
|
55
|
+
"description": "Get full message: body (plain text), recipients, attachments, subject, date, labels."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "list_inbox",
|
|
59
|
+
"description": "List inbox messages (date desc)."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "list_starred",
|
|
63
|
+
"description": "List starred/flagged messages."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "list_sent",
|
|
67
|
+
"description": "List sent messages."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "list_trash",
|
|
71
|
+
"description": "List trash messages."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "list_spam",
|
|
75
|
+
"description": "List spam/junk messages."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "search_messages",
|
|
79
|
+
"description": "Search emails with hybrid FTS + semantic engine."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "fetch_message_body",
|
|
83
|
+
"description": "Trigger server body download."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "list_attachments",
|
|
87
|
+
"description": "List message attachments."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "get_unread_counts",
|
|
91
|
+
"description": "Unread count per account inbox."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "search_contacts",
|
|
95
|
+
"description": "Search contacts/recent senders by name or email."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "get_draft",
|
|
99
|
+
"description": "Get draft content: body, recipients, attachments."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "delete_draft",
|
|
103
|
+
"description": "Permanently delete a draft."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "get_message_thread",
|
|
107
|
+
"description": "Get all messages in a conversation thread (date asc)."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "get_attachment",
|
|
111
|
+
"description": "Get the content of a specific attachment."
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "list_windows",
|
|
115
|
+
"description": "List open Airmail windows (composers, readers)."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "export_eml",
|
|
119
|
+
"description": "Export message as .eml (RFC 822) file saved to disk (returns file_path)."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "semantic_search",
|
|
123
|
+
"description": "Search by meaning via vector embeddings (macOS 14+)."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "semantic_index_status",
|
|
127
|
+
"description": "Check/trigger semantic index."
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "mark_messages",
|
|
131
|
+
"description": "Apply flags."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "archive_messages",
|
|
135
|
+
"description": "Move messages to archive."
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "trash_messages",
|
|
139
|
+
"description": "Move messages to trash."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "move_messages",
|
|
143
|
+
"description": "Move messages to a folder."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "copy_messages",
|
|
147
|
+
"description": "Copy messages to a folder (originals stay)."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "snooze_messages",
|
|
151
|
+
"description": "Snooze messages."
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"name": "add_to_list",
|
|
155
|
+
"description": "Add messages to Todo, Memo, or Done list."
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "delete_messages",
|
|
159
|
+
"description": "Permanently delete."
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "empty_folder",
|
|
163
|
+
"description": "Empty trash or spam across all accounts."
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "refresh_inbox",
|
|
167
|
+
"description": "Fetch new emails from server."
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "enable_disable_account",
|
|
171
|
+
"description": "Enable or disable an email account."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "share_icloud",
|
|
175
|
+
"description": "Upload a file to iCloud Drive and return a public share link."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "compose_email",
|
|
179
|
+
"description": "Open compose window pre-filled."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "reply_to_message",
|
|
183
|
+
"description": "Open reply window."
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"name": "forward_message",
|
|
187
|
+
"description": "Open forward window."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "quick_reply",
|
|
191
|
+
"description": "SENDS IMMEDIATELY."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"name": "send_email",
|
|
195
|
+
"description": "SENDS IMMEDIATELY without compose window."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "save_as_draft",
|
|
199
|
+
"description": "Save email as draft without opening compose window."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "list_drafts",
|
|
203
|
+
"description": "List drafts."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "create_folder",
|
|
207
|
+
"description": "Create a new folder."
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"name": "rename_folder",
|
|
211
|
+
"description": "Rename a folder."
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "delete_folder",
|
|
215
|
+
"description": "Delete a custom folder."
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "analyze_email_history",
|
|
219
|
+
"description": "Scan sent folder to build behavior profile (tone, contacts, topics)."
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"name": "batch_triage_inbox",
|
|
223
|
+
"description": "Inbox messages with suggested actions (reply/archive/star/defer) and priority scores."
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "get_user_profile",
|
|
227
|
+
"description": "Get user behavior profile: reply patterns, contacts, templates."
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "update_user_profile",
|
|
231
|
+
"description": "Update profile: custom instructions, reply tone/length, add/remove templates."
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "suggest_folder",
|
|
235
|
+
"description": "Suggest folder for a message based on learned move patterns."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "get_behavior_stats",
|
|
239
|
+
"description": "Behavior tracker stats: total actions, top sender→folder and domain rules."
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"name": "list_calendars",
|
|
243
|
+
"description": "List calendars (IDs, titles, types)."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"name": "list_events",
|
|
247
|
+
"description": "List events in date range (default next 7 days)."
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "get_event",
|
|
251
|
+
"description": "Get event details: attendees, alarms, recurrence, notes."
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"name": "create_event",
|
|
255
|
+
"description": "Create calendar event."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"name": "update_event",
|
|
259
|
+
"description": "Update event."
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"name": "delete_event",
|
|
263
|
+
"description": "Delete event."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "list_reminders",
|
|
267
|
+
"description": "List reminders."
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"name": "create_reminder",
|
|
271
|
+
"description": "Create reminder."
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"name": "complete_reminder",
|
|
275
|
+
"description": "Mark reminder completed/uncompleted."
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"name": "delete_reminder",
|
|
279
|
+
"description": "Delete reminder."
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"name": "list_contacts_book",
|
|
283
|
+
"description": "List address book contacts."
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"name": "get_contact",
|
|
287
|
+
"description": "Get full contact details."
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"name": "search_contacts_book",
|
|
291
|
+
"description": "Search contacts by name or email."
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"name": "create_contact",
|
|
295
|
+
"description": "Create a contact."
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"name": "update_contact",
|
|
299
|
+
"description": "Update contact."
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"name": "delete_contact",
|
|
303
|
+
"description": "Delete a contact."
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"name": "list_contact_groups",
|
|
307
|
+
"description": "List contact groups."
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"name": "list_vips",
|
|
311
|
+
"description": "List VIP senders."
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"name": "add_vip",
|
|
315
|
+
"description": "Add a sender to VIP list."
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"name": "remove_vip",
|
|
319
|
+
"description": "Remove a sender from VIP list."
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"name": "list_blocked",
|
|
323
|
+
"description": "List blocked senders."
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"name": "add_blocked",
|
|
327
|
+
"description": "Block a sender."
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"name": "remove_blocked",
|
|
331
|
+
"description": "Unblock a sender."
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"name": "get_preferences",
|
|
335
|
+
"description": "Read app preferences."
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "set_preferences",
|
|
339
|
+
"description": "Modify app preferences."
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"name": "list_rules",
|
|
343
|
+
"description": "List all email rules with name, guid, enabled, conditions/actions summary, direction."
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"name": "get_rule",
|
|
347
|
+
"description": "Get full details of a rule: conditions, actions, shortcut, order."
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"name": "create_rule",
|
|
351
|
+
"description": "Create a new email rule."
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"name": "delete_rule",
|
|
355
|
+
"description": "Delete a rule by GUID."
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"name": "toggle_rule",
|
|
359
|
+
"description": "Enable or disable a rule."
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"name": "list_signatures",
|
|
363
|
+
"description": "List email signatures."
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"name": "create_signature",
|
|
367
|
+
"description": "Create a new email signature for an account."
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"name": "update_signature",
|
|
371
|
+
"description": "Update a signature name, HTML body, or default status."
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"name": "delete_signature",
|
|
375
|
+
"description": "Delete a signature."
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"name": "list_smart_folders",
|
|
379
|
+
"description": "List smart folders with uuid, name, search query."
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"name": "create_smart_folder",
|
|
383
|
+
"description": "Create a smart folder with a search query (same syntax as search_messages)."
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"name": "update_smart_folder",
|
|
387
|
+
"description": "Update a smart folder name and/or search query."
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"name": "delete_smart_folder",
|
|
391
|
+
"description": "Delete a smart folder."
|
|
392
|
+
}
|
|
393
|
+
],
|
|
394
|
+
"keywords": [
|
|
395
|
+
"email",
|
|
396
|
+
"mail",
|
|
397
|
+
"calendar",
|
|
398
|
+
"contacts",
|
|
399
|
+
"airmail",
|
|
400
|
+
"macos",
|
|
401
|
+
"productivity"
|
|
402
|
+
],
|
|
403
|
+
"license": "MIT",
|
|
404
|
+
"user_config": {
|
|
405
|
+
"auth_token": {
|
|
406
|
+
"type": "string",
|
|
407
|
+
"title": "Auth Token",
|
|
408
|
+
"description": "Bearer token from Airmail Preferences > MCP. If not set, the bridge will try to read it from the macOS Keychain (requires user approval).",
|
|
409
|
+
"sensitive": true,
|
|
410
|
+
"required": false
|
|
411
|
+
},
|
|
412
|
+
"port": {
|
|
413
|
+
"type": "string",
|
|
414
|
+
"title": "MCP Server Port",
|
|
415
|
+
"description": "Port number for Airmail's local MCP server. Only change if you modified the default port in Airmail.",
|
|
416
|
+
"required": false,
|
|
417
|
+
"default": "9876"
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
"compatibility": {
|
|
421
|
+
"claude_desktop": ">=0.10.0",
|
|
422
|
+
"platforms": [
|
|
423
|
+
"darwin"
|
|
424
|
+
],
|
|
425
|
+
"runtimes": {
|
|
426
|
+
"node": ">=18.0.0"
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
"privacy_policies": [
|
|
430
|
+
"https://airmailapp.com/privacy"
|
|
431
|
+
]
|
|
432
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "airmail-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"mcpName": "io.github.airmail/airmail-mcp",
|
|
5
|
+
"description": "Manage emails, calendars, contacts, and more from Claude using Airmail's MCP server.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"airmail-mcp": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"manifest.json",
|
|
14
|
+
"logo.png",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"watch": "tsc --watch",
|
|
20
|
+
"sync-tools": "node scripts/sync-tools.mjs",
|
|
21
|
+
"prepublishOnly": "npm run sync-tools && npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"airmail",
|
|
26
|
+
"email",
|
|
27
|
+
"calendar",
|
|
28
|
+
"contacts",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"claude",
|
|
31
|
+
"ai",
|
|
32
|
+
"macos"
|
|
33
|
+
],
|
|
34
|
+
"author": "Airmail <support@airmailapp.com>",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.10.0",
|
|
39
|
+
"typescript": "^5.3.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"os": [
|
|
45
|
+
"darwin"
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/Airmail/airmail-mcp.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://airmailapp.com",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/Airmail/airmail-mcp/issues"
|
|
54
|
+
}
|
|
55
|
+
}
|