@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 +18 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{server-Bp7LsHS8.js → server-C_7IK_LY.js} +349 -7
- package/dist/server-C_7IK_LY.js.map +1 -0
- package/package.json +5 -5
- package/dist/server-Bp7LsHS8.js.map +0 -1
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-
|
|
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
|
-
*
|
|
801
|
-
*
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":";;;;;;cAkBa,YAAY;;;;;;;;;;;;;;;;;;cCSnB,cAAY,EAAA
|
|
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-
|
|
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: "
|
|
17
|
-
gitCommitDate: "2026-
|
|
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
|
|
2087
|
-
* runtime
|
|
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, {
|
|
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-
|
|
2479
|
+
//# sourceMappingURL=server-C_7IK_LY.js.map
|