@mgcrea/mcp-apple-messages 1.3.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,9 @@ That is a capability downgrade reported through `diagnostics`, never a throw.
68
68
 
69
69
  Read: `diagnostics`, `list_chats`, `list_messages`, `search_messages`, `get_message`.
70
70
 
71
+ Opt-in read: `find_codes`, behind `APPLE_MESSAGES_ALLOW_CODES` — see [Configuration](#configuration).
72
+ Off by default, and not covered by the write gate.
73
+
71
74
  Write: `send_message`, and that is the whole dictionary. `sdef` lists three commands — `send`,
72
75
  `login` and `logout` — and the other two would sign the user out of iMessage on every device they
73
76
  own. There is no edit, delete, mark-as-read or reaction verb to expose, so **everything this server
@@ -110,8 +113,23 @@ records exactly what that leaves open; the safe way to measure it is a message t
110
113
  | `APPLE_MESSAGES_DEFAULT_RANGE_DAYS` | `30` | Window when only a start is given. |
111
114
  | `APPLE_MESSAGES_MAX_RESULTS` | `50` | Default page size. |
112
115
  | `APPLE_MESSAGES_ALLOW_WRITES` | off | Register `send_message` at all. |
116
+ | `APPLE_MESSAGES_ALLOW_CODES` | off | Register `find_codes` at all. |
113
117
  | `APPLE_MESSAGES_SEND_RECONCILE_MS` | `5000` | How long to wait for the sent row. |
114
118
 
119
+ `APPLE_MESSAGES_ALLOW_CODES` gates `find_codes`, which extracts one-time 2FA codes from recently
120
+ received messages. It is a **read**, and it is deliberately not folded into `ALLOW_WRITES`: reaching
121
+ a read through the write gate would mean granting the right to send a message in order to get it.
122
+
123
+ It is gated at all because of what it combines with. This server already holds the conversation
124
+ history and `@mgcrea/mcp-apple-mail` holds the inbox — between them, the password-_reset_ channel.
125
+ Adding live authentication codes completes an account-takeover primitive out of parts that were each
126
+ individually reasonable, so it defaults off and the two gates are independent in both directions.
127
+
128
+ Codes are matched by signal rather than by a `\d{4,8}` regex, and every result carries a
129
+ `confidence` and a `matched` saying how it was found; anything below `high` should be checked
130
+ against the message body before use. See `src/client/codes.ts`, and `docs/passwords.md` in the
131
+ repo for why the Passwords app itself is unreachable and this is what ships in its place.
132
+
115
133
  ## Notes that will bite you
116
134
 
117
135
  - **Dates do not fit in a JavaScript number.** Every date column is nanoseconds since 2001 —
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { A as MESSAGES_SURFACE, V as BUILD_INFO, a as loadConfig, r as createServer } from "./server-Bp7LsHS8.js";
2
+ import { A as MESSAGES_SURFACE, V as BUILD_INFO, a as loadConfig, r as createServer } from "./server-C_7IK_LY.js";
3
3
  import { runStdioServer } from "@mgcrea/mcp-apple-core";
4
4
  //#region src/cli.ts
5
5
  const LOG_PREFIX = "apple-messages-mcp";
package/dist/index.d.ts CHANGED
@@ -40,6 +40,7 @@ declare const ConfigSchema: z.ZodObject<{
40
40
  attachmentDir: z.ZodDefault<z.ZodString>;
41
41
  defaultRangeDays: z.ZodDefault<z.ZodNumber>;
42
42
  sendReconcileMs: z.ZodDefault<z.ZodNumber>;
43
+ allowCodes: z.ZodDefault<z.ZodBoolean>;
43
44
  }, z.core.$strict>;
44
45
  type Config = z.infer<typeof ConfigSchema>;
45
46
  declare const loadConfig: (env?: NodeJS.ProcessEnv) => Config;
@@ -788,6 +789,11 @@ type ToolContext = {
788
789
  * survives it intact.
789
790
  */
790
791
  allowWrites: boolean;
792
+ /**
793
+ * Gates `find_codes` alone, and is independent of `allowWrites` on purpose —
794
+ * see `config.ts` for why a read got a switch of its own.
795
+ */
796
+ allowCodes: boolean;
791
797
  };
792
798
  /**
793
799
  * Register the Apple Messages tools.
@@ -796,9 +802,17 @@ type ToolContext = {
796
802
  * surface the flag carries a permission claim as well as a safety one: with it
797
803
  * off no Apple Event is ever sent, so no Automation grant is ever requested.
798
804
  * What is needed either way is Full Disk Access, absolutely — see `diagnostics`.
805
+ * One further read, `find_codes`, only when `allowCodes` is on.
806
+ *
807
+ * The registered set is a pure function of STATIC CONFIGURATION, never of
808
+ * runtime state. In particular it does NOT vary with whether the store is
809
+ * readable: MCP clients cache the tool list, so a set that shrank when a grant
810
+ * was missing would stay shrunk after the grant arrived.
799
811
  *
800
- * The registered set does NOT vary with whether the store is readable. That is a
801
- * runtime condition, and MCP clients cache the tool list.
812
+ * That invariant used to read "a pure function of `allowWrites` and nothing
813
+ * else". `allowCodes` widened the input without weakening the guarantee, which
814
+ * was always about runtime conditions rather than about there being exactly one
815
+ * flag. `test/tools.test.ts` asserts each arm separately.
802
816
  */
803
817
  declare const registerTools: (server: McpServer, client: AppleMessagesClient, ctx: ToolContext) => void;
804
818
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/build-info.ts","../src/config.ts","../src/client/locate.ts","../src/client/store.ts","../src/client/messages.ts","../src/client/dates.ts","../src/client/errors.ts","../src/client/ref.ts","../src/client/typedstream.ts","../src/client/jxa/core.ts","../src/client/jxa/write.ts","../src/server.ts","../src/tools/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/build-info.ts","../src/config.ts","../src/client/locate.ts","../src/client/store.ts","../src/client/messages.ts","../src/client/dates.ts","../src/client/errors.ts","../src/client/ref.ts","../src/client/typedstream.ts","../src/client/jxa/core.ts","../src/client/jxa/write.ts","../src/server.ts","../src/tools/index.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as MESSAGES_SURFACE, B as toAppleSeconds, C as locateStore, D as ChatNotFoundError, E as AppleMessagesError, F as SendTargetNotFoundError, I as CORE_DATA_EPOCH_OFFSET, L as appleSecondsSql, M as MessagesUnavailableError, N as SchemaDriftError, O as IndexUnavailableError, P as SendFailedError, R as fromAppleSeconds, S as defaultStorePath, T as PRELUDE, V as BUILD_INFO, _ as decodeMessageRef, a as loadConfig, b as ATTACHMENTS_RELATIVE, c as introspect, d as decodeAttributedBody, f as outline, g as decodeChatRef, h as MESSAGE_REF_VERSION, i as registerTools, j as MessageNotFoundError, k as MESSAGES_BUNDLE_ID, l as openStore, m as InvalidMessageRefError, n as SERVER_VERSION, o as AppleMessagesClient, p as CHAT_REF_VERSION, r as createServer, s as MessagesStore, t as SERVER_NAME, u as reactionLabel, v as encodeChatRef, w as SEND_MESSAGE, x as STORE_RELATIVE, y as encodeMessageRef, z as renderInstant } from "./server-Bp7LsHS8.js";
1
+ import { A as MESSAGES_SURFACE, B as toAppleSeconds, C as locateStore, D as ChatNotFoundError, E as AppleMessagesError, F as SendTargetNotFoundError, I as CORE_DATA_EPOCH_OFFSET, L as appleSecondsSql, M as MessagesUnavailableError, N as SchemaDriftError, O as IndexUnavailableError, P as SendFailedError, R as fromAppleSeconds, S as defaultStorePath, T as PRELUDE, V as BUILD_INFO, _ as decodeMessageRef, a as loadConfig, b as ATTACHMENTS_RELATIVE, c as introspect, d as decodeAttributedBody, f as outline, g as decodeChatRef, h as MESSAGE_REF_VERSION, i as registerTools, j as MessageNotFoundError, k as MESSAGES_BUNDLE_ID, l as openStore, m as InvalidMessageRefError, n as SERVER_VERSION, o as AppleMessagesClient, p as CHAT_REF_VERSION, r as createServer, s as MessagesStore, t as SERVER_NAME, u as reactionLabel, v as encodeChatRef, w as SEND_MESSAGE, x as STORE_RELATIVE, y as encodeMessageRef, z as renderInstant } from "./server-C_7IK_LY.js";
2
2
  export { ATTACHMENTS_RELATIVE, AppleMessagesClient, AppleMessagesError, BUILD_INFO, CHAT_REF_VERSION, CORE_DATA_EPOCH_OFFSET, ChatNotFoundError, IndexUnavailableError, InvalidMessageRefError, MESSAGES_BUNDLE_ID, MESSAGES_SURFACE, MESSAGE_REF_VERSION, MessageNotFoundError, MessagesStore, MessagesUnavailableError, PRELUDE, SEND_MESSAGE, SERVER_NAME, SERVER_VERSION, STORE_RELATIVE, SchemaDriftError, SendFailedError, SendTargetNotFoundError, appleSecondsSql, createServer, decodeAttributedBody, decodeChatRef, decodeMessageRef, defaultStorePath, encodeChatRef, encodeMessageRef, fromAppleSeconds, introspect, loadConfig, locateStore, openStore, outline, reactionLabel, registerTools, renderInstant, toAppleSeconds };
@@ -13,8 +13,8 @@ const pkg = readPackageIdentity(new URL("../package.json", import.meta.url), {
13
13
  const BUILD_INFO = {
14
14
  name: pkg.name,
15
15
  version: pkg.version,
16
- gitCommit: "312147f",
17
- gitCommitDate: "2026-08-27T16:06:37+02:00"
16
+ gitCommit: "90f269d",
17
+ gitCommitDate: "2026-09-01T00:21:33+02:00"
18
18
  };
19
19
  //#endregion
20
20
  //#region src/client/dates.ts
@@ -1733,7 +1733,23 @@ const ConfigSchema = BaseConfigSchema.extend({
1733
1733
  * network; a miss is reported as `pending`, never as a failure. Zero means one
1734
1734
  * immediate check and no polling, which is what the test suite uses.
1735
1735
  */
1736
- sendReconcileMs: z.number().int().min(0).max(6e4).default(5e3)
1736
+ sendReconcileMs: z.number().int().min(0).max(6e4).default(5e3),
1737
+ /**
1738
+ * Gates `find_codes`, and deliberately NOT folded into `allowWrites`.
1739
+ *
1740
+ * Two reasons, and the second is the real one. It is a read, so putting it
1741
+ * behind a write gate would mean granting the right to send a message in
1742
+ * order to get a read tool — the two are unrelated and bundling them makes
1743
+ * both switches mean less.
1744
+ *
1745
+ * And it is a read of a different tier. This server already holds the
1746
+ * conversation history; a sibling holds Mail. Between them that is the
1747
+ * password-RESET channel, and adding live authentication codes to the same
1748
+ * process completes an account-takeover primitive out of parts that were each
1749
+ * individually reasonable. That is a real change in what a leaked transcript
1750
+ * costs, so it gets a switch of its own and defaults off.
1751
+ */
1752
+ allowCodes: z.boolean().default(false)
1737
1753
  }).strict();
1738
1754
  const loadConfig$1 = (env = process.env) => parseConfig(ConfigSchema, {
1739
1755
  allowWrites: parseBool(env.APPLE_MESSAGES_ALLOW_WRITES),
@@ -1745,6 +1761,7 @@ const loadConfig$1 = (env = process.env) => parseConfig(ConfigSchema, {
1745
1761
  attachmentDir: trimmed(env.APPLE_MESSAGES_ATTACHMENT_DIR),
1746
1762
  defaultRangeDays: parseIntOpt(env.APPLE_MESSAGES_DEFAULT_RANGE_DAYS),
1747
1763
  sendReconcileMs: parseIntOpt(env.APPLE_MESSAGES_SEND_RECONCILE_MS),
1764
+ allowCodes: parseBool(env.APPLE_MESSAGES_ALLOW_CODES),
1748
1765
  osascriptPath: trimmed(env.APPLE_MESSAGES_OSASCRIPT_PATH),
1749
1766
  osascriptTimeoutMs: parseIntOpt(env.APPLE_MESSAGES_OSASCRIPT_TIMEOUT_MS),
1750
1767
  maxResults: parseIntOpt(env.APPLE_MESSAGES_MAX_RESULTS)
@@ -2027,6 +2044,319 @@ const registerChatTools = (server, client) => {
2027
2044
  }, async ({ limit }) => wrap(async () => client.listChats(limit)));
2028
2045
  };
2029
2046
  //#endregion
2047
+ //#region src/client/codes.ts
2048
+ /**
2049
+ * One-time-code extraction, as a pure function over message text.
2050
+ *
2051
+ * No I/O by design: everything here is decided from a string plus one bit about
2052
+ * the sender, so the whole thing tests offline against a table. That also keeps
2053
+ * it liftable to Mail, where the same problem exists with messier input.
2054
+ *
2055
+ * ── WHY THIS IS NOT A REGEX ──────────────────────────────────────────────────
2056
+ *
2057
+ * The obvious implementation is `/\b\d{4,8}\b/` and it is wrong in a way that
2058
+ * matters more than usual: a caller asks for a login code, gets the last four
2059
+ * digits of an order number, and pastes it into an auth prompt. The failure is
2060
+ * silent and the retry costs the user an account lockout. So the digit run is
2061
+ * the CANDIDATE here, never the answer — it has to survive disqualification and
2062
+ * then earn a score.
2063
+ *
2064
+ * The false positives are not hypothetical. A real inbox carries order numbers,
2065
+ * tracking numbers, prices, street numbers, years, flight numbers and phone
2066
+ * numbers, and every one of them is a 4-to-8 digit run in a message that also
2067
+ * contains the word "code" somewhere.
2068
+ *
2069
+ * ── THE SIGNALS, STRONGEST FIRST ─────────────────────────────────────────────
2070
+ *
2071
+ * domain-bound `@example.com #123456` — the WebOTP/AutoFill convention
2072
+ * Apple and Chrome both parse. Unambiguous by construction:
2073
+ * the origin is bound to the code, so there is nothing to
2074
+ * guess. When present it wins outright.
2075
+ * keyword A code word adjacent to the digits. "adjacent" is measured
2076
+ * in characters, not words, because the two orders both occur
2077
+ * ("your code is 123456" and "123456 is your code") and a word
2078
+ * window would need two passes.
2079
+ * shortcode Sender is a shortcode — a bank, a courier, a 2FA sender,
2080
+ * never a person. Corroborating only: it raises a weak match
2081
+ * to usable, never creates one on its own.
2082
+ *
2083
+ * ── WHAT `confidence` IS FOR ─────────────────────────────────────────────────
2084
+ *
2085
+ * The tool reports it, and the tool description tells the model to check the
2086
+ * body on anything below "high". This mirrors `apple_safari_list_tabs`'
2087
+ * `historyMatch`: say how the match was made so the caller is never guessing
2088
+ * whether to trust it.
2089
+ */
2090
+ /** A code word. "code" carries both English and French, conveniently. */
2091
+ const KEYWORDS = [
2092
+ "code",
2093
+ "verification",
2094
+ "verify",
2095
+ "one-time",
2096
+ "onetime",
2097
+ "one time",
2098
+ "otp",
2099
+ "passcode",
2100
+ "pin",
2101
+ "2fa",
2102
+ "two-factor",
2103
+ "authentication",
2104
+ "authenticate",
2105
+ "security",
2106
+ "log in",
2107
+ "login",
2108
+ "sign in",
2109
+ "signin",
2110
+ "confirm",
2111
+ "verification",
2112
+ "usage unique",
2113
+ "mot de passe",
2114
+ "connexion",
2115
+ "identification",
2116
+ "securite",
2117
+ "sécurité",
2118
+ "vérification"
2119
+ ];
2120
+ /**
2121
+ * Phrases where "code" means something else entirely.
2122
+ *
2123
+ * This is a denylist and `docs/surfaces.md` warns that a denylist can never be
2124
+ * finished — correctly, and it is used narrowly here because of that. It only
2125
+ * ever SUPPRESSES the keyword signal; it never decides the outcome by itself,
2126
+ * and a message carrying both "promo code" and a real domain-bound code still
2127
+ * resolves through the stronger signal.
2128
+ */
2129
+ const ANTI_KEYWORDS = [
2130
+ "promo code",
2131
+ "promotional code",
2132
+ "discount code",
2133
+ "coupon code",
2134
+ "referral code",
2135
+ "invite code",
2136
+ "area code",
2137
+ "zip code",
2138
+ "postal code",
2139
+ "qr code",
2140
+ "barcode",
2141
+ "bar code",
2142
+ "country code",
2143
+ "code promo",
2144
+ "code postal",
2145
+ "code de reduction",
2146
+ "code de réduction",
2147
+ "code parrainage"
2148
+ ];
2149
+ /** How far from the digits a keyword still counts, in characters. */
2150
+ const NEAR = 32;
2151
+ const ADJACENT = 12;
2152
+ /**
2153
+ * The WebOTP format: a last line of `@host #code`, optionally with `?` params.
2154
+ * Anchored to a `@host` so a bare `#1234` (an order number, a hashtag) does not
2155
+ * qualify.
2156
+ */
2157
+ const DOMAIN_BOUND = /@([a-z0-9][a-z0-9.-]*\.[a-z]{2,})\s+#([0-9]{4,8})\b/i;
2158
+ /**
2159
+ * A maximal run of digits and the separators a phone number or a formatted
2160
+ * quantity is allowed to contain. Used to reject, not to match: a span holding
2161
+ * more than 8 digits in total is a phone number, an account number or an
2162
+ * amount, and every digit run inside it is disqualified along with it.
2163
+ */
2164
+ const NUMBER_SPAN = /\d[\d\s().+-]*\d|\d+/g;
2165
+ const DIGIT_RUN = /\d{4,8}/g;
2166
+ const normalise = (s) => s.toLowerCase().replace(/ /g, " ");
2167
+ /** Spans that hold too many digits to be a code. Returns [start, end) pairs. */
2168
+ const disqualifiedSpans = (text) => {
2169
+ const out = [];
2170
+ for (const m of text.matchAll(NUMBER_SPAN)) if (m[0].replace(/\D/g, "").length > 8) out.push([m.index, m.index + m[0].length]);
2171
+ return out;
2172
+ };
2173
+ const inSpan = (spans, start, end) => spans.some(([a, b]) => start >= a && end <= b);
2174
+ /**
2175
+ * Rejections that look at the characters touching the digits.
2176
+ *
2177
+ * Each of these was a real false positive shape before it was a rule; see
2178
+ * `test/codes.test.ts`, where every one has a case.
2179
+ */
2180
+ const looksLikeSomethingElse = (text, start, end) => {
2181
+ const before = text.slice(Math.max(0, start - 12), start);
2182
+ const after = text.slice(end, end + 12);
2183
+ if (/[$€£¥]\s*$/.test(before)) return true;
2184
+ if (/^[.,]\d/.test(after)) return true;
2185
+ if (/\d[.,]$/.test(before)) return true;
2186
+ if (/[a-z]$/i.test(before)) return true;
2187
+ if (/^[a-z]/i.test(after) && !/^[a-z]{0,2}\b/i.test(after)) return true;
2188
+ if (/^\s*%/.test(after)) return true;
2189
+ return false;
2190
+ };
2191
+ /** 1900-2099. Rejected unless a keyword sits right against it. */
2192
+ const looksLikeYear = (digits) => digits.length === 4 && /^(19|20)\d{2}$/.test(digits);
2193
+ /**
2194
+ * Distance in characters from a digit run to the nearest keyword, or null.
2195
+ *
2196
+ * Both directions are searched because both orders are common in the wild:
2197
+ * "your code is 123456" and "123456 is your Google verification code".
2198
+ *
2199
+ * The slice is widened by the longest keyword before searching, and the
2200
+ * distance checked afterwards. Slicing to exactly NEAR instead is wrong in a
2201
+ * way that is easy to miss: it cuts the keyword in half at the boundary, so
2202
+ * "authentication" (14 chars) would need to sit 14 characters closer than
2203
+ * "otp" to register at all. The window bounds the GAP, not the keyword.
2204
+ */
2205
+ const LONGEST_KEYWORD = Math.max(...KEYWORDS.map((k) => k.length));
2206
+ const keywordDistance = (lower, start, end) => {
2207
+ const from = Math.max(0, start - NEAR - LONGEST_KEYWORD);
2208
+ const before = lower.slice(from, start);
2209
+ const after = lower.slice(end, end + NEAR + LONGEST_KEYWORD);
2210
+ let best = null;
2211
+ for (const kw of KEYWORDS) {
2212
+ const b = before.lastIndexOf(kw);
2213
+ if (b !== -1) {
2214
+ const d = before.length - (b + kw.length);
2215
+ if (d <= NEAR && (best === null || d < best)) best = d;
2216
+ }
2217
+ const a = after.indexOf(kw);
2218
+ if (a !== -1 && a <= NEAR && (best === null || a < best)) best = a;
2219
+ }
2220
+ return best;
2221
+ };
2222
+ /** True when a code word near the digits is one of the decoy phrases. */
2223
+ const LONGEST_ANTI = Math.max(...ANTI_KEYWORDS.map((k) => k.length));
2224
+ const suppressedByAntiKeyword = (lower, start, end) => {
2225
+ const window = lower.slice(Math.max(0, start - NEAR - LONGEST_ANTI), end + NEAR + LONGEST_ANTI);
2226
+ return ANTI_KEYWORDS.some((k) => window.includes(k));
2227
+ };
2228
+ /**
2229
+ * Pull the one-time code out of a message, or return null.
2230
+ *
2231
+ * Null is the common and correct answer for most messages, and callers must
2232
+ * treat it as "no code here" rather than retrying with something looser.
2233
+ */
2234
+ const extractCode = (text, { fromShortcode = false } = {}) => {
2235
+ if (!text) return null;
2236
+ if (text.length > 400) return null;
2237
+ const lower = normalise(text);
2238
+ const bound = DOMAIN_BOUND.exec(text);
2239
+ const boundHost = bound?.[1];
2240
+ const boundCode = bound?.[2];
2241
+ if (boundHost && boundCode) return {
2242
+ code: boundCode,
2243
+ confidence: "high",
2244
+ matched: "domain-bound",
2245
+ boundTo: boundHost
2246
+ };
2247
+ const dead = disqualifiedSpans(text);
2248
+ const candidates = [];
2249
+ for (const m of text.matchAll(DIGIT_RUN)) {
2250
+ const digits = m[0];
2251
+ const start = m.index;
2252
+ const end = start + digits.length;
2253
+ if (inSpan(dead, start, end)) continue;
2254
+ if (looksLikeSomethingElse(text, start, end)) continue;
2255
+ const distance = keywordDistance(lower, start, end);
2256
+ const suppressed = distance !== null && suppressedByAntiKeyword(lower, start, end);
2257
+ const keyword = distance !== null && !suppressed;
2258
+ if (looksLikeYear(digits) && !(keyword && distance <= ADJACENT)) continue;
2259
+ if (keyword) {
2260
+ const adjacent = distance <= ADJACENT;
2261
+ candidates.push({
2262
+ code: digits,
2263
+ confidence: adjacent ? "high" : "medium",
2264
+ matched: "keyword",
2265
+ rank: adjacent ? 0 : 1
2266
+ });
2267
+ continue;
2268
+ }
2269
+ if (fromShortcode && text.length <= 120) candidates.push({
2270
+ code: digits,
2271
+ confidence: "low",
2272
+ matched: "shortcode",
2273
+ rank: 2
2274
+ });
2275
+ }
2276
+ candidates.sort((a, b) => a.rank - b.rank);
2277
+ const [best, second] = candidates;
2278
+ if (!best) return null;
2279
+ const ambiguous = second !== void 0 && second.rank === best.rank && second.code !== best.code;
2280
+ return {
2281
+ code: best.code,
2282
+ confidence: ambiguous && best.confidence === "high" ? "medium" : best.confidence,
2283
+ matched: ambiguous ? `${best.matched}-ambiguous` : best.matched
2284
+ };
2285
+ };
2286
+ //#endregion
2287
+ //#region src/tools/codes.ts
2288
+ /**
2289
+ * The one-time-code tool, registered only when `allowCodes` is on.
2290
+ *
2291
+ * It exists because the Passwords app cannot be reached at all — every lane is
2292
+ * closed, see `docs/passwords.md` — while the channel 2FA codes actually arrive
2293
+ * on is a store this server already reads under a grant the user has already
2294
+ * given. The value it adds over `apple_messages_search_messages` is not access,
2295
+ * which was never missing; it is EXTRACTION, which is where the mistakes live.
2296
+ * See `client/codes.ts` for why that is not a regex.
2297
+ *
2298
+ * The window defaults tight on purpose. A one-time code is only interesting
2299
+ * while it is live, and a generous default turns this into a tool that reads
2300
+ * months of authentication history for no benefit.
2301
+ */
2302
+ const MAX_WINDOW_MINUTES = 60;
2303
+ const DEFAULT_WINDOW_MINUTES = 10;
2304
+ const registerCodeTools = (server, client) => {
2305
+ server.registerTool("apple_messages_find_codes", {
2306
+ description: `Find one-time authentication codes (2FA/OTP) in recently received messages. Looks back ${DEFAULT_WINDOW_MINUTES} minutes by default, ${MAX_WINDOW_MINUTES} at most, and only at messages you received — never at ones you sent.
2307
+
2308
+ CHECK \`ageSeconds\` BEFORE USING A CODE. An expired code is still in the store and reads exactly like a live one; most issuers expire them within 5-10 minutes, and this tool cannot tell which have. If the newest match is older than the window the user expected, say so rather than offering it.
2309
+
2310
+ \`confidence\` says how the match was made and is not decoration. "high" means the code was bound to a domain (\`@site.com #123456\`) or sat directly against a word like "verification code" — safe to use. "medium" means the keyword was further away, or two candidates tied. "low" means there was no keyword at all and only the sender being a shortcode suggested it. On anything below high, read the message with apple_messages_get_message and confirm against the body before using the digits.
2311
+
2312
+ A \`matched\` value ending in \`-ambiguous\` means the message held more than one plausible code and this tool did not guess — confirm which one is wanted.
2313
+
2314
+ Returning nothing is the normal result when no code has arrived. It does NOT mean the code was missed, and it is not a reason to re-run with a longer window: if a code was sent it is in the store within seconds. Prefer asking the user to trigger a new one.`,
2315
+ inputSchema: {
2316
+ service: z.string().optional().describe("Narrow to one issuer by matching the sender or the message text — \"Google\", \"bank\", \"Github\". Case-insensitive substring. Omit to see every recent code."),
2317
+ withinMinutes: z.number().int().min(1).max(MAX_WINDOW_MINUTES).optional().describe(`How far back to look, in minutes. Default ${DEFAULT_WINDOW_MINUTES}, max ${MAX_WINDOW_MINUTES}. Keep it short: a code older than a few minutes has usually expired, and a wide window just returns history.`),
2318
+ limit: limitArg
2319
+ },
2320
+ annotations: {
2321
+ readOnlyHint: true,
2322
+ idempotentHint: false
2323
+ }
2324
+ }, async ({ service, withinMinutes, limit }) => wrap(async () => {
2325
+ const minutes = withinMinutes ?? DEFAULT_WINDOW_MINUTES;
2326
+ const since = /* @__PURE__ */ new Date(Date.now() - minutes * 6e4);
2327
+ const { fromApple } = client.window(since);
2328
+ const needle = service?.trim().toLowerCase();
2329
+ const codes = client.listMessages({
2330
+ ...fromApple === void 0 ? {} : { fromApple },
2331
+ limit
2332
+ }).filter((m) => !m.fromMe).filter((m) => {
2333
+ if (!needle) return true;
2334
+ return `${m.from.name ?? ""} ${m.from.handle ?? ""} ${m.text ?? ""}`.toLowerCase().includes(needle);
2335
+ }).flatMap((m) => {
2336
+ const match = extractCode(m.text, { fromShortcode: m.from.resolution === "shortcode" });
2337
+ if (!match) return [];
2338
+ const sentAtMs = m.sentAt ? Date.parse(m.sentAt) : NaN;
2339
+ return [{
2340
+ code: match.code,
2341
+ confidence: match.confidence,
2342
+ matched: match.matched,
2343
+ ...match.boundTo ? { boundTo: match.boundTo } : {},
2344
+ from: m.from,
2345
+ sentAt: m.sentAt,
2346
+ ageSeconds: Number.isNaN(sentAtMs) ? null : Math.max(0, Math.round((Date.now() - sentAtMs) / 1e3)),
2347
+ ref: m.ref,
2348
+ chat: m.chat
2349
+ }];
2350
+ });
2351
+ return {
2352
+ windowMinutes: minutes,
2353
+ searchedSince: since.toISOString(),
2354
+ count: codes.length,
2355
+ codes
2356
+ };
2357
+ }));
2358
+ };
2359
+ //#endregion
2030
2360
  //#region src/tools/messages.ts
2031
2361
  /** ISO-8601 in, or a clear refusal. No relative grammar on this surface yet. */
2032
2362
  const parseBound = (raw, field) => {
@@ -2082,14 +2412,23 @@ const registerMessageTools = (server, client) => {
2082
2412
  * surface the flag carries a permission claim as well as a safety one: with it
2083
2413
  * off no Apple Event is ever sent, so no Automation grant is ever requested.
2084
2414
  * What is needed either way is Full Disk Access, absolutely — see `diagnostics`.
2415
+ * One further read, `find_codes`, only when `allowCodes` is on.
2085
2416
  *
2086
- * The registered set does NOT vary with whether the store is readable. That is a
2087
- * runtime condition, and MCP clients cache the tool list.
2417
+ * The registered set is a pure function of STATIC CONFIGURATION, never of
2418
+ * runtime state. In particular it does NOT vary with whether the store is
2419
+ * readable: MCP clients cache the tool list, so a set that shrank when a grant
2420
+ * was missing would stay shrunk after the grant arrived.
2421
+ *
2422
+ * That invariant used to read "a pure function of `allowWrites` and nothing
2423
+ * else". `allowCodes` widened the input without weakening the guarantee, which
2424
+ * was always about runtime conditions rather than about there being exactly one
2425
+ * flag. `test/tools.test.ts` asserts each arm separately.
2088
2426
  */
2089
2427
  const registerTools = (server, client, ctx) => {
2090
2428
  registerDiagnosticsTools(server, client);
2091
2429
  registerChatTools(server, client);
2092
2430
  registerMessageTools(server, client);
2431
+ if (ctx.allowCodes) registerCodeTools(server, client);
2093
2432
  if (!ctx.allowWrites) return;
2094
2433
  registerActionTools(server, client);
2095
2434
  registerAttachmentTools(server, client);
@@ -2116,7 +2455,10 @@ const createServer = (opts) => {
2116
2455
  ...opts.home ? { home: opts.home } : {},
2117
2456
  ...opts.contacts === void 0 ? {} : { contacts: opts.contacts }
2118
2457
  });
2119
- registerTools(server, client, { allowWrites: config.allowWrites });
2458
+ registerTools(server, client, {
2459
+ allowWrites: config.allowWrites,
2460
+ allowCodes: config.allowCodes
2461
+ });
2120
2462
  if (config.exposePrompts) {
2121
2463
  registerPrompts(server, config.allowWrites);
2122
2464
  registerSurfaceResources(server, {
@@ -2134,4 +2476,4 @@ const createServer = (opts) => {
2134
2476
  //#endregion
2135
2477
  export { MESSAGES_SURFACE as A, toAppleSeconds as B, locateStore as C, ChatNotFoundError as D, AppleMessagesError as E, SendTargetNotFoundError as F, CORE_DATA_EPOCH_OFFSET as I, appleSecondsSql as L, MessagesUnavailableError as M, SchemaDriftError$1 as N, IndexUnavailableError$1 as O, SendFailedError as P, fromAppleSeconds as R, defaultStorePath as S, PRELUDE as T, BUILD_INFO as V, decodeMessageRef as _, loadConfig$1 as a, ATTACHMENTS_RELATIVE as b, introspect as c, decodeAttributedBody as d, outline as f, decodeChatRef as g, MESSAGE_REF_VERSION as h, registerTools as i, MessageNotFoundError as j, MESSAGES_BUNDLE_ID as k, openStore as l, InvalidMessageRefError as m, SERVER_VERSION as n, AppleMessagesClient as o, CHAT_REF_VERSION as p, createServer as r, MessagesStore as s, SERVER_NAME as t, reactionLabel as u, encodeChatRef as v, SEND_MESSAGE as w, STORE_RELATIVE as x, encodeMessageRef as y, renderInstant as z };
2136
2478
 
2137
- //# sourceMappingURL=server-Bp7LsHS8.js.map
2479
+ //# sourceMappingURL=server-C_7IK_LY.js.map