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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +5 -3
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/dist/apple-keys.js +22 -18
- package/dist/bundle.js +207 -54
- package/dist/calendar/tools.js +5 -1
- package/dist/music/tools-write.js +5 -2
- package/dist/tools/_confirm.js +3 -0
- package/dist/version.js +1 -1
- package/mint.yaml +7 -0
- package/package.json +2 -2
- package/server.json +8 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Unofficial Apple Music, iCloud Calendar/Contacts/Mail, Apple Maps and WeatherKit tools for Claude Code",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.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.
|
|
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.
|
|
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
|
-
"
|
|
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. |
|
|
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
|
|
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
|
|
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
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -22,7 +22,7 @@ MPL-2.0 is a file-level license: it covers ical.js's own files and does not exte
|
|
|
22
22
|
|
|
23
23
|
| Package | Version | License | Source |
|
|
24
24
|
|---|---|---|---|
|
|
25
|
-
| @chrischall/mcp-utils |
|
|
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@
|
|
116
|
+
### @chrischall/mcp-utils@3.0.0
|
|
117
117
|
|
|
118
118
|
- License: MIT
|
|
119
119
|
- Source: https://github.com/chrischall/mcp-utils
|
package/dist/apple-keys.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
80
|
+
const own = OWN_VARS[service];
|
|
77
81
|
const teamId = readEnvVar('APPLE_TEAM_ID', { env });
|
|
78
|
-
const ownKeyId = readEnvVar(
|
|
79
|
-
const ownKey = readEnvVar(
|
|
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 =
|
|
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 ${
|
|
105
|
+
missing.push(`APPLE_KEY_ID (or ${own.keyId})`);
|
|
102
106
|
if (rawKey === undefined)
|
|
103
|
-
missing.push(`APPLE_PRIVATE_KEY (or ${
|
|
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 ?
|
|
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
|
|
134
|
-
const ownKeyId = readEnvVar(
|
|
135
|
-
const ownKey = readEnvVar(
|
|
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, `${
|
|
146
|
-
'a key id must name the key that signs the token.', [
|
|
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, `${
|
|
150
|
-
'a key id must name the key that signs the token.', [
|
|
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
|
|
36089
|
-
const
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 }, {
|
|
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
|
-
|
|
36486
|
-
|
|
36487
|
-
|
|
36488
|
-
|
|
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
|
|
36493
|
-
|
|
36494
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
|
37066
|
+
const own2 = OWN_VARS[service];
|
|
36925
37067
|
const teamId = readEnvVar("APPLE_TEAM_ID", { env });
|
|
36926
|
-
const ownKeyId = readEnvVar(
|
|
36927
|
-
const ownKey = readEnvVar(
|
|
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 =
|
|
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 ${
|
|
36945
|
-
if (rawKey === void 0) missing.push(`APPLE_PRIVATE_KEY (or ${
|
|
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 ?
|
|
37112
|
+
source: `${ownKeyId ? own2.keyId : "APPLE_KEY_ID"} + ${keyVar}`
|
|
36971
37113
|
};
|
|
36972
37114
|
}
|
|
36973
37115
|
function assertPairMatches(service, env) {
|
|
36974
|
-
const
|
|
36975
|
-
const ownKeyId = readEnvVar(
|
|
36976
|
-
const ownKey = readEnvVar(
|
|
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
|
-
`${
|
|
36984
|
-
[
|
|
36985
|
-
`Set ${
|
|
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
|
-
`${
|
|
36992
|
-
[
|
|
36993
|
-
`Set ${
|
|
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
|
|
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
|
-
|
|
37045
|
-
music: "
|
|
37046
|
-
maps: "
|
|
37047
|
-
weather: "
|
|
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:
|
|
37762
|
+
default: DEFAULT_REQUEST_TIMEOUT_MS2,
|
|
37621
37763
|
min: 1e3,
|
|
37622
37764
|
max: MAX_REQUEST_TIMEOUT_MS
|
|
37623
37765
|
});
|
|
37624
|
-
return v ??
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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");
|
package/dist/calendar/tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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');
|
package/dist/tools/_confirm.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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": "^
|
|
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.
|
|
9
|
+
"version": "0.3.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "apple-icloud-mcp",
|
|
14
|
-
"version": "0.3.
|
|
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).",
|