apple-icloud-mcp 0.3.2 → 0.3.4

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.2"
9
+ "version": "0.3.4"
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.2",
17
+ "version": "0.3.4",
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.2",
4
+ "version": "0.3.4",
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"
@@ -21,5 +21,5 @@
21
21
  "mcp"
22
22
  ],
23
23
  "skills": "./skills/",
24
- "mcp": "./.mcp.json"
24
+ "mcpServers": "./.mcp.json"
25
25
  }
package/README.md CHANGED
@@ -270,7 +270,7 @@ read in `DISPLAY_TZ` — so are calendar events stored without a time zone of th
270
270
  | `apple_music_get_ratings` | Whether you have loved or disliked songs, albums, playlists, music videos or stations — catalog or library ids — returning love, dislike or none per id. | read | |
271
271
  | `apple_music_create_playlist` | Create a new playlist in your Apple Music library, optionally with tracks (up to 500 catalog or library song ids; added 100 at a time), a description, a folder and public visibility (not in APPLE_WRITE_MODE=additive). | additive | |
272
272
  | `apple_music_add_playlist_tracks` | Append songs (up to 500 catalog or library ids) to the end of one of your library playlists. | additive | |
273
- | `apple_music_create_folder` | Create a playlist folder in your Apple Music library, at the top level or inside another folder. | additive | |
273
+ | `apple_music_create_folder` | Create a playlist folder in your Apple Music library, at the top level or inside another folder. | all | |
274
274
  | `apple_music_add_to_library` | Add catalog songs, albums, playlists or music videos to your Apple Music library by catalog id (up to 100 per type). | additive | |
275
275
  | `apple_music_add_favorites` | Mark catalog songs, albums, playlists, artists or music videos as favorites (the star in Apple Music; favorite songs go to your Favorite Songs playlist), by catalog id, up to 100 per type. | additive | |
276
276
  | `apple_music_set_rating` | Set your rating on a song, album, playlist, music video or station (catalog or library id): love, dislike, or none to clear it. | all | |
@@ -351,7 +351,7 @@ read in `DISPLAY_TZ` — so are calendar events stored without a time zone of th
351
351
  | Value | What is registered |
352
352
  |---|---|
353
353
  | `none` | Read tools only. |
354
- | `additive` | Reads, plus writes that only **add** to your own account: create a (private) playlist or folder, append tracks, add to library/favorites, create an event (without attendees, and not in a calendar shared with other people) or a contact. Nothing existing is modified or removed and nothing is sent to anyone. |
354
+ | `additive` | Reads, plus writes that only **add** to your own account: create a (private) playlist, append tracks, add to library/favorites, create an event (without attendees, and not in a calendar shared with other people) or a contact. Nothing existing is modified or removed and nothing is sent to anyone. |
355
355
  | `all` (default) | Everything. |
356
356
 
357
357
  Gated tools are not registered at all below their mode, so no prompt or injected instruction can call them.
@@ -363,13 +363,14 @@ An unrecognised value fails closed to `none`. `APPLE_SERVICES` (comma-separated:
363
363
  Irreversible actions and anything that reaches another person ask for confirmation first: **sending mail**,
364
364
  **deleting** an event, contact, playlist or library item, **removing or reordering** playlist tracks, and
365
365
  **creating or changing an event with attendees** (iCloud emails them). A client that can show a prompt
366
- (Claude Code) asks you directly. One that cannot (claude.ai) gets a two-step flow: the first call does nothing
366
+ (Claude Code) asks you directly, unless `MCP_CONFIRM_ELICITATION=off`. One that cannot (claude.ai) gets a two-step flow: the first call does nothing
367
367
  and returns a preview plus a `confirmToken`; only a repeat call with that token acts, and it is refused if the
368
368
  target changed in between.
369
369
 
370
370
  | Variable | Meaning |
371
371
  |---|---|
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
+ | `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). |
373
374
  | `MCP_CONFIRM_TTL_SECONDS` | How long a token is valid (default 600). |
374
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
376
 
@@ -430,6 +431,7 @@ All optional; each service activates when its credentials are present. Values th
430
431
  | Variable | Meaning |
431
432
  |---|---|
432
433
  | `MCP_CONFIRM_MODE` | How confirm-gated writes (send mail, deletes, removing tracks, invitations) behave on a client with no prompt, like claude.ai: "ask-user" (default: preview + confirmToken, the model must get your OK), "auto", or "refuse". Unknown values mean refuse. |
434
+ | `MCP_CONFIRM_ELICITATION` | "off" never shows a confirmation prompt, so every client gets the MCP_CONFIRM_MODE flow. Set it for a client that says it can prompt but never does (the gated call hangs, e.g. opencode 2.0.x). Any other value stays "on" (with a stderr warning). |
433
435
  | `MCP_CONFIRM_TTL_SECONDS` | Lifetime of a confirmToken in seconds (default 600). |
434
436
  | `MCP_CONFIRM_SECRET` 🔒 | Signing key for confirmTokens. Random per process by default; set it so a token issued just before a restart or redeploy still works (spent tokens are recorded on disk, so none can be replayed). |
435
437
 
@@ -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 | 2.13.1 | MIT | https://github.com/chrischall/mcp-utils |
25
+ | @chrischall/mcp-utils | 3.0.0 | 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 | — |
@@ -113,7 +113,7 @@ MPL-2.0 is a file-level license: it covers ical.js's own files and does not exte
113
113
 
114
114
  ## License and notice texts
115
115
 
116
- ### @chrischall/mcp-utils@2.13.1
116
+ ### @chrischall/mcp-utils@3.0.0
117
117
 
118
118
  - License: MIT
119
119
  - Source: https://github.com/chrischall/mcp-utils
@@ -2,10 +2,14 @@ import { createPrivateKey } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { expandPath, readEnvVar, signEs256Jwt } from '@chrischall/mcp-utils';
4
4
  import { ConfigError, rememberSecret } from './errors.js';
5
- const PREFIX = {
6
- music: 'APPLE_MUSIC',
7
- maps: 'APPLE_MAPS',
8
- weather: 'APPLE_WEATHERKIT',
5
+ /**
6
+ * Each service's own key pair, spelled out in full (not built from a name
7
+ * stem) so every variable the server reads appears literally in the code.
8
+ */
9
+ const OWN_VARS = {
10
+ music: { keyId: 'APPLE_MUSIC_KEY_ID', privateKey: 'APPLE_MUSIC_PRIVATE_KEY' },
11
+ maps: { keyId: 'APPLE_MAPS_KEY_ID', privateKey: 'APPLE_MAPS_PRIVATE_KEY' },
12
+ weather: { keyId: 'APPLE_WEATHERKIT_KEY_ID', privateKey: 'APPLE_WEATHERKIT_PRIVATE_KEY' },
9
13
  };
10
14
  /**
11
15
  * Accept the shapes a .p8 actually arrives in through an env block:
@@ -73,16 +77,16 @@ function readKeyFile(path, varName, service) {
73
77
  */
74
78
  export function resolveDeveloperKey(service, env = process.env) {
75
79
  const svcName = service;
76
- const prefix = PREFIX[service];
80
+ const own = OWN_VARS[service];
77
81
  const teamId = readEnvVar('APPLE_TEAM_ID', { env });
78
- const ownKeyId = readEnvVar(`${prefix}_KEY_ID`, { env });
79
- const ownKey = readEnvVar(`${prefix}_PRIVATE_KEY`, { env });
82
+ const ownKeyId = readEnvVar(own.keyId, { env });
83
+ const ownKey = readEnvVar(own.privateKey, { env });
80
84
  const keyId = ownKeyId ?? readEnvVar('APPLE_KEY_ID', { env });
81
85
  let rawKey;
82
86
  let keyVar;
83
87
  if (ownKey !== undefined) {
84
88
  rawKey = ownKey;
85
- keyVar = `${prefix}_PRIVATE_KEY`;
89
+ keyVar = own.privateKey;
86
90
  }
87
91
  else if (readEnvVar('APPLE_PRIVATE_KEY', { env }) !== undefined) {
88
92
  rawKey = readEnvVar('APPLE_PRIVATE_KEY', { env });
@@ -98,9 +102,9 @@ export function resolveDeveloperKey(service, env = process.env) {
98
102
  if (!teamId)
99
103
  missing.push('APPLE_TEAM_ID');
100
104
  if (!keyId)
101
- missing.push(`APPLE_KEY_ID (or ${prefix}_KEY_ID)`);
105
+ missing.push(`APPLE_KEY_ID (or ${own.keyId})`);
102
106
  if (rawKey === undefined)
103
- missing.push(`APPLE_PRIVATE_KEY (or ${prefix}_PRIVATE_KEY / APPLE_PRIVATE_KEY_PATH)`);
107
+ missing.push(`APPLE_PRIVATE_KEY (or ${own.privateKey} / APPLE_PRIVATE_KEY_PATH)`);
104
108
  if (missing.length === 0)
105
109
  assertPairMatches(service, env);
106
110
  if (missing.length > 0) {
@@ -116,7 +120,7 @@ export function resolveDeveloperKey(service, env = process.env) {
116
120
  teamId: teamId,
117
121
  keyId: keyId,
118
122
  privateKeyPem: pem,
119
- source: `${ownKeyId ? `${prefix}_KEY_ID` : 'APPLE_KEY_ID'} + ${keyVar}`,
123
+ source: `${ownKeyId ? own.keyId : 'APPLE_KEY_ID'} + ${keyVar}`,
120
124
  };
121
125
  }
122
126
  /**
@@ -130,9 +134,9 @@ export function resolveDeveloperKey(service, env = process.env) {
130
134
  * APPLE_PRIVATE_KEY for one Music key) and is allowed.
131
135
  */
132
136
  function assertPairMatches(service, env) {
133
- const prefix = PREFIX[service];
134
- const ownKeyId = readEnvVar(`${prefix}_KEY_ID`, { env });
135
- const ownKey = readEnvVar(`${prefix}_PRIVATE_KEY`, { env });
137
+ const own = OWN_VARS[service];
138
+ const ownKeyId = readEnvVar(own.keyId, { env });
139
+ const ownKey = readEnvVar(own.privateKey, { env });
136
140
  const sharedKeyId = readEnvVar('APPLE_KEY_ID', { env });
137
141
  const sharedKeyVar = readEnvVar('APPLE_PRIVATE_KEY', { env }) !== undefined
138
142
  ? 'APPLE_PRIVATE_KEY'
@@ -142,12 +146,12 @@ function assertPairMatches(service, env) {
142
146
  if (sharedKeyId === undefined || sharedKeyVar === undefined)
143
147
  return;
144
148
  if (ownKey !== undefined && ownKeyId === undefined) {
145
- throw new ConfigError(service, `${prefix}_PRIVATE_KEY is set but ${prefix}_KEY_ID is not, and APPLE_KEY_ID belongs to the shared key in ${sharedKeyVar} — ` +
146
- 'a key id must name the key that signs the token.', [`${prefix}_KEY_ID`], `Set ${prefix}_KEY_ID to the Key ID of the key in ${prefix}_PRIVATE_KEY (Apple Developer portal → Keys), or unset ${prefix}_PRIVATE_KEY to use the shared key.`);
149
+ throw new ConfigError(service, `${own.privateKey} is set but ${own.keyId} is not, and APPLE_KEY_ID belongs to the shared key in ${sharedKeyVar} — ` +
150
+ 'a key id must name the key that signs the token.', [own.keyId], `Set ${own.keyId} to the Key ID of the key in ${own.privateKey} (Apple Developer portal → Keys), or unset ${own.privateKey} to use the shared key.`);
147
151
  }
148
152
  if (ownKeyId !== undefined && ownKey === undefined && ownKeyId !== sharedKeyId) {
149
- throw new ConfigError(service, `${prefix}_KEY_ID is set but ${prefix}_PRIVATE_KEY is not, and the shared key in ${sharedKeyVar} belongs to APPLE_KEY_ID — ` +
150
- 'a key id must name the key that signs the token.', [`${prefix}_PRIVATE_KEY`], `Set ${prefix}_PRIVATE_KEY to the .p8 contents of key ${prefix}_KEY_ID, or unset ${prefix}_KEY_ID to use the shared key.`);
153
+ throw new ConfigError(service, `${own.keyId} is set but ${own.privateKey} is not, and the shared key in ${sharedKeyVar} belongs to APPLE_KEY_ID — ` +
154
+ 'a key id must name the key that signs the token.', [own.privateKey], `Set ${own.privateKey} to the .p8 contents of key ${own.keyId}, or unset ${own.keyId} to use the shared key.`);
151
155
  }
152
156
  }
153
157
  function serviceLabel(service) {
package/dist/bundle.js CHANGED
@@ -35348,17 +35348,30 @@ function redactJsonCookie(quote) {
35348
35348
  function redactSecrets(text2) {
35349
35349
  return text2.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(GOOGLE_OAUTH_TOKEN_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_COOKIE_DQ_RE, redactJsonCookie('"')).replace(JSON_COOKIE_SQ_RE, redactJsonCookie("'")).replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_NUM_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
35350
35350
  }
35351
+ function messageOf(err) {
35352
+ if (err instanceof Error)
35353
+ return err.message;
35354
+ return String(err);
35355
+ }
35351
35356
  var McpToolError, HEADER_NAME_RUN, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, GOOGLE_OAUTH_TOKEN_RE, QUERY_SECRET_RE, OAUTH_CODE_RE, FORM_PASSWORD_RE, HEADER_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE, JSON_SECRET_NUM_RE, JSON_COOKIE_DQ_RE, JSON_COOKIE_SQ_RE, DQ_STRING_RE, SQ_STRING_RE, ERROR_REDACTION_INPUT_MAX;
35352
35357
  var init_errors3 = __esm({
35353
35358
  "node_modules/@chrischall/mcp-utils/dist/errors/index.js"() {
35354
35359
  McpToolError = class extends Error {
35355
35360
  /** Actionable remediation text, when one applies. */
35356
35361
  hint;
35362
+ /** The upstream HTTP status this failure came from, when known. */
35363
+ status;
35364
+ /** What kind of failure this is, when the thrower knows. */
35365
+ kind;
35357
35366
  constructor(message, opts) {
35358
35367
  super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
35359
35368
  this.name = "McpToolError";
35360
35369
  if (opts?.hint !== void 0)
35361
35370
  this.hint = opts.hint;
35371
+ if (opts?.status !== void 0)
35372
+ this.status = opts.status;
35373
+ if (opts?.kind !== void 0)
35374
+ this.kind = opts.kind;
35362
35375
  Object.setPrototypeOf(this, new.target.prototype);
35363
35376
  }
35364
35377
  };
@@ -35383,7 +35396,7 @@ var init_errors3 = __esm({
35383
35396
  // webhook signing secret (Stripe-style)
35384
35397
  ].map((p) => `\\b${p}`).join("|"), "g");
35385
35398
  GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
35386
- 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|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
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");
35387
35400
  OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
35388
35401
  FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
35389
35402
  HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
@@ -35394,6 +35407,8 @@ var init_errors3 = __esm({
35394
35407
  "api[_-]?secret",
35395
35408
  "(?:x[_-])?api[_-]?key",
35396
35409
  "private[_-]?key",
35410
+ "code[_-]?verifier",
35411
+ // PKCE; a bare `verifier` key is too generic for JSON
35397
35412
  `x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
35398
35413
  // bounded like HEADER_SECRET_RE
35399
35414
  "(?:proxy-)?authorization",
@@ -35629,6 +35644,15 @@ ${JSON.stringify(preview, null, 2)}`,
35629
35644
  };
35630
35645
  const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
35631
35646
  const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
35647
+ if (options.elicitation === false) {
35648
+ return textResult({
35649
+ confirmed: false,
35650
+ dispatched: false,
35651
+ action: options.action,
35652
+ reason: "confirmation-unsupported",
35653
+ note: options.unsupportedNote ? `${DISABLED_NOTE}. ${options.unsupportedNote}` : `${DISABLED_NOTE}.`
35654
+ });
35655
+ }
35632
35656
  if (response.kind === "missing") {
35633
35657
  if (callerAcceptsFormElicitation(ctx) === false) {
35634
35658
  return textResult({
@@ -35659,7 +35683,7 @@ ${JSON.stringify(preview, null, 2)}`,
35659
35683
  note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
35660
35684
  });
35661
35685
  }
35662
- var DEFAULT_REQUEST_KEY, DEFAULT_CONFIRMATION_LABEL, UNSUPPORTED_NOTE, STATE_PREFIX;
35686
+ var DEFAULT_REQUEST_KEY, DEFAULT_CONFIRMATION_LABEL, UNSUPPORTED_NOTE, DISABLED_NOTE, STATE_PREFIX;
35663
35687
  var init_confirmation = __esm({
35664
35688
  "node_modules/@chrischall/mcp-utils/dist/server/confirmation.js"() {
35665
35689
  init_dist();
@@ -35670,6 +35694,7 @@ var init_confirmation = __esm({
35670
35694
  DEFAULT_REQUEST_KEY = "confirmation";
35671
35695
  DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
35672
35696
  UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
35697
+ DISABLED_NOTE = "Nothing was done because confirmation prompts are turned off for this server, and this action is never taken without one";
35673
35698
  STATE_PREFIX = "mcpu.confirm.v1.";
35674
35699
  }
35675
35700
  });
@@ -35823,7 +35848,7 @@ async function tokenConfirmation(action, fb) {
35823
35848
  }
35824
35849
  async function requireConfirmationWithFallback(ctx, options) {
35825
35850
  const { tokenFallback, ...confirmation } = options;
35826
- if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
35851
+ if (tokenFallback && (options.elicitation === false || callerAcceptsFormElicitation(ctx) === false)) {
35827
35852
  return tokenConfirmation(options.action, tokenFallback);
35828
35853
  }
35829
35854
  return requireConfirmation(ctx, confirmation);
@@ -35862,7 +35887,7 @@ function readEnvVar(key, opts = {}) {
35862
35887
  if (typeof raw === "string") {
35863
35888
  const trimmed = raw.trim();
35864
35889
  if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
35865
- return trimmed;
35890
+ return opts.trim === false ? raw : trimmed;
35866
35891
  }
35867
35892
  }
35868
35893
  return opts.default;
@@ -36030,6 +36055,20 @@ function readConfirmMode(env = process.env) {
36030
36055
  }
36031
36056
  return "refuse";
36032
36057
  }
36058
+ function readConfirmElicitation(env = process.env) {
36059
+ const raw = readEnvVar("MCP_CONFIRM_ELICITATION", { env })?.trim().toLowerCase();
36060
+ if (!raw || raw === "on")
36061
+ return "on";
36062
+ if (raw === "off")
36063
+ return "off";
36064
+ const key = `elicitation:${raw}`;
36065
+ if (!warned.has(key)) {
36066
+ warned.add(key);
36067
+ process.stderr.write(`MCP_CONFIRM_ELICITATION="${raw}" is not one of on, off; treating it as on.
36068
+ `);
36069
+ }
36070
+ return "on";
36071
+ }
36033
36072
  function readConfirmTtl(env) {
36034
36073
  const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
36035
36074
  if (!raw)
@@ -36082,19 +36121,29 @@ function boundArgs(args) {
36082
36121
  }
36083
36122
  function confirmationFromEnv(options) {
36084
36123
  const { tool, account, confirmToken, subject, instruction, args, env = process.env, spent: callerSpent, ...rest } = options;
36124
+ if (args === void 0 || args === null) {
36125
+ throw new TypeError(`confirmationFromEnv: ${tool} passed no args; pass the tool's validated arguments ({} for a tool with none).`);
36126
+ }
36085
36127
  const spent = callerSpent ?? spentTokenStoreFromEnv(env);
36086
36128
  const mode = readConfirmMode(env);
36087
36129
  const ttlSeconds = readConfirmTtl(env);
36088
- const bound = args === void 0 ? void 0 : boundArgs(args);
36089
- const confirmation = bound === void 0 || rest.binding ? rest : { ...rest, binding: { key: confirmKeyFromEnv(env), args: bound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 } };
36130
+ const elicitationOff = readConfirmElicitation(env) === "off";
36131
+ const bound = boundArgs(args);
36132
+ const elicitationBound = account === void 0 ? { args: bound } : { account, args: bound };
36133
+ const bindingAdded = rest.binding ? rest : {
36134
+ ...rest,
36135
+ binding: { key: confirmKeyFromEnv(env), args: elicitationBound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 }
36136
+ };
36137
+ const confirmation = elicitationOff ? { ...bindingAdded, elicitation: false } : bindingAdded;
36090
36138
  if (mode === "refuse" || ttlSeconds === void 0) {
36091
- const hint = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
36139
+ const reason = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
36140
+ const hint = elicitationOff ? `${ELICITATION_OFF_HINT} ${reason}` : reason;
36092
36141
  return {
36093
36142
  ...confirmation,
36094
36143
  unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
36095
36144
  };
36096
36145
  }
36097
- const boundSubject = bound === void 0 ? subject : async () => {
36146
+ const boundSubject = async () => {
36098
36147
  const read = await subject();
36099
36148
  if ("content" in read)
36100
36149
  return read;
@@ -36114,7 +36163,7 @@ function confirmationFromEnv(options) {
36114
36163
  }
36115
36164
  };
36116
36165
  }
36117
- var MODES, DEFAULT_TTL_SECONDS2, warned, processKey, fileStores, CONFIRM_TOKEN_AUTO_INSTRUCTION, REFUSE_HINT, BAD_TTL_HINT;
36166
+ var MODES, DEFAULT_TTL_SECONDS2, warned, processKey, fileStores, CONFIRM_TOKEN_AUTO_INSTRUCTION, REFUSE_HINT, ELICITATION_OFF_HINT, BAD_TTL_HINT;
36118
36167
  var init_confirm_env = __esm({
36119
36168
  "node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js"() {
36120
36169
  init_config();
@@ -36126,6 +36175,7 @@ var init_confirm_env = __esm({
36126
36175
  fileStores = /* @__PURE__ */ new Map();
36127
36176
  CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
36128
36177
  REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
36178
+ ELICITATION_OFF_HINT = "MCP_CONFIRM_ELICITATION=off on the server turns confirmation prompts off.";
36129
36179
  BAD_TTL_HINT = "MCP_CONFIRM_TTL_SECONDS on the server is not a positive whole number of seconds; fix it to allow two-step confirmation.";
36130
36180
  }
36131
36181
  });
@@ -36146,12 +36196,14 @@ var init_merge_write = __esm({
36146
36196
 
36147
36197
  // node_modules/@chrischall/mcp-utils/dist/server/index.js
36148
36198
  function hintResultOrRethrow(err) {
36199
+ if (err instanceof ProtocolError && err.code === ProtocolErrorCode.UrlElicitationRequired)
36200
+ throw err;
36149
36201
  if (err instanceof McpToolError && err.hint) {
36150
36202
  return errorResult(`${err.message}
36151
36203
 
36152
36204
  Hint: ${err.hint}`);
36153
36205
  }
36154
- throw err;
36206
+ return errorResult(messageOf(err));
36155
36207
  }
36156
36208
  function mcpRequestFrom(args) {
36157
36209
  const ctx = args.at(-1);
@@ -36222,7 +36274,10 @@ function surfaceToolHints(server) {
36222
36274
  ));
36223
36275
  }
36224
36276
  async function createMcpServer(opts) {
36225
- const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
36277
+ const server = new McpServer({ name: opts.name, version: opts.version }, {
36278
+ supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS],
36279
+ ...opts.maxToolInputElements !== void 0 ? { maxToolInputElements: opts.maxToolInputElements } : {}
36280
+ });
36226
36281
  if (opts.surfaceHints !== false)
36227
36282
  surfaceToolHints(server);
36228
36283
  if (opts.banner !== void 0) {
@@ -36367,6 +36422,7 @@ var init_write = __esm({
36367
36422
  var init_output = __esm({
36368
36423
  "node_modules/@chrischall/mcp-utils/dist/fs/output.js"() {
36369
36424
  init_config();
36425
+ init_errors3();
36370
36426
  init_confine();
36371
36427
  init_write();
36372
36428
  }
@@ -36438,6 +36494,7 @@ var init_scrape = __esm({
36438
36494
  // node_modules/@chrischall/mcp-utils/dist/http/throttle.js
36439
36495
  var init_throttle = __esm({
36440
36496
  "node_modules/@chrischall/mcp-utils/dist/http/throttle.js"() {
36497
+ init_cancel();
36441
36498
  }
36442
36499
  });
36443
36500
 
@@ -36446,6 +36503,8 @@ function createResponseCache(opts) {
36446
36503
  const now = opts.now ?? Date.now;
36447
36504
  const maxEntries = opts.maxEntries ?? RESPONSE_CACHE_MAX_ENTRIES;
36448
36505
  const store2 = /* @__PURE__ */ new Map();
36506
+ const inflight2 = /* @__PURE__ */ new Map();
36507
+ let generation = 0;
36449
36508
  const ttlFor = (tier) => opts.ttlMs[tier] ?? 0;
36450
36509
  function evictForInsert() {
36451
36510
  if (store2.size < maxEntries)
@@ -36482,25 +36541,77 @@ function createResponseCache(opts) {
36482
36541
  store2.delete(key);
36483
36542
  store2.set(key, { expiresAt: now() + ttl, value });
36484
36543
  }
36485
- return {
36486
- get,
36487
- set: set2,
36488
- async fetchThrough(key, load, tier = "dynamic") {
36544
+ function startLoad(key, load, tier, signal) {
36545
+ const gen = generation;
36546
+ const shared = (async () => load())().then((value) => {
36547
+ if (gen === generation)
36548
+ set2(key, value, tier);
36549
+ return value;
36550
+ });
36551
+ const entry = { promise: shared, signal };
36552
+ inflight2.set(key, entry);
36553
+ const drop = () => {
36554
+ if (inflight2.get(key) === entry)
36555
+ inflight2.delete(key);
36556
+ };
36557
+ shared.then(drop, drop);
36558
+ return entry;
36559
+ }
36560
+ async function fetchThrough(key, load, tier = "dynamic", options = {}) {
36561
+ const { signal } = options;
36562
+ for (; ; ) {
36563
+ signal?.throwIfAborted();
36489
36564
  const hit = get(key);
36490
36565
  if (hit !== void 0)
36491
36566
  return hit;
36492
- const value = await load();
36493
- set2(key, value, tier);
36494
- return value;
36495
- },
36567
+ const pending = inflight2.get(key);
36568
+ if (!pending)
36569
+ return withSignal(startLoad(key, load, tier, signal).promise, signal);
36570
+ try {
36571
+ return await withSignal(pending.promise, signal);
36572
+ } catch (err) {
36573
+ const cancelled = pending.signal?.aborted === true || isCancellation(err);
36574
+ if (cancelled && !signal?.aborted)
36575
+ continue;
36576
+ throw err;
36577
+ }
36578
+ }
36579
+ }
36580
+ return {
36581
+ get,
36582
+ set: set2,
36583
+ fetchThrough,
36496
36584
  clear() {
36497
36585
  store2.clear();
36586
+ inflight2.clear();
36587
+ generation += 1;
36498
36588
  },
36499
36589
  get size() {
36500
36590
  return store2.size;
36501
36591
  }
36502
36592
  };
36503
36593
  }
36594
+ function withSignal(promise2, signal) {
36595
+ if (!signal)
36596
+ return promise2;
36597
+ return new Promise((resolve6, reject) => {
36598
+ const onAbort = () => reject(signal.reason);
36599
+ signal.addEventListener("abort", onAbort, { once: true });
36600
+ promise2.then((v) => {
36601
+ signal.removeEventListener("abort", onAbort);
36602
+ resolve6(v);
36603
+ }, (e) => {
36604
+ signal.removeEventListener("abort", onAbort);
36605
+ reject(e);
36606
+ });
36607
+ });
36608
+ }
36609
+ function isCancellation(err) {
36610
+ if (typeof err !== "object" || err === null)
36611
+ return false;
36612
+ const name2 = err.name;
36613
+ return name2 === "AbortError" || name2 === "CancelledError" || name2 === "CanceledError";
36614
+ }
36504
36615
  var RESPONSE_CACHE_MAX_ENTRIES;
36505
36616
  var init_response_cache = __esm({
36506
36617
  "node_modules/@chrischall/mcp-utils/dist/http/response-cache.js"() {
@@ -36524,6 +36635,31 @@ var init_net_atoms = __esm({
36524
36635
  }
36525
36636
  });
36526
36637
 
36638
+ // node_modules/@chrischall/mcp-utils/dist/http/body-limit.js
36639
+ var init_body_limit = __esm({
36640
+ "node_modules/@chrischall/mcp-utils/dist/http/body-limit.js"() {
36641
+ init_errors3();
36642
+ }
36643
+ });
36644
+
36645
+ // node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js
36646
+ var init_fetch_bounded = __esm({
36647
+ "node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js"() {
36648
+ init_cancel();
36649
+ init_errors3();
36650
+ init_body_limit();
36651
+ init_http();
36652
+ }
36653
+ });
36654
+
36655
+ // node_modules/@chrischall/mcp-utils/dist/http/url-safety.js
36656
+ var init_url_safety = __esm({
36657
+ "node_modules/@chrischall/mcp-utils/dist/http/url-safety.js"() {
36658
+ init_errors3();
36659
+ init_config();
36660
+ }
36661
+ });
36662
+
36527
36663
  // node_modules/@chrischall/mcp-utils/dist/internal/retry-after.js
36528
36664
  var init_retry_after = __esm({
36529
36665
  "node_modules/@chrischall/mcp-utils/dist/internal/retry-after.js"() {
@@ -36566,7 +36702,11 @@ var init_http = __esm({
36566
36702
  init_throttle();
36567
36703
  init_response_cache();
36568
36704
  init_net_atoms();
36705
+ init_fetch_bounded();
36706
+ init_url_safety();
36707
+ init_body_limit();
36569
36708
  init_net_atoms();
36709
+ init_url_safety();
36570
36710
  init_retry_after();
36571
36711
  EDGE_BLOCK_SCAN_MAX = 8 * 1024;
36572
36712
  EDGE_BLOCK_MARKERS = [
@@ -36631,6 +36771,7 @@ var init_concurrency = __esm({
36631
36771
  // node_modules/@chrischall/mcp-utils/dist/dates/index.js
36632
36772
  var init_dates = __esm({
36633
36773
  "node_modules/@chrischall/mcp-utils/dist/dates/index.js"() {
36774
+ init_config();
36634
36775
  }
36635
36776
  });
36636
36777
 
@@ -36664,7 +36805,7 @@ var init_zod2 = __esm({
36664
36805
  NonNegInt = external_exports.number().int().nonnegative();
36665
36806
  NonEmptyString = external_exports.string().min(1);
36666
36807
  IsoDate = external_exports.iso.date();
36667
- IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
36808
+ IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30").overwrite((v) => v.padStart(5, "0"));
36668
36809
  NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
36669
36810
  SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
36670
36811
  schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
@@ -36743,6 +36884,7 @@ var init_auth = __esm({
36743
36884
  init_config();
36744
36885
  init_errors3();
36745
36886
  init_http();
36887
+ init_cancel();
36746
36888
  init_cached_token();
36747
36889
  init_es256();
36748
36890
  }
@@ -36772,7 +36914,7 @@ var VERSION;
36772
36914
  var init_version = __esm({
36773
36915
  "src/version.ts"() {
36774
36916
  "use strict";
36775
- VERSION = "0.3.2";
36917
+ VERSION = "0.3.4";
36776
36918
  }
36777
36919
  });
36778
36920
 
@@ -36793,7 +36935,7 @@ function errorMessage(err) {
36793
36935
  if (err instanceof Error) return scrub(err.message);
36794
36936
  return scrub(String(err));
36795
36937
  }
36796
- var AppleToolError, ConfigError, CredentialsRejectedError, UpstreamError, ResponseTooLargeError, TransportError, InvalidArgumentError, UnconfirmedWriteError, secretLiterals, PEM_PRIVATE_KEY_RE;
36938
+ var AppleToolError, ConfigError, CredentialsRejectedError, UpstreamError, ResponseTooLargeError2, TransportError, InvalidArgumentError, UnconfirmedWriteError, secretLiterals, PEM_PRIVATE_KEY_RE;
36797
36939
  var init_errors4 = __esm({
36798
36940
  "src/errors.ts"() {
36799
36941
  "use strict";
@@ -36842,7 +36984,7 @@ var init_errors4 = __esm({
36842
36984
  if (opts.upstreamCode !== void 0) this.upstreamCode = opts.upstreamCode;
36843
36985
  }
36844
36986
  };
36845
- ResponseTooLargeError = class extends UpstreamError {
36987
+ ResponseTooLargeError2 = class extends UpstreamError {
36846
36988
  constructor(service, status2, message) {
36847
36989
  super(service, status2, message);
36848
36990
  this.name = "ResponseTooLargeError";
@@ -36921,16 +37063,16 @@ function readKeyFile(path2, varName, service) {
36921
37063
  }
36922
37064
  function resolveDeveloperKey(service, env = process.env) {
36923
37065
  const svcName = service;
36924
- const prefix = PREFIX2[service];
37066
+ const own2 = OWN_VARS[service];
36925
37067
  const teamId = readEnvVar("APPLE_TEAM_ID", { env });
36926
- const ownKeyId = readEnvVar(`${prefix}_KEY_ID`, { env });
36927
- const ownKey = readEnvVar(`${prefix}_PRIVATE_KEY`, { env });
37068
+ const ownKeyId = readEnvVar(own2.keyId, { env });
37069
+ const ownKey = readEnvVar(own2.privateKey, { env });
36928
37070
  const keyId = ownKeyId ?? readEnvVar("APPLE_KEY_ID", { env });
36929
37071
  let rawKey;
36930
37072
  let keyVar;
36931
37073
  if (ownKey !== void 0) {
36932
37074
  rawKey = ownKey;
36933
- keyVar = `${prefix}_PRIVATE_KEY`;
37075
+ keyVar = own2.privateKey;
36934
37076
  } else if (readEnvVar("APPLE_PRIVATE_KEY", { env }) !== void 0) {
36935
37077
  rawKey = readEnvVar("APPLE_PRIVATE_KEY", { env });
36936
37078
  keyVar = "APPLE_PRIVATE_KEY";
@@ -36941,8 +37083,8 @@ function resolveDeveloperKey(service, env = process.env) {
36941
37083
  }
36942
37084
  const missing = [];
36943
37085
  if (!teamId) missing.push("APPLE_TEAM_ID");
36944
- if (!keyId) missing.push(`APPLE_KEY_ID (or ${prefix}_KEY_ID)`);
36945
- if (rawKey === void 0) missing.push(`APPLE_PRIVATE_KEY (or ${prefix}_PRIVATE_KEY / APPLE_PRIVATE_KEY_PATH)`);
37086
+ if (!keyId) missing.push(`APPLE_KEY_ID (or ${own2.keyId})`);
37087
+ if (rawKey === void 0) missing.push(`APPLE_PRIVATE_KEY (or ${own2.privateKey} / APPLE_PRIVATE_KEY_PATH)`);
36946
37088
  if (missing.length === 0) assertPairMatches(service, env);
36947
37089
  if (missing.length > 0) {
36948
37090
  throw new ConfigError(
@@ -36967,30 +37109,30 @@ function resolveDeveloperKey(service, env = process.env) {
36967
37109
  teamId,
36968
37110
  keyId,
36969
37111
  privateKeyPem: pem,
36970
- source: `${ownKeyId ? `${prefix}_KEY_ID` : "APPLE_KEY_ID"} + ${keyVar}`
37112
+ source: `${ownKeyId ? own2.keyId : "APPLE_KEY_ID"} + ${keyVar}`
36971
37113
  };
36972
37114
  }
36973
37115
  function assertPairMatches(service, env) {
36974
- const prefix = PREFIX2[service];
36975
- const ownKeyId = readEnvVar(`${prefix}_KEY_ID`, { env });
36976
- const ownKey = readEnvVar(`${prefix}_PRIVATE_KEY`, { env });
37116
+ const own2 = OWN_VARS[service];
37117
+ const ownKeyId = readEnvVar(own2.keyId, { env });
37118
+ const ownKey = readEnvVar(own2.privateKey, { env });
36977
37119
  const sharedKeyId = readEnvVar("APPLE_KEY_ID", { env });
36978
37120
  const sharedKeyVar = readEnvVar("APPLE_PRIVATE_KEY", { env }) !== void 0 ? "APPLE_PRIVATE_KEY" : readEnvVar("APPLE_PRIVATE_KEY_PATH", { env }) !== void 0 ? "APPLE_PRIVATE_KEY_PATH" : void 0;
36979
37121
  if (sharedKeyId === void 0 || sharedKeyVar === void 0) return;
36980
37122
  if (ownKey !== void 0 && ownKeyId === void 0) {
36981
37123
  throw new ConfigError(
36982
37124
  service,
36983
- `${prefix}_PRIVATE_KEY is set but ${prefix}_KEY_ID is not, and APPLE_KEY_ID belongs to the shared key in ${sharedKeyVar} \u2014 a key id must name the key that signs the token.`,
36984
- [`${prefix}_KEY_ID`],
36985
- `Set ${prefix}_KEY_ID to the Key ID of the key in ${prefix}_PRIVATE_KEY (Apple Developer portal \u2192 Keys), or unset ${prefix}_PRIVATE_KEY to use the shared key.`
37125
+ `${own2.privateKey} is set but ${own2.keyId} is not, and APPLE_KEY_ID belongs to the shared key in ${sharedKeyVar} \u2014 a key id must name the key that signs the token.`,
37126
+ [own2.keyId],
37127
+ `Set ${own2.keyId} to the Key ID of the key in ${own2.privateKey} (Apple Developer portal \u2192 Keys), or unset ${own2.privateKey} to use the shared key.`
36986
37128
  );
36987
37129
  }
36988
37130
  if (ownKeyId !== void 0 && ownKey === void 0 && ownKeyId !== sharedKeyId) {
36989
37131
  throw new ConfigError(
36990
37132
  service,
36991
- `${prefix}_KEY_ID is set but ${prefix}_PRIVATE_KEY is not, and the shared key in ${sharedKeyVar} belongs to APPLE_KEY_ID \u2014 a key id must name the key that signs the token.`,
36992
- [`${prefix}_PRIVATE_KEY`],
36993
- `Set ${prefix}_PRIVATE_KEY to the .p8 contents of key ${prefix}_KEY_ID, or unset ${prefix}_KEY_ID to use the shared key.`
37133
+ `${own2.keyId} is set but ${own2.privateKey} is not, and the shared key in ${sharedKeyVar} belongs to APPLE_KEY_ID \u2014 a key id must name the key that signs the token.`,
37134
+ [own2.privateKey],
37135
+ `Set ${own2.privateKey} to the .p8 contents of key ${own2.keyId}, or unset ${own2.keyId} to use the shared key.`
36994
37136
  );
36995
37137
  }
36996
37138
  }
@@ -37035,16 +37177,16 @@ function decodeJwtPayload(token) {
37035
37177
  return void 0;
37036
37178
  }
37037
37179
  }
37038
- var PREFIX2, PEM_BLOCK_RE, MUSIC_TOKEN_TTL_S, MAPS_AUTH_TOKEN_TTL_S, WEATHERKIT_TOKEN_TTL_S;
37180
+ var OWN_VARS, PEM_BLOCK_RE, MUSIC_TOKEN_TTL_S, MAPS_AUTH_TOKEN_TTL_S, WEATHERKIT_TOKEN_TTL_S;
37039
37181
  var init_apple_keys = __esm({
37040
37182
  "src/apple-keys.ts"() {
37041
37183
  "use strict";
37042
37184
  init_dist2();
37043
37185
  init_errors4();
37044
- PREFIX2 = {
37045
- music: "APPLE_MUSIC",
37046
- maps: "APPLE_MAPS",
37047
- weather: "APPLE_WEATHERKIT"
37186
+ OWN_VARS = {
37187
+ music: { keyId: "APPLE_MUSIC_KEY_ID", privateKey: "APPLE_MUSIC_PRIVATE_KEY" },
37188
+ maps: { keyId: "APPLE_MAPS_KEY_ID", privateKey: "APPLE_MAPS_PRIVATE_KEY" },
37189
+ weather: { keyId: "APPLE_WEATHERKIT_KEY_ID", privateKey: "APPLE_WEATHERKIT_PRIVATE_KEY" }
37048
37190
  };
37049
37191
  PEM_BLOCK_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
37050
37192
  MUSIC_TOKEN_TTL_S = 12 * 60 * 60;
@@ -37617,23 +37759,23 @@ function systemTimeZone() {
37617
37759
  function getRequestTimeoutMs(env = process.env) {
37618
37760
  const v = readIntEnv("APPLE_REQUEST_TIMEOUT_MS", {
37619
37761
  env,
37620
- default: DEFAULT_REQUEST_TIMEOUT_MS,
37762
+ default: DEFAULT_REQUEST_TIMEOUT_MS2,
37621
37763
  min: 1e3,
37622
37764
  max: MAX_REQUEST_TIMEOUT_MS
37623
37765
  });
37624
- return v ?? DEFAULT_REQUEST_TIMEOUT_MS;
37766
+ return v ?? DEFAULT_REQUEST_TIMEOUT_MS2;
37625
37767
  }
37626
37768
  function isDebugLog(env = process.env) {
37627
37769
  return parseBoolEnv("APPLE_DEBUG_LOG", { env });
37628
37770
  }
37629
- var SERVICES, warnedServices, WRITE_MODES, warnedWriteMode, warnedTz, DEFAULT_REQUEST_TIMEOUT_MS, MAX_REQUEST_TIMEOUT_MS;
37771
+ var SERVICES, warnedServices, WRITE_MODES, warnedWriteMode, warnedTz, DEFAULT_REQUEST_TIMEOUT_MS2, MAX_REQUEST_TIMEOUT_MS;
37630
37772
  var init_config2 = __esm({
37631
37773
  "src/config.ts"() {
37632
37774
  "use strict";
37633
37775
  init_dist2();
37634
37776
  SERVICES = ["music", "calendar", "contacts", "mail", "maps", "weather", "itunes"];
37635
37777
  WRITE_MODES = ["none", "additive", "all"];
37636
- DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
37778
+ DEFAULT_REQUEST_TIMEOUT_MS2 = 3e4;
37637
37779
  MAX_REQUEST_TIMEOUT_MS = 6e5;
37638
37780
  }
37639
37781
  });
@@ -38125,7 +38267,7 @@ async function httpRequest(req) {
38125
38267
  if (mayHaveLanded) {
38126
38268
  throw new UnconfirmedWriteError(req.service, `${what} (HTTP ${res.status}); the change may have been applied.`);
38127
38269
  }
38128
- throw new ResponseTooLargeError(req.service, res.status, `${what}.`);
38270
+ throw new ResponseTooLargeError2(req.service, res.status, `${what}.`);
38129
38271
  }
38130
38272
  const timedOut = controller.signal.aborted;
38131
38273
  const why = timedOut ? `timed out reading the response after ${timeoutMs} ms` : `lost the connection reading the response`;
@@ -38568,6 +38710,7 @@ var init_session = __esm({
38568
38710
  "node_modules/@chrischall/mcp-utils/dist/session/index.js"() {
38569
38711
  init_response();
38570
38712
  init_config();
38713
+ init_errors3();
38571
38714
  init_http();
38572
38715
  init_edge_block();
38573
38716
  init_login_page();
@@ -46129,6 +46272,9 @@ function confirmWrite(ctx, opts) {
46129
46272
  details: opts.preview,
46130
46273
  unsupportedNote: "Make this change in the Apple app (Music, Calendar, Contacts or Mail) instead.",
46131
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,
46132
46278
  confirmToken: opts.confirmToken,
46133
46279
  args: opts.args,
46134
46280
  // Built per call: the data dir, APPLE_STATE_CACHE and the key are read now.
@@ -56649,7 +56795,11 @@ function registerCalendarTools(server, deps = {}) {
56649
56795
  attendees: attendeesParam.optional().describe("People to invite. iCloud emails each one an invitation."),
56650
56796
  confirmToken: confirmTokenParam
56651
56797
  }),
56652
- annotations: ANNOTATIONS.additive,
56798
+ // With APPLE_WRITE_MODE=all it accepts attendees, and iCloud emails each an
56799
+ // invitation: that reaches another person, and apple_calendar_delete_event
56800
+ // cannot un-send it, so it is destructive there. APPLE_WRITE_MODE=additive
56801
+ // refuses attendees and shared calendars, leaving a plain, deletable addition.
56802
+ annotations: accessAllowed("all") ? ANNOTATIONS.send : ANNOTATIONS.additive,
56653
56803
  handler: async (args, ctx) => {
56654
56804
  const zones2 = resolveZones(args.timeZone);
56655
56805
  const { zone } = zones2;
@@ -57853,7 +58003,7 @@ async function downloadInBatches(session, into) {
57853
58003
  try {
57854
58004
  res = await session.client.report(session.bookUrl, addressbookMultigetBody(batch), 1);
57855
58005
  } catch (err) {
57856
- if (!(err instanceof ResponseTooLargeError)) throw err;
58006
+ if (!(err instanceof ResponseTooLargeError2)) throw err;
57857
58007
  if (batch.length === 1) {
57858
58008
  into.tooLarge += 1;
57859
58009
  return;
@@ -57881,7 +58031,7 @@ async function download(session, version4) {
57881
58031
  try {
57882
58032
  res = await session.client.report(session.bookUrl, addressbookQueryBody(ADDRESS_DATA_PROPS), 1);
57883
58033
  } catch (err) {
57884
- if (!(err instanceof ResponseTooLargeError)) throw err;
58034
+ if (!(err instanceof ResponseTooLargeError2)) throw err;
57885
58035
  }
57886
58036
  if (res) {
57887
58037
  into.unreadable += res.skipped;
@@ -122577,14 +122727,17 @@ function registerLibraryWriteTools(server, client) {
122577
122727
  defineTool(server, {
122578
122728
  name: "apple_music_create_folder",
122579
122729
  service: "music",
122580
- access: "additive",
122730
+ // No tool in this server removes a playlist folder (delete_playlist deletes playlists only), so a created
122731
+ // folder cannot be undone here: destructive by the inverse test, and so outside APPLE_WRITE_MODE=additive,
122732
+ // whose promise is that every write it registers is non-destructive.
122733
+ access: "all",
122581
122734
  title: "Create an Apple Music playlist folder",
122582
122735
  description: "Create a playlist folder in your Apple Music library, at the top level or inside another folder. Returns the new folder id (p.\u2026) for apple_music_create_playlist / apple_music_move_playlist. Needs APPLE_MUSIC_USER_TOKEN or web-player mode.",
122583
122736
  inputSchema: external_exports.strictObject({
122584
122737
  name: external_exports.string().trim().min(1).max(200).describe("Folder name."),
122585
122738
  parentFolderId: external_exports.string().min(1).max(140).optional().describe('Create it inside this folder (p.\u2026; default "root", the top level).')
122586
122739
  }),
122587
- annotations: ANNOTATIONS.additive,
122740
+ annotations: { ...ANNOTATIONS.additive, destructiveHint: true },
122588
122741
  handler: async (args) => {
122589
122742
  const parent = folderIdArg(args.parentFolderId ?? "root");
122590
122743
  assertLibraryPlaylistId(parent, "parentFolderId", "folder");
@@ -471,7 +471,11 @@ export function registerCalendarTools(server, deps = {}) {
471
471
  attendees: attendeesParam.optional().describe('People to invite. iCloud emails each one an invitation.'),
472
472
  confirmToken: confirmTokenParam,
473
473
  }),
474
- annotations: ANNOTATIONS.additive,
474
+ // With APPLE_WRITE_MODE=all it accepts attendees, and iCloud emails each an
475
+ // invitation: that reaches another person, and apple_calendar_delete_event
476
+ // cannot un-send it, so it is destructive there. APPLE_WRITE_MODE=additive
477
+ // refuses attendees and shared calendars, leaving a plain, deletable addition.
478
+ annotations: accessAllowed('all') ? ANNOTATIONS.send : ANNOTATIONS.additive,
475
479
  handler: async (args, ctx) => {
476
480
  const zones = resolveZones(args.timeZone);
477
481
  const { zone } = zones;
@@ -302,7 +302,10 @@ export function registerLibraryWriteTools(server, client) {
302
302
  defineTool(server, {
303
303
  name: 'apple_music_create_folder',
304
304
  service: 'music',
305
- access: 'additive',
305
+ // No tool in this server removes a playlist folder (delete_playlist deletes playlists only), so a created
306
+ // folder cannot be undone here: destructive by the inverse test, and so outside APPLE_WRITE_MODE=additive,
307
+ // whose promise is that every write it registers is non-destructive.
308
+ access: 'all',
306
309
  title: 'Create an Apple Music playlist folder',
307
310
  description: 'Create a playlist folder in your Apple Music library, at the top level or inside another folder. Returns the new ' +
308
311
  'folder id (p.…) for apple_music_create_playlist / apple_music_move_playlist. Needs APPLE_MUSIC_USER_TOKEN or web-player mode.',
@@ -310,7 +313,7 @@ export function registerLibraryWriteTools(server, client) {
310
313
  name: z.string().trim().min(1).max(200).describe('Folder name.'),
311
314
  parentFolderId: z.string().min(1).max(140).optional().describe('Create it inside this folder (p.…; default "root", the top level).'),
312
315
  }),
313
- annotations: ANNOTATIONS.additive,
316
+ annotations: { ...ANNOTATIONS.additive, destructiveHint: true },
314
317
  handler: async (args) => {
315
318
  const parent = folderIdArg(args.parentFolderId ?? 'root');
316
319
  assertLibraryPlaylistId(parent, 'parentFolderId', 'folder');
@@ -40,6 +40,9 @@ export function confirmWrite(ctx, opts) {
40
40
  details: opts.preview,
41
41
  unsupportedNote: 'Make this change in the Apple app (Music, Calendar, Contacts or Mail) instead.',
42
42
  tool: opts.tool,
43
+ // Single-account server: every write acts as the one set of credentials in the env
44
+ // (ICLOUD_USERNAME / the Apple Music user token), so there is no principal to choose between.
45
+ account: undefined,
43
46
  confirmToken: opts.confirmToken,
44
47
  args: opts.args,
45
48
  // Built per call: the data dir, APPLE_STATE_CACHE and the key are read now.
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.2'; // x-release-please-version
4
+ export const VERSION = '0.3.4'; // x-release-please-version
package/mint.yaml CHANGED
@@ -59,6 +59,13 @@ env:
59
59
  invitations) behave on a client with no prompt, like claude.ai: "ask-user"
60
60
  (default: preview + confirmToken, the model must get your OK), "auto", or
61
61
  "refuse". Unknown values mean refuse.
62
+ - name: MCP_CONFIRM_ELICITATION
63
+ required: false
64
+ help: >-
65
+ "off" never shows a confirmation prompt, so every client gets the
66
+ MCP_CONFIRM_MODE flow. Set it for a client that says it can prompt but
67
+ never does (the gated call hangs, e.g. opencode 2.0.x). Any other value
68
+ stays "on" (with a stderr warning).
62
69
  - name: MCP_CONFIRM_SECRET
63
70
  secret: true
64
71
  required: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-icloud-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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": "^2.13.1",
57
+ "@chrischall/mcp-utils": "^3.0.0",
58
58
  "@modelcontextprotocol/server": "^2.3.0",
59
59
  "@xmldom/xmldom": "^0.9.12",
60
60
  "dotenv": "^18.0.5",
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.2",
9
+ "version": "0.3.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "apple-icloud-mcp",
14
- "version": "0.3.2",
14
+ "version": "0.3.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -193,6 +193,12 @@
193
193
  "isRequired": false,
194
194
  "format": "string"
195
195
  },
196
+ {
197
+ "name": "MCP_CONFIRM_ELICITATION",
198
+ "description": "\"off\" never shows a confirmation prompt, so every client gets the MCP_CONFIRM_MODE flow. Set it for a client that says it can prompt but never does (the gated call hangs, e.g. opencode 2.0.x). Any other value stays \"on\" (with a stderr warning).",
199
+ "isRequired": false,
200
+ "format": "string"
201
+ },
196
202
  {
197
203
  "name": "MCP_CONFIRM_TTL_SECONDS",
198
204
  "description": "Lifetime of a confirmToken in seconds (default 600).",