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.
@@ -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.5"
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.5",
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.5",
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
- 58 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).
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 (`confirm-spent.json`), so none can be replayed (unless `APPLE_STATE_CACHE=false` or the directory is unwritable — a warning says so). |
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.json` — digests of used confirmation tokens, so none can be replayed after a restart.
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).
@@ -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.0 | MIT | https://github.com/chrischall/mcp-utils |
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.5 | MIT | https://github.com/colinhacks/zod |
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.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.5
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: 5
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(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
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 /_cf_chl_opt/.test(html) || /<title[^>]*>\s*Just a moment/i.test(html);
36601
+ return html.includes("_cf_chl_opt") || hasJustAMomentTitle(html);
36484
36602
  }
36485
- var NEXT_DATA_MAX_CHARS;
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.5";
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 confirmWrite(ctx, opts) {
46261
- const subject = {
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
- return requireConfirmationWithFallback(
46268
- ctx,
46269
- confirmationFromEnv({
46270
- action: opts.action,
46271
- message: opts.message,
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 warnOnce(kind, env) {
46307
- if (warned2.has(kind) || !hasStableConfirmKey(env)) return;
46308
- warned2.add(kind);
46309
- console.error(
46310
- `[apple-icloud-mcp] WARNING: ${kind === "disabled" ? "APPLE_STATE_CACHE=false, so a" : "could not record a"} used confirmToken ${kind === "disabled" ? "is" : "on disk; it 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.`
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, SPENT_TOKENS_FILE, MAX_SPENT_TOKENS, nonceDigest, processSpent, warned2;
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
- SPENT_TOKENS_FILE = "confirm-spent.json";
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
- warned2 = /* @__PURE__ */ new Set();
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 confirmWrite(ctx, {
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 confirmWrite(ctx, {
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 confirmWrite(ctx, {
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 confirmWrite(ctx, {
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: join6 } = __require("path");
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"] || join6(__dirname, "lib", "worker.js");
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: join6, isAbsolute: isAbsolute4, sep } = __require("node:path");
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"] || join6(__dirname, "worker.js");
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"] || join6(__dirname, "worker.js");
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 join6(__dirname, "..", "file.js");
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 join6 = ",";
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
- join6 = `,
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 += join6;
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 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
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
- join6 = `,
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 = join6;
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 = join6;
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 join6 = ",";
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
- join6 = `,
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 += join6;
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 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
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
- join6 = `,
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 = join6;
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 join7 = `,
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 += join7;
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 += `${join7}"... ${getItemCount(removedKeys)} not stringified"`;
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 join6 = `,
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, join6, maximumBreadth);
64294
+ res += stringifyTypedArray(value, join7, maximumBreadth);
64197
64295
  keys = keys.slice(value.length);
64198
64296
  maximumPropertiesToStringify -= value.length;
64199
- separator = join6;
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 = join6;
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 = join6;
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 replaceTags(str5, prefix, pattern, replacement) {
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 = replaceTags(str5, /<a\b/gi, /<a\b[^>]*href\s*=\s*["']?([^\s"']+)[^>]*>/iy, (match, href) => ` (${href}) `);
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 = replaceTags(str5, /<li\b/gi, /<li\b[^>]*>[\n\u0001\s]*/iy, () => "* ");
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 confirmWrite(ctx, {
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 confirmWrite(ctx, {
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 confirmWrite(ctx, {
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 confirmWrite(ctx, {
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 confirmWrite(ctx, {
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 dirname3, join as join5 } from "node:path";
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: join5(dirname3(fileURLToPath(import.meta.url)), "..", ".env") });
125574
+ await loadDotenvSafely({ path: join6(dirname4(fileURLToPath(import.meta.url)), "..", ".env") });
125477
125575
  } catch {
125478
125576
  }
125479
125577
  if (process.argv[2] === "music-auth") {
@@ -1,7 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { isAbsolute } from 'node:path';
3
- import { confirmKeyFromEnv, confirmTtlFromEnv, confirmationFromEnv, confirmTokenParam, createSpentTokenStore, readEnvVar, requireConfirmationWithFallback, } from '@chrischall/mcp-utils';
4
- import { isStateCacheEnabled, stateCache } from '../state.js';
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
- * Both paths commit to the same thing: the arguments AND the state the user was
22
- * shown (target, revision, payload, preview). A prompt's acceptance is bound
23
- * explicitly for that reason — mcp-utils' default binding covers the arguments
24
- * only, so an acceptance given while "position 1" was one song would have
25
- * removed whatever song sat there when the retry re-read the playlist. Now a
26
- * retry whose fresh read differs asks again, with the new preview.
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
- const subject = {
32
- target: opts.target,
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
- confirmToken: opts.confirmToken,
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
- // Built per call: the data dir, APPLE_STATE_CACHE and the key are read now.
49
- spent: createFileSpentTokenStore(),
50
- subject: () => subject,
51
- // A caller-supplied binding replaces the args-only default (and is used as given: strip the token here).
52
- binding: {
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
- * mcp-utils remembers a used token's nonce in process memory only (unless its
70
- * own durable store is in play), while a stable key — MCP_CONFIRM_SECRET, or
71
- * the MCP_HOST_CONFIRM_SECRET mcp-host derives per child beside an absolute
72
- * MCP_DATA_DIR — makes the token itself valid in every process that shares it. So after a
73
- * restart inside the token's lifetime — a crash, a redeploy, the child dying
74
- * right after the SMTP hand-off — an already-used token verified again. For a
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
- * The store below records each spent token on disk as well
79
- * (`$MCP_DATA_DIR/.apple-icloud-mcp/confirm-spent.json`, 0600): a SHA-256 of its nonce
80
- * and the time it would have expired anyway — nothing that could be replayed.
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
- * An in-memory mirror is ALWAYS kept as well, so single use within the process
87
- * never depends on the disk. With APPLE_STATE_CACHE=false, or when the file
88
- * cannot be written, only the mirror remains — reported once on stderr when a
89
- * shared secret makes that matter.
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 SPENT_TOKENS_FILE = 'confirm-spent.json';
92
- /** Most spent tokens kept on disk; the ones expiring soonest go first. */
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 warned = new Set();
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
- function warnOnce(kind, env) {
116
- // Without a stable key a restart invalidates every token anyway, so the
117
- // memory-only store loses nothing and there is nothing to warn about.
118
- if (warned.has(kind) || !hasStableConfirmKey(env))
119
- return;
120
- warned.add(kind);
121
- console.error(`[apple-icloud-mcp] WARNING: ${kind === 'disabled' ? 'APPLE_STATE_CACHE=false, so a' : 'could not record a'} used confirmToken ` +
122
- `${kind === 'disabled' ? 'is' : 'on disk; it is'} remembered by this process only. With MCP_CONFIRM_SECRET ` +
123
- "(or mcp-host's MCP_HOST_CONFIRM_SECRET) set, a restart before it expires would accept it again.");
124
- }
125
- /**
126
- * A {@link SpentTokenStore} backed by `confirm-spent.json` plus an in-memory
127
- * mirror. `has` consults both (the file is re-read, so a token spent by an
128
- * earlier process — or a sibling sharing the data dir — counts); `add` writes
129
- * both. Only phase-2 calls touch it, so the extra reads cost nothing. With
130
- * APPLE_STATE_CACHE=false the file side is a no-op and this is the mirror alone.
131
- */
132
- export function createFileSpentTokenStore(options = {}) {
133
- const env = options.env ?? process.env;
134
- const memory = options.memory ?? processSpent;
135
- const key = createHash('sha256').update(confirmKeyFromEnv(env)).digest('hex');
136
- const file = stateCache(SPENT_TOKENS_FILE, `confirm-spent-v1:${key}`, validateSpent, env);
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 warnings. */
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
- warned.clear();
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.5'; // x-release-please-version
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.5",
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.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.5",
9
+ "version": "0.3.6",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "apple-icloud-mcp",
14
- "version": "0.3.5",
14
+ "version": "0.3.6",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },