apple-tools-mcp 2.1.5 → 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 +77 -2
- 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/processMode.js +42 -1
- package/package.json +16 -8
package/index.js
CHANGED
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
6
|
import {
|
|
6
7
|
CallToolRequestSchema,
|
|
7
8
|
ListToolsRequestSchema,
|
|
8
9
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import http from "http";
|
|
9
11
|
import fs from "fs";
|
|
10
12
|
import path from "path";
|
|
11
13
|
import { validateEmailPath, stripHtmlTags, unfoldRfc822Headers, validateLimit, validateDaysBack, validateWeekOffset, toUnixMillis } from "./lib/validators.js";
|
|
12
14
|
import { cycleEndFlags, indexUnavailableMessage, indexQueryGate } from "./lib/indexGate.js";
|
|
13
|
-
import { isIndexerMode, isPermissionsMode } from "./lib/processMode.js";
|
|
15
|
+
import { isIndexerMode, isPermissionsMode, isHttpMode, isHttpTokenMode } from "./lib/processMode.js";
|
|
14
16
|
import { runPermissionsCommand } from "./lib/permissions.js";
|
|
15
|
-
import { loadResolvedIndexInterval, logResolvedInterval } from "./lib/config.js";
|
|
17
|
+
import { loadResolvedIndexInterval, logResolvedInterval, resolveHttpServerConfig } from "./lib/config.js";
|
|
18
|
+
import { loadOrCreateHttpAuthToken, verifyAuthHeader } from "./lib/httpAuth.js";
|
|
19
|
+
import { createHttpRequestHandler } from "./lib/httpTransport.js";
|
|
16
20
|
import { createIndexerLock, DEFAULT_LOCK_HEARTBEAT_MS } from "./lib/indexerLock.js";
|
|
17
21
|
import {
|
|
18
22
|
shouldConnectMcpStdio,
|
|
@@ -40,8 +44,13 @@ const PACKAGE_VERSION = JSON.parse(
|
|
|
40
44
|
|
|
41
45
|
// Canonical indexer entrypoint: `node index.js --mode=indexer` or `apple-tools-indexer`.
|
|
42
46
|
// Permissions CLI: `apple-tools-mcp permissions` — short-lived, no MCP / indexer.
|
|
47
|
+
// HTTP transport: `node index.js --transport=http` or `apple-tools-http` — same
|
|
48
|
+
// tools as stdio, but bearer-token authenticated (see lib/httpAuth.js).
|
|
49
|
+
// Token CLI: `apple-tools-mcp http-token` — short-lived, prints the token.
|
|
43
50
|
const PERMISSIONS_MODE = isPermissionsMode();
|
|
44
51
|
const INDEXER_MODE = isIndexerMode();
|
|
52
|
+
const HTTP_MODE = isHttpMode();
|
|
53
|
+
const HTTP_TOKEN_MODE = isHttpTokenMode();
|
|
45
54
|
const resolvedIndexInterval = loadResolvedIndexInterval();
|
|
46
55
|
const INDEX_INTERVAL = resolvedIndexInterval.ms;
|
|
47
56
|
const LOCK_HEARTBEAT_MS = DEFAULT_LOCK_HEARTBEAT_MS;
|
|
@@ -153,9 +162,10 @@ process.on("unhandledRejection", (reason, promise) => {
|
|
|
153
162
|
shutdownIndexing(1);
|
|
154
163
|
});
|
|
155
164
|
|
|
156
|
-
// MCP stdio clients exit when the host closes stdin. The indexer daemon
|
|
157
|
-
//
|
|
158
|
-
|
|
165
|
+
// MCP stdio clients exit when the host closes stdin. The indexer daemon,
|
|
166
|
+
// HTTP server, and short-lived CLIs must not — LaunchAgent / KeepAlive
|
|
167
|
+
// often attaches stdin to /dev/null, and the CLIs exit on their own.
|
|
168
|
+
bindStdinCloseExit(process.stdin, INDEXER_MODE || PERMISSIONS_MODE || HTTP_MODE || HTTP_TOKEN_MODE, () => {
|
|
159
169
|
console.error("Client disconnected. Exiting.");
|
|
160
170
|
shutdownIndexing(0);
|
|
161
171
|
});
|
|
@@ -458,6 +468,18 @@ if (PERMISSIONS_MODE) {
|
|
|
458
468
|
console.error(`Permissions command error: ${e.message}`);
|
|
459
469
|
process.exit(1);
|
|
460
470
|
});
|
|
471
|
+
} else if (HTTP_TOKEN_MODE) {
|
|
472
|
+
try {
|
|
473
|
+
// Token itself goes to stdout only, so `apple-tools-mcp http-token` is
|
|
474
|
+
// scriptable (`$(apple-tools-mcp http-token)`). Any one-time generation
|
|
475
|
+
// banner still goes to stderr from loadOrCreateHttpAuthToken() itself.
|
|
476
|
+
const { token } = loadOrCreateHttpAuthToken();
|
|
477
|
+
console.log(token);
|
|
478
|
+
process.exit(0);
|
|
479
|
+
} catch (e) {
|
|
480
|
+
console.error(`Could not read/create HTTP auth token: ${e.message}`);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
461
483
|
} else {
|
|
462
484
|
initializeIndexing().catch((e) => {
|
|
463
485
|
console.error(`Indexing startup failed: ${e.message}`);
|
|
@@ -1036,561 +1058,566 @@ function formatPersonSearchResults(results) {
|
|
|
1036
1058
|
|
|
1037
1059
|
// ============ MCP SERVER SETUP ============
|
|
1038
1060
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1061
|
+
function createServer() {
|
|
1062
|
+
const server = new Server(
|
|
1063
|
+
{ name: "apple-tools-mcp", version: PACKAGE_VERSION },
|
|
1064
|
+
{ capabilities: { tools: {} } }
|
|
1065
|
+
);
|
|
1066
|
+
|
|
1067
|
+
// Define available tools
|
|
1068
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1069
|
+
tools: [
|
|
1070
|
+
// ============ SMART SEARCH (AGENTIC) ============
|
|
1071
|
+
{
|
|
1072
|
+
name: "smart_search",
|
|
1073
|
+
description: "Intelligent search across Mail, Messages, and Calendar. Automatically determines which sources to search based on your query. Returns results grouped by time when multiple sources match. Use this for complex queries that might span multiple data sources.",
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
type: "object",
|
|
1076
|
+
properties: {
|
|
1077
|
+
query: { type: "string", description: "Natural language search query (e.g., 'meeting with John', 'budget discussion', 'what happened yesterday')" },
|
|
1078
|
+
limit: { type: "number", description: "Max results per source (default 5)" },
|
|
1079
|
+
synthesize: { type: "boolean", description: "Group results by time proximity (default true)" }
|
|
1080
|
+
},
|
|
1081
|
+
required: ["query"],
|
|
1057
1082
|
},
|
|
1058
|
-
required: ["query"],
|
|
1059
1083
|
},
|
|
1060
|
-
},
|
|
1061
1084
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1085
|
+
// ============ EMAIL TOOLS ============
|
|
1086
|
+
{
|
|
1087
|
+
name: "mail_search",
|
|
1088
|
+
description: "Semantic search for emails using AI embeddings. Finds emails by meaning, not just keywords. Supports filtering by sender, recipient, attachments, mailbox, sent/received, and flagged.",
|
|
1089
|
+
inputSchema: {
|
|
1090
|
+
type: "object",
|
|
1091
|
+
properties: {
|
|
1092
|
+
query: { type: "string", description: "Natural language search (e.g., 'invoices', 'meeting notes', 'from John about project')" },
|
|
1093
|
+
limit: { type: "number", description: "Maximum results (default 30)" },
|
|
1094
|
+
days_back: { type: "number", description: "Only emails from last N days (0 = all time)" },
|
|
1095
|
+
sender: { type: "string", description: "Filter by sender name or email address" },
|
|
1096
|
+
recipient: { type: "string", description: "Filter by recipient name or email address" },
|
|
1097
|
+
has_attachment: { type: "boolean", description: "Filter to only emails with attachments (true) or without (false)" },
|
|
1098
|
+
mailbox: { type: "string", description: "Filter by mailbox name (e.g., 'INBOX', 'Archive', 'Sent Messages')" },
|
|
1099
|
+
sent_only: { type: "boolean", description: "true = only sent emails, false = only received emails, omit for all" },
|
|
1100
|
+
flagged_only: { type: "boolean", description: "Only show flagged/starred emails" },
|
|
1101
|
+
include_junk: { type: "boolean", description: "Include emails from Junk/Trash folders (excluded by default)" },
|
|
1102
|
+
sort_by: { type: "string", enum: ["relevance", "date"], description: "Sort by relevance (default) or date (newest first)" }
|
|
1103
|
+
},
|
|
1104
|
+
required: ["query"],
|
|
1080
1105
|
},
|
|
1081
|
-
required: ["query"],
|
|
1082
1106
|
},
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1107
|
+
{
|
|
1108
|
+
name: "mail_recent",
|
|
1109
|
+
description: "Get most recent emails without semantic search. Use this when the user asks for 'recent emails', 'latest emails', 'what emails did I get', or 'unread emails'.",
|
|
1110
|
+
inputSchema: {
|
|
1111
|
+
type: "object",
|
|
1112
|
+
properties: {
|
|
1113
|
+
limit: { type: "number", description: "Maximum results (default 30)" },
|
|
1114
|
+
days_back: { type: "number", description: "Only emails from last N days (default 7)" },
|
|
1115
|
+
unread_only: { type: "boolean", description: "Only show unread emails (queries Mail.app for read status)" },
|
|
1116
|
+
include_junk: { type: "boolean", description: "Include emails from Junk/Trash folders (excluded by default)" }
|
|
1117
|
+
},
|
|
1094
1118
|
},
|
|
1095
1119
|
},
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1120
|
+
{
|
|
1121
|
+
name: "mail_date",
|
|
1122
|
+
description: "Get all emails from a specific date. Supports natural language like 'today', 'yesterday', 'November 13', 'last Friday'. Use this when the user asks for emails on a specific date.",
|
|
1123
|
+
inputSchema: {
|
|
1124
|
+
type: "object",
|
|
1125
|
+
properties: {
|
|
1126
|
+
date: { type: "string", description: "Date to retrieve emails (e.g., 'today', 'yesterday', 'Nov 13', '2025-01-15')" },
|
|
1127
|
+
include_junk: { type: "boolean", description: "Include emails from Junk/Trash folders (excluded by default)" }
|
|
1128
|
+
},
|
|
1129
|
+
required: ["date"],
|
|
1105
1130
|
},
|
|
1106
|
-
required: ["date"],
|
|
1107
1131
|
},
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1132
|
+
{
|
|
1133
|
+
name: "mail_read",
|
|
1134
|
+
description: "Read full email content. Use the file_path from mail_search or mail_recent results.",
|
|
1135
|
+
inputSchema: {
|
|
1136
|
+
type: "object",
|
|
1137
|
+
properties: {
|
|
1138
|
+
file_path: { type: "string", description: "File path from mail_search results" },
|
|
1139
|
+
},
|
|
1140
|
+
required: ["file_path"],
|
|
1116
1141
|
},
|
|
1117
|
-
required: ["file_path"],
|
|
1118
1142
|
},
|
|
1119
|
-
},
|
|
1120
1143
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1144
|
+
// ============ MESSAGES TOOLS ============
|
|
1145
|
+
{
|
|
1146
|
+
name: "messages_search",
|
|
1147
|
+
description: "Semantic search for iMessages/SMS using AI embeddings. Finds messages by meaning. Supports filtering by contact, group chats, specific group name, and attachments.",
|
|
1148
|
+
inputSchema: {
|
|
1149
|
+
type: "object",
|
|
1150
|
+
properties: {
|
|
1151
|
+
query: { type: "string", description: "Natural language search (e.g., 'dinner plans', 'about the trip', 'address')" },
|
|
1152
|
+
limit: { type: "number", description: "Maximum results (default 30)" },
|
|
1153
|
+
days_back: { type: "number", description: "Only messages from last N days (0 = all time)" },
|
|
1154
|
+
contact: { type: "string", description: "Filter by contact name or phone number" },
|
|
1155
|
+
group_chat_only: { type: "boolean", description: "Only show messages from group chats" },
|
|
1156
|
+
group_chat_name: { type: "string", description: "Filter by specific group chat name" },
|
|
1157
|
+
has_attachment: { type: "boolean", description: "Filter to messages with attachments (photos, files)" },
|
|
1158
|
+
sort_by: { type: "string", enum: ["relevance", "date"], description: "Sort by relevance (default) or date (newest first)" }
|
|
1159
|
+
},
|
|
1160
|
+
required: ["query"],
|
|
1136
1161
|
},
|
|
1137
|
-
required: ["query"],
|
|
1138
1162
|
},
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1163
|
+
{
|
|
1164
|
+
name: "messages_recent",
|
|
1165
|
+
description: "Get most recent messages without semantic search. Use this when the user asks for 'recent messages', 'latest texts', or 'what messages did I get'.",
|
|
1166
|
+
inputSchema: {
|
|
1167
|
+
type: "object",
|
|
1168
|
+
properties: {
|
|
1169
|
+
limit: { type: "number", description: "Maximum results (default 30)" },
|
|
1170
|
+
days_back: { type: "number", description: "Only messages from last N days (default 1)" }
|
|
1171
|
+
},
|
|
1148
1172
|
},
|
|
1149
1173
|
},
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1174
|
+
{
|
|
1175
|
+
name: "messages_conversation",
|
|
1176
|
+
description: "Get full conversation history with a specific contact. Shows messages in chronological order.",
|
|
1177
|
+
inputSchema: {
|
|
1178
|
+
type: "object",
|
|
1179
|
+
properties: {
|
|
1180
|
+
contact: { type: "string", description: "Contact name or phone number" },
|
|
1181
|
+
limit: { type: "number", description: "Maximum messages to return (default 50)" }
|
|
1182
|
+
},
|
|
1183
|
+
required: ["contact"],
|
|
1159
1184
|
},
|
|
1160
|
-
required: ["contact"],
|
|
1161
1185
|
},
|
|
1162
|
-
},
|
|
1163
1186
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1187
|
+
// ============ CALENDAR TOOLS ============
|
|
1188
|
+
{
|
|
1189
|
+
name: "calendar_search",
|
|
1190
|
+
description: "Semantic search for calendar events using AI embeddings. Finds events by meaning. Supports filtering by calendar name and all-day events.",
|
|
1191
|
+
inputSchema: {
|
|
1192
|
+
type: "object",
|
|
1193
|
+
properties: {
|
|
1194
|
+
query: { type: "string", description: "Natural language search (e.g., 'meetings', 'doctor appointments', 'lunch')" },
|
|
1195
|
+
limit: { type: "number", description: "Maximum results (default 30)" },
|
|
1196
|
+
days_back: { type: "number", description: "Include events from last N days (0 = none)" },
|
|
1197
|
+
days_ahead: { type: "number", description: "Include events in next N days (0 = none). Use for 'today', 'this week', etc." },
|
|
1198
|
+
calendar_name: { type: "string", description: "Filter to specific calendar (e.g., 'Work', 'Personal')" },
|
|
1199
|
+
all_day_only: { type: "boolean", description: "Only show all-day events" },
|
|
1200
|
+
sort_by: { type: "string", enum: ["relevance", "date"], description: "Sort by relevance (default) or date (chronological)" }
|
|
1201
|
+
},
|
|
1202
|
+
required: ["query"],
|
|
1178
1203
|
},
|
|
1179
|
-
required: ["query"],
|
|
1180
1204
|
},
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1205
|
+
{
|
|
1206
|
+
name: "calendar_date",
|
|
1207
|
+
description: "Get all events on a specific date. Supports natural language dates like 'today', 'tomorrow', 'next Tuesday', 'Jan 15'.",
|
|
1208
|
+
inputSchema: {
|
|
1209
|
+
type: "object",
|
|
1210
|
+
properties: {
|
|
1211
|
+
date: { type: "string", description: "Date to check (e.g., 'today', 'tomorrow', 'next Monday', '2025-01-15')" }
|
|
1212
|
+
},
|
|
1213
|
+
required: ["date"],
|
|
1189
1214
|
},
|
|
1190
|
-
required: ["date"],
|
|
1191
1215
|
},
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1216
|
+
{
|
|
1217
|
+
name: "calendar_free_time",
|
|
1218
|
+
description: "Find free time slots on a specific date. Analyzes calendar to find available time windows.",
|
|
1219
|
+
inputSchema: {
|
|
1220
|
+
type: "object",
|
|
1221
|
+
properties: {
|
|
1222
|
+
date: { type: "string", description: "Date to check (e.g., 'today', 'tomorrow', 'next Monday')" },
|
|
1223
|
+
start_hour: { type: "number", description: "Start of working hours (default 9 = 9 AM)" },
|
|
1224
|
+
end_hour: { type: "number", description: "End of working hours (default 17 = 5 PM)" },
|
|
1225
|
+
calendar_name: { type: "string", description: "Only consider events from this calendar" }
|
|
1226
|
+
},
|
|
1227
|
+
required: ["date"],
|
|
1203
1228
|
},
|
|
1204
|
-
required: ["date"],
|
|
1205
1229
|
},
|
|
1206
|
-
},
|
|
1207
1230
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1231
|
+
// ============ NEW TOOLS - PHASE 1 ============
|
|
1232
|
+
|
|
1233
|
+
// Mail tools
|
|
1234
|
+
{
|
|
1235
|
+
name: "mail_senders",
|
|
1236
|
+
description: "List most frequent email senders. Helps identify who you communicate with most.",
|
|
1237
|
+
inputSchema: {
|
|
1238
|
+
type: "object",
|
|
1239
|
+
properties: {
|
|
1240
|
+
limit: { type: "number", description: "Maximum senders to return (default 30)" },
|
|
1241
|
+
days_back: { type: "number", description: "Only count emails from last N days (0 = all time)" },
|
|
1242
|
+
include_junk: { type: "boolean", description: "Include senders from Junk/Trash folders (excluded by default)" }
|
|
1243
|
+
},
|
|
1220
1244
|
},
|
|
1221
1245
|
},
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1246
|
+
{
|
|
1247
|
+
name: "rebuild_index",
|
|
1248
|
+
description: "Rebuild the search index for one or more data sources. This clears the existing index and re-indexes all content from scratch. Use this if search results are stale, missing, or if the index is corrupted. Can rebuild emails, messages, calendar, or all sources at once.",
|
|
1249
|
+
inputSchema: {
|
|
1250
|
+
type: "object",
|
|
1251
|
+
properties: {
|
|
1252
|
+
sources: {
|
|
1253
|
+
type: "array",
|
|
1254
|
+
items: { type: "string", enum: ["emails", "messages", "calendar"] },
|
|
1255
|
+
description: "Which sources to rebuild. Defaults to all sources if not specified."
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1234
1258
|
},
|
|
1235
1259
|
},
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1260
|
+
{
|
|
1261
|
+
name: "audit_index",
|
|
1262
|
+
description: "Audit search index against source data with 0% tolerance. Reports missing items, orphaned entries, and duplicates with detailed file paths and remediation suggestions. Validates 100% of source data.",
|
|
1263
|
+
inputSchema: {
|
|
1264
|
+
type: "object",
|
|
1265
|
+
properties: {
|
|
1266
|
+
sources: {
|
|
1267
|
+
type: "array",
|
|
1268
|
+
items: { type: "string", enum: ["emails", "messages", "calendar"] },
|
|
1269
|
+
description: "Data sources to audit (default: all)"
|
|
1270
|
+
},
|
|
1271
|
+
max_items: {
|
|
1272
|
+
type: "number",
|
|
1273
|
+
description: "Max items to list per category (default: 100, use 0 for unlimited)"
|
|
1274
|
+
}
|
|
1247
1275
|
},
|
|
1248
|
-
max_items: {
|
|
1249
|
-
type: "number",
|
|
1250
|
-
description: "Max items to list per category (default: 100, use 0 for unlimited)"
|
|
1251
|
-
}
|
|
1252
1276
|
},
|
|
1253
1277
|
},
|
|
1254
|
-
},
|
|
1255
1278
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1279
|
+
// Messages tools
|
|
1280
|
+
{
|
|
1281
|
+
name: "messages_contacts",
|
|
1282
|
+
description: "List all contacts you've messaged, sorted by most recent. Shows message count and last message date.",
|
|
1283
|
+
inputSchema: {
|
|
1284
|
+
type: "object",
|
|
1285
|
+
properties: {
|
|
1286
|
+
limit: { type: "number", description: "Maximum contacts to return (default 50)" }
|
|
1287
|
+
},
|
|
1264
1288
|
},
|
|
1265
1289
|
},
|
|
1266
|
-
},
|
|
1267
1290
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1291
|
+
// Calendar tools
|
|
1292
|
+
{
|
|
1293
|
+
name: "calendar_upcoming",
|
|
1294
|
+
description: "Get next N upcoming events across all calendars. Simpler than calendar_search for quick schedule overview.",
|
|
1295
|
+
inputSchema: {
|
|
1296
|
+
type: "object",
|
|
1297
|
+
properties: {
|
|
1298
|
+
limit: { type: "number", description: "Maximum events to return (default 10)" }
|
|
1299
|
+
},
|
|
1276
1300
|
},
|
|
1277
1301
|
},
|
|
1278
|
-
},
|
|
1279
1302
|
|
|
1280
|
-
|
|
1303
|
+
// ============ NEW TOOLS - PHASE 2 ============
|
|
1281
1304
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1305
|
+
{
|
|
1306
|
+
name: "calendar_week",
|
|
1307
|
+
description: "Get all events for the current week or a future week. Shows events grouped by day.",
|
|
1308
|
+
inputSchema: {
|
|
1309
|
+
type: "object",
|
|
1310
|
+
properties: {
|
|
1311
|
+
week_offset: { type: "number", description: "0 = this week, 1 = next week, 2 = week after, etc. (default 0)" }
|
|
1312
|
+
},
|
|
1289
1313
|
},
|
|
1290
1314
|
},
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1315
|
+
// ============ NEW TOOLS - PHASE 3 ============
|
|
1316
|
+
|
|
1317
|
+
{
|
|
1318
|
+
name: "mail_thread",
|
|
1319
|
+
description: "Get all emails in a conversation thread. Finds related emails by matching subject lines.",
|
|
1320
|
+
inputSchema: {
|
|
1321
|
+
type: "object",
|
|
1322
|
+
properties: {
|
|
1323
|
+
file_path: { type: "string", description: "File path to any email in the thread" },
|
|
1324
|
+
limit: { type: "number", description: "Maximum emails to return (default 30)" }
|
|
1325
|
+
},
|
|
1326
|
+
required: ["file_path"],
|
|
1302
1327
|
},
|
|
1303
|
-
required: ["file_path"],
|
|
1304
1328
|
},
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1329
|
+
{
|
|
1330
|
+
name: "calendar_recurring",
|
|
1331
|
+
description: "List recurring events (events that appear multiple times). Shows upcoming occurrences.",
|
|
1332
|
+
inputSchema: {
|
|
1333
|
+
type: "object",
|
|
1334
|
+
properties: {
|
|
1335
|
+
limit: { type: "number", description: "Maximum recurring events to return (default 30)" }
|
|
1336
|
+
},
|
|
1313
1337
|
},
|
|
1314
1338
|
},
|
|
1315
|
-
},
|
|
1316
1339
|
|
|
1317
|
-
|
|
1340
|
+
// ============ CONTACTS TOOLS ============
|
|
1318
1341
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1342
|
+
{
|
|
1343
|
+
name: "contacts_search",
|
|
1344
|
+
description: "Search your contacts by name, email, phone, or organization. Returns matching contacts with all their details.",
|
|
1345
|
+
inputSchema: {
|
|
1346
|
+
type: "object",
|
|
1347
|
+
properties: {
|
|
1348
|
+
query: { type: "string", description: "Search query (e.g., 'John', 'Acme Corp', 'john@example.com')" },
|
|
1349
|
+
limit: { type: "number", description: "Maximum results (default 30)" }
|
|
1350
|
+
},
|
|
1351
|
+
required: ["query"],
|
|
1327
1352
|
},
|
|
1328
|
-
required: ["query"],
|
|
1329
1353
|
},
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1354
|
+
{
|
|
1355
|
+
name: "contacts_lookup",
|
|
1356
|
+
description: "Look up a specific contact by email, phone number, or name. Returns full contact details including all emails and phone numbers.",
|
|
1357
|
+
inputSchema: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
properties: {
|
|
1360
|
+
identifier: { type: "string", description: "Email address, phone number, or name to look up" }
|
|
1361
|
+
},
|
|
1362
|
+
required: ["identifier"],
|
|
1338
1363
|
},
|
|
1339
|
-
required: ["identifier"],
|
|
1340
1364
|
},
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1365
|
+
{
|
|
1366
|
+
name: "person_search",
|
|
1367
|
+
description: "Search ALL communication with a specific person across Mail, Messages, and Calendar. Automatically finds their emails and phone numbers from Contacts to search all sources.",
|
|
1368
|
+
inputSchema: {
|
|
1369
|
+
type: "object",
|
|
1370
|
+
properties: {
|
|
1371
|
+
name: { type: "string", description: "Person's name to search for (will resolve to all their email addresses and phone numbers)" },
|
|
1372
|
+
limit: { type: "number", description: "Maximum results per source (default 10)" }
|
|
1373
|
+
},
|
|
1374
|
+
required: ["name"],
|
|
1350
1375
|
},
|
|
1351
|
-
required: ["name"],
|
|
1352
1376
|
},
|
|
1353
|
-
},
|
|
1354
1377
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
}));
|
|
1378
|
+
// ============ WRITE TOOLS (2.0.0) ============
|
|
1379
|
+
// Mail / Messages / Calendar / Contacts writes with dry_run + confirm.
|
|
1380
|
+
...WRITE_TOOL_DEFINITIONS,
|
|
1381
|
+
],
|
|
1382
|
+
}));
|
|
1360
1383
|
|
|
1361
|
-
// Handle tool calls
|
|
1362
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
try {
|
|
1366
|
-
let result;
|
|
1367
|
-
|
|
1368
|
-
// Writes never wait on the vector index: they talk to Mail / Messages /
|
|
1369
|
-
// Calendar / Contacts directly and must keep working while the indexer
|
|
1370
|
-
// daemon holds the lock.
|
|
1371
|
-
if (isWriteTool(name)) {
|
|
1372
|
-
const writeResult = await dispatchWriteTool(name, args || {}, {
|
|
1373
|
-
indexerMode: INDEXER_MODE,
|
|
1374
|
-
socketPath: WRITE_SOCKET_PATH,
|
|
1375
|
-
log: (msg) => console.error(msg)
|
|
1376
|
-
});
|
|
1377
|
-
return mcpWriteResult(writeResult);
|
|
1378
|
-
}
|
|
1384
|
+
// Handle tool calls
|
|
1385
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1386
|
+
const { name, arguments: args } = request.params;
|
|
1379
1387
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
result = await mailSearch(args.query, {
|
|
1392
|
-
limit: validateLimit(args?.limit, 30),
|
|
1393
|
-
daysBack: validateDaysBack(args?.days_back),
|
|
1394
|
-
sender: args?.sender || null,
|
|
1395
|
-
recipient: args?.recipient || null,
|
|
1396
|
-
hasAttachment: args?.has_attachment ?? null,
|
|
1397
|
-
mailbox: args?.mailbox || null,
|
|
1398
|
-
sentOnly: args?.sent_only ?? null,
|
|
1399
|
-
flaggedOnly: args?.flagged_only || false,
|
|
1400
|
-
includeJunk: args?.include_junk || false,
|
|
1401
|
-
sortBy: args?.sort_by || "relevance"
|
|
1388
|
+
try {
|
|
1389
|
+
let result;
|
|
1390
|
+
|
|
1391
|
+
// Writes never wait on the vector index: they talk to Mail / Messages /
|
|
1392
|
+
// Calendar / Contacts directly and must keep working while the indexer
|
|
1393
|
+
// daemon holds the lock.
|
|
1394
|
+
if (isWriteTool(name)) {
|
|
1395
|
+
const writeResult = await dispatchWriteTool(name, args || {}, {
|
|
1396
|
+
indexerMode: INDEXER_MODE,
|
|
1397
|
+
socketPath: WRITE_SOCKET_PATH,
|
|
1398
|
+
log: (msg) => console.error(msg)
|
|
1402
1399
|
});
|
|
1403
|
-
|
|
1400
|
+
return mcpWriteResult(writeResult);
|
|
1401
|
+
}
|
|
1404
1402
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1403
|
+
switch (name) {
|
|
1404
|
+
// Smart search (agentic)
|
|
1405
|
+
case "smart_search":
|
|
1406
|
+
result = await smartSearch(args.query, {
|
|
1407
|
+
limit: validateLimit(args?.limit, 5, 100),
|
|
1408
|
+
synthesize: args?.synthesize !== false
|
|
1409
|
+
});
|
|
1410
|
+
break;
|
|
1413
1411
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1412
|
+
// Email tools
|
|
1413
|
+
case "mail_search":
|
|
1414
|
+
result = await mailSearch(args.query, {
|
|
1415
|
+
limit: validateLimit(args?.limit, 30),
|
|
1416
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1417
|
+
sender: args?.sender || null,
|
|
1418
|
+
recipient: args?.recipient || null,
|
|
1419
|
+
hasAttachment: args?.has_attachment ?? null,
|
|
1420
|
+
mailbox: args?.mailbox || null,
|
|
1421
|
+
sentOnly: args?.sent_only ?? null,
|
|
1422
|
+
flaggedOnly: args?.flagged_only || false,
|
|
1423
|
+
includeJunk: args?.include_junk || false,
|
|
1424
|
+
sortBy: args?.sort_by || "relevance"
|
|
1425
|
+
});
|
|
1426
|
+
break;
|
|
1417
1427
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1428
|
+
case "mail_recent":
|
|
1429
|
+
result = await mailRecent(
|
|
1430
|
+
validateLimit(args?.limit, 30),
|
|
1431
|
+
validateDaysBack(args?.days_back) || 7,
|
|
1432
|
+
args?.unread_only || false,
|
|
1433
|
+
args?.include_junk || false
|
|
1434
|
+
);
|
|
1435
|
+
break;
|
|
1421
1436
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
limit: validateLimit(args?.limit, 30),
|
|
1426
|
-
daysBack: validateDaysBack(args?.days_back),
|
|
1427
|
-
contact: args?.contact || null,
|
|
1428
|
-
groupChatOnly: args?.group_chat_only || false,
|
|
1429
|
-
groupChatName: args?.group_chat_name || null,
|
|
1430
|
-
hasAttachment: args?.has_attachment ?? null,
|
|
1431
|
-
sortBy: args?.sort_by || "relevance"
|
|
1432
|
-
});
|
|
1433
|
-
break;
|
|
1437
|
+
case "mail_date":
|
|
1438
|
+
result = await mailDate(args.date, args?.include_junk || false);
|
|
1439
|
+
break;
|
|
1434
1440
|
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
validateDaysBack(args?.days_back) || 1
|
|
1439
|
-
);
|
|
1440
|
-
break;
|
|
1441
|
+
case "mail_read":
|
|
1442
|
+
result = readFullEmail(args.file_path);
|
|
1443
|
+
break;
|
|
1441
1444
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
+
// Messages tools
|
|
1446
|
+
case "messages_search":
|
|
1447
|
+
result = await messagesSearch(args.query, {
|
|
1448
|
+
limit: validateLimit(args?.limit, 30),
|
|
1449
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1450
|
+
contact: args?.contact || null,
|
|
1451
|
+
groupChatOnly: args?.group_chat_only || false,
|
|
1452
|
+
groupChatName: args?.group_chat_name || null,
|
|
1453
|
+
hasAttachment: args?.has_attachment ?? null,
|
|
1454
|
+
sortBy: args?.sort_by || "relevance"
|
|
1455
|
+
});
|
|
1456
|
+
break;
|
|
1445
1457
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
calendarName: args?.calendar_name || null,
|
|
1453
|
-
allDayOnly: args?.all_day_only || false,
|
|
1454
|
-
sortBy: args?.sort_by || "relevance"
|
|
1455
|
-
});
|
|
1456
|
-
break;
|
|
1458
|
+
case "messages_recent":
|
|
1459
|
+
result = await messagesRecent(
|
|
1460
|
+
validateLimit(args?.limit, 30),
|
|
1461
|
+
validateDaysBack(args?.days_back) || 1
|
|
1462
|
+
);
|
|
1463
|
+
break;
|
|
1457
1464
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1465
|
+
case "messages_conversation":
|
|
1466
|
+
result = await messagesConversation(args.contact, validateLimit(args?.limit, 50));
|
|
1467
|
+
break;
|
|
1461
1468
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
+
// Calendar tools
|
|
1470
|
+
case "calendar_search":
|
|
1471
|
+
result = await calendarSearch(args.query, {
|
|
1472
|
+
limit: validateLimit(args?.limit, 30),
|
|
1473
|
+
daysBack: validateDaysBack(args?.days_back),
|
|
1474
|
+
daysAhead: validateDaysBack(args?.days_ahead),
|
|
1475
|
+
calendarName: args?.calendar_name || null,
|
|
1476
|
+
allDayOnly: args?.all_day_only || false,
|
|
1477
|
+
sortBy: args?.sort_by || "relevance"
|
|
1478
|
+
});
|
|
1479
|
+
break;
|
|
1469
1480
|
|
|
1470
|
-
|
|
1481
|
+
case "calendar_date":
|
|
1482
|
+
result = await calendarDate(args.date);
|
|
1483
|
+
break;
|
|
1471
1484
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
break;
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
result = formatSendersResults(await getFrequentSenders(
|
|
1482
|
-
validateLimit(args?.limit, 30),
|
|
1483
|
-
validateDaysBack(args?.days_back),
|
|
1484
|
-
args?.include_junk || false
|
|
1485
|
-
));
|
|
1486
|
-
break;
|
|
1487
|
-
|
|
1488
|
-
case "rebuild_index":
|
|
1489
|
-
// Check if indexing is already in progress in this session
|
|
1490
|
-
if (indexingInProgress) {
|
|
1491
|
-
result = "Indexing is already in progress. Please wait for it to complete before starting a rebuild.";
|
|
1485
|
+
case "calendar_free_time":
|
|
1486
|
+
result = await calendarFreeTime(args.date, {
|
|
1487
|
+
startHour: args?.start_hour || 9,
|
|
1488
|
+
endHour: args?.end_hour || 17,
|
|
1489
|
+
calendarName: args?.calendar_name || null
|
|
1490
|
+
});
|
|
1492
1491
|
break;
|
|
1493
|
-
}
|
|
1494
1492
|
|
|
1495
|
-
//
|
|
1496
|
-
|
|
1497
|
-
|
|
1493
|
+
// ============ NEW TOOLS - PHASE 1 ============
|
|
1494
|
+
|
|
1495
|
+
// Mail tools
|
|
1496
|
+
case "mail_senders":
|
|
1497
|
+
{
|
|
1498
|
+
const blocked = await requireIndex("emails");
|
|
1499
|
+
if (blocked) {
|
|
1500
|
+
result = blocked;
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
result = formatSendersResults(await getFrequentSenders(
|
|
1505
|
+
validateLimit(args?.limit, 30),
|
|
1506
|
+
validateDaysBack(args?.days_back),
|
|
1507
|
+
args?.include_junk || false
|
|
1508
|
+
));
|
|
1498
1509
|
break;
|
|
1499
|
-
}
|
|
1500
1510
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
),
|
|
1514
|
-
errors: rebuildResult.errors.length
|
|
1515
|
-
}));
|
|
1516
|
-
}).catch(e => {
|
|
1517
|
-
console.error("Index rebuild error:", e.message);
|
|
1518
|
-
applyCycleEnd(false);
|
|
1519
|
-
});
|
|
1511
|
+
case "rebuild_index":
|
|
1512
|
+
// Check if indexing is already in progress in this session
|
|
1513
|
+
if (indexingInProgress) {
|
|
1514
|
+
result = "Indexing is already in progress. Please wait for it to complete before starting a rebuild.";
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Acquire lock to prevent parallel rebuilds across multiple MCP instances
|
|
1519
|
+
if (!acquireLock()) {
|
|
1520
|
+
result = "Indexing is already in progress in a different session. Please wait for it to complete before starting a rebuild.";
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1520
1523
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1524
|
+
// Start rebuild in background and return immediately
|
|
1525
|
+
indexingInProgress = true;
|
|
1526
|
+
sessionIndexComplete = false;
|
|
1527
|
+
const rebuildSources = args?.sources || ["emails", "messages", "calendar"];
|
|
1528
|
+
|
|
1529
|
+
// Fire and forget - don't await
|
|
1530
|
+
rebuildIndex(rebuildSources).then((rebuildResult) => {
|
|
1531
|
+
applyCycleEnd(true);
|
|
1532
|
+
console.error("Index rebuild completed:", JSON.stringify({
|
|
1533
|
+
cleared: rebuildResult.cleared,
|
|
1534
|
+
indexed: Object.fromEntries(
|
|
1535
|
+
Object.entries(rebuildResult.indexed).map(([k, v]) => [k, v?.added || 0])
|
|
1536
|
+
),
|
|
1537
|
+
errors: rebuildResult.errors.length
|
|
1538
|
+
}));
|
|
1539
|
+
}).catch(e => {
|
|
1540
|
+
console.error("Index rebuild error:", e.message);
|
|
1541
|
+
applyCycleEnd(false);
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
result = `🔄 Index rebuild started for: ${rebuildSources.join(", ")}.\n\nThis runs in the background and may take several minutes for large mailboxes. You can continue using other tools - searches will use the new index once complete.`;
|
|
1545
|
+
break;
|
|
1523
1546
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1547
|
+
case "audit_index":
|
|
1548
|
+
{
|
|
1549
|
+
const auditSources = args?.sources || ["emails", "messages", "calendar"];
|
|
1550
|
+
const maxItems = args?.max_items !== undefined ? args.max_items : 100;
|
|
1528
1551
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1552
|
+
console.error(`Starting audit for: ${auditSources.join(", ")}`);
|
|
1553
|
+
const auditResults = await auditAll({ sources: auditSources, maxItems });
|
|
1554
|
+
result = formatAuditReport(auditResults);
|
|
1555
|
+
}
|
|
1556
|
+
break;
|
|
1534
1557
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1558
|
+
// Messages tools
|
|
1559
|
+
case "messages_contacts":
|
|
1560
|
+
result = formatMessageContactsResults(getMessageContacts(validateLimit(args?.limit, 50, 500)));
|
|
1561
|
+
break;
|
|
1539
1562
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1563
|
+
// Calendar tools
|
|
1564
|
+
case "calendar_upcoming":
|
|
1565
|
+
result = formatUpcomingEventsResults(getUpcomingEvents(validateLimit(args?.limit, 30, 100)));
|
|
1566
|
+
break;
|
|
1544
1567
|
|
|
1545
|
-
|
|
1568
|
+
// ============ NEW TOOLS - PHASE 2 ============
|
|
1546
1569
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1570
|
+
case "calendar_week":
|
|
1571
|
+
result = formatWeekEventsResults(getWeekEvents(validateWeekOffset(args?.week_offset)));
|
|
1572
|
+
break;
|
|
1550
1573
|
|
|
1551
|
-
|
|
1574
|
+
// ============ NEW TOOLS - PHASE 3 ============
|
|
1552
1575
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1576
|
+
case "mail_thread":
|
|
1577
|
+
{
|
|
1578
|
+
const blocked = await requireIndex("emails");
|
|
1579
|
+
if (blocked) {
|
|
1580
|
+
result = blocked;
|
|
1581
|
+
break;
|
|
1582
|
+
}
|
|
1559
1583
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
break;
|
|
1584
|
+
result = formatEmailThreadResults(await getEmailThread(args.file_path, validateLimit(args?.limit, 30)));
|
|
1585
|
+
break;
|
|
1563
1586
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1587
|
+
case "calendar_recurring":
|
|
1588
|
+
result = formatRecurringEventsResults(getRecurringEvents(validateLimit(args?.limit, 30, 100)));
|
|
1589
|
+
break;
|
|
1567
1590
|
|
|
1568
|
-
|
|
1591
|
+
// ============ CONTACTS TOOLS ============
|
|
1592
|
+
|
|
1593
|
+
case "contacts_search":
|
|
1594
|
+
result = formatContactsSearchResults(searchContacts(args.query, validateLimit(args?.limit, 30)));
|
|
1595
|
+
break;
|
|
1569
1596
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1597
|
+
case "contacts_lookup":
|
|
1598
|
+
result = formatContactLookupResult(lookupContact(args.identifier));
|
|
1599
|
+
break;
|
|
1573
1600
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1601
|
+
case "person_search":
|
|
1602
|
+
result = await personSearch(args.name, validateLimit(args?.limit, 10));
|
|
1603
|
+
break;
|
|
1577
1604
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1605
|
+
default:
|
|
1606
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1607
|
+
}
|
|
1581
1608
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1609
|
+
return { content: [{ type: "text", text: result }] };
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
return {
|
|
1612
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
1613
|
+
isError: true,
|
|
1614
|
+
};
|
|
1584
1615
|
}
|
|
1616
|
+
});
|
|
1617
|
+
return server;
|
|
1618
|
+
}
|
|
1585
1619
|
|
|
1586
|
-
|
|
1587
|
-
} catch (error) {
|
|
1588
|
-
return {
|
|
1589
|
-
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
1590
|
-
isError: true,
|
|
1591
|
-
};
|
|
1592
|
-
}
|
|
1593
|
-
});
|
|
1620
|
+
const server = createServer();
|
|
1594
1621
|
|
|
1595
1622
|
// Start the server
|
|
1596
1623
|
async function main() {
|
|
@@ -1601,6 +1628,45 @@ async function main() {
|
|
|
1601
1628
|
// fallback on this stdio process only if indexer.lock is free.
|
|
1602
1629
|
}
|
|
1603
1630
|
|
|
1604
|
-
|
|
1631
|
+
/**
|
|
1632
|
+
* Read-only-vs-write is unchanged from stdio mode — every tool the stdio
|
|
1633
|
+
* server exposes is exposed here too, including the write tools. What
|
|
1634
|
+
* changes is reachability (LAN / Tailscale instead of only this process's
|
|
1635
|
+
* parent) and, because of that, every request must carry a bearer token
|
|
1636
|
+
* (see lib/httpAuth.js) — stdio has no equivalent check because a locally
|
|
1637
|
+
* spawned child process is already trusted by whoever spawned it.
|
|
1638
|
+
*
|
|
1639
|
+
* A fresh Server + transport pair is created per request rather than
|
|
1640
|
+
* reusing the module-level `server`: the SDK's StreamableHTTPServerTransport
|
|
1641
|
+
* in stateless mode (sessionIdGenerator: undefined) is single-use — the
|
|
1642
|
+
* Protocol class throws "Already connected to a transport" on a second
|
|
1643
|
+
* connect() before the first has closed, which a shared instance would hit
|
|
1644
|
+
* under any concurrent access. This matches the SDK's own stateless example.
|
|
1645
|
+
*/
|
|
1646
|
+
async function startHttpServer(host, port) {
|
|
1647
|
+
const { token } = loadOrCreateHttpAuthToken();
|
|
1648
|
+
|
|
1649
|
+
const httpServer = http.createServer(createHttpRequestHandler({
|
|
1650
|
+
token,
|
|
1651
|
+
verifyAuthHeader,
|
|
1652
|
+
createServer,
|
|
1653
|
+
StreamableHTTPServerTransport,
|
|
1654
|
+
packageVersion: PACKAGE_VERSION
|
|
1655
|
+
}));
|
|
1656
|
+
|
|
1657
|
+
await new Promise((resolve) => {
|
|
1658
|
+
httpServer.listen(port, host, resolve);
|
|
1659
|
+
});
|
|
1660
|
+
console.error(`Apple Tools MCP HTTP server (v${PACKAGE_VERSION}) listening on http://${host}:${port}/mcp`);
|
|
1661
|
+
return httpServer;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
if (HTTP_MODE) {
|
|
1665
|
+
const { host, port } = resolveHttpServerConfig();
|
|
1666
|
+
startHttpServer(host, port).catch((e) => {
|
|
1667
|
+
console.error(`HTTP server failed to start: ${e.message}`);
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
});
|
|
1670
|
+
} else if (!PERMISSIONS_MODE && !HTTP_TOKEN_MODE && shouldConnectMcpStdio(INDEXER_MODE)) {
|
|
1605
1671
|
main().catch(console.error);
|
|
1606
1672
|
}
|