apple-icloud-mcp 0.3.5 → 0.3.6
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/THIRD_PARTY_NOTICES.md +4 -4
- package/dist/bundle.js +229 -131
- package/dist/tools/_confirm.js +68 -119
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Unofficial Apple Music, iCloud Calendar/Contacts/Mail, Apple Maps and WeatherKit tools for Claude Code",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.6"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"displayName": "Apple iCloud",
|
|
15
15
|
"source": "./",
|
|
16
16
|
"description": "Apple Music playlists and library, iCloud Calendar, Contacts and Mail, Apple Maps, WeatherKit and iTunes search for Claude via MCP — runs anywhere, no Mac required. Unofficial; not affiliated with Apple.",
|
|
17
|
-
"version": "0.3.
|
|
17
|
+
"version": "0.3.6",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-icloud",
|
|
3
3
|
"displayName": "Apple iCloud",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.6",
|
|
5
5
|
"description": "Apple Music playlists and library, iCloud Calendar, Contacts and Mail, Apple Maps, WeatherKit and iTunes search for Claude via MCP — runs anywhere, no Mac required. Unofficial; not affiliated with Apple.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Chall"
|
package/README.md
CHANGED
|
@@ -244,7 +244,7 @@ read in `DISPLAY_TZ` — so are calendar events stored without a time zone of th
|
|
|
244
244
|
## Tools
|
|
245
245
|
|
|
246
246
|
<!-- TOOLS:START -->
|
|
247
|
-
|
|
247
|
+
59 tools. **Mode** is the lowest [`APPLE_WRITE_MODE`](#write-protection-apple_write_mode) that registers the tool; **Confirm** marks tools that ask first ([details](#confirmation-mcp_confirm_mode)); 🌐 marks Apple Music tools that need [web-player mode](#apple-music--web-player-mode-opt-in-unofficial).
|
|
248
248
|
|
|
249
249
|
### Health
|
|
250
250
|
|
|
@@ -372,7 +372,7 @@ target changed in between.
|
|
|
372
372
|
| `MCP_CONFIRM_MODE` | `ask-user` (default — the model must get your OK before repeating the call), `auto` (the model may confirm after reviewing the preview), `refuse` (never). Unknown values mean `refuse`. |
|
|
373
373
|
| `MCP_CONFIRM_ELICITATION` | `on` (default) or `off`. `off` never shows a prompt, so every client gets the two-step flow under `MCP_CONFIRM_MODE`. Set it for a client that says it can prompt but never does (the gated call hangs — opencode 2.0.x). Any other value stays `on` (with a stderr warning). |
|
|
374
374
|
| `MCP_CONFIRM_TTL_SECONDS` | How long a token is valid (default 600). |
|
|
375
|
-
| `MCP_CONFIRM_SECRET` | Token signing key; random per process by default (on mcp-host, the host's per-child `MCP_HOST_CONFIRM_SECRET` beside an absolute `MCP_DATA_DIR` is used instead). Set it so a token survives a restart; spent tokens are recorded on disk (
|
|
375
|
+
| `MCP_CONFIRM_SECRET` | Token signing key; random per process by default (on mcp-host, the host's per-child `MCP_HOST_CONFIRM_SECRET` beside an absolute `MCP_DATA_DIR` is used instead). Set it so a token survives a restart; spent tokens are recorded on disk (mcp-utils' shared store, `$MCP_DATA_DIR/.mcp-confirm/spent/`, or `~/.apple-icloud-mcp/confirm-spent/` without an absolute `MCP_DATA_DIR`), so none can be replayed. With `APPLE_STATE_CACHE=false` they are remembered by the process only (a warning says so); if the directory cannot be written, confirmed writes are refused rather than risk a replay. |
|
|
376
376
|
|
|
377
377
|
## Environment variables
|
|
378
378
|
|
|
@@ -498,7 +498,9 @@ source checkout, `node dist/index.js doctor` also reads the repo's `.env`.
|
|
|
498
498
|
- `mail-login.json` — which IMAP login form iCloud accepted;
|
|
499
499
|
- `icloud-rejected.json` — salted digests of rejected Apple ID/password pairs and when, so a revoked
|
|
500
500
|
password is not re-sent for 24 hours even after a restart;
|
|
501
|
-
- `confirm-spent
|
|
501
|
+
- `confirm-spent/` — one file per used confirmation token (its random id and expiry), so none can be replayed
|
|
502
|
+
after a restart. Hosted with an absolute `MCP_DATA_DIR`, this lives in mcp-utils' shared
|
|
503
|
+
`$MCP_DATA_DIR/.mcp-confirm/spent/` instead.
|
|
502
504
|
|
|
503
505
|
Delete the directory to remove them, or set `APPLE_STATE_CACHE=false` to never write them (the latch and the
|
|
504
506
|
spent-token record then last only as long as the process, and a warning says so).
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -22,7 +22,7 @@ MPL-2.0 is a file-level license: it covers ical.js's own files and does not exte
|
|
|
22
22
|
|
|
23
23
|
| Package | Version | License | Source |
|
|
24
24
|
|---|---|---|---|
|
|
25
|
-
| @chrischall/mcp-utils | 3.0.
|
|
25
|
+
| @chrischall/mcp-utils | 3.0.3 | MIT | https://github.com/chrischall/mcp-utils |
|
|
26
26
|
| @esbuild/aix-ppc64 | 0.28.2 | MIT | — |
|
|
27
27
|
| @esbuild/android-arm | 0.28.2 | MIT | — |
|
|
28
28
|
| @esbuild/android-arm64 | 0.28.2 | MIT | — |
|
|
@@ -109,11 +109,11 @@ MPL-2.0 is a file-level license: it covers ical.js's own files and does not exte
|
|
|
109
109
|
| split2 | 4.2.0 | ISC | https://github.com/mcollina/split2 |
|
|
110
110
|
| thread-stream | 4.2.0 | MIT | https://github.com/mcollina/thread-stream |
|
|
111
111
|
| timezones-ical-library | 2.3.2 | Apache-2.0 | https://github.com/add2cal/timezones-ical-library |
|
|
112
|
-
| zod | 4.6.
|
|
112
|
+
| zod | 4.6.7 | MIT | https://github.com/colinhacks/zod |
|
|
113
113
|
|
|
114
114
|
## License and notice texts
|
|
115
115
|
|
|
116
|
-
### @chrischall/mcp-utils@3.0.
|
|
116
|
+
### @chrischall/mcp-utils@3.0.3
|
|
117
117
|
|
|
118
118
|
- License: MIT
|
|
119
119
|
- Source: https://github.com/chrischall/mcp-utils
|
|
@@ -2899,7 +2899,7 @@ SOFTWARE.
|
|
|
2899
2899
|
limitations under the License.
|
|
2900
2900
|
```
|
|
2901
2901
|
|
|
2902
|
-
### zod@4.6.
|
|
2902
|
+
### zod@4.6.7
|
|
2903
2903
|
|
|
2904
2904
|
- License: MIT
|
|
2905
2905
|
- Source: https://github.com/colinhacks/zod
|
package/dist/bundle.js
CHANGED
|
@@ -248,6 +248,7 @@ __export(util_exports, {
|
|
|
248
248
|
required: () => required,
|
|
249
249
|
safeExtend: () => safeExtend,
|
|
250
250
|
shallowClone: () => shallowClone,
|
|
251
|
+
sizeCaps: () => sizeCaps,
|
|
251
252
|
slugify: () => slugify,
|
|
252
253
|
stringifyPrimitive: () => stringifyPrimitive,
|
|
253
254
|
toZod: () => toZod,
|
|
@@ -671,6 +672,18 @@ function prefixIssues(path2, issues) {
|
|
|
671
672
|
function unwrapMessage(message) {
|
|
672
673
|
return typeof message === "string" ? message : message?.message;
|
|
673
674
|
}
|
|
675
|
+
function sizeCaps(defChecks) {
|
|
676
|
+
const caps = [];
|
|
677
|
+
for (const ch of defChecks ?? []) {
|
|
678
|
+
const d = ch._zod.def;
|
|
679
|
+
if (!/_(length|size)$|_equals$/.test(d.check))
|
|
680
|
+
break;
|
|
681
|
+
const max = d.maximum ?? d.length ?? d.size;
|
|
682
|
+
if (max >= 0)
|
|
683
|
+
caps.push([max, ch]);
|
|
684
|
+
}
|
|
685
|
+
return caps;
|
|
686
|
+
}
|
|
674
687
|
function attachSchema(issues, start, inst) {
|
|
675
688
|
var _a3;
|
|
676
689
|
for (let i = start; i < issues.length; i++) {
|
|
@@ -2234,7 +2247,7 @@ var init_versions = __esm({
|
|
|
2234
2247
|
version = {
|
|
2235
2248
|
major: 4,
|
|
2236
2249
|
minor: 6,
|
|
2237
|
-
patch:
|
|
2250
|
+
patch: 7
|
|
2238
2251
|
};
|
|
2239
2252
|
}
|
|
2240
2253
|
});
|
|
@@ -2396,6 +2409,28 @@ function isValidJWT(token, algorithm = null) {
|
|
|
2396
2409
|
return false;
|
|
2397
2410
|
}
|
|
2398
2411
|
}
|
|
2412
|
+
function capSize(inst, size) {
|
|
2413
|
+
const caps = sizeCaps(inst._zod.def.checks);
|
|
2414
|
+
if (!caps.length)
|
|
2415
|
+
return;
|
|
2416
|
+
const cap = Math.min(...caps.map((c) => c[0]));
|
|
2417
|
+
const parse5 = inst._zod.parse;
|
|
2418
|
+
inst._zod.parse = (payload, ctx) => {
|
|
2419
|
+
const n = size(payload.value);
|
|
2420
|
+
if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
|
|
2421
|
+
for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
|
|
2422
|
+
if (n > max && ch._zod.def.when(payload)) {
|
|
2423
|
+
const start = payload.issues.length;
|
|
2424
|
+
ch._zod.check(payload);
|
|
2425
|
+
payload.issues[start].continue = false;
|
|
2426
|
+
attachSchema(payload.issues, start, inst);
|
|
2427
|
+
return payload;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
return parse5(payload, ctx);
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2399
2434
|
function handleArrayResult(result, final, index) {
|
|
2400
2435
|
if (result.issues.length) {
|
|
2401
2436
|
final.issues.push(...prefixIssues(index, result.issues));
|
|
@@ -2896,7 +2931,7 @@ function handleRefineResult(result, payload, input2, inst) {
|
|
|
2896
2931
|
payload.issues.push(issue(_iss));
|
|
2897
2932
|
}
|
|
2898
2933
|
}
|
|
2899
|
-
var $ZodType, toStandardResult, $ZodString, $ZodStringFormat, $ZodGUID, $ZodUUID, $ZodEmail, URL_BAD_FORMAT, URL_UNPARSEABLE, asciiTabOrNewline, $ZodURL, $ZodEmoji, $ZodNanoID, $ZodCUID, $ZodCUID2, $ZodULID, $ZodXID, $ZodKSUID, $ZodISODateTime, $ZodISODate, $ZodISOTime, $ZodISODuration, $ZodIPv4, ipv6Alphabet, $ZodIPv6, $ZodMAC, $ZodCIDRv4, $ZodCIDRv6, base64Charset, $ZodBase64, base64urlCharset, $ZodBase64URL, $ZodE164, CC_SANITIZE, $ZodCreditCard, $ZodIBAN, $ZodJWT, $ZodCustomStringFormat, $ZodNumber, $ZodNumberFormat, $ZodBoolean, $ZodBigInt, $ZodBigIntFormat, $ZodSymbol, $ZodUndefined, $ZodNull, $ZodAny, $ZodUnknown, $ZodNever, $ZodVoid, $ZodDate, $ZodArray, NO_SYMBOL_KEYS, $ZodObject, $ZodObjectJIT, $ZodUnion, $ZodXor, $ZodDiscriminatedUnion, $ZodIntersection, $ZodTuple, $ZodRecord, $ZodMap, $ZodSet, $ZodEnum, $ZodLiteral, $ZodFile, $ZodTransform, $ZodOptional, $ZodExactOptional, $ZodNullable, $ZodDefault, $ZodPrefault, $ZodNonOptional, $ZodSuccess, $ZodCatch, $ZodNaN, $ZodPipe, $ZodCodec, $ZodPreprocess, $ZodReadonly, $ZodTemplateLiteral, $ZodFunction, $ZodPromise, $ZodLazy, $ZodCustom;
|
|
2934
|
+
var $ZodType, toStandardResult, $ZodString, $ZodStringFormat, $ZodGUID, $ZodUUID, $ZodEmail, URL_BAD_FORMAT, URL_UNPARSEABLE, asciiTabOrNewline, $ZodURL, $ZodEmoji, $ZodNanoID, $ZodCUID, $ZodCUID2, $ZodULID, $ZodXID, $ZodKSUID, $ZodISODateTime, $ZodISODate, $ZodISOTime, $ZodISODuration, $ZodIPv4, ipv6Alphabet, $ZodIPv6, $ZodMAC, $ZodCIDRv4, $ZodCIDRv6, base64Charset, $ZodBase64, base64urlCharset, $ZodBase64URL, $ZodE164, CC_SANITIZE, $ZodCreditCard, $ZodIBAN, $ZodJWT, $ZodCustomStringFormat, $ZodNumber, $ZodNumberFormat, $ZodBoolean, $ZodBigInt, $ZodBigIntFormat, $ZodSymbol, $ZodUndefined, $ZodNull, $ZodAny, $ZodUnknown, $ZodNever, $ZodVoid, $ZodDate, arraySize, mapSize, setSize, $ZodArray, NO_SYMBOL_KEYS, $ZodObject, $ZodObjectJIT, $ZodUnion, $ZodXor, $ZodDiscriminatedUnion, $ZodIntersection, $ZodTuple, $ZodRecord, $ZodMap, $ZodSet, $ZodEnum, $ZodLiteral, $ZodFile, $ZodTransform, $ZodOptional, $ZodExactOptional, $ZodNullable, $ZodDefault, $ZodPrefault, $ZodNonOptional, $ZodSuccess, $ZodCatch, $ZodNaN, $ZodPipe, $ZodCodec, $ZodPreprocess, $ZodReadonly, $ZodTemplateLiteral, $ZodFunction, $ZodPromise, $ZodLazy, $ZodCustom;
|
|
2900
2935
|
var init_schemas = __esm({
|
|
2901
2936
|
"node_modules/zod/v4/core/schemas.js"() {
|
|
2902
2937
|
init_checks();
|
|
@@ -3503,6 +3538,9 @@ var init_schemas = __esm({
|
|
|
3503
3538
|
return payload;
|
|
3504
3539
|
};
|
|
3505
3540
|
});
|
|
3541
|
+
arraySize = (input2) => Array.isArray(input2) ? input2.length : -1;
|
|
3542
|
+
mapSize = (input2) => input2 instanceof Map ? input2.size : -1;
|
|
3543
|
+
setSize = (input2) => input2 instanceof Set ? input2.size : -1;
|
|
3506
3544
|
$ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
|
|
3507
3545
|
$ZodType.init(inst, def);
|
|
3508
3546
|
const memo4 = globalConfig.memoizer;
|
|
@@ -3540,6 +3578,7 @@ var init_schemas = __esm({
|
|
|
3540
3578
|
}
|
|
3541
3579
|
return payload;
|
|
3542
3580
|
};
|
|
3581
|
+
capSize(inst, arraySize);
|
|
3543
3582
|
});
|
|
3544
3583
|
NO_SYMBOL_KEYS = [];
|
|
3545
3584
|
$ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
@@ -4178,6 +4217,7 @@ var init_schemas = __esm({
|
|
|
4178
4217
|
return Promise.all(proms).then(() => payload);
|
|
4179
4218
|
return payload;
|
|
4180
4219
|
};
|
|
4220
|
+
capSize(inst, mapSize);
|
|
4181
4221
|
});
|
|
4182
4222
|
$ZodSet = /* @__PURE__ */ $constructor("$ZodSet", (inst, def) => {
|
|
4183
4223
|
$ZodType.init(inst, def);
|
|
@@ -4214,6 +4254,7 @@ var init_schemas = __esm({
|
|
|
4214
4254
|
return Promise.all(proms).then(() => payload);
|
|
4215
4255
|
return payload;
|
|
4216
4256
|
};
|
|
4257
|
+
capSize(inst, setSize);
|
|
4217
4258
|
});
|
|
4218
4259
|
$ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
|
|
4219
4260
|
$ZodType.init(inst, def);
|
|
@@ -14003,9 +14044,15 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
|
|
|
14003
14044
|
doc.write(`}`);
|
|
14004
14045
|
return outputVar;
|
|
14005
14046
|
}
|
|
14047
|
+
function generateSizeGate(doc, schema, size) {
|
|
14048
|
+
for (const [max, check2] of sizeCaps(schema._zod.def.checks)) {
|
|
14049
|
+
doc.write(`if (${size} > ${numericOperand(max, check2._zod.def.check)}) return INVALID;`);
|
|
14050
|
+
}
|
|
14051
|
+
}
|
|
14006
14052
|
function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
|
|
14007
14053
|
const def = schema._zod.def;
|
|
14008
14054
|
doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
|
|
14055
|
+
generateSizeGate(doc, schema, `${accessor}.length`);
|
|
14009
14056
|
const outputVar = buildsValue ? newVar(ctx) : null;
|
|
14010
14057
|
const iVar = newVar(ctx);
|
|
14011
14058
|
const elemVar = newVar(ctx);
|
|
@@ -14405,6 +14452,7 @@ function literalPropertyKey(ctx, key) {
|
|
|
14405
14452
|
function generateMapCheck(doc, ctx, schema, accessor) {
|
|
14406
14453
|
const def = schema._zod.def;
|
|
14407
14454
|
doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
|
|
14455
|
+
generateSizeGate(doc, schema, `${accessor}.size`);
|
|
14408
14456
|
const outputVar = newVar(ctx);
|
|
14409
14457
|
const kVar = newVar(ctx);
|
|
14410
14458
|
const valVar = newVar(ctx);
|
|
@@ -14421,6 +14469,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
|
|
|
14421
14469
|
function generateSetCheck(doc, ctx, schema, accessor) {
|
|
14422
14470
|
const def = schema._zod.def;
|
|
14423
14471
|
doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
|
|
14472
|
+
generateSizeGate(doc, schema, `${accessor}.size`);
|
|
14424
14473
|
const outputVar = newVar(ctx);
|
|
14425
14474
|
const valVar = newVar(ctx);
|
|
14426
14475
|
doc.write(`const ${outputVar} = new Set();`);
|
|
@@ -35396,7 +35445,7 @@ var init_errors3 = __esm({
|
|
|
35396
35445
|
// webhook signing secret (Stripe-style)
|
|
35397
35446
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
35398
35447
|
GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
|
|
35399
|
-
QUERY_SECRET_RE = new RegExp(`([
|
|
35448
|
+
QUERY_SECRET_RE = new RegExp(`([?&#;](?:(?:access|refresh|id|auth|session|csrf|xsrf|api)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|secret|token|auth|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
|
|
35400
35449
|
OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
|
|
35401
35450
|
FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
|
|
35402
35451
|
HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
|
|
@@ -35407,6 +35456,8 @@ var init_errors3 = __esm({
|
|
|
35407
35456
|
"api[_-]?secret",
|
|
35408
35457
|
"(?:x[_-])?api[_-]?key",
|
|
35409
35458
|
"private[_-]?key",
|
|
35459
|
+
"aws[_-]?secret[_-]?access[_-]?key",
|
|
35460
|
+
// fleet-audit#1297
|
|
35410
35461
|
"code[_-]?verifier",
|
|
35411
35462
|
// PKCE; a bare `verifier` key is too generic for JSON
|
|
35412
35463
|
`x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
|
|
@@ -36083,9 +36134,6 @@ function readConfirmTtl(env) {
|
|
|
36083
36134
|
}
|
|
36084
36135
|
return void 0;
|
|
36085
36136
|
}
|
|
36086
|
-
function confirmTtlFromEnv(env = process.env) {
|
|
36087
|
-
return readConfirmTtl(env) ?? DEFAULT_TTL_SECONDS2;
|
|
36088
|
-
}
|
|
36089
36137
|
function durableDataDir(env) {
|
|
36090
36138
|
const dir = readEnvVar("MCP_DATA_DIR", { env });
|
|
36091
36139
|
return dir && isAbsolute2(dir) ? dir : void 0;
|
|
@@ -36181,10 +36229,71 @@ var init_confirm_env = __esm({
|
|
|
36181
36229
|
});
|
|
36182
36230
|
|
|
36183
36231
|
// node_modules/@chrischall/mcp-utils/dist/server/confirm-write.js
|
|
36232
|
+
function withoutConfirmToken(args) {
|
|
36233
|
+
if (args === null || typeof args !== "object" || Array.isArray(args) || !("confirmToken" in args))
|
|
36234
|
+
return args;
|
|
36235
|
+
const { confirmToken, ...rest } = args;
|
|
36236
|
+
void confirmToken;
|
|
36237
|
+
return rest;
|
|
36238
|
+
}
|
|
36239
|
+
async function confirmWrite(ctx, options) {
|
|
36240
|
+
const { tool, action, summary, account, request, payload, willSend, args, confirmToken } = options;
|
|
36241
|
+
if (typeof tool !== "string" || tool.trim() === "")
|
|
36242
|
+
throw new TypeError("confirmWrite: tool must be a non-empty string.");
|
|
36243
|
+
if (typeof action !== "string" || action.trim() === "") {
|
|
36244
|
+
throw new TypeError("confirmWrite: action must be a non-empty string.");
|
|
36245
|
+
}
|
|
36246
|
+
if (request === void 0 && (payload === void 0 || payload === null)) {
|
|
36247
|
+
throw new TypeError(`confirmWrite: ${tool} gave neither a request nor a payload; bind what the write will send.`);
|
|
36248
|
+
}
|
|
36249
|
+
const extra = options.preview ?? {};
|
|
36250
|
+
const spoofed = RESERVED_PREVIEW_KEYS.filter((k) => Object.hasOwn(extra, k));
|
|
36251
|
+
if (spoofed.length > 0) {
|
|
36252
|
+
throw new TypeError(`confirmWrite: preview may not set the reserved field(s) ${spoofed.join(", ")}.`);
|
|
36253
|
+
}
|
|
36254
|
+
const query = request?.query ? Object.fromEntries(Object.entries(request.query).filter(([, v]) => v !== void 0)) : void 0;
|
|
36255
|
+
const hasQuery = query !== void 0 && Object.keys(query).length > 0;
|
|
36256
|
+
const shown = willSend !== void 0 ? willSend : request?.body !== void 0 ? request.body : payload;
|
|
36257
|
+
const preview = {
|
|
36258
|
+
...summary === void 0 ? {} : { action: summary },
|
|
36259
|
+
...request === void 0 ? {} : { method: request.method, path: request.path },
|
|
36260
|
+
...shown === void 0 ? {} : { willSend: shown },
|
|
36261
|
+
...hasQuery ? { willSendQuery: query } : {},
|
|
36262
|
+
...extra
|
|
36263
|
+
};
|
|
36264
|
+
const target = options.target === void 0 ? "" : String(options.target);
|
|
36265
|
+
const revision = options.revision ?? void 0;
|
|
36266
|
+
const bound = {
|
|
36267
|
+
...account === void 0 ? {} : { account },
|
|
36268
|
+
target,
|
|
36269
|
+
...revision === void 0 ? {} : { revision },
|
|
36270
|
+
...request === void 0 ? {} : { request: { method: request.method, path: request.path, query: hasQuery ? query : void 0, body: request.body } },
|
|
36271
|
+
...payload === void 0 ? {} : { payload },
|
|
36272
|
+
shown: preview,
|
|
36273
|
+
...args === void 0 ? {} : { args: withoutConfirmToken(args) }
|
|
36274
|
+
};
|
|
36275
|
+
return requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
36276
|
+
action,
|
|
36277
|
+
message: options.message ?? (summary ? `Review and confirm: ${summary}` : "Review and confirm this change:"),
|
|
36278
|
+
details: preview,
|
|
36279
|
+
...options.unsupportedNote === void 0 ? {} : { unsupportedNote: options.unsupportedNote },
|
|
36280
|
+
...options.confirmationLabel === void 0 ? {} : { confirmationLabel: options.confirmationLabel },
|
|
36281
|
+
tool,
|
|
36282
|
+
account,
|
|
36283
|
+
confirmToken,
|
|
36284
|
+
args: bound,
|
|
36285
|
+
subject: () => ({ target, ...revision === void 0 ? {} : { revision }, payload: bound, preview }),
|
|
36286
|
+
...options.instruction === void 0 ? {} : { instruction: options.instruction },
|
|
36287
|
+
...options.spent === void 0 ? {} : { spent: options.spent },
|
|
36288
|
+
...options.env === void 0 ? {} : { env: options.env }
|
|
36289
|
+
}));
|
|
36290
|
+
}
|
|
36291
|
+
var RESERVED_PREVIEW_KEYS;
|
|
36184
36292
|
var init_confirm_write = __esm({
|
|
36185
36293
|
"node_modules/@chrischall/mcp-utils/dist/server/confirm-write.js"() {
|
|
36186
36294
|
init_confirm_env();
|
|
36187
36295
|
init_confirm_token();
|
|
36296
|
+
RESERVED_PREVIEW_KEYS = ["action", "method", "path", "willSend", "willSendQuery"];
|
|
36188
36297
|
}
|
|
36189
36298
|
});
|
|
36190
36299
|
|
|
@@ -36467,6 +36576,9 @@ var init_headers = __esm({
|
|
|
36467
36576
|
});
|
|
36468
36577
|
|
|
36469
36578
|
// node_modules/@chrischall/mcp-utils/dist/internal/ascii.js
|
|
36579
|
+
function asciiLower(s) {
|
|
36580
|
+
return s.replace(/[A-Z]+/g, (m) => m.toLowerCase());
|
|
36581
|
+
}
|
|
36470
36582
|
var init_ascii = __esm({
|
|
36471
36583
|
"node_modules/@chrischall/mcp-utils/dist/internal/ascii.js"() {
|
|
36472
36584
|
}
|
|
@@ -36478,16 +36590,42 @@ var init_code_point = __esm({
|
|
|
36478
36590
|
}
|
|
36479
36591
|
});
|
|
36480
36592
|
|
|
36593
|
+
// node_modules/@chrischall/mcp-utils/dist/internal/tags.js
|
|
36594
|
+
var init_tags = __esm({
|
|
36595
|
+
"node_modules/@chrischall/mcp-utils/dist/internal/tags.js"() {
|
|
36596
|
+
}
|
|
36597
|
+
});
|
|
36598
|
+
|
|
36481
36599
|
// node_modules/@chrischall/mcp-utils/dist/scrape/index.js
|
|
36482
36600
|
function isCloudflareChallenge(html) {
|
|
36483
|
-
return
|
|
36601
|
+
return html.includes("_cf_chl_opt") || hasJustAMomentTitle(html);
|
|
36484
36602
|
}
|
|
36485
|
-
|
|
36603
|
+
function hasJustAMomentTitle(html) {
|
|
36604
|
+
const lower2 = asciiLower(html);
|
|
36605
|
+
let i = 0;
|
|
36606
|
+
for (; ; ) {
|
|
36607
|
+
const open2 = lower2.indexOf("<title", i);
|
|
36608
|
+
if (open2 < 0)
|
|
36609
|
+
return false;
|
|
36610
|
+
const gt = lower2.indexOf(">", open2 + 6);
|
|
36611
|
+
if (gt < 0)
|
|
36612
|
+
return false;
|
|
36613
|
+
let j2 = gt + 1;
|
|
36614
|
+
while (j2 < lower2.length && /\s/.test(lower2[j2]))
|
|
36615
|
+
j2++;
|
|
36616
|
+
if (lower2.startsWith(JUST_A_MOMENT, j2))
|
|
36617
|
+
return true;
|
|
36618
|
+
i = gt + 1;
|
|
36619
|
+
}
|
|
36620
|
+
}
|
|
36621
|
+
var NEXT_DATA_MAX_CHARS, JUST_A_MOMENT;
|
|
36486
36622
|
var init_scrape = __esm({
|
|
36487
36623
|
"node_modules/@chrischall/mcp-utils/dist/scrape/index.js"() {
|
|
36488
36624
|
init_ascii();
|
|
36489
36625
|
init_code_point();
|
|
36626
|
+
init_tags();
|
|
36490
36627
|
NEXT_DATA_MAX_CHARS = 16 * 1024 * 1024;
|
|
36628
|
+
JUST_A_MOMENT = "just a moment";
|
|
36491
36629
|
}
|
|
36492
36630
|
});
|
|
36493
36631
|
|
|
@@ -36639,6 +36777,7 @@ var init_net_atoms = __esm({
|
|
|
36639
36777
|
var init_body_limit = __esm({
|
|
36640
36778
|
"node_modules/@chrischall/mcp-utils/dist/http/body-limit.js"() {
|
|
36641
36779
|
init_errors3();
|
|
36780
|
+
init_headers();
|
|
36642
36781
|
}
|
|
36643
36782
|
});
|
|
36644
36783
|
|
|
@@ -36914,7 +37053,7 @@ var VERSION;
|
|
|
36914
37053
|
var init_version = __esm({
|
|
36915
37054
|
"src/version.ts"() {
|
|
36916
37055
|
"use strict";
|
|
36917
|
-
VERSION = "0.3.
|
|
37056
|
+
VERSION = "0.3.6";
|
|
36918
37057
|
}
|
|
36919
37058
|
});
|
|
36920
37059
|
|
|
@@ -46256,91 +46395,50 @@ var init_time = __esm({
|
|
|
46256
46395
|
|
|
46257
46396
|
// src/tools/_confirm.ts
|
|
46258
46397
|
import { createHash as createHash5 } from "node:crypto";
|
|
46259
|
-
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
46260
|
-
function
|
|
46261
|
-
|
|
46398
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join5 } from "node:path";
|
|
46399
|
+
function confirmWrite2(ctx, opts) {
|
|
46400
|
+
return confirmWrite(ctx, {
|
|
46401
|
+
tool: opts.tool,
|
|
46402
|
+
action: opts.action,
|
|
46403
|
+
message: opts.message,
|
|
46404
|
+
// Single-account server: every write acts as the one set of credentials in the env
|
|
46405
|
+
// (ICLOUD_USERNAME / the Apple Music user token), so there is no principal to choose between.
|
|
46406
|
+
account: void 0,
|
|
46262
46407
|
target: opts.target,
|
|
46263
46408
|
...opts.revision !== void 0 ? { revision: opts.revision } : {},
|
|
46264
46409
|
payload: opts.payload,
|
|
46265
|
-
preview: opts.preview
|
|
46266
|
-
|
|
46267
|
-
|
|
46268
|
-
|
|
46269
|
-
|
|
46270
|
-
|
|
46271
|
-
|
|
46272
|
-
details: opts.preview,
|
|
46273
|
-
unsupportedNote: "Make this change in the Apple app (Music, Calendar, Contacts or Mail) instead.",
|
|
46274
|
-
tool: opts.tool,
|
|
46275
|
-
// Single-account server: every write acts as the one set of credentials in the env
|
|
46276
|
-
// (ICLOUD_USERNAME / the Apple Music user token), so there is no principal to choose between.
|
|
46277
|
-
account: void 0,
|
|
46278
|
-
confirmToken: opts.confirmToken,
|
|
46279
|
-
args: opts.args,
|
|
46280
|
-
// Built per call: the data dir, APPLE_STATE_CACHE and the key are read now.
|
|
46281
|
-
spent: createFileSpentTokenStore2(),
|
|
46282
|
-
subject: () => subject,
|
|
46283
|
-
// A caller-supplied binding replaces the args-only default (and is used as given: strip the token here).
|
|
46284
|
-
binding: {
|
|
46285
|
-
key: confirmKeyFromEnv(),
|
|
46286
|
-
args: { args: withoutConfirmToken(opts.args), ...subject },
|
|
46287
|
-
ttlSeconds: confirmTtlFromEnv()
|
|
46288
|
-
}
|
|
46289
|
-
})
|
|
46290
|
-
);
|
|
46291
|
-
}
|
|
46292
|
-
function withoutConfirmToken(args) {
|
|
46293
|
-
const { confirmToken, ...rest } = args;
|
|
46294
|
-
void confirmToken;
|
|
46295
|
-
return rest;
|
|
46296
|
-
}
|
|
46297
|
-
function validateSpent(raw) {
|
|
46298
|
-
const spent = raw?.spent;
|
|
46299
|
-
if (typeof spent !== "object" || spent === null || Array.isArray(spent)) return null;
|
|
46300
|
-
return { spent: Object.fromEntries(Object.entries(spent).filter(([, exp]) => Number.isFinite(exp))) };
|
|
46410
|
+
preview: opts.preview,
|
|
46411
|
+
args: opts.args,
|
|
46412
|
+
confirmToken: opts.confirmToken,
|
|
46413
|
+
unsupportedNote: "Make this change in the Apple app (Music, Calendar, Contacts or Mail) instead.",
|
|
46414
|
+
// Chosen per call: the data dir, APPLE_STATE_CACHE and the key are read now.
|
|
46415
|
+
spent: spentTokenStore()
|
|
46416
|
+
});
|
|
46301
46417
|
}
|
|
46302
46418
|
function hasStableConfirmKey(env) {
|
|
46303
46419
|
if (readEnvVar("MCP_CONFIRM_SECRET", { env }) !== void 0) return true;
|
|
46304
46420
|
return readEnvVar("MCP_HOST_CONFIRM_SECRET", { env }) !== void 0 && isAbsolute3(readEnvVar("MCP_DATA_DIR", { env }) ?? "");
|
|
46305
46421
|
}
|
|
46306
|
-
function
|
|
46307
|
-
if (
|
|
46308
|
-
|
|
46309
|
-
|
|
46310
|
-
|
|
46311
|
-
|
|
46312
|
-
|
|
46313
|
-
function createFileSpentTokenStore2(options = {}) {
|
|
46314
|
-
const env = options.env ?? process.env;
|
|
46315
|
-
const memory2 = options.memory ?? processSpent;
|
|
46316
|
-
const key = createHash5("sha256").update(confirmKeyFromEnv(env)).digest("hex");
|
|
46317
|
-
const file2 = stateCache(SPENT_TOKENS_FILE, `confirm-spent-v1:${key}`, validateSpent, env);
|
|
46318
|
-
const onDisk = () => file2.load()?.spent ?? {};
|
|
46319
|
-
return {
|
|
46320
|
-
has: (nonce) => {
|
|
46321
|
-
const d = nonceDigest(nonce);
|
|
46322
|
-
return memory2.has(d) || Object.hasOwn(onDisk(), d);
|
|
46323
|
-
},
|
|
46324
|
-
add: (nonce, expiresAtMs) => {
|
|
46325
|
-
const d = nonceDigest(nonce);
|
|
46326
|
-
memory2.add(d, expiresAtMs);
|
|
46327
|
-
const now = Date.now();
|
|
46328
|
-
const kept = Object.entries(onDisk()).filter(([other, exp]) => other !== d && exp >= now).sort(([, a], [, b]) => b - a).slice(0, MAX_SPENT_TOKENS - 1);
|
|
46329
|
-
if (!file2.save({ spent: Object.fromEntries([[d, expiresAtMs], ...kept]) })) {
|
|
46330
|
-
warnOnce(isStateCacheEnabled(env) ? "write" : "disabled", env);
|
|
46331
|
-
}
|
|
46332
|
-
},
|
|
46333
|
-
prune: (now) => memory2.prune(now),
|
|
46334
|
-
// the file is pruned on every write
|
|
46335
|
-
clear: () => {
|
|
46336
|
-
memory2.clear();
|
|
46337
|
-
file2.clear();
|
|
46338
|
-
},
|
|
46339
|
-
/** Distinct spent tokens known to either copy. */
|
|
46340
|
-
get size() {
|
|
46341
|
-
return memory2.size + Object.keys(onDisk()).filter((d) => !memory2.has(d)).length;
|
|
46422
|
+
function spentTokenStore(env = process.env) {
|
|
46423
|
+
if (!isStateCacheEnabled(env)) {
|
|
46424
|
+
if (!warnedDisabled && hasStableConfirmKey(env)) {
|
|
46425
|
+
warnedDisabled = true;
|
|
46426
|
+
console.error(
|
|
46427
|
+
"[apple-icloud-mcp] WARNING: APPLE_STATE_CACHE=false, so a used confirmToken is remembered by this process only. With MCP_CONFIRM_SECRET (or mcp-host's MCP_HOST_CONFIRM_SECRET) set, a restart before it expires would accept it again."
|
|
46428
|
+
);
|
|
46342
46429
|
}
|
|
46343
|
-
|
|
46430
|
+
return processSpent;
|
|
46431
|
+
}
|
|
46432
|
+
const shared = spentTokenStoreFromEnv(env);
|
|
46433
|
+
if (shared) return shared;
|
|
46434
|
+
if (!hasStableConfirmKey(env)) return processSpent;
|
|
46435
|
+
const dir = join5(dirname3(resolveStateFile({ env, subdir: STATE_SUBDIR, fileName: SPENT_TOKENS_DIR })), SPENT_TOKENS_DIR);
|
|
46436
|
+
let store2 = localStores.get(dir);
|
|
46437
|
+
if (!store2) {
|
|
46438
|
+
store2 = createFileSpentTokenStore(dir);
|
|
46439
|
+
localStores.set(dir, store2);
|
|
46440
|
+
}
|
|
46441
|
+
return store2;
|
|
46344
46442
|
}
|
|
46345
46443
|
function stateRevision(value) {
|
|
46346
46444
|
const canonical = JSON.stringify(
|
|
@@ -46349,18 +46447,18 @@ function stateRevision(value) {
|
|
|
46349
46447
|
);
|
|
46350
46448
|
return `s1:${createHash5("sha256").update(canonical ?? "undefined").digest("base64url").slice(0, 22)}`;
|
|
46351
46449
|
}
|
|
46352
|
-
var CONFIRM_NOTE,
|
|
46450
|
+
var CONFIRM_NOTE, SPENT_TOKENS_DIR, processSpent, localStores, warnedDisabled;
|
|
46353
46451
|
var init_confirm = __esm({
|
|
46354
46452
|
"src/tools/_confirm.ts"() {
|
|
46355
46453
|
"use strict";
|
|
46356
46454
|
init_dist2();
|
|
46455
|
+
init_session();
|
|
46357
46456
|
init_state();
|
|
46358
46457
|
CONFIRM_NOTE = "Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call performs NO write and returns a preview of exactly what would happen plus a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).";
|
|
46359
|
-
|
|
46360
|
-
MAX_SPENT_TOKENS = 500;
|
|
46361
|
-
nonceDigest = (nonce) => createHash5("sha256").update(nonce).digest("base64url");
|
|
46458
|
+
SPENT_TOKENS_DIR = "confirm-spent";
|
|
46362
46459
|
processSpent = createSpentTokenStore();
|
|
46363
|
-
|
|
46460
|
+
localStores = /* @__PURE__ */ new Map();
|
|
46461
|
+
warnedDisabled = false;
|
|
46364
46462
|
}
|
|
46365
46463
|
});
|
|
46366
46464
|
|
|
@@ -56846,7 +56944,7 @@ function registerCalendarTools(server, deps = {}) {
|
|
|
56846
56944
|
const draft = occurrenceFor(eventParts(vcal), void 0, zones2);
|
|
56847
56945
|
const planned = formatOccurrence(draft, { calendar, baseId: formatEventId(calendar.id, `${uid}.ics`), ...zones2 });
|
|
56848
56946
|
if (invites.length > 0) {
|
|
56849
|
-
const gate = await
|
|
56947
|
+
const gate = await confirmWrite2(ctx, {
|
|
56850
56948
|
tool: "apple_calendar_create_event",
|
|
56851
56949
|
action: "apple.calendar.event.create",
|
|
56852
56950
|
message: `Create "${args.title}" and have iCloud email an invitation to ${invites.length} ${invites.length === 1 ? "person" : "people"}?`,
|
|
@@ -56926,7 +57024,7 @@ function registerCalendarTools(server, deps = {}) {
|
|
|
56926
57024
|
if (plan.notifiesAttendees) {
|
|
56927
57025
|
const people = (list3) => (list3 ?? []).map(personLabel);
|
|
56928
57026
|
const emailed = [.../* @__PURE__ */ new Set([...people(plan.result.expected.attendees), ...people(plan.before.attendees)])];
|
|
56929
|
-
const gate = await
|
|
57027
|
+
const gate = await confirmWrite2(ctx, {
|
|
56930
57028
|
tool: "apple_calendar_update_event",
|
|
56931
57029
|
action: "apple.calendar.event.update",
|
|
56932
57030
|
message: `Change "${String(plan.before.title)}" (${plan.scope})? It has attendees, so iCloud will email them the update.`,
|
|
@@ -57045,7 +57143,7 @@ function registerCalendarTools(server, deps = {}) {
|
|
|
57045
57143
|
const loaded = await loadEvent(dav, args.eventId, zones2);
|
|
57046
57144
|
const span = args.span ?? "thisEvent";
|
|
57047
57145
|
const plan = planDelete(loaded, span, { ...zones2, now: now() });
|
|
57048
|
-
const gate = await
|
|
57146
|
+
const gate = await confirmWrite2(ctx, {
|
|
57049
57147
|
tool: "apple_calendar_delete_event",
|
|
57050
57148
|
action: "apple.calendar.event.delete",
|
|
57051
57149
|
message: `Permanently delete "${String(plan.preview.event)}" (${plan.scope})? Calendar has no trash; it cannot be undone.`,
|
|
@@ -59022,7 +59120,7 @@ function registerContactsTools(server, deps = {}) {
|
|
|
59022
59120
|
Addresses: v.addresses.length ? v.addresses.map((a) => a.formatted).join(" | ") : void 0,
|
|
59023
59121
|
"Contact id": id2
|
|
59024
59122
|
});
|
|
59025
|
-
const gate = await
|
|
59123
|
+
const gate = await confirmWrite2(ctx, {
|
|
59026
59124
|
tool: "apple_contacts_delete",
|
|
59027
59125
|
action: "apple.contacts.contact.delete",
|
|
59028
59126
|
message: `Permanently delete the contact "${v.name}" from iCloud Contacts on all devices? This cannot be undone here.`,
|
|
@@ -62248,7 +62346,7 @@ var require_thread_stream = __commonJS({
|
|
|
62248
62346
|
var { version: version4 } = require_package();
|
|
62249
62347
|
var { EventEmitter: EventEmitter3 } = __require("events");
|
|
62250
62348
|
var { Worker } = __require("worker_threads");
|
|
62251
|
-
var { join:
|
|
62349
|
+
var { join: join7 } = __require("path");
|
|
62252
62350
|
var { pathToFileURL } = __require("url");
|
|
62253
62351
|
var { wait } = require_wait();
|
|
62254
62352
|
var {
|
|
@@ -62299,7 +62397,7 @@ var require_thread_stream = __commonJS({
|
|
|
62299
62397
|
function createWorker(stream, opts) {
|
|
62300
62398
|
const { filename, workerData } = opts;
|
|
62301
62399
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
62302
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
62400
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join7(__dirname, "lib", "worker.js");
|
|
62303
62401
|
const worker = new Worker(toExecute, {
|
|
62304
62402
|
...opts.workerOpts,
|
|
62305
62403
|
name: opts.workerOpts?.name || "thread-stream",
|
|
@@ -62767,7 +62865,7 @@ var require_transport = __commonJS({
|
|
|
62767
62865
|
var { createRequire } = __require("module");
|
|
62768
62866
|
var { existsSync: existsSync4 } = __require("node:fs");
|
|
62769
62867
|
var getCallers = require_caller();
|
|
62770
|
-
var { join:
|
|
62868
|
+
var { join: join7, isAbsolute: isAbsolute4, sep } = __require("node:path");
|
|
62771
62869
|
var { fileURLToPath: fileURLToPath2 } = __require("node:url");
|
|
62772
62870
|
var sleep3 = require_atomic_sleep();
|
|
62773
62871
|
var onExit = require_on_exit_leak_free();
|
|
@@ -62920,7 +63018,7 @@ var require_transport = __commonJS({
|
|
|
62920
63018
|
throw new Error("only one of target or targets can be specified");
|
|
62921
63019
|
}
|
|
62922
63020
|
if (targets) {
|
|
62923
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
63021
|
+
target = bundlerOverrides["pino-worker"] || join7(__dirname, "worker.js");
|
|
62924
63022
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
62925
63023
|
return {
|
|
62926
63024
|
...dest,
|
|
@@ -62938,7 +63036,7 @@ var require_transport = __commonJS({
|
|
|
62938
63036
|
});
|
|
62939
63037
|
});
|
|
62940
63038
|
} else if (pipeline) {
|
|
62941
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
63039
|
+
target = bundlerOverrides["pino-worker"] || join7(__dirname, "worker.js");
|
|
62942
63040
|
options.pipelines = [pipeline.map((dest) => {
|
|
62943
63041
|
return {
|
|
62944
63042
|
...dest,
|
|
@@ -62961,7 +63059,7 @@ var require_transport = __commonJS({
|
|
|
62961
63059
|
return origin;
|
|
62962
63060
|
}
|
|
62963
63061
|
if (origin === "pino/file") {
|
|
62964
|
-
return
|
|
63062
|
+
return join7(__dirname, "..", "file.js");
|
|
62965
63063
|
}
|
|
62966
63064
|
let fixTarget2;
|
|
62967
63065
|
for (const filePath of callers) {
|
|
@@ -63941,7 +64039,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
63941
64039
|
return circularValue;
|
|
63942
64040
|
}
|
|
63943
64041
|
let res = "";
|
|
63944
|
-
let
|
|
64042
|
+
let join7 = ",";
|
|
63945
64043
|
const originalIndentation = indentation;
|
|
63946
64044
|
if (Array.isArray(value)) {
|
|
63947
64045
|
if (value.length === 0) {
|
|
@@ -63955,7 +64053,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
63955
64053
|
indentation += spacer;
|
|
63956
64054
|
res += `
|
|
63957
64055
|
${indentation}`;
|
|
63958
|
-
|
|
64056
|
+
join7 = `,
|
|
63959
64057
|
${indentation}`;
|
|
63960
64058
|
}
|
|
63961
64059
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -63963,13 +64061,13 @@ ${indentation}`;
|
|
|
63963
64061
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
63964
64062
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
63965
64063
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
63966
|
-
res +=
|
|
64064
|
+
res += join7;
|
|
63967
64065
|
}
|
|
63968
64066
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
63969
64067
|
res += tmp !== void 0 ? tmp : "null";
|
|
63970
64068
|
if (value.length - 1 > maximumBreadth) {
|
|
63971
64069
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
63972
|
-
res += `${
|
|
64070
|
+
res += `${join7}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
63973
64071
|
}
|
|
63974
64072
|
if (spacer !== "") {
|
|
63975
64073
|
res += `
|
|
@@ -63990,7 +64088,7 @@ ${originalIndentation}`;
|
|
|
63990
64088
|
let separator = "";
|
|
63991
64089
|
if (spacer !== "") {
|
|
63992
64090
|
indentation += spacer;
|
|
63993
|
-
|
|
64091
|
+
join7 = `,
|
|
63994
64092
|
${indentation}`;
|
|
63995
64093
|
whitespace = " ";
|
|
63996
64094
|
}
|
|
@@ -64004,13 +64102,13 @@ ${indentation}`;
|
|
|
64004
64102
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
64005
64103
|
if (tmp !== void 0) {
|
|
64006
64104
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
64007
|
-
separator =
|
|
64105
|
+
separator = join7;
|
|
64008
64106
|
}
|
|
64009
64107
|
}
|
|
64010
64108
|
if (keyLength > maximumBreadth) {
|
|
64011
64109
|
const removedKeys = keyLength - maximumBreadth;
|
|
64012
64110
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
64013
|
-
separator =
|
|
64111
|
+
separator = join7;
|
|
64014
64112
|
}
|
|
64015
64113
|
if (spacer !== "" && separator.length > 1) {
|
|
64016
64114
|
res = `
|
|
@@ -64051,7 +64149,7 @@ ${originalIndentation}`;
|
|
|
64051
64149
|
}
|
|
64052
64150
|
const originalIndentation = indentation;
|
|
64053
64151
|
let res = "";
|
|
64054
|
-
let
|
|
64152
|
+
let join7 = ",";
|
|
64055
64153
|
if (Array.isArray(value)) {
|
|
64056
64154
|
if (value.length === 0) {
|
|
64057
64155
|
return "[]";
|
|
@@ -64064,7 +64162,7 @@ ${originalIndentation}`;
|
|
|
64064
64162
|
indentation += spacer;
|
|
64065
64163
|
res += `
|
|
64066
64164
|
${indentation}`;
|
|
64067
|
-
|
|
64165
|
+
join7 = `,
|
|
64068
64166
|
${indentation}`;
|
|
64069
64167
|
}
|
|
64070
64168
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -64072,13 +64170,13 @@ ${indentation}`;
|
|
|
64072
64170
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
64073
64171
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
64074
64172
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
64075
|
-
res +=
|
|
64173
|
+
res += join7;
|
|
64076
64174
|
}
|
|
64077
64175
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
64078
64176
|
res += tmp !== void 0 ? tmp : "null";
|
|
64079
64177
|
if (value.length - 1 > maximumBreadth) {
|
|
64080
64178
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
64081
|
-
res += `${
|
|
64179
|
+
res += `${join7}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
64082
64180
|
}
|
|
64083
64181
|
if (spacer !== "") {
|
|
64084
64182
|
res += `
|
|
@@ -64091,7 +64189,7 @@ ${originalIndentation}`;
|
|
|
64091
64189
|
let whitespace = "";
|
|
64092
64190
|
if (spacer !== "") {
|
|
64093
64191
|
indentation += spacer;
|
|
64094
|
-
|
|
64192
|
+
join7 = `,
|
|
64095
64193
|
${indentation}`;
|
|
64096
64194
|
whitespace = " ";
|
|
64097
64195
|
}
|
|
@@ -64100,7 +64198,7 @@ ${indentation}`;
|
|
|
64100
64198
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
64101
64199
|
if (tmp !== void 0) {
|
|
64102
64200
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
64103
|
-
separator =
|
|
64201
|
+
separator = join7;
|
|
64104
64202
|
}
|
|
64105
64203
|
}
|
|
64106
64204
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -64158,20 +64256,20 @@ ${originalIndentation}`;
|
|
|
64158
64256
|
indentation += spacer;
|
|
64159
64257
|
let res2 = `
|
|
64160
64258
|
${indentation}`;
|
|
64161
|
-
const
|
|
64259
|
+
const join8 = `,
|
|
64162
64260
|
${indentation}`;
|
|
64163
64261
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
64164
64262
|
let i = 0;
|
|
64165
64263
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
64166
64264
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
64167
64265
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
64168
|
-
res2 +=
|
|
64266
|
+
res2 += join8;
|
|
64169
64267
|
}
|
|
64170
64268
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
64171
64269
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
64172
64270
|
if (value.length - 1 > maximumBreadth) {
|
|
64173
64271
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
64174
|
-
res2 += `${
|
|
64272
|
+
res2 += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
64175
64273
|
}
|
|
64176
64274
|
res2 += `
|
|
64177
64275
|
${originalIndentation}`;
|
|
@@ -64187,16 +64285,16 @@ ${originalIndentation}`;
|
|
|
64187
64285
|
return '"[Object]"';
|
|
64188
64286
|
}
|
|
64189
64287
|
indentation += spacer;
|
|
64190
|
-
const
|
|
64288
|
+
const join7 = `,
|
|
64191
64289
|
${indentation}`;
|
|
64192
64290
|
let res = "";
|
|
64193
64291
|
let separator = "";
|
|
64194
64292
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
64195
64293
|
if (isTypedArrayWithEntries(value)) {
|
|
64196
|
-
res += stringifyTypedArray(value,
|
|
64294
|
+
res += stringifyTypedArray(value, join7, maximumBreadth);
|
|
64197
64295
|
keys = keys.slice(value.length);
|
|
64198
64296
|
maximumPropertiesToStringify -= value.length;
|
|
64199
|
-
separator =
|
|
64297
|
+
separator = join7;
|
|
64200
64298
|
}
|
|
64201
64299
|
if (deterministic) {
|
|
64202
64300
|
keys = sort(keys, comparator);
|
|
@@ -64207,13 +64305,13 @@ ${indentation}`;
|
|
|
64207
64305
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
64208
64306
|
if (tmp !== void 0) {
|
|
64209
64307
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
64210
|
-
separator =
|
|
64308
|
+
separator = join7;
|
|
64211
64309
|
}
|
|
64212
64310
|
}
|
|
64213
64311
|
if (keyLength > maximumBreadth) {
|
|
64214
64312
|
const removedKeys = keyLength - maximumBreadth;
|
|
64215
64313
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
64216
|
-
separator =
|
|
64314
|
+
separator = join7;
|
|
64217
64315
|
}
|
|
64218
64316
|
if (separator !== "") {
|
|
64219
64317
|
res = `
|
|
@@ -115433,7 +115531,7 @@ function replaceClosedTags(str5, pattern, replacement) {
|
|
|
115433
115531
|
const end = str5.lastIndexOf(">") + 1;
|
|
115434
115532
|
return str5.slice(0, end).replace(pattern, replacement) + str5.slice(end);
|
|
115435
115533
|
}
|
|
115436
|
-
function
|
|
115534
|
+
function replaceTags2(str5, prefix, pattern, replacement) {
|
|
115437
115535
|
const parts = [];
|
|
115438
115536
|
let copied = 0;
|
|
115439
115537
|
let searchFrom = 0;
|
|
@@ -115533,9 +115631,9 @@ function htmlToText(str5) {
|
|
|
115533
115631
|
str5 = stripThroughLastTag(str5, /<!doctype\b/gi);
|
|
115534
115632
|
str5 = stripFromFirstTag(str5, /<\/body\b/gi);
|
|
115535
115633
|
str5 = stripFromFirstTag(str5, /<\/html\b/gi);
|
|
115536
|
-
str5 =
|
|
115634
|
+
str5 = replaceTags2(str5, /<a\b/gi, /<a\b[^>]*href\s*=\s*["']?([^\s"']+)[^>]*>/iy, (match, href) => ` (${href}) `);
|
|
115537
115635
|
str5 = replaceClosedTags(str5, /<\/?(span|em|i|strong|b|u|a)\b[^>]*>/gi, "");
|
|
115538
|
-
str5 =
|
|
115636
|
+
str5 = replaceTags2(str5, /<li\b/gi, /<li\b[^>]*>[\n\u0001\s]*/iy, () => "* ");
|
|
115539
115637
|
str5 = replaceClosedTags(str5, /<hr\b[^>]*>/g, "\n-------------\n");
|
|
115540
115638
|
str5 = replaceClosedTags(str5, /<[^>]*>/g, " ");
|
|
115541
115639
|
str5 = str5.replace(/\u0001/g, "\n").replace(/[ \t]+/g, " ").replace(/^\s+$/gm, "").replace(/\n\n+/g, "\n\n").replace(/^\n+/, "\n").replace(/\n+$/, "\n");
|
|
@@ -119923,7 +120021,7 @@ ${q.quoted}
|
|
|
119923
120021
|
assertNotLatched(account.latchCreds, "mail");
|
|
119924
120022
|
const proxy = mailProxyFor(SMTP_HOST);
|
|
119925
120023
|
if (proxy !== void 0) assertHttpProxy(proxy);
|
|
119926
|
-
const gate = await
|
|
120024
|
+
const gate = await confirmWrite2(ctx, {
|
|
119927
120025
|
tool: "apple_mail_send",
|
|
119928
120026
|
action: "apple.mail.message.send",
|
|
119929
120027
|
message: `Send this email from ${account.address} to ${count} recipient${count === 1 ? "" : "s"}?`,
|
|
@@ -123952,7 +124050,7 @@ function registerExtendedTools(server, client) {
|
|
|
123952
124050
|
tracksAfter: n - removing.length,
|
|
123953
124051
|
...notInPlaylist.length > 0 ? { notInPlaylist } : {}
|
|
123954
124052
|
};
|
|
123955
|
-
const gate = await
|
|
124053
|
+
const gate = await confirmWrite2(ctx, {
|
|
123956
124054
|
tool: "apple_music_remove_playlist_tracks",
|
|
123957
124055
|
action: "apple.music.playlist.tracks.remove",
|
|
123958
124056
|
message: `Remove ${removing.length} track(s) from the playlist "${name2}"?`,
|
|
@@ -124110,7 +124208,7 @@ function registerExtendedTools(server, client) {
|
|
|
124110
124208
|
firstBefore: labels(current),
|
|
124111
124209
|
firstAfter: labels(next)
|
|
124112
124210
|
};
|
|
124113
|
-
const gate = await
|
|
124211
|
+
const gate = await confirmWrite2(ctx, {
|
|
124114
124212
|
tool: "apple_music_reorder_playlist",
|
|
124115
124213
|
action: "apple.music.playlist.tracks.replace",
|
|
124116
124214
|
message: `Rewrite the order of "${name2}" (${summary})${removed > 0 ? `, removing ${removed} track(s): ${someNames(dropped)}` : ""}?`,
|
|
@@ -124232,7 +124330,7 @@ function registerExtendedTools(server, client) {
|
|
|
124232
124330
|
const description = playlistFields(pl).description;
|
|
124233
124331
|
if (description) preview.description = description;
|
|
124234
124332
|
if (a.hasCatalog === true) preview.note = "This is a playlist saved from Apple Music's catalog; deleting removes it from your library only.";
|
|
124235
|
-
const gate = await
|
|
124333
|
+
const gate = await confirmWrite2(ctx, {
|
|
124236
124334
|
tool: "apple_music_delete_playlist",
|
|
124237
124335
|
action: "apple.music.playlist.delete",
|
|
124238
124336
|
message: `Delete the playlist "${name2}" (${String(preview.tracks)} tracks)? This cannot be undone.`,
|
|
@@ -124290,7 +124388,7 @@ function registerExtendedTools(server, client) {
|
|
|
124290
124388
|
const a = attrs(f);
|
|
124291
124389
|
return { id: f.id, name: nameOf2(f), ...str3(a.artistName) ? { artistName: a.artistName } : {} };
|
|
124292
124390
|
});
|
|
124293
|
-
const gate = await
|
|
124391
|
+
const gate = await confirmWrite2(ctx, {
|
|
124294
124392
|
tool: "apple_music_remove_from_library",
|
|
124295
124393
|
action: "apple.music.library.remove",
|
|
124296
124394
|
message: `Remove ${removing.length} ${args.type} from your Apple Music library?`,
|
|
@@ -125470,10 +125568,10 @@ var init_registry = __esm({
|
|
|
125470
125568
|
// src/index.ts
|
|
125471
125569
|
init_dist2();
|
|
125472
125570
|
init_version();
|
|
125473
|
-
import { dirname as
|
|
125571
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
125474
125572
|
import { fileURLToPath } from "node:url";
|
|
125475
125573
|
try {
|
|
125476
|
-
await loadDotenvSafely({ path:
|
|
125574
|
+
await loadDotenvSafely({ path: join6(dirname4(fileURLToPath(import.meta.url)), "..", ".env") });
|
|
125477
125575
|
} catch {
|
|
125478
125576
|
}
|
|
125479
125577
|
if (process.argv[2] === "music-auth") {
|
package/dist/tools/_confirm.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { isAbsolute } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { confirmTokenParam, confirmWrite as sharedConfirmWrite, createFileSpentTokenStore, createSpentTokenStore, readEnvVar, spentTokenStoreFromEnv, } from '@chrischall/mcp-utils';
|
|
4
|
+
import { resolveStateFile } from '@chrischall/mcp-utils/session';
|
|
5
|
+
import { STATE_SUBDIR, isStateCacheEnabled } from '../state.js';
|
|
5
6
|
export { confirmTokenParam };
|
|
6
7
|
/** The sentence every confirm-gated tool's description ends with. */
|
|
7
8
|
export const CONFIRM_NOTE = 'Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call '
|
|
@@ -18,89 +19,65 @@ export const CONFIRM_NOTE = 'Asks the user to confirm first: a confirmation prom
|
|
|
18
19
|
* before the write, with the freshly built payload and a freshly read
|
|
19
20
|
* revision — the re-read is what makes a stale token fail.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* A thin wrapper over mcp-utils' `confirmWrite` (chrischall/fleet-audit#1217),
|
|
23
|
+
* which binds the arguments AND the state the user was shown (target,
|
|
24
|
+
* revision, payload, preview) on both rails — so an acceptance given while
|
|
25
|
+
* "position 1" was one song cannot remove whatever sits there when the retry
|
|
26
|
+
* re-reads the playlist. The preview gains the shared `willSend` field (the
|
|
27
|
+
* payload). What this wrapper adds: the single-account `account: undefined`,
|
|
28
|
+
* the Apple-app refusal hint, and the spent-token store choice below.
|
|
27
29
|
*
|
|
28
30
|
* Returns `undefined` to proceed, otherwise the result to return unchanged.
|
|
29
31
|
*/
|
|
30
32
|
export function confirmWrite(ctx, opts) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
...(opts.revision !== undefined ? { revision: opts.revision } : {}),
|
|
34
|
-
payload: opts.payload,
|
|
35
|
-
preview: opts.preview,
|
|
36
|
-
};
|
|
37
|
-
return requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
33
|
+
return sharedConfirmWrite(ctx, {
|
|
34
|
+
tool: opts.tool,
|
|
38
35
|
action: opts.action,
|
|
39
36
|
message: opts.message,
|
|
40
|
-
details: opts.preview,
|
|
41
|
-
unsupportedNote: 'Make this change in the Apple app (Music, Calendar, Contacts or Mail) instead.',
|
|
42
|
-
tool: opts.tool,
|
|
43
37
|
// Single-account server: every write acts as the one set of credentials in the env
|
|
44
38
|
// (ICLOUD_USERNAME / the Apple Music user token), so there is no principal to choose between.
|
|
45
39
|
account: undefined,
|
|
46
|
-
|
|
40
|
+
target: opts.target,
|
|
41
|
+
...(opts.revision !== undefined ? { revision: opts.revision } : {}),
|
|
42
|
+
payload: opts.payload,
|
|
43
|
+
preview: opts.preview,
|
|
47
44
|
args: opts.args,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
key: confirmKeyFromEnv(),
|
|
54
|
-
args: { args: withoutConfirmToken(opts.args), ...subject },
|
|
55
|
-
ttlSeconds: confirmTtlFromEnv(),
|
|
56
|
-
},
|
|
57
|
-
}));
|
|
58
|
-
}
|
|
59
|
-
/** The arguments as bound: the token differs between the phases, so it is never part of the commitment. */
|
|
60
|
-
function withoutConfirmToken(args) {
|
|
61
|
-
const { confirmToken, ...rest } = args;
|
|
62
|
-
void confirmToken;
|
|
63
|
-
return rest;
|
|
45
|
+
confirmToken: opts.confirmToken,
|
|
46
|
+
unsupportedNote: 'Make this change in the Apple app (Music, Calendar, Contacts or Mail) instead.',
|
|
47
|
+
// Chosen per call: the data dir, APPLE_STATE_CACHE and the key are read now.
|
|
48
|
+
spent: spentTokenStore(),
|
|
49
|
+
});
|
|
64
50
|
}
|
|
65
51
|
// ---------------------------------------------------------------------------
|
|
66
52
|
// Spent tokens: single use that survives a restart
|
|
67
53
|
// ---------------------------------------------------------------------------
|
|
68
54
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* target with no revision to rotate (a new mail, an invitation-sending create)
|
|
76
|
-
* that replayed the whole write: a second email nobody approved.
|
|
55
|
+
* A stable key — MCP_CONFIRM_SECRET, or the MCP_HOST_CONFIRM_SECRET mcp-host
|
|
56
|
+
* derives per child beside an absolute MCP_DATA_DIR — makes a token valid in
|
|
57
|
+
* every process that shares it, so a used one must be remembered on disk or a
|
|
58
|
+
* restart inside its lifetime (a crash, a redeploy, the child dying right
|
|
59
|
+
* after the SMTP hand-off) would accept it again: for a target with no
|
|
60
|
+
* revision to rotate (a new mail) that is a second email nobody approved.
|
|
77
61
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* The file is bound to (a digest of) the HMAC key, so rotating the secret
|
|
82
|
-
* discards it, and so does a restart without a secret — every earlier token is
|
|
83
|
-
* TOKEN_INVALID under the new random key then anyway. Expired entries are
|
|
84
|
-
* pruned on every write and the file is capped at {@link MAX_SPENT_TOKENS}.
|
|
62
|
+
* mcp-utils' durable store does this (`$MCP_DATA_DIR/.mcp-confirm/spent`, one
|
|
63
|
+
* O_EXCL-claimed 0600 file per spent nonce, failing closed when it cannot
|
|
64
|
+
* look). On top of it this server keeps two things of its own:
|
|
85
65
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
66
|
+
* - `APPLE_STATE_CACHE=false` opts out of every on-disk file, this one too:
|
|
67
|
+
* spends are then remembered by this process only — reported once on stderr
|
|
68
|
+
* when a stable key makes that matter.
|
|
69
|
+
* - A local install with MCP_CONFIRM_SECRET but no absolute MCP_DATA_DIR keeps
|
|
70
|
+
* its spends beside the other state caches
|
|
71
|
+
* (`$HOME/.apple-icloud-mcp/confirm-spent/`), in the same shared format.
|
|
72
|
+
*
|
|
73
|
+
* Without a stable key a restart invalidates every token anyway, so the
|
|
74
|
+
* process-wide in-memory store is enough.
|
|
90
75
|
*/
|
|
91
|
-
export const
|
|
92
|
-
/**
|
|
93
|
-
export const MAX_SPENT_TOKENS = 500;
|
|
94
|
-
function validateSpent(raw) {
|
|
95
|
-
const spent = raw?.spent;
|
|
96
|
-
if (typeof spent !== 'object' || spent === null || Array.isArray(spent))
|
|
97
|
-
return null;
|
|
98
|
-
return { spent: Object.fromEntries(Object.entries(spent).filter(([, exp]) => Number.isFinite(exp))) };
|
|
99
|
-
}
|
|
100
|
-
const nonceDigest = (nonce) => createHash('sha256').update(nonce).digest('base64url');
|
|
101
|
-
/** The process-wide mirror, keyed by nonce digest like the file — and all there is when nothing persists. */
|
|
76
|
+
export const SPENT_TOKENS_DIR = 'confirm-spent';
|
|
77
|
+
/** The process-wide in-memory store, when nothing persists. */
|
|
102
78
|
const processSpent = createSpentTokenStore();
|
|
103
|
-
const
|
|
79
|
+
const localStores = new Map();
|
|
80
|
+
let warnedDisabled = false;
|
|
104
81
|
/**
|
|
105
82
|
* Whether the confirm key outlives the process — the same rule as mcp-utils'
|
|
106
83
|
* `confirmKeyFromEnv`: the operator's MCP_CONFIRM_SECRET, else mcp-host's
|
|
@@ -112,61 +89,33 @@ function hasStableConfirmKey(env) {
|
|
|
112
89
|
return true;
|
|
113
90
|
return readEnvVar('MCP_HOST_CONFIRM_SECRET', { env }) !== undefined && isAbsolute(readEnvVar('MCP_DATA_DIR', { env }) ?? '');
|
|
114
91
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const onDisk = () => file.load()?.spent ?? {};
|
|
138
|
-
return {
|
|
139
|
-
has: (nonce) => {
|
|
140
|
-
const d = nonceDigest(nonce);
|
|
141
|
-
return memory.has(d) || Object.hasOwn(onDisk(), d);
|
|
142
|
-
},
|
|
143
|
-
add: (nonce, expiresAtMs) => {
|
|
144
|
-
const d = nonceDigest(nonce);
|
|
145
|
-
memory.add(d, expiresAtMs);
|
|
146
|
-
const now = Date.now();
|
|
147
|
-
const kept = Object.entries(onDisk())
|
|
148
|
-
.filter(([other, exp]) => other !== d && exp >= now)
|
|
149
|
-
.sort(([, a], [, b]) => b - a)
|
|
150
|
-
.slice(0, MAX_SPENT_TOKENS - 1);
|
|
151
|
-
if (!file.save({ spent: Object.fromEntries([[d, expiresAtMs], ...kept]) })) {
|
|
152
|
-
warnOnce(isStateCacheEnabled(env) ? 'write' : 'disabled', env);
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
prune: (now) => memory.prune(now), // the file is pruned on every write
|
|
156
|
-
clear: () => {
|
|
157
|
-
memory.clear();
|
|
158
|
-
file.clear();
|
|
159
|
-
},
|
|
160
|
-
/** Distinct spent tokens known to either copy. */
|
|
161
|
-
get size() {
|
|
162
|
-
return memory.size + Object.keys(onDisk()).filter((d) => !memory.has(d)).length;
|
|
163
|
-
},
|
|
164
|
-
};
|
|
92
|
+
/** The spent-token store for a gated call, per the rules above. */
|
|
93
|
+
export function spentTokenStore(env = process.env) {
|
|
94
|
+
if (!isStateCacheEnabled(env)) {
|
|
95
|
+
if (!warnedDisabled && hasStableConfirmKey(env)) {
|
|
96
|
+
warnedDisabled = true;
|
|
97
|
+
console.error('[apple-icloud-mcp] WARNING: APPLE_STATE_CACHE=false, so a used confirmToken is remembered by this process only. '
|
|
98
|
+
+ "With MCP_CONFIRM_SECRET (or mcp-host's MCP_HOST_CONFIRM_SECRET) set, a restart before it expires would accept it again.");
|
|
99
|
+
}
|
|
100
|
+
return processSpent;
|
|
101
|
+
}
|
|
102
|
+
const shared = spentTokenStoreFromEnv(env);
|
|
103
|
+
if (shared)
|
|
104
|
+
return shared;
|
|
105
|
+
if (!hasStableConfirmKey(env))
|
|
106
|
+
return processSpent;
|
|
107
|
+
const dir = join(dirname(resolveStateFile({ env, subdir: STATE_SUBDIR, fileName: SPENT_TOKENS_DIR })), SPENT_TOKENS_DIR);
|
|
108
|
+
let store = localStores.get(dir);
|
|
109
|
+
if (!store) {
|
|
110
|
+
store = createFileSpentTokenStore(dir);
|
|
111
|
+
localStores.set(dir, store);
|
|
112
|
+
}
|
|
113
|
+
return store;
|
|
165
114
|
}
|
|
166
|
-
/** Test seam: forget the in-memory spent tokens (what a restart does) and the
|
|
115
|
+
/** Test seam: forget the in-memory spent tokens (what a restart does) and the warning. */
|
|
167
116
|
export function resetSpentTokenMemory() {
|
|
168
117
|
processSpent.clear();
|
|
169
|
-
|
|
118
|
+
warnedDisabled = false;
|
|
170
119
|
}
|
|
171
120
|
/**
|
|
172
121
|
* A revision string for a record as just read, for `ConfirmWriteOptions.revision`
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// The server's self-reported version. release-please bumps this line (it is
|
|
2
2
|
// registered in release-please-config.json `extra-files`); tests/version-sync
|
|
3
3
|
// fails CI if it ever drifts from package.json.
|
|
4
|
-
export const VERSION = '0.3.
|
|
4
|
+
export const VERSION = '0.3.6'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-icloud-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"mcpName": "io.github.chrischall/apple-icloud-mcp",
|
|
6
6
|
"description": "Unofficial MCP server for Apple's services — Apple Music playlists and library, iCloud Calendar, Contacts and Mail, Apple Maps, WeatherKit and iTunes search — that runs anywhere, no Mac needed. Not affiliated with Apple. Developed and maintained by AI (Claude Code)",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@chrischall/mcp-utils": "^3.0.
|
|
57
|
+
"@chrischall/mcp-utils": "^3.0.3",
|
|
58
58
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
59
59
|
"@xmldom/xmldom": "^0.9.12",
|
|
60
60
|
"dotenv": "^18.0.6",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/apple-icloud-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.6",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "apple-icloud-mcp",
|
|
14
|
-
"version": "0.3.
|
|
14
|
+
"version": "0.3.6",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|