Skip to content

Releases: sweetrb/apple-photos-mcp

v2.1.11

Choose a tag to compare

@github-actions github-actions released this 13 Aug 06:16
84a47a0

Documentation

  • Retagged the project-scope .mcp.json entrypoint excerpt from json to text. The block is a single "args": [...] key/value fragment, not a JSON document, so it never parsed — a reader copying it as JSON got a syntax error. Added guards that keep every documented example honest: every ```json fence across README.md, CLAUDE.md and docs/ must parse, every documented APPLE_*_MCP_* environment variable must exist under src/, and the README `## Tool Reference` must document exactly the tools the built server advertises — in both directions, so neither an undocumented new tool nor a leftover entry for a removed one can pass.

v2.1.10

Choose a tag to compare

@github-actions github-actions released this 12 Aug 22:37
fff9a17

Fixed

  • Every tool was refused by the client, because all 42 advertised schemas declared JSON Schema draft-07. MCP has standardized on JSON Schema 2020-12, and clients now reject anything else outright — Tool '<name>' has an invalid outputSchema: JSON Schema declares an unsupported dialect ("$schema": "http://json-schema.org/draft-07/schema#"). The default validator supports JSON Schema 2020-12 only. The server connected fine; not one of its 21 tools was usable. The dialect is not ours to choose at the registration site: the SDK's server/mcp.js calls toJsonSchemaCompat(obj, { strictUnions, pipeStrategy }) with no target, so mapMiniTarget(undefined) resolves to 'draft-7' and both the emitted inputSchema and outputSchema are stamped draft-07. Upgrading zod does not fix it — the v4 (zod/v4-mini toJSONSchema) branch falls back to the same target, verified empirically against SDK 1.30.0 + zod 4.4.3 — so this is fixed at the transport boundary instead, the only public seam that does not reach into SDK internals: src/index.ts wraps the stdio transport in withJsonSchema2020_12(), which rewrites the outgoing tools/list payload. The converter also handles the keywords whose spelling changed in 2020-12 (definitions$defs and the #/definitions/ refs pointing at it, tuple itemsprefixItems, additionalItemsitems, dependenciesdependentRequired/dependentSchemas, boolean exclusiveMinimum/exclusiveMaximum → numeric). None of those paths is reachable from today's schemas — they are already dialect-portable, and the rewritten payload is byte-identical to the old one once $schema is stripped — so the conversion is a no-op in practice and exists so the seam stays correct if a new zod construct ever introduces one. Origin: sweetrb/apple-mail-mcp#147; fixed identically in all four servers.

Added

  • The outputSchema contract test now pins the advertised dialect. Two assertions against the real built server over stdio: every inputSchema/outputSchema must declare https://json-schema.org/draft/2020-12/schema, and no advertised schema may contain a draft-07-only construct (a draft-07 mention anywhere, a #/definitions/ $ref, a definitions/additionalItems/dependencies keyword, a boolean exclusiveMinimum/exclusiveMaximum, or a $schema on any node but the root). The existing checks could not see this class at all — they inspect schema contents, and a schema can be perfectly shaped while declaring a dialect that makes the client discard the tool before it is ever called. A new unit suite covers the converter itself, including the cases today's schemas do not exercise.

Documentation

  • README's Troubleshooting section now carries the "unsupported dialect" symptom with the exact error text and the fix (upgrade to 2.1.10, restart the host app), since a user on any earlier version sees it on every tool and has no other way to connect it to a server version. The Architecture section states that schemas are advertised as 2020-12 and points at the normalizer, and CONTRIBUTING.md records the wrapper as load-bearing — it is a transport.send interception with no local caller, exactly the shape a later cleanup would remove without knowing that doing so makes the whole server unusable.

Security

  • Bumped the pinned pnpm/action-setup to v6.0.10 and github/codeql-action/* to v4.37.6. Dependabot's weekly github-actions group PR landed these in apple-mail-mcp (#146) and apple-numbers-mcp (#62) but skipped this repo, so conformance-check.sh reported drift in ci.yml, publish.yml, dependabot-rebuild.yml, codeql.yml and scorecard.yml. The four servers are meant to carry a byte-identical workflow set, and a silent group-skip is the recurring way that breaks — this is the same failure recorded for the scorecard pin in 2026-08-05. .github/ does not ship, so this owes no version bump.

  • Floored js-yaml to ^4.3.1, clearing GHSA-5p4m-2wfm-xmqj (high). Quadratic CPU consumption while resolving !!omap keys — a malicious YAML document can be made to burn CPU superlinearly in the number of map entries. The advisory notes the CVE-2026-59870 fix was never backported to the 3.x line, so 4.3.1 is the first complete release. js-yaml reaches the tree as eslint -> js-yaml, which is development scope, and it does not appear in the committed build/index.js — verified, 0 references — so no published artifact ever carried it and this owes no version bump. No js-yaml override existed here before; apple-mail-mcp carried one pinned at ^4.2.0 — below this fix — which is how the gap was found.

v2.1.9

Choose a tag to compare

@github-actions github-actions released this 06 Aug 12:07
46d1ed6

Fixed

  • Every tool advertised an output schema that rejected undeclared keys, discarding otherwise-correct results. The MCP client validates a result's structuredContent against the JSON Schema the server advertised, not against the server's own zod object — and a bare zod raw shape renders as additionalProperties: false. So any field a handler emits that its schema doesn't enumerate is a hard client-side -32602 … data must NOT have additional properties, throwing away a payload the handler computed correctly. The server never notices, because zod's own parse silently strips unknown keys instead of failing, which is exactly why the registerTool/outputSchema migration's "all fields optional, no .strict()" read as permissive: it covered optionality, not undeclared keys. All 21 tools in this repo were advertising additionalProperties: false. Every tool now registers through a wrapper applying .passthrough(), advertising additionalProperties: true — the contract that migration intended. Found while fixing the same defect in the sibling apple-mail-mcp (sweetrb/apple-mail-mcp#135), where it was not latent: it broke get-mail-stats on every call for anyone with IMAP configured.

Added

  • The outputSchema contract test now asserts that every tool tolerates undeclared keys. The existing checks — every tool has an outputSchema, none requires a field — could not see this class, because they inspect the advertised schema and round-trip only the diagnostic tools; a tool whose payload carries an undeclared key passes CI and fails in the user's client. The suite now fails any tool advertising additionalProperties: false, so this cannot silently return.

v2.1.8

Choose a tag to compare

@github-actions github-actions released this 06 Aug 11:59
07aa3e2

Changed

  • Dependency bump via Dependabot; committed bundle rebuilt. (automated)

Security

  • Bumped the pinned ossf/scorecard-action to 2d1146689b8cda280b9bc96326124645441f03bc (v2.4.4). Dependabot's weekly github-actions group PR landed this in apple-mail-mcp (#134) and apple-numbers-mcp (#56) but skipped this repo and apple-notes-mcp, so conformance-check.sh reported DRIFT: .github/workflows/scorecard.yml differs. The four servers are meant to carry a byte-identical workflow set, and a silent group-skip is the recurring way that breaks. .github/ does not ship, so this owes no version bump.

v2.1.7

Choose a tag to compare

@github-actions github-actions released this 04 Aug 02:55
3b51fa5

Security

  • Floored hono to ^4.12.34, clearing GHSA-8j4g-w8fx-2239 (moderate). This was deferred earlier the same day: the fix release was still inside the repo's 24-hour minimumReleaseAge soak — it missed by under three minutes — and no minimumReleaseAgeExclude carve-out was added, because the soak is the point. It matured at 2026-08-04T02:36:40Z and is floored now. pnpm audit reports no known vulnerabilities.

  • Floored fast-uri on its fix for GHSA-7p8r-x3mc-p8w7 (high) and corrected an exact pin that had silently become a ceiling. The override was written as the bare pin fast-uri: 3.1.4 back when 3.1.4 was the security floor for an earlier advisory. When 3.1.5 shipped the fix for GHSA-7p8r-x3mc-p8w7, that pin inverted into a ceiling: it held the tree on the vulnerable version and made the advisory permanently unclearable, with no warning — an override that pins rather than floors cannot distinguish "this is the minimum safe version" from "this is the only allowed version". The entry is now the caret range ^3.1.5, which stays inside the major ajv expects while letting patch fixes flow in. This one is genuinely in the product rather than the dev toolchain: fast-uri arrives via ajv@modelcontextprotocol/sdk, and ajv is inlined into the committed build/index.js, so the published bundle carried the vulnerable parser verbatim. The rebuilt bundle now carries 3.1.5, which is why this release owes a version bump. Every other floor in pnpm-workspace.yaml was audited for the same shape; the brace-expansion entries are already two-sided ranges and are unaffected. Matches apple-mail-mcp#128.

  • Floored ip-address on 10.3.1 for GHSA-mwp4-54f8-5fhr (high), which also carries GHSA-4xrf-jv44-h6hh and GHSA-22jq-vg5j-6vgg. Reached as express-rate-limit@modelcontextprotocol/sdk, which capped it at 10.2.0 — below the first fully patched release. Written as ^10.3.1 rather than a pin, for the reason above; it resolves 10.4.0. The dependency is runtime-scope, but the SDK's HTTP transport is not part of this server's bundle (no ip-address reference appears in build/index.js), so the exposure was supply-chain rather than a live code path in the published package.

  • Floored postcss on 8.5.23 for GHSA-fxqj-rqcc-2cmp (moderate) — the incomplete-fix follow-up to GHSA-6g55-p6wh-862q, where an attacker-controlled sourceMappingURL can read arbitrary .map files when from is unset. Development scope only (vitestvitepostcss, absent from the shipped bundle), and vite's own range is already ^8.5.6, so the floor costs nothing; it resolves 8.5.25.

  • Deferred: hono (GHSA-8j4g-w8fx-2239, moderate). The fix is in 4.12.34, but that release was still inside this repo's declared 1440-minute minimumReleaseAge soak, so pnpm install refuses it with ERR_PNPM_NO_MATURE_MATCHING_VERSION. No minimumReleaseAgeExclude carve-out was added and no audit suppression was applied — the soak is a deliberate supply-chain control, and squeezing a release past it by minutes defeats the point of having one. hono reaches the tree as @hono/node-server@modelcontextprotocol/sdk and is not in the shipped bundle, so nothing in the published package is exposed. The follow-up (add hono: ^4.12.34) is recorded as a comment beside the other floors in pnpm-workspace.yaml. pnpm audit --audit-level=high is clean; this advisory is the single remaining moderate.

v2.1.6

Choose a tag to compare

@github-actions github-actions released this 04 Aug 02:28
f0c6211

Added

  • version-guard now requires every version bump to be documented under a real ## [X.Y.Z] CHANGELOG heading. The guard already refused a bump to a version that was already on npm, but it never checked that the new version was described anywhere. Notes parked under ## [Unreleased] are orphaned the moment the release ships: nothing in the release path renames that section — the version lifecycle script only syncs the plugin manifests — so the published version goes out undocumented while its release notes sit under a heading still claiming they are unreleased. apple-notes-mcp shipped 2.6.10 and 2.6.11 exactly that way before this check existed. A bump whose version has no matching heading now hard-fails the PR, with an error naming the heading to add. Keep an empty ## [Unreleased] at the top regardless — dependabot-rebuild.yml hard-exits without that marker, and since it already inserts a real heading, bot PRs pass unchanged. The guard file lives in .github/, which does not ship, so this owes no version bump. Matches apple-mail-mcp#124, keeping the guard identical across the four servers. (#61)

Fixed

  • version-guard no longer demands a version bump for byte-neutral src/ changes. The shipped-bytes detector treated every non-test file under src/ as shipped, but TypeScript there reaches users only after esbuild inlines it into build/index.js — so a comment-, formatting- or type-only edit that leaves the committed bundle byte-identical was hard-blocked, leaving only two bad options: publish a release of literally nothing, or do not write the comment. src/**/*.ts is now a first-cause detector that implies a bump only when build/** changed too. The exemption is sound rather than merely convenient: ci.yml's "Verify committed build/ matches source" step rebuilds and requires git diff --quiet build/, and it runs in the test job whose test (22)/test (24) contexts are required by branch protection — so at merge time an unchanged build/ provably matches src/. Everything else under src/ (the verbatim-shipped *_reader.py sidecars), requirements.txt and build/** stay unconditional detectors, and the rule is written fail-safe: only .ts counts as bundle-only, so any new file type under src/ still requires a bump.
  • Timeout errors no longer point at an environment variable that cannot help. APPLE_PHOTOS_MCP_TIMEOUT sets only the default sidecar budget — ten tools pass an explicit timeoutMs that runPhotosReader uses in preference to it (get-selected-photos 2 min; find-duplicates, create-album, add-to-album, set-photo-metadata, set-keywords, set-photo-date 5 min; remove-from-album, import-photos 10 min; export 30 min). The single shared timeout string named the variable unconditionally, so a find-duplicates timeout on a big library told the user to raise a setting that is inert for that call: they raise it, nothing changes, and no doc admitted those budgets are fixed. The message now branches — the default path keeps "Raise APPLE_PHOTOS_MCP_TIMEOUT (ms)…", a fixed-budget call instead says its timeout is fixed at Nms and that the variable does not apply — and README, CLAUDE.md and docs/LIMITATIONS.md list which tools carry which budget.
  • The "Python 3 not found on PATH" setup error now names the doctor tool, matching every other setup-failure string in the server (setupHint(), the missing-photoscript error, the write-gate error, the Full-Disk-Access remediation) and the invariant stated in src/utils/docsUrls.ts. It is the one first-run failure that ended with a docs URL alone, steering the user toward a manual install guess instead of the tool that distinguishes a Python problem from an osxphotos or Full-Disk-Access one. doctor's own copy of the string is deliberately left alone — a doctor pointer inside doctor is circular.

Documentation

  • Retired the pre-2.0 "read-only except export" model from every surface that still carried it. Seven opt-in tools have mutated the Photos library since 2.1.0, but export's own MCP Safety: line still called itself "the only side-effecting tool", and so did src/utils/exportPath.ts's module docstring, CLAUDE.md's behavioral-contract bullet, SECURITY.md's posture bullet and CONTRIBUTING.md's sidecar guidelines. Each is now scoped to what remains true — export is the only tool that writes files outside the library — and points at the APPLE_PHOTOS_MCP_ENABLE_WRITES gate. Swept with them: README's "All five tools below" heading a section documenting seven; docs/WRITE-BACKEND.md's Writes row (missing set-photo-date and import-photos) and its cost argument weighed against "five opt-in tools"; README's doctor entry listing four checks when doctor has run six since 2.0.0; docs/FULL-DISK-ACCESS.md's "no tool works … every tool ultimately reads the SQLite database" (the write path needs macOS Automation, not FDA — a -1743 failure is not an FDA problem) and its three-item "full setup diagnostic"; SECURITY.md's Supported Versions table still covering only 1.x.x; CLAUDE.md's "Every tool accepts an optional library path" (ten of the twenty-one do not, and the same file said so 47 lines earlier); the find-duplicates remediation that routed quarantining through Photos.app by hand rather than create-album + add-to-album; and the Antigravity marketplace blurb still advertising the Hermes packaging removed in 2.1.5.
  • Documented three limits and one failure mode that only existed in the source. get-thumbnail's 8 MB response cap (derivative selection pre-filters on it, but a sips-rendered fallback from a very high-resolution original can still exceed it); the schema maxima on find-duplicates.limit (10000), list-keywords.limit / list-persons.limit (100000), set-photo-metadata's title (255) and description (2048), and set-keywords' per-keyword 255; and remove-from-album's apple-photos-mcp-tmp-<hex> scratch album — a rebuild killed part-way (its fixed 10-minute budget expiring while copying a very large album) leaves the original album and every photo intact, but can strand that scratch album, one per interrupted attempt, since the name is collision-checked per call.
  • docs/WRITE-BACKEND.md now lists get-selected-photos as the one photoscript-backed read, so the Path→Backend table accounts for every AppleScript caller rather than implying the split is purely reads-vs-writes.
  • README's link to docs/WRITE-BACKEND.md is now an absolute GitHub URL, matching every other cross-file link in the file — it was the last relative one, and relative links do not resolve on npmjs.com.
  • Recorded the export progress contract as a deliberate decision. The terminal done === total progress notification is best effort: an MCP client deletes a request's progress handler when the response arrives and discards later notifications for that token, so a notification emitted immediately before the result is intermittently thrown away (measured: server sent four, client delivered one). Per-photo notifications are the progress signal; the tool result's exportedCount / skippedCount is the completion signal. The notification is still emitted on purpose — nothing depends on it, it usually arrives, and removing it would bake a workaround for a client-side lifecycle bug into this server's public behaviour. Noted at the emission site and in CLAUDE.md so it is not "cleaned up" later.

Security

  • Floored all three dev-only brace-expansion majors on their complete fixes for GHSA-mh99-v99m-4gvg / CVE-2026-14257 (high)1.1.161.1.18, 2.1.32.1.4, and both 5.0.7 and 5.0.85.0.9. Three separate majors are reachable through the dev toolchain (eslintminimatch@3 on v1, minimatch@9 on v2, minimatch@10 on v5), and they are not API-compatible — minimatch 3 requires the v1 CommonJS API, so a single floor spanning them fails with expand is not a function. Each major therefore carries its own two-sided floor; the bounds must be two-sided because a bare <5.0.9 also matches 1.1.18 and 2.1.4 under semver and would drag the CommonJS path onto the v5 ESM API. The advisory's own first-patched versions (1.1.17 / 2.1.3 / 5.0.8) are not sufficient: they bound the accumulator in combine but never thread maxLength into expandSequence, so the sequence path ({1..N}, {a..z..k}) stays capped only by item count and a padded sequence still materialises ~100,000 intermediate strings before the outer bound truncates (measured 4,606 ms / 176 MB RSS on 1.1.17 vs 9 ms / 61 MB on 1.1.18, identical final output). Two of the four paths resolved here (1.1.16, 5.0.7) were below even the advisory's floor. Adopted only after every release cleared this repo's 24-hour minimumReleaseAge gate, with no minimumReleaseAgeExclude carve-out and no audit suppression — pnpm audit will keep reporting the advisory until GitHub's metadata (which still lists 5.0.8 as first-patched, and so marks the entire v1 line vulnerable under semver) catches up. Dev toolchain only: brace-expansion is not in the shipped bundle, so the published package is unaffected, the committed bundle is byte-identical, and no version bump is owed. Matches apple-mail-mcp#123 — thanks to @jjoanna2-debug for the original finding.

v2.1.5

Choose a tag to compare

@github-actions github-actions released this 30 Jul 00:29
97b5b52

Fixed

  • Export progress notifications could be silently dropped — most often the final done === total one. The export tool sent them fire-and-forget (void extra.sendNotification(...)), so the sends raced the tool's own response. That race is not benign: an MCP client deletes the request's progress handler the moment the response arrives (Protocol._onresponse), and Protocol._onprogress discards a notification whose token no longer has a handler (reporting "progress notification for an unknown token"). A notification that lost the race was therefore lost outright, never delivered late — and the terminal notification, being emitted last, was the most exposed, so a client surfacing completion from it could intermittently never see an export finish. The tool now awaits its sends before returning, putting every notification on the wire ahead of the result. Each send still swallows its own failure, so this cannot fail an export; it only orders the writes.
  • pnpm version no longer breaks with the .hermes-plugin/ removal. The version lifecycle script listed .hermes-plugin in its git add; git add exits 128 on a pathspec that matches nothing, which would have broken the documented release step (pnpm version <patch|minor|major> --no-git-tag-version) for every subsequent release. The stale path is dropped from the git add list.
  • Dependabot auto-bump silently stopped staging its own changes. dependabot-rebuild.yml's bump step writes the patch version, syncs the plugin manifests and prepends a CHANGELOG entry, then staged them with git add package.json CHANGELOG.md build .claude-plugin .agents codex .hermes-plugin .antigravity-plugin. Once .hermes-plugin/ was removed that pathspec matched nothing, and git add is all-or-nothing — it exited 128 and staged none of the others, with 2>/dev/null || true hiding the failure. The following step re-adds only build/, so a Dependabot PR would have committed a rebuilt bundle with no version bump and no changelog entry, failing require-version-bump and blocking the automation that is meant to run without a human. Dropped the stale path, and dropped the error suppression so a future missing path fails loudly instead of silently skipping the bump.
  • persistent-sidecar integration flake. The expected 3 to be 4 failure that intermittently blocked unrelated PRs was this dropped notification, not a slow one, and specifically the terminal one. Two earlier attempts were wrong: widening the assertion's poll from 10s to 30s could not help (a discarded message never arrives, and a 30s budget failed identically on main), and the ordering fix above — correct on its own merits — cannot stop the client tearing down its progress handler first. Measured directly: with the synthetic sidecar's inter-photo spacing removed so all four notifications arrive as one burst, the server sent all four and the client delivered one, rejecting the rest with "progress notification for an unknown token". The test now asserts the three per-photo notifications — what the server controls and the client reliably delivers — and no longer asserts the terminal one, with the mechanism and both failed attempts recorded inline so they are not retried.

Removed

  • .hermes-plugin/ packaging docs (README.md, config.yaml). Hermes Agent has no plugin/marketplace drop-in, so a directory of manifest-looking files was easy to misread as an installable package. The setup it documented is not lost — the hermes mcp add command, the ~/.hermes/config.yaml mcp_servers: snippet, and the restart note now live inline in the README's "Other Hosts" section. Matches apple-mail-mcp#116, keeping multi-host packaging parity across the four Apple MCP servers. No effect on the published package: .hermes-plugin/ was never in package.json files[].

v2.1.4

Choose a tag to compare

@github-actions github-actions released this 23 Jul 02:37
62fa74f

Security

  • Override the MCP SDK's transitive @hono/node-server and fast-uri dependencies to patched releases (@hono/node-server 2.0.10, fast-uri 3.1.4), clearing the Hono static-file path-traversal advisory and the two fast-uri host-confusion advisories that the SDK's own ranges still resolve to. Fleet-wide companion to sweetrb/apple-notes-mcp#104 (@oliverames).

v2.1.3

Choose a tag to compare

@github-actions github-actions released this 21 Jul 01:40
a69f31c

Changed

  • Bump the Python sidecar's osxphotos pin from 0.75.9 to 0.76.1. This is a runtime dependency — the sidecar is what reads the Photos library — so the pin ships. Verified before release against a real 31,463-item library rather than the mocked unit tests: health reports osxphotos 0.76.1 and the correct photo count; library-info returns 30,892 photos / 571 movies / 47 albums / 44 keywords / 217 persons; and query, list-albums, list-keywords and list-persons all return correct records. photoscript stays pinned at 0.5.3 — the two are kept in step deliberately, since photos_reader.py uses its script_loader.run_script for bulk album-id calls.

v2.1.2

Choose a tag to compare

@github-actions github-actions released this 20 Jul 17:54
809038d

Changed

  • CI/release hardening: version-guard now treats the committed build/ bundle as shipped bytes (closing the lockfile-only and devDep silent-never-publish vectors) with an npm version-collision check; publish.yml gained a daily self-healing watchdog, manual dispatch, exact-version skip, CI-validated-commit checkout, and GitHub-Release self-heal; Dependabot bundle rebuilds now auto-bump a patch version; CI boots the committed bundle standalone on Node 20 every run; the bundle is now built with --target=node20, making the engines.node >= 20 claim enforced at build time.
  • requirements.txt is now exact-pinned and under Dependabot pip management; CodeQL scans the Python sidecar.

Documentation

  • docs/FULL-DISK-ACCESS.md — adds a "still failing after granting every host app?" section (#37 follow-up): on at least one confirmed setup, TCC attributed the FDA check directly to the node binary itself, not to any wrapper app — responsibility never climbed past node to see those grants. Documents the definitive diagnostic (log stream --predicate 'subsystem == "com.apple.TCC"', reading the Resp: process from the denial) and the fix (grant FDA to the exact node path the log names), plus the ad-hoc-vs-Developer-ID-signature note on why the grant may or may not survive a Node upgrade.