All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Audit pipeline self-disabled on mixed-version history — stores written by interleaved old/new builds left a signed-but-
seq-less row mid-chain; the strict scanner classified it as malformed, every flush then failed the pre-append integrity check, and after five consecutive failures auditing revoked itself (auditDisabled: true) with the 5-minute recovery window re-failing immediately — a de-facto permanent disable that ran unnoticed for weeks. The summarizer silently counted only the verified prefix (92 of 145 rows on the reference store). Genuine mixed-legacy breaks (self-sealed rows linked to verified history) now migrate once per process: the verified prefix stays active, the anomalous tail is preserved byte-for-byte behind the existingaudit.legacy-untrusted.*boundary, the checkpoint is re-pinned, and a sealed__audit_chain_migrationmarker records the event. Tampered bytes, forged rows, re-rooted parents, and checkpoint breaks still fail closed.audit_summarynow reportsunverifiedTailRows,unsignedLegacyRows,quarantined, andlegacyMixedBreakinstead of undercounting silently, anddoctor --deepdistinguishes tampering from mixed history. (#435) - Test harnesses could write to the real usage profile — jest ran isolated, but
cleanBootEnv-booted harness gates ran withAIRMCP_TEST_MODE=1against the realHOME, leaving development residue (tool_a,foo,test_tool) in~/.airmcp/profile.jsonwhere it contaminated real-usage analysis. Test-mode processes now refuse profile reads/writes without an explicit path,cleanBootEnvsetsAIRMCP_USAGE_TRACKING=false, andscripts/clean-profile-residue.mjsremoves existing contamination (dry-run by default, atomic rewrite with--apply). (#435) - Signed app's embedded Node runtime crashed on launch —
notarize-app.shsigned the bundled Node binary with the hardened runtime and no entitlements. V8 needs writable-then-executable JIT pages, which the hardened runtime forbids by default, so the re-signed binary died with SIGTRAP beforemain(): the bundle signed, notarized, and stapled cleanly while its embedded server could never start. Found on the first real signed run of v2.16.3, whereverify-bundle-structure.shcaught it by executing the bundled runtime — after Apple had already accepted the broken artifact. The Node binary is now signed withallow-jitandallow-unsigned-executable-memoryfromscripts/lib/node-runtime-entitlements.plist; the Swift bridge stays on the default entitlement set.
- Release pipeline reported failure on a shipped publish — the add-on publish gate waited 60s for the npm registry read path to confirm an already-successful immutable publish.
npm viewresolves the packument, and for a brand-new scoped package name that lagged by over five minutes, so v2.16.2 published every package and still went red thirteen times. Because the add-on step aborts on that failure, everything downstream was skipped:@heznpc/airmcp-powerautomatewent unpublished, and the tag, GitHub Release, signed app dispatch, and release verification never ran. The window is now 600s,--retry-secondsaccepts up to 900, and the timeout message states that the publish succeeded and that re-running resumes by skipping packages whose registry SRI and gitHead already match. An identity mismatch still fails immediately. - Glama listing was not discoverable — the registry builds every listed server from a Dockerfile, inferring one when the repository has none, and withholds distribution when that build fails: the server drops out of search results, category listings, and recommendations. AirMCP had no Dockerfile and the inferred build could not succeed because
package.jsondeclares"os": ["darwin"]. Adds a registry-introspection image (explicitly not a supported way to run AirMCP — tool calls need macOS) plus aregistry-introspectionCI job that builds it on Linux and asserts the server boots, states its identity, and answerstools/list. - Signed app lane requested approval for runs that could not sign —
release-appdeclaredenvironment: releasewith no condition, so a deployment approval was requested before the signing-secret preflight, which is a step inside that job. Dispatched withrequire_signing=false, that preflight then exited 0 and recorded a green "signed release" with no artifact. The lane is now gated on theSIGNED_APP_RELEASErepository variable, evaluated before the environment gate, and CD passesrequire_signing=trueso a missing secret fails instead of skipping. - Signing guards asserted an identity no certificate could have — both guards hard-coded
Heznpc, the GitHub owner handle, as the certificate subject includingorganizationName. Apple does not put a handle in a Developer ID subject. Expected values now live inscripts/lib/signing-identity.shas a single source of truth forverify-signing-identity.shandverify-signed-app.sh, pinned to the project's real Developer ID with an independent team-ID check. list_mailboxesshipped an unwrapped array against an object schema — the script emits a bare array and the handler passed it straight tostructuredContentwhileoutputSchemadeclares{ mailboxes }. Both of its mocks claimed the script already returned the wrapped object, so the runtime tests validated the mock's shape rather than the tool's output.search_eventsandtoday_eventsomittedreturnedfromoutputSchema— both the EventKit Swift bridge and the JXA fallback have always emitted it, so the field was undeclared to clients. The JXA upcoming/today scripts also emitted a nulllocationagainst a non-nullablez.string()while Swift types it non-optional; the scripts now emit'', matching both backends.doctorcleared Node versions npm refuses to install — the environment check passed at>= 18whileengines.noderequires>=20.0.0.doctor's module denominator contradicted the published catalogue — it reported11/29fromMODULE_NAMESagainst a documented 32 modules. The gap is exactlyOPT_IN_MODULE_NAMES, which no profile enables; the label now names both denominators.- Documentation described tools that do not exist, and one module backwards — Keynote was documented with a delete-slide tool it has never had, and Speech was described as text-to-speech and voice synthesis in both the module overview and the app's
module.speech.descstring, when its three tools are transcription and recognition. Embedding defaults on the docs site were stale againstsrc/shared/constants.ts. - Tag and release history had gaps — v2.8.1, v2.9.0, v2.16.0, and v2.16.1 shipped to npm without a complete tag/Release pair. Reconstructed from the
gitHeadnpm recorded for each version; all 28 published versions now have both.
- README leads with what the reader can do — the hero states the capability and shows plain-language asks before introducing the control layer, which moves to a reason-to-trust section rather than the headline.
llms.txtgains a Start Here section before the module catalogue — a reader that meets 32 modules first concludes the job is to call a tool. The seven curated workflows are now listed ahead of it with their actual prompts, generated fromsrc/shared/workflows-catalog.jsonso the section cannot drift from what ships. The identity and governance intro is unchanged.- Trust claims narrowed to the scope the code enforces —
SERVER_INSTRUCTIONSsaid an emergency stop "can halt everything" when the gate isdestructive && isEmergencyStopActive(), and led with tool discovery, which trains a fresh session to treat calling a tool as the goal. It now scopes the stop to destructive calls, starts from the user's task, and states that a successful call is evidence a step ran rather than proof the task completed.assurance: operator-attestedis documented as non-derivability rather than non-repudiation, since thekeyfilevariant is readable by any same-user process.SECURITY.mdgains an enforcement-scope table, andprivacy-sensitive-tools.tsstates that no runtime path imports it.
- Script/
outputSchemashape contract across calendar, notes, reminders, mail, and finder — eachscripts.tsexports hand-maintained*_EXAMPLEconstants typed against the interfaces itsJSON.stringify(...)produces, parsed through the tool's real declared schema. Runtime tests mock the automation layer, so the mock return value becomesstructuredContentverbatim and cannot detect this class of drift. Strictness now reaches inside arrays, which tightened the pre-existing messages, shortcuts, and health contracts as well. - Injection-focused generator suites for Pages, Numbers, and Keynote — 66 tests asserting the escaped form rather than mere presence, covering document names, sheet and cell references, presenter notes, export paths, the TypeScript-side quoting decision in
setCellScript, and raw numeric interpolation. No production escaping defect was found. - CI gates for the checks that existed but nothing ran —
format:checkwas inpackage.jsonbut no workflow invoked it, sosrc/server/front-door-tools.tssat unformatted on main while every run passed. - Contribution template and drift guards (external contributions) —
CONTRIBUTING.mdrefreshed against the current registration and drift workflow, issue and PR templates synced toMODULE_MANIFESTwith a test that keeps them in sync in both directions,summarize-audit.mjsmade fixture-driven, and the PR template now asks forCloses #so merges close their issue.
- Classify Apple TCC permission denials as
permission_denied(with the System Settings recovery hint) instead of genericjxa_error/swift_errorin every module catch path. profile_statusnow reports config-disabled modules inmodulesDisabledinstead of always returning an empty list.- Release tooling parses
npm pack --json/npm view --jsonacross npm 10–12 output formats; CD pins npm 12 for trusted publishing.
- Universal runtime error-contract harness invoking every module tool with synthesized arguments under executor permission failure.
- Doctor ↔ runtime consistency contract (shared resolver,
doctorCLI, and a real stdio boot must agree on profile and module set). - Xcode 26.3 coding-agent connection guide in the docs site.
- npm
2.16.0and2.16.1were published from the same lineage during the trusted-publishing rollout but have no GitHub Release or add-on packages;2.16.2is the canonical release for this cycle.
- Honest
assuranceverdict onairmcp://trust—governedstays true under a host-derived fallback key (the record is still tamper-evident), so a consumer keying only on the boolean over-trusts a re-derivable key as non-repudiation. A new top-levelassurancetier (operator-attested/tamper-evident/audit-halted/tampered) factors the key grade in, and the resource description plus the governed-flow demo now direct any one-line trust readout atassurance, never baregoverned. - Operator audit key by default — audit-chain key resolution is now
AIRMCP_AUDIT_HMAC_KEYenv ><store>/audit-hmac.keykey file > host-derived fallback, resolved once at module load.npx airmcp initgenerates the key file (random 32 bytes, 0600, never clobbering an existing file) so a fresh install is non-derivable without env plumbing; the attestation exposes the backing variant asaudit.keySource(env/keyfile/host-derived). Generation refuses over an already-sealed chain — rows HMAC'd under the old key would otherwise scan as a falsetampered+ fail-closed audit — and instead prints the explicit archive-then-rerun migration path. Key files that drift group/other-readable are tightened back to 0600 on load, and an unreadable (non-ENOENT) key file warns instead of silently downgrading. npx airmcp verify— one-screen honest trust readout for the real local audit store: leads with the key-grade-awareassuranceverdict, locates the first chain break on tampering, prints the exact key-upgrade path (with the re-key caveat), and exits non-zero for scripting:tampered(1) /audit-halted(2) / could-not-verify (3, operational errors deliberately distinct from tamper).npx airmcp doctorgains the same check as an "Audit trust" section.airmcp://trustattestation resource — an unconditionally listed first-party resource composing whole-chain HMAC audit verification, the active HITL approval level, rate-limit/emergency-stop posture, and the audit key grade into a singlegovernedverdict. It mirrors the identity string the server sends atinitialize, so the "governed runtime, not an agent" claim is falsifiable at runtime instead of living only in documentation.preview_actionpre-hoc governance preview — previews exactly how governance would treat a proposed tool call (HITL requirement, destructive/sensitive classification, rate-limit budget) with zero side effects, before anything runs; the tool registry now records a per-toolsensitiveHint.- Actor provenance in audit —
audit_logrows expose which actor drove each call, andaudit_summaryadds abyActorbreakdown, making "what did the daemon run without me" a first-class query. - Server identity at
initialize— the server now sends MCPinstructionsat handshake stating the governed-runtime/connector identity, the progressive front door, and the no-model/no-agent-loop boundary. Clients previously received no identity statement at all.
- Consent-first setup, exact scope activation, and client connection — opening Setup or Trust Center is observational: it does not create the app-runtime token, start the runtime, or register AirMCP with Codex. First-run Finish with no runtime is save-only; when an app-owned runtime already exists and the selection changed, Finish may serialize stop → verified config write → restart so the persisted selection, authenticated runtime fingerprint, and client receipt are the same generation. Runtime start and each client configuration remain explicit actions; a new automatic-start opt-in is committed only after an authenticated ready receipt, while token, manual-runtime, activation, and scope-change failures restore the previous preference. Client writes revalidate the runtime receipt immediately before mutation. Codex changes are transactional, preserve advanced settings, fail closed when an existing entry cannot be inspected, restore the original bytes and mode on failure, and keep token-bearing files owner-only. Incomplete Setup resumes its draft, completed Setup reopens with the current module selection, save failure keeps the window open, and the complete Setup and Trust Center surfaces ship in nine locales (the rest of the app retains its existing English/Korean coverage and English fallback).
- Single-instance, runtime ownership, and stale-port diagnostics — LaunchServices plus a deterministic in-process fallback prevent duplicate app copies from racing to own port 3847. One lifecycle generation serializes probe, start, stop, and delayed crash restart; lifecycle authority requires the authenticated runtime PID plus an app-only generation fingerprint, so a manual same-token runtime is never adopted or killed; authenticated readiness requires the pinned AirMCP version and effective scope; and HTTP, TLS, timeout, or otherwise unrecognized listeners produce a visible port-owner diagnostic instead of an EADDRINUSE restart loop.
- Governed Trust Center evidence — activity, safeguards, permissions, emergency stop, live HITL decisions, HMAC verdicts, and owner-only redacted exports share one localized view. Audit history is never read automatically or in the background: only an explicit Load or Refresh opens a 150-second task-scoped allowlist—covering the app's 120-second maximum approval window with bounded dispatch headroom—and delegates hidden
audit_logthroughrun_tool, preserving progressive exposure, rate limits, per-call HITL, emergency stop, and HMAC audit without wideningtools/list. The session is explicitly ended after the read, and its bounded TTL contains a lost cleanup response. Live pending approvals remain visible regardless of history search and status filters. - Codex CLI config-root parity — The
npx airmcp codexstatus, enable, disable, replacement, and rollback paths now resolve the same user config as their child Codex CLI:AIRMCP_CODEX_CONFIG_PATH, thenCODEX_HOME/config.toml, then~/.codex/config.toml. Relative inputs are fixed against the invoking working directory, the resolved override must be namedconfig.toml, project-local overrides remain read-only, and unrelated user entries remain byte-preserved. The macOS Setup connector retains its separate consent-first~/.codex/config.tomlcontract.
- Production dependency audit restored — the blocking and advisory npm audit lanes now match RFC 0003's shipped-runtime boundary with
--omit=dev, while the MCP SDK and Hono Node adapter move to patched releases. A CI contract test prevents the workflow and advisory helper from drifting back to all-dependency audit semantics that let a dev-only advisory skip every runtime verification step. - Audit chain fail-closed hardening — audit append/rotation/checkpoint work is serialized in-process and protected by a recoverable cross-process writer lock. Listed-but-unreadable history, partial or ambiguous appends, stale reaper locks, checkpoint gaps, rotation collisions, fsync failures, and a bounded-spool overflow revoke audit authority for the process; graceful signal and stdio shutdown paths drain accepted outcome rows before exit. Checkpoint replacement is atomic and directory-synced, the first signed upgrade write moves unsigned legacy rows behind an owner-only untrusted quarantine boundary, and trusted summaries never include that prefix. Verification authenticates the exact emitted JSON body bytes and per-call random
approvalId. The trust contract states the remaining boundary explicitly: after restart, a complete older log/checkpoint pair or deletion of both cannot be distinguished from an intentional restore/fresh state without an external monotonic anchor. - Browser OAuth and local-origin contract completed — allowed browser origins receive canonical CORS responses and unauthenticated preflight across MCP discovery and transport without bypassing bearer authentication. Loopback binding no longer treats arbitrary browser origins as trusted: browser callers must be explicitly allow-listed while native no-Origin clients remain compatible by default. JWT verification discovers and validates issuer metadata/JWKS over HTTPS, requires finite
expand non-emptysub, keeps explicit JWKS overrides, canonicalizes Chrome extension origins, and publishes only operator-declared RFC 8414 token-endpoint authentication methods. - Governance invariants closed — every OAuth
resources/*request now requires cumulativemcp:read; registered live resources traverse rate limiting and HMAC outcome audit, and built-in Apple-data resources require per-call HITL with the exact durableapprovalIdbefore content is fetched. Emergency stop remains enforced when normal rate limiting is disabled, and autonomous skill triggers dispatch through the originating server's tool registry so audit/HITL policy cannot drift to another server instance.
- Governed workflow and artifact identity are release gates — app release preflight completes the real governed approval/audit workflow before archive checks, alongside bundle, localization, runtime, and protocol validation. The npm lane is restricted to an exact
mainSHA; an occupied root or add-on version is resumed only when the clean local tarball SRI and registrygitHeadmatch that SHA, and post-publish verification binds npm, the Git tag, and GitHub Release target to it. The signed lane is explicit-dispatch only, checks out the immutable Release tag, requires main ancestry plus tag/package/app/widget agreement and a complete Widget extension, and fails before secret use unless GitHub's API reports a required reviewer, disabled admin bypass, and a matching tag-only deployment policy. That externalreleaseenvironment configuration and Apple secret enrollment remain a deliberate HOLD rather than a claim of completed signing. The final signed-artifact probe uses an isolated token and app-only owner fingerprint to verify the exact runtime PID without broad process killing, and public logs suppress certificate/account/notary identifiers. Scoped add-on verification also activates the opt-in spatial, webhook, and Power Automate packages and proves representative tools register from their clean-installedexternal-onlyartifacts. - Symmetric i18n gate —
npm run i18n:checknow fails on extra locale keys as well as missing ones, so removing anenkey can no longer pass CI silently.
- Inbound webhooks + Power Automate — two new opt-in modules (
webhooks,powerautomate) ported from the standalonenewtriabridge (same author) and relicensed MIT.webhooksadds awebhook_receivedevent source:webhook_listen_startopens a loopback HTTP listener (HMAC-SHA256 overx-airmcp-signature, 1 MiB body cap → 413, non-loopback bind requires a ≥32-char secret) that turns each verifiedPOSTinto awebhook_receivedevent any skill can bind to — AirMCP's first inbound event source alongside its nine local observers.powerautomateaddscloudflow_trigger(POST to a Cloud Flow HTTP trigger URL via SAS or OAuth Bearer, hard 120 s timeout, streamed response-size cap). Both stay off in every profile — includingfull— untilAIRMCP_ENABLE_WEBHOOKS=true/AIRMCP_ENABLE_POWERAUTOMATE=true, and both are governed by the existing HITL / audit / rate-limit / network-policy layers.count-statsnow counts the fullMODULE_MANIFEST(matching thetool-count-driftguard) so the opt-in modules are reflected in the module count without being added to the standard profile surface.
- Apple ecosystem positioning — README, landing metadata, documentation, package/plugin manifests, registry pitches, and all localized landing copy now describe AirMCP as a governed MCP runtime for the Apple ecosystem instead of a product permanently named “for macOS.” Public surfaces distinguish macOS as available, iOS/iPadOS as preview, and visionOS/watchOS as roadmap targets with platform-specific roles.
- Universal npm + MCPB artifacts restored — the root npm tarball and
.mcpbnow retain every standard JavaScript module entrypoint instead of stripping non-core packs during packaging. Logical profiles and progressive exposure still keep the default context small, while shipped-artifact gates now clean-install and boot both the default surface andfull/fullacross representative module packs. - Scoped add-ons retained as compatibility artifacts — optional physical packages keep their existing
@heznpc/airmcp-<pack>names. The default import mode is nowbundled, preventing stale installed add-ons from overriding a newer universal root; operators can explicitly chooseprefer-installedorexternal-onlyfor compatibility deployments.
- Add-on package staging + lazy harness split —
npm run addons:buildstages tarball-ready physical add-on packages for every non-core module pack, andnpm run addons:checkis wired into CI/release preflight. Runtime module loading prefers installed add-on packages (AIRMCP_ADDON_PACKAGE_MODE=prefer-installed) before bundled fallback, withexternal-onlyavailable as a strict package-boundary probe. Tool discovery returns compact descriptions by default and exposesdescribe_toolfor full per-tool detail on demand. Task-session policy moved behind harness adapters (compatible,strict,app-runtime,agent) so client-specific behavior is no longer hard-coded inside server setup. - Module runtime hardening —
npx airmcp moduleslists, enables, disables, and doctors DLC-like module add-ons using the samemodulePacksconfig consumed by the runtime.npm run tokens:checkadds a CI budget for tool-description token drift,profiles:checkincludes restricted-pack wire cases plus discovery golden queries, and generated app/CLI configs setrequireToolSession: true. - DLC-like module pack contract —
src/shared/module-packs.tsdefines runtime packs and their future scoped add-on package names.AIRMCP_MODULE_PACKSandconfig.json -> modulePacksrestrict the available pack set while always keepingcore;list_module_packsandprofile_statusexpose the resulting availability contract. - Strict task-session harness mode —
AIRMCP_REQUIRE_TOOL_SESSION=truemakesrun_toolrefuse hidden-tool dispatch unless the caller passes a validsessionId. Directly exposed tools keep the compatible no-session path,profile_statusreports the effective setting, andprofiles:checknow boots a real MCP case that discovers hiddencreate_note, verifies the no-session rejection, then proves a scoped session reaches the target tool's own validation. - Task-scoped tool sessions — new front-door MCP tools (
start_tool_session,tool_session_status,end_tool_session) let clients create short-lived allowlists.discover_tools({ sessionId })searches only that allowlist, andrun_tool({ sessionId })refuses out-of-session calls while preserving the existing no-session behavior for compatible clients.profiles:checknow exercises the allow/deny path over the real MCP wire and prints init/tools-list timings. - Release automation hardening —
release:verifychecks the public npm version, npmlatest, freshnpx, and GitHub Release.mcpbasset after publish.cd.ymlnow logs the npm auth mode before publish and runs the verifier after creating the release.release-app.ymlskips cleanly on tag push when Apple Developer ID secrets are absent, while manual dispatch can setrequire_signing=trueto fail hard. - Modular install RFC —
docs/rfc/0015-modular-install-and-task-harness.mdrecords the boundary between the implemented task harness and a future module-pack split so AirMCP does not grow package complexity before size/startup evidence justifies it.
- Profile-first runtime default — AirMCP now boots the
starterprofile withprogressivetool exposure by default instead of advertising the entire catalog intools/list. New first-class profiles arestarter,communications-safe,productivity, andfull;discover_tools+run_toolremain the front door for hidden-but-registered tools.npx airmcp init --profile <name> --yes, MCPB user config, the menubar app config model, smoke/package checks, and README/site/registry metadata are aligned to this contract. - Progressive dispatch hardening —
run_toolnow validates arguments against each target tool's capturedinputSchema, and the singleton tool registry prunes registrations from older server generations after the active profile finishes booting. This keeps hidden progressive tools callable without widening validation or leaking a previous wider HTTP session's tool registry into a narrower profile. - Profile exposure release gate — new
npm run profiles:checkboots four real MCP wire cases (starterprogressive,communications-safeprogressive,productivityprofile,fullfull) and asserts exposed vs registered tool counts. CI now runs this matrix, andrelease:preflightincludes it before package/MCPB verification. - Version and discovery drift cleanup —
scripts/sync-version.mjsnow checksdocs/index.htmlstructured-datasoftwareVersion;.well-known/mcp.jsonadvertises the active exposed tool surface instead of hidden full inventory; registry submission notes no longer claim notarized app distribution before Developer ID notarization ships. - Plugin submission readiness —
claude plugin validate .passes (the one warning is the gitignored localCLAUDE.md, which is absent from the published repo, so it does not appear in the marketplace review)..claude-plugin/plugin.jsongainsdisplayName: "AirMCP"andauthor.url; the publishedairmcp@<version>npm package boots clean (verified end-to-end via the MCP Inspector). Remaining step before listing is the operator-side submission atclau.de/plugin-directory-submission. - README hero — lead line is now "Open action runtime for Apple-native agents" (was "Apple-native agent runtime for any MCP client"), and the client list adds Xcode 27 agents (Xcode has spoken MCP since 26.3) to reflect the post-WWDC two-layer reality (see
docs/rfc/0011-post-wwdc-2026.md). - Claude Code plugin package at repo root (
.claude-plugin/plugin.json+.mcp.json) ready for submission toanthropics/claude-plugins-community. The npm version that.mcp.jsoninvokes is pinned to the same value as the plugin manifest, so the marketplace SHA approval and the runtime users actually run can never diverge. npm run mcp:validate+ new CI stepMCP Inspector validate(timeout-minutes 2). Wraps a pinned@modelcontextprotocol/inspector --cliagainstdist/index.js, captures both stdout and stderr, and exits non-zero on non-zero child exit, embedded"error"/"isError":trueenvelopes, zero-tool responses, or unparseable JSON. Wire-shape gate, not a substitute for the HMAC / HITL / audit test suites.scripts/sync-version.mjsextended to covermcp.json,.claude-plugin/plugin.json, and.mcp.json'sairmcp@<version>pin. Closes the gap wherenpm run version:patchsilently left those three files at the old version.scripts/count-stats.mjsextended to cover.claude-plugin/plugin.jsondescription for the(N) tools across (M) modulesdrift check.src/shared/banner.tsskips the ANSI logo + typewriter delays whenprocess.stderr.isTTYis false (plugin host pipe, log collector, non-interactive CI). Pipe consumers now see a single plain identity line instead of raw ANSI control bytes and seconds of artificial startup latency.
Each entry below corresponds to a finding in the audit report. Severity classes:
- BLOCKER — feature shipped that didn't actually work end-to-end
- HIGH — data loss, scope bypass, or exfiltration risk
- MEDIUM — defense-in-depth and drift cleanup
__event__stdout lines now reach the event bus (src/shared/swift.ts) — the persistent Swift bridge writes RPC responses AND native observer events on the same stdout stream, with events taggedid: "__event__". The read loop didpending.get(msg.id)thenif (!entry) continue;— every event was silently dropped. Six of nine documented triggers (calendar_changed,reminders_changed,pasteboard_changed,focus_mode_changed,file_modified,screen_locked/unlocked) and four built-in skills (calendar-alert,evening-winddown,focus-guardian,clipboard-url-to-reading) were demo-only despite being shipped. Now routes the raw__event__line (not the BridgeResponse projection, which stripsevent/data/timestamp) toeventBus.processLineso triggers fire. New test:tests/swift-event-routing.test.jsdrives a syntheticchild_process.spawn, pushes acalendar_changedline, and asserts the bus actually receives the typed event within 2s — pre-fix this hung forever.
MemoryStoreconsolidated to a process-wide singleton (newsrc/memory/instance.ts) —src/memory/tools.tsandsrc/shared/resources.tseach heldnew MemoryStore(). Each instance owned an independent in-memory cache layered over the same on-disk JSON file, somemory_putagainst the tools instance never appeared inmemory://recent(resources instance) within the same process lifetime. Both call sites now route throughgetMemoryStore(). The store handles its own load dedupe + atomic write + TTL sweep — none of that requires multiple instances. New test:tests/memory-singleton.test.jsasserts identity stability + put-then-query observability through the same instance.VectorStore.saveis now atomic (src/semantic/store.ts) — in-placewriteFilewould leave a half-written JSON on SIGKILL / power loss / OOM-kill.load()silently fell back to{ version: 1, entries: {} }, wiping the embedding index with no warning. Switched to temp+rename (single inode swap on APFS/ext4) withmode: 0o600and best-effort temp cleanup on failure. New test:tests/semantic-store-atomic.test.jsinjectswriteFilefailure and asserts the on-disk file remains intact (3 entries) plus no.tmpdebris.
actorfield stamped on every audit line emitted from a daemon skill (src/shared/request-context.ts,src/skills/triggers.ts,src/shared/tool-registry.ts) — autonomous skill executions opened no AsyncLocalStorage context, so tool-registry's audit hook stampedactor: undefined. Every autonomous tool call was indistinguishable from a human-initiated one during audit review. The trigger path now wrapsexecuteSkillinrunWithRequestContext({ actor: "daemon-skill:<name>", correlationId }), and the 4 audit emit sites intool-registry.tspropagate the value viagetActor(). Retried executions inherit the same correlationId via AsyncLocalStorage propagation throughsetTimeout. New test:tests/skills-triggers-actor.test.jsmocksexecuteSkillto capture the ctx at exactly the moment the registry would read it and asserts bothactorandcorrelationIdare set.- Symlink traversal guards on 4 file-touching tools (
src/messages/tools.ts,src/intelligence/tools.ts,src/shortcuts/tools.ts) —zFilePathonly rejected literal..segments. A symlink inside HOME pointing at/etc/secretsslipped past.send_filecould exfiltrate arbitrary files via iMessage attachment;generate_imagecould overwrite system caches with a tilde-rooted but symlink-redirected outputPath;export_shortcut/import_shortcuthad the same exposure. Each now callsresolveAndGuard()to require the realpath resolution stays inside HOME.generate_imagekeeps the guard conditional onoutputPathbeing provided (default goes to Swift's/tmp). New test:tests/symlink-guard-traversal.test.jscreates a real symlink in HOME pointing at/etcand asserts the mocked bridges are never invoked for any of the 4 tools.
- Cross-rotation chain resume (
src/shared/audit.ts) —resumeChainHeadpreviously read onlyaudit.jsonl. A process exit inside the "rotation just happened, no new flush yet" window would land withaudit.jsonlmissing →lastHmacstayed atHMAC_GENESIS→ the next flush sealed entries with_prev = HMAC_GENESIS, andverifyAuditChainreportedverified: falseat the seam. One false-positive corrodes the strongest trust signal in the codebase. Resume now falls back to the most recent rotated file (lex-sorted descending by 13-digitDate.now()filename, chronological for the next 200+ years). New test:tests/audit-rotation-resume.test.jscovers happy path /audit.jsonlmissing / both missing (cold start → genesis). - First chained line now verified against
HMAC_GENESIS—verifyAuditChainaccepted the first chained line's_prevregardless of value becausechainStartedwas false on entry. An attacker with the HMAC key could replace the entire file with a chain that internally verifies, rooted at any arbitrary_prev, and the verifier reportedverified: true. The genesis check now folds into the existingprev_mismatchpath:expectedPrev = chainStarted ? prev : HMAC_GENESIS. New test:tests/audit-genesis-check.test.jscovers genuine genesis chain + attacker-rooted chain at'f'*64+ attacker-rooted at arbitrary hex. - HMAC chain tamper detection test suite (new
tests/audit-tamper-detection.test.js) — the codebase shippedsummarizeAuditEntries().verifiedas one of the strongest trust signals but nothing asserted it actually fired under tampering. 5 mutation shapes covered: clean chain (verified) / body mutation (hmac_mismatch) /_prevmutation with recomputed_hmac(prev_mismatch, isolated from body-tamper) / malformed_hmacvalue (malformed) / attacker-appended unauthorized entry (verified: false).
AIRMCP_LOCAL_ONLY=truehard switch for embeddings (src/semantic/embeddings.ts,docs/environment.md) —detectProvider()will not returngeminiorhybridwhile local-only is set; an explicitAIRMCP_EMBEDDING_PROVIDER=gemini/hybridis overridden with a stderr warning. ThehybridSwift→Gemini fallback refuses to run and re-throws the original Swift error so note titles + previews never silently cross the network. Without local-only, every fallback now writes an__embedding_fallbackaudit line ({ from: "swift", to: "gemini", reason }) so the cloud crossing is visible throughaudit_summary.getEmbeddingConfig()reportslocalOnly: booleansodoctorsurfaces the effective privacy posture. New test:tests/embeddings-local-only.test.jscovers 6 detect cases + 2 fallback cases (with/without LOCAL_ONLY) + 2 config-surface cases.
drop_pin/share_location/search_nearbylat/lng now bounded (src/maps/tools.ts) — barez.number()accepted±Infinity, JXA received the literalInfinitykeyword, parsed cleanly but produced garbage map state. Now bounded[-90, 90]/[-180, 180]to match the lone-already-correctreverse_geocode. New test:tests/maps-coordinate-bounds.test.jscovers in/out-of-range + Infinity rejection + a regression pin onreverse_geocode.
- Test infrastructure: ignore stale
.claude/worktrees (jest.config.js) —testPathIgnorePatterns+modulePathIgnorePatternsskip/\\.claude/. Pre-fix, jest-haste-map threw on duplicate module names and discovery ran pre-rebase versions of every suite when multiple worktrees existed. - JXA / AppleScript AST validator (new
tests/jxa-scripts-ast.test.js) — every*/scripts.tsbuilder output is parsed through Node'svm.Script. JXA scripts wrap in(function(){...})to mirror the implicitrun()handler that osascript provides (top-levelreturnis legal there but rejected by Script-mode parse). AppleScript fixtures fall back to a pattern match (tell application/^--/^on/^script). Catches structural breakage (unbalanced braces, missing semicolons after template literals, broken interpolation, stray backticks) BEFORE the script ever reaches osascript — addresses the broader "mock toContain tautology" class of test misses flagged in the audit. - README / mcpb manifest / shortcuts.md drift fixed —
229 Shortcuts / Siri AppIntents→ 232 (auto-counted fromMCPIntents.swift),34 prompts→ 32 (auto-counted fromserver.prompt(registrations),270+ tools across 29 moduleslong-description aligned to canonical counts.scripts/count-stats.mjs --checknow enforces both patterns so future drift can't reach a release build. Affected files:README.md,mcpb/manifest.template.json,docs/REGISTRY_SUBMISSIONS.md,docs/shortcuts.md. AIRMCP_VECTOR_STORE_DIRenv opt-in (src/shared/constants.ts) — primarily for tests that need a tmpdir-rooted audit + vector store. Without it the suite would write to the developer's real~/.airmcp/.
Headline shifts: every tool error now carries a typed category and a Trace: <id> line so a single failed call threads end-to-end through audit log + telemetry; RFC 0008 (Elicitation) Phase 1 lands the capability gate + env opt-out; RFC 0009 (iWork depth) Phase 1 ships the first three Numbers tools (tool surface 269 → 272); npx airmcp doctor --deep walks live audit-chain integrity + Swift bridge ping + module boot smoke for user-reported troubleshooting; the OAuth /.well-known/oauth-protected-resource document picks up SEP-985 alignment with DPoP advertisement + RFC 9728 optional fields. RFC 0001 typed-error adoption reached 29/32 modules with three intentional skips justified inline. Issue #145 (list_calendars empty after a fresh permission grant on macOS 15.7.5) closed via a one-line EKEventStore.reset() in the shared authorize helper. Two future-direction drafts (RFC 0010 progressive disclosure / RFC 0011 post-WWDC) staked out.
Reading guide: entries are grouped by RFC track and audience.
- For users — features visible in the AI's tool surface, error messages, or
npx airmcp doctoroutput.- For developers — internal helpers, test infra, RFC 0001 helpers, error envelope shape.
- For operators — env var changes, audit / OAuth / rate-limit / network policy adjustments. See docs/environment.md for the full env knob index.
The toolError(action, e) fallback classifier in result.ts got companion errJxaFor / errSwiftFor / errUpstreamFor catch helpers (PR #173) that auto-attach cause.origin and the "Failed to <action>: <message>" prefix. Across PRs #166, #168, #169, #170, #171, #172, #173, #185, #186, #187, #188 every module that wraps runJxa / runSwift / runAutomation / HTTP fetches migrated their catch blocks from the fallback to the typed helpers. Adoption is now 29/32 tools.ts files.
The remaining three modules intentionally keep toolError because the fallback's internal_error classification is the right answer for them:
audit/tools.ts— fs reads of the on-disk JSONL audit log. ENOENT / EACCES are already handled inside the audit reader, and zod surfacesinvalid_inputautomatically for thesinceISO-8601 parameter. No category gain from migrating.memory/tools.ts— fs + JSON parse of~/.cache/airmcp/memory.json. Same shape asaudit: storage failures areinternal_error, input validation already runs through zod +errInvalidInput. PR #154 already hardened the write path (atomic temp+rename + serialized op queue + JSON-reviver prototype-pollution guard); the catch-block category isn't where the action is.podcasts/tools.ts— module is fully broken on macOS 26+ (Apple removed the Podcasts JXA scripting dictionary, see RFC 0004 /compatibility.brokenOn: [26]block). Migrating the dead-on-arrival catches isn't worth the noise; the deprecation is already advertised throughairmcp doctorandprint-compat-report.
toolError itself stays exported for these three modules and as the safety net for any future tool that hasn't yet picked up a typed origin.
- RFC 0008 Phase 1 — Elicitation capability gate + env opt-out (PR #196) — on review the elicitation path was already wired in
installHitlGuard(tryElicitApprovalcallsinner.elicitInputwhen the inner Server exposes it). RFC 0008 §3.2 + §3.3 named two gaps that hadn't landed yet: (1)AIRMCP_ELICITATION_DISABLE=trueenv opt-out for end-to-end scripted destructive pipelines that don't want any user prompt — falls through to the socket HITL channel exactly like a client that doesn't advertise elicitation; (2) explicitgetClientCapabilities()check before issuing the elicit request, avoiding a doomed call when the client declared no elicitation support. The existing try/catch stays as belt-and-suspenders for clients that lie about their capabilities. RFC 0008 status: Draft → Phase 1 Implemented. - RFC 0009 Phase 1 first batch — 3 Numbers depth tools (PR #197) —
numbers_list_tablesreturns{ name, rowCount, columnCount }per table; multi-table sheets are common (totals + breakdown + chart-source) but existing tools always readtables[0], so this lets a caller pick by name.numbers_get_formulareturns the literal expression behind a cell (=SUM(A1:A10)) instead of the evaluated value; returnsnullwhen the cell holds a constant — pairs with the existingnumbers_get_cell.numbers_rename_sheetrenames in place; Numbers does NOT allow duplicate sheet names so the JXA call throws and surfaces aserrJxa. Same-pass cleanup: migrated existing 9 numbers catches fromtoolErrortoerrJxaFor(RFC 0001 §3.1 cleanup that was missed in the earlier wave). Brings tool surface 269 → 272 across all auto-synced artifacts (manifest / llms / README / 232 AppIntents). 14 more Numbers tools queued in RFC 0009 §4.1; followups can lift this PR's pattern verbatim. npx airmcp doctor --deep(PR #198, polish in PR #200) — defaultdoctorstays fast.--deepopts into slower live probes for user-reported troubleshooting: HMAC chain verification across all on-disk audit JSONL files (surfaces single-line tampering with the exact file + line number viasummarizeAuditEntries.verifiedFirstBreak); Swift bridge live ping (distinguishes "not built" from "built but unresponsive"); module registry boot smoke (actually imports everytools.ts+prompts.tsto surface typos / missing transitive deps that ride the eager-import path at startup). Footer of defaultdoctorprints the hint pointing at--deep. PR #200 widened the audit-chain bad message with actionable follow-up: "Inspect the surrounding lines, then callaudit_summaryto see the full break window."initwalkthrough closes with example prompts (i18n across 9 locales) (PR #199, polish in PR #200) —npx airmcp initfinishes with a "Try asking your AI:" block of three representative prompts so the user has a concrete path from "setup complete" to first interaction (doctor --deephint added in passing). Three new i18n keys (prompt_calendar_today,prompt_summarize_notes,prompt_overdue_reminders) translated across all 9 locales the wizard already supports — PR #200 caught the original implementation hardcoded the strings in English even when the wizard ran in another locale.permission_deniedauto-hint pointing at System Settings (PR #199, polish in PR #200) —errPermissionnow adds a default hint pointing at System Settings → Privacy & Security when the caller didn't supply one. macOS-only — non-darwin (CI runners, etc.) fall through to no hint. Caller-supplied hints win verbatim. Closes the recurring "permission denied — but where?" UX gap. PR #200 hoisted the platform check to a module-levelDEFAULT_PERMISSION_HINTconst (resolved once at load instead of every error).- outputSchema Wave 5 — 4 photos read tools (
list_photos,search_photos,get_photo_info,list_favorites) — extends Wave 4's pattern to the photos module.list_photos/search_photos/list_favoritesroute throughokUntrustedLinkedStructured(photo metadata is user content);get_photo_infousesokUntrustedStructured.list_albumsdeferred — bareAlbumItem[]return shape, same array-vs-object breaking-change risk ascompare_notes. Weather forecast tools (get_daily_forecast/get_hourly_forecast) also deferred for the same reason. 7 new drift guards intests/output-schema-wave5.test.jscovering full / null-EXIF / empty-list shapes;tests/output-schema-structured.test.jsexhaustive coverage check picks up 4 fixtures so any future tool that adds outputSchema without a fixture breaks the build. - iOS Bearer token Keychain persistence (
ios/Sources/AirMCPServer/KeychainTokenStore.swift) — closes the (C) "Apple-native deeper, two devices" promise's pairing gap. PreviouslyMCPHTTPServer.init(token: nil)generated a fresh random token on every process start, so any client paired with the previous boot's token (Windows Claude Desktop, a Mac MCP client over the same Wi-Fi, etc.) silently broke. The newMCPHTTPServer.make(...)async factory routes through aKeychainTokenStoreactor which reads the persisted token (or generates + persists a fresh one on first boot). Stored askSecClassGenericPasswordwithkSecAttrAccessibleAfterFirstUnlockThisDeviceOnly— survives reboot but does NOT iCloud-sync (matches RFC 0002's loopback-by-default network policy: pairing is per-device on purpose).kSecAttrServicenamespaces by build flavour (com.airmcp.ios.token, override viaAIRMCP_KEYCHAIN_SERVICEenv for fork installs that share a device). Failure modes (Keychain unavailable in unentitled CLI runs, simulator quirks) fall back to a per-process random token with a stderr warning so the server still functions; that token doesn't persist and the operator sees the regression in logs. Newclear()API for "rotate token" / "unpair all clients" UI flows. The synchronousMCPHTTPServer.init(token: String)is retained as a non-nullable explicit-pairing entry point for tests + flows where the caller already has a token in hand.App.swiftupdated to callMCPHTTPServer.make(...). The legacyprivate static func generateToken()onMCPHTTPServerremoved — token generation lives on the persistence layer that owns the bytes.
- Per-tenant rate-limit buckets keyed on OAuth subject (PR #159) — multi-tenant deployments behind OAuth get isolated rate-limit budgets per
subclaim. Stdio / single-tenant flows fall through to the shared global bucket. Closes the noisy-neighbor gap on shared HTTP transports. /.well-known/oauth-protected-resourcealigned with SEP-985 / RFC 9728 (PR #193) — MCP SEP-985 (active 2026-05) standardizes the OAuth protected resource metadata document beyond the MCP 2025-11-25 spec minimum. Three additions, all backwards-compatible: (1) DPoP advertisement (honest, not enforced) —dpop_signing_alg_values_supported: ["ES256", "RS256"]+dpop_bound_access_tokens_required: false. AirMCP doesn't bind tokens to a DPoP proof yet; declaring the flag honestly lets a future token-binding SEP flip it without renegotiating discovery. Symmetric (HS*) andnonestay excluded — same key-confusion defense as theresource_signing_alglist. (2) RFC 9728 §2 optional fields via env:AIRMCP_OAUTH_RESOURCE_DOCS→resource_documentation,AIRMCP_OAUTH_RESOURCE_POLICY→resource_policy_uri,AIRMCP_OAUTH_RESOURCE_TOS→resource_tos_uri. Fields are omitted when unset so crawlers don't render dead links. (3) +4 test cases covering full SEP-985 baseline shape, DPoP honesty, env knobs surface when set, optional fields omitted when unset.docs/environment.md— full env knob index (77 vars) (PR #199) — Quickstart table for the five most-asked operator setups (token auth, OAuth, debug a flaky module, expand description budget, audit cross-host integrity). Categorized tables (Network/Auth, Rate limit, Audit, HITL, Module control, Embedding/AI, Telemetry, Timeouts/Buffers, Triggers, Tooling, Internal/test) with default + use case for each var. Source pointers at the bottom for deeper reading. README's Safety bullet now links to the page so operators can find the index without grepping the source.- OAuth 2.1 browser PKCE setup guide (
docs/oauth-browser-pkce.md) — RFC 0005 Step 3. Full walkthrough for browser-resident MCP clients (Claude in Chrome, Managed Agents, custom extensions) negotiating the Authorization Code + PKCE flow against AirMCP'swith-oauth*endpoints. Covers: 9-step happy-path sequence diagram with every HTTP hop spelled out including the RFC 8707resourceparameter on both authorize + token requests; server env var checklist (AIRMCP_OAUTH_ISSUER,AIRMCP_OAUTH_AUDIENCE,AIRMCP_ALLOWED_ORIGINS); Claude in Chrome wiring (Anthropic's public redirect URI + AS client configuration); custom-client integration notes with vetted library picks (@openid/appauth,golang.org/x/oauth2,authlib) + a reviewer-friendly client-side hardening checklist (cryptorandom verifier,S256challenge method, state param CSRF guard, token storage rules); scope → tool mapping table cross-referencingsrc/shared/oauth-scope.ts;npm run dev:oauthfast-loop recipe; 8-row troubleshooting table covering thewrong_audience/wrong_issuer/unsupported_alg/jwks_unreachable/ scope-gate forbidden / CORS-preflight-403 / startup-refused /invalid_grantcases with specific remediation. README Features row now cross-links the new guide; RFC 0005 status entry points at the doc instead of "in progress".
- Correlation-id threading across request-context, tool-registry, audit (PR #190) — async tool calls were untraceable across log lines: an audit entry, a telemetry trace, and a thrown error from the same call had no shared identifier so debugging required reconstructing wall-clock timing.
RequestContextgains an optionalcorrelationIdfield; the tool-registry wrapper opens arunWithRequestContextscope on first entry and stamps arandomUUID()correlation ID. If the call already arrived inside a context with a correlationId set (e.g. an HTTP middleware seeded one for distributed tracing), the existing ID is honored.auditLogauto-attaches the active correlationId to every entry; explicit caller-supplied IDs win. Audit log entries already in production keep working —correlationIdis optional in the JSON shape. HTTP transport middleware can seed an inboundRequest-Idheader into the context for distributed tracing systems. +5 test cases covering absence / verbatim / await-boundary persistence / concurrent isolation (Promise.all branches see only their own ID) / OAuth-claim coexistence. - Correlation-id
Trace: <id>line in error envelope (PR #198) —toolErr()now auto-attaches the active context's correlation id tostructuredContent.error.correlationId(machine-readable) and a trailingTrace: <id>line in the human-readable text. PR #190 already added the id to every audit row; this closes the loop on the user side so a failed tool call carries the id needed forgrep <id> ~/.airmcp/audit.jsonl.ToolErrorOptionsgains an explicitcorrelationIdoverride for tests. npm run tokens— measure compactDescription savings (PR #165) — total bytes / token estimate before vs after compaction. Made the v2.12 RFC 0010 stub possible by quantifying the description budget at ~3.8K tokens after compaction (~50% reduction over the raw 7.5K).npm run llms:check— drift guard forllms.txt(scripts/gen-llms-txt.mjs) — regenerates llms.txt in-memory and diffs against the checked-in copy; CI now runs this so any tool/prompt/module addition withoutnpm run llmsfails the build instead of silently shipping a stale catalog (the long-standing "258 tools / 30 modules" drift bug that survived multiple releases).llms-full.txtis.gitignored and intentionally skipped from the check (oversize for review diffs); only the public-facing summary is pinned. New CI step in.github/workflows/ci.ymlbetweencount-stats --checkandsync-version --check.docs/ROADMAP.md— public 4-week + quarterly priorities — externally-readable roadmap separated from the gitignored internalTODO.mdscratch file. P0 / P1 / P2 / P3 buckets, each item linked to the relevant src file or RFC.docs/REGISTRY_SUBMISSIONS.mdextended to 19 directories — added cursor.directory, MCP.so, mcphub.io, Modelo MCP Hub, mcpservers.org, awesome-mcp-servers (GitHub), LobeHub MCP, MCP Index, Composio MCP, HyperMCP, MCP Discover (12 new entries on top of the existing 7). Each row carries submission URL, audience pitch, and the v2.11 differentiator angle. First-wave priority list pinned at the top.- CHANGELOG reader guide (PR #199) — one-block "For users / For developers / For operators" guide at the top of
[Unreleased]so a release reader can skim the audience that applies to them. The full structure (### Added — for users / operators / developersheaders) lands in this v2.12 release cut.
list_calendarsreturns empty after fresh permission grant (swift/Sources/AirMCPKit/EventKitService.swift) — closes #145 on macOS 15.7.5 (PR #201). Reproduces via the Swift bridge:list_calendarsreturns an empty array even though Calendar.app shows the user's calendars and the Calendar permission has been granted. Root cause: a freshly-grantedEKEventStorekeeps the snapshot it had from before authorization.calendars(for: .event)reads that stale snapshot rather than the now-readable backing data, so it surfaces an empty list. Fix: in the sharedauthorize()helper (used by both events and reminders paths inEventKitService), callstore.reset()between the granted check and flagging the store as authorized. Idempotent across subsequent calls because the flag short-circuits —reset()only fires the first time through after a grant. Consolidating the reset in the helper covers both events + reminders in one place. JXA path is unaffected — Calendar.app scripting reads through Calendar's own data store, not EventKit.AskAirMCPIntentrejects empty / whitespace-only prompts (swift/Sources/AirMCPKit/AskAirMCPIntent.swift) — Siri "Ask AirMCP" with no follow-up text used to hitLanguageModelSession.respond(to: "")→ opaque framework error → generic Shortcuts failure. Now trim and return a recoverable user-facing message ("Please ask a question — I need something to look up.").gen-llms-txt.mjscount drift fully closed (scripts/gen-llms-txt.mjs) — was reporting265 tools / 32 moduleswhilecount-stats.mjs(canonical) said269 / 29. Two roots: strictextractToolsregex missed 4 tools whose registration spans the regex boundary;walkDirovercounted cross / semantic / skills / server / shared as separate modules. Fix: broad-regex pass for totals (matchescount-stats), canonical module count parsed fromMODULE_NAMES. Per-module list keeps strict regex for rich rendering. Headline now269 / 29everywhere (and272 / 29after PR #197's RFC 0009 batch).safariandpodcastsmodules carrycompatibility.deprecationfor macOS 26 (src/shared/modules.ts) — RFC 0004 G-5. Apple removed Safarimake new bookmarkJXA verb (just the one tool, replaced byadd_to_reading_list) and the entire Podcasts JXA dictionary in macOS 26. Both now declarecompatibility.brokenOn: [26]+deprecationblocks (since,removeAt,replacement,reason) so RFC 0004'sprint-compat-reportandairmcp doctorsurface the regression with the exact replacement (Safari) or upstream-removal note (Podcasts).- Event listener leak on HTTP session cleanup (
src/server/mcp-setup.ts:505) —cleanupEventListenersonly removed 3 of the 9 listeners thatevent_subscriberegisters. v2.10 added 6 new event types (mail_unread / focus_mode / now_playing / file_modified / screen_locked / screen_unlocked) but the cleanup wasn't extended in lockstep. HTTP servers that idle-timed-out sessions accumulated 6 listeners per session and eventually hit Node'sMaxListenersExceededWarning; the unremoved closures also kept references to staleMcpServerinstances alive, causing silent failures when later.sendResourceListChanged()calls fired against closed transports. All 9 listeners now mirror the registration list. - Banner displayed wrong skill count (
src/server/mcp-setup.ts:494) — hardcodedskillsBuiltin: 7while the actual count had grown to 14 YAML files indist/skills/builtins/.registerSkillEnginenow returns{ builtinCount, userCount }andbannerInforeads from that, so the count auto-updates as built-ins are added or pruned. - Duplicate
AppShortcutsProviderin app target (app/Sources/AirMCPApp/AppIntents.swift:133) —AirMCPShortcuts(hand-written, 7 entries, predates RFC 0007 codegen) andAirMCPGeneratedShortcuts(auto-generated, 10 entries, inswift/Sources/AirMCPKit/Generated/MCPIntents.swift:6850) both conformed toAppShortcutsProviderin the same app bundle. Apple constrains an app target to a single conformer; having both produced ambiguity at build time and a Siri suggestion tie that Apple resolves arbitrarily. The hand-written provider is removed; the unique intent types it referenced (DailyBriefingIntent,HealthSummaryIntent) remain defined and stay invocable via the Shortcuts app, Spotlight, and Action Button — they just aren't pinned as Siri-first phrases. A future codegenAPP_SHORTCUTS_TOPentry can re-pin them once the correspondingdaily_briefing/health_summarytools graduate to first-party manifest entries.swift buildconfirms the conflict is gone (app target compiles clean). - Tool count drift in user-facing docs —
llms.txtsaid258 tools across 30 modules(regenerated to current vianpm run llms);docs/shortcuts.mdsaid154 read-only tools/144 intents(now correctly 229 tools / 219 non-pinned intents reflecting RFC 0007 Phase A's full write-capable surface). The deeper discrepancy betweencount-stats.mjs(29 modules / 269 tools — canonical fromMODULE_NAMES) andgen-llms-txt.mjs(32 modules / 265 tools — walkssrc/for any dir withregisterTool) was tracked for follow-up and fully closed in PR #157.
MemoryStoreatomic write + serialized op queue + prototype-pollution guard (src/memory/store.ts) — three audit-flagged risks closed in one pass. (1)save()now stages JSON in a sibling tempfile (<path>.<random>.tmp) andrename()s it over the canonical path, so a SIGKILL / power loss mid-write leaves either the old or new content — never a half-flushed JSON file (which would have silently restored to an empty store on nextload()and lost every fact / entity / episode). (2)put()/forget()/stats()route through a new privateenqueue()op queue so concurrent invocations (an agent loop dispatchingmemory_putin parallel with amemory_querythat triggers a sweep, or two skill steps writing different keys) chain instead of trampling the on-disk file's last writer. The chain swallows queue-level errors so a single failure doesn't poison the next op. (3)JSON.parsenow uses a reviver that drops__proto__/constructor/prototypekeys so a hand-edited or attacker-supplied store file can't pollute Object.prototype when loaded back.- AppIntent handler injection race window shrunk (
app/Sources/AirMCPApp/AppIntents.swift:162) —installMCPIntentRouterForMacOS()now usesTask { @MainActor in … }at default priority instead ofTask.detached(priority: .utility). The actor hop intoMCPIntentRouter.shared.setHandleris unavoidable (router is an actor), but raising the priority + dropping.detachedshrinks the cold-launch race window from "seconds" (utility queue can sit behind other low-priority work) to "milliseconds before the first runloop tick" — far before any Siri / Shortcuts cold-launch first-invocation can reach the router. The existingMCPIntentError.handlerNotInstallederror path is preserved as a safety net with the requested tool name embedded. bulk_move_notes— dryRun + stopOnError + meta visibility (src/notes/scripts.ts:377,src/notes/tools.ts:522) —dryRun: true(default false) returns the list of notes that would move, the original folder, and an explicitmetaPreservationblock stating which fields would be lost (creationDate / modificationDate / attachments — Notes JXA cannot set those on the new note copy).stopOnError: true(default true) halts on the first failure so the source/target stays at a recoverable mid-state instead of running through 50 more failures and producing 50 more orphaned partial moves; passfalsefor best-effort partial completion. The script also detects same-folder no-ops (originalFolder === targetFolder) and reports them asunchanged: trueinstead of doing the body-copy → delete dance and silently nuking the metadata. Per-note successful-move results carry the originalmetaLost: { creationDate, modificationDate }so the caller has a record of what was discarded.
audit_logHMAC chain — single-line tampering detection (src/shared/audit.ts) — every flushed audit line now carries_prev+_hmac(HMAC-SHA256). The chain spans process restarts: on first flush after boot the module reads the disk tail, picks up the latest_hmac, and continues — no per-process forks.summarizeAuditEntrieswalks every chained line and reportsverified: boolean+verifiedFirstBreak: { file, lineIndex, reason }so a single deletion / mutation surfaces with the exact location. Legacy un-chained lines (written before this version) are tolerated and skipped — but inserting a legacy-shaped line in the middle still breaks_prevon the next chained line, so verification cannot be laundered. Key source:AIRMCP_AUDIT_HMAC_KEYenv var (preferred — operator-provided, enables cross-machine integrity check) or a host-derived fallback (airmcp-audit::<hostname>::<platform>— tamper-detection grade only).audit_summarytool gainsverified,verifiedFirstBreak, andauditDisabledfields.auditDisabledno longer permanent (src/shared/audit.ts) — previously one transient disk-full incident latched audit logging off until the process restarted. Now afterMAX_FLUSH_FAILURESconsecutive failures the module enters a 5-minute backoff, then the nextauditLog()call triggersmaybeAttemptRecovery()which clears the disabled flag and retries flushing. The auditDisabled state is also surfaced throughsummarizeAuditEntriesso a doctor / health check can flag the situation in real time.SENSITIVE_TOOL_PATTERNSbroadened (PR #192) —oauth_*/password/credential/tokensubstring patterns added. Catches RFC 0005 OAuth tools (oauth_authorize,oauth_refresh, …) plus any future credential-bearing tool name without per-tool opt-in. Defense-in-depth on top of the args-side sanitizer for tools that pack credentials into nested args the per-key sanitizer can miss.resumeChainHeadmalformed-line counter (PR #192) — was silently skipping garbage lines while scanning the disk tail for HMAC chain resumption. Now logs a one-shot stderr warning with the malformed count + recovery point so an operator notices tampering or corruption before it gets buried under fresh entries. Behavior unchanged; the message is the only delta. Six new test cases close the audit / correlationId / sanitize-redaction gap (explicitcorrelationIdpreserved verbatim, omittedcorrelationIdstays undefined outside an active context,oauth_authorizeargs replaced with_redactedmarker, tool names containingcredentialortokenredacted, non-sensitive tool flows throughsanitizeArgsunchanged).ai_agentwrite-tool bypass closed (swift/Sources/AirMCPKit/FoundationModelsBridge.swift:134-152) — verification of a multi-session audit finding (P0-2) confirmed the concern:FoundationModelsBridge.run()registered 5 tools (TodayEventsTool,DueRemindersTool,SearchContactsTool,CreateReminderTool,CreateNoteTool) on theLanguageModelSession, and the model autonomously called them in a tool-calling loop. The two write tools invokedEventKitService.createReminder()directly in Swift, bypassing the Node-sidetoolRegistrypre-handler — meaning HITL approval, rate-limit, and audit-log enforcement were all skipped on the agentic path.ai_agent'sdestructiveHint: falsewas technically truthful but the absence of read-only-only signaling made the bypass surface invisible to clients. Fix:allTools()now exposes only the 3 read-only tools; theCreate*Toolclasses remain defined as future-use API but are no longer registered with the session.ai_agentdescription rewritten to spell out the read-only constraint and explain the bypass concern;readOnlyHint: truecorrected. Restoring write capability to the agent surface requires a Swift→Node loop-back transport so calls re-enter the toolRegistry — designed in TODO/ROADMAP for v2.12+. Earlier the #150 PR body had marked this finding as "MISCLASSIFIED" based on a partial main.swift read; the audit was correct, this PR is the proper retraction + fix.
- RFC 0001 Wave 2+ — error shape migration across 13 modules (PR #144) — every
return err(...)site in tool-handler code now routes through a typed error helper (errPermission/errInvalidInput/errNotFound/errUpstream/errSwift/errDeprecated) so clients can branch onstructuredContent.error.categoryinstead of string-matching the text content. Converts 59 sites across: mail (2×errPermissionfor send-disabled), messages (2×errPermission), google (15×errUpstreamfor upstream Gmail/Drive/Sheets/Calendar/Docs/Tasks/People failures + 3×errPermissionfor send/destructive gates + 2×errInvalidInputfor service-name / sanitization validation), health (5×errSwiftfor bridge-required), intelligence (2×errSwift), speech (3×errSwift), mcp-setup (2×errSwift+ 1×errNotFoundfor unknown-prompt + 1×errUpstreamfor workflow-fetch-failed), semantic (3×errSwift+ 1×errNotFound+ 1×errUpstream), cross (1×errInvalidInput+ 2×errUpstream+ 1×toolErrfor build-snapshot failure), skills register (2×errUpstreamfor skill step/execution failure), hitl-guard (2×errPermissionfor denial paths), ui (4×errInvalidInputfor click/query criterion validation), memory (1×errInvalidInputfor exactly-one-of), safari (6×errInvalidInputfor URL validation + 1×errPermissionfor JavaScript-disabled + 1×errDeprecatedfor macOS 26add_bookmarkstub). Wire format per RFC 0001: text content becomes[<category>] <message>(was bare message);structuredContent.error = { category, message, retryable }now populated. Existing clients that readisError: trueor substring-match the message keep working; clients that want structured recovery branches gain a stable category contract. No behavioral change — same conditions trigger the same errors; only the wire shape gains structure.tests/skills-register.test.jsupdated to asserterrUpstreaminstead oferron the three skill-failure paths (mock list also extended for the new export so the ESM binding doesn't fail on load). - Docs sync for v2.11.0 — public-facing docs catch up to the shipped v2.11 surface.
README.mdFeatures block bumps tool count 270+ → 269, pins 229 AppIntents line, adds OAuth 2.1 + Resource Indicators row, adds.well-knownsessionless discovery row, notes notarized menubar app; "Why AirMCP?" comparison table, security section, and architecture section all align on 269 tools / 29 modules (was a mix of 262 / 270+ / 262 across five locations); "Future" section drops OAuth 2.1 + GUI .app distribution (both shipped) in favour of Step 3 PKCE guide, stateless streamable HTTP, iOS/visionOS exploration.docs/index.htmlJSON-LDsoftwareVersioncorrected2.7.3→2.11.0(3 majors stale, SEO-visible), meta/Twitter descriptions + Schema.org description + glass-stats card +why_1_titleall bumped from 27 modules / 262 tools to 29 / 269.docs/REGISTRY_SUBMISSIONS.mdstatus table refreshed to v2.11 baseline with.mcpb+ OAuth as new differentiators; new row for Claude Desktop Extensions directory submission opportunity. RFC status transitions: 0001 Draft → Accepted (Waves 0+1 shipped, Wave 2+ in progress), 0004 Draft → Accepted (runtime activation shipped in v2.10), 0005 Draft → Accepted (Steps 1+2 shipped in v2.11, Step 3 in progress), 0007 Draft → Phase A Accepted (PRs #101-#137 closed Phase A).CONTRIBUTING.mdadds drift-guard commands (stats:check,gen:manifest:check,gen:intents:check) and a new OAuth local-development section pointing atnpm run dev:oauth. No code change. - Docs sync for v2.12.0 (this release) —
README.md(5 sites: hero, comparison table, Safety bullet, well-known crawler example, Architecture scope bullet) anddocs/index.html(5 sites: meta description, Twitter description, JSON-LD description, glass-stat,why_1_title) bumped 269 → 272 to reflect PR #197's RFC 0009 first batch.docs/index.htmlsoftwareVersioncorrected2.11.0→2.12.0. Internal-mention drift indocs/environment.md,docs/skills.md,docs/REGISTRY_SUBMISSIONS.md,docs/ROADMAP.md,docs/rfc/0010-progressive-disclosure.mddeferred to a v2.12.1 docs cleanup PR — those each carry context-specific number references that need individual review (e.g. ROADMAP "2026-04-30 기준" snapshot, REGISTRY "v2.10 baseline"). - README — Safety & Operations bullet now mentions correlation-id + HMAC chain (PR #194) — PR #190 added per-call correlation IDs to every audit entry, and PR #152 added HMAC chain integrity. The README's bullet hadn't caught up. One-sentence expansion so users + integrators see the feature without reading the CHANGELOG.
- Simplify pass over PRs #198 / #199 (PR #200) — three findings: (1)
result.tserrPermissioncollapsed dead-code hint composition (opts?.hint ?? ''could only ever produce''); hoisted platform check to module-levelDEFAULT_PERMISSION_HINTconst (resolved once at load instead of every error). (2)doctor.tsdeepFlagvariable — pulledprocess.argv.includes('--deep')into a single local const reused for both the deep-checks block and the trailing footer hint; widened the audit-chain bad message to include actionable follow-up. (3)init.tsexample prompts — i18n via the existingt()helper (4 new keys translated across 9 locales). Three skipped findings documented as false positives.
- RFC 0008 — MCP Elicitation for destructive tools (PR #191; Phase 1 implemented in PR #196) —
@modelcontextprotocol/sdk1.29.0 already exposesserver.elicitInput(). Phase 1: confirmation-only elicit wrapper fordestructiveHint=truetools, capability-gated (only when client advertiseselicitation), env-opt-out (AIRMCP_ELICITATION_DISABLE=true), threadscorrelationId(PR #190) for the audit trail. Phase 2 deferred: form-mode parameter capture, URL-mode consent flows, capability-driven "this tool will prompt" hint intools/list. Backwards-compatibility matrix: Elicitation client → new prompt; App Intents → existingrequestConfirmation(RFC 0007 §A.3); Plain HTTP/stdio without elicit → today's behavior (rate-limit + scope gate). - RFC 0009 — iWork (Pages / Numbers / Keynote) coverage depth (PR #191; Phase 1 first batch in PR #197) — pre-implementation gap audit vs
iwork_mcp(113-tool reference). Phase 1 (~40 new tools): Numbers cell/range/formula CRUD, Pages section/style ops, Keynote master/layout/transitions. Phase 2 deferred: NL→formula codegen via Foundation Models, master propagation, chart manipulation. Risks captured: JXA dictionary inconsistency, large-spreadsheet performance, context-budget bloat (re-runnpm run tokensafter Phase 1). - RFC 0010 — Progressive tool disclosure (SEP-1888 alignment) (PR #195) — AirMCP advertises 269 tools eagerly;
npm run tokens(PR #165) measured the description budget at ~3.8K tokens aftercompactDescription— already a 50% reduction over the raw 7.5K, but a meaningful chunk of every model context still goes to "things you might call." SEP-1888 (active May 2026) replaces "register N narrow tools" with library tool +searchToolsprogressive disclosure; the Anthropic Skills-over-MCP charter calls out the same direction. Stub-only because: the SEP shape isn't ratified yet — premature migration would rename the public surface twice. AirMCP's existingdiscover_toolsalready handles the search backend; the gap is the discoverability contract, not the implementation. - RFC 0011 — Post-WWDC 2026 positioning placeholder (PR #195) — WWDC 2026 keynote June 8–12 has credible leaks for: Siri 2.0 with third-party AI extensions, possible Apple-blessed system MCP API, "Snow Leopard year" refinement focus. Any individually has a measurable AirMCP impact; together they could redraw the competitive map. Placeholder is intentionally light — pre-keynote prohibition on pivots, just a guaranteed landing spot for the post-WWDC sprint with action items for the day-of and following 48h.
Headline shifts: AirMCP becomes installable with one click (.mcpb Desktop Extension for Claude Desktop), discoverable without a session (.well-known/mcp.json + .well-known/oauth-protected-resource), deployable with real OAuth 2.1 (JWT verification + RFC 8707 Resource Indicators + scope-gated tool calls), and packageable as a self-contained macOS app with Developer ID signing/notarization automation prepared. This does not claim that a notarized distribution was completed; the signed lane still requires protected release-environment review and Apple credentials. The Swift AppIntent codegen stream reached Phase A closure — 229 auto-generated intents, 50 Interactive Snippet views, 17 AppEnum pickers — with the pure helpers extracted into scripts/lib/codegen-helpers.mjs (134 unit tests) so regressions localize to a one-line diff instead of a golden-file firestorm. Four outputSchema waves established matching runtime structuredContent contracts for 34 high-traffic read tools.
- outputSchema Wave 4 — 7 additional high-traffic read tools now declare typed
outputSchemawith matchingstructuredContent:read_message,search_messages(mail),search_files,recent_files(finder),read_page_content,search_tabs(safari),scan_notes(notes). This raises the exact four-wave runtime-contract set from 27 to 34 tools. Mail tools route throughokUntrustedLinkedStructuredso the primary text block is still wrapped with the external-content marker (prompt-injection guard preserved); finder read tools useokStructured/okLinkedStructured(system metadata isn't user content); safari read tools useokUntrustedLinkedStructured(page source + tab URLs are external).read_message.id+search_messages.idscripts nowString(m.id())the Mail.app-returned numeric id so the wire contract matches Wave 2'slist_messages.id: z.string()declaration — silent Wave-2 runtime/schema mismatch fixed in passing.search_filesper-file size/modificationDate declared optional to tolerate the script's stat-failure fallback ({path, name}-only rows on permission errors). 13 new drift guards intests/output-schema-wave4.test.jsacross normal/null/empty/truncated cases, andtests/output-schema-structured.test.jspicks up 7 fixtures so the exhaustive contract check stays green. Wave 4 notably skipscompare_notes— the script returns a bare array and wrapping it in an object would break clients thatJSON.parsethe text content; tracked as a separate follow-up.gen:intents:checkbyte-identical (229 intents) confirms the new typed outputs slot into the existing Swift codegen without drift. .mcpbDesktop Extensions bundle for Claude Desktop one-click install —npm run build:mcpbproducesbuild/mcpb/airmcp-<version>.mcpb(5.25 MB, self-contained:server/dist/+ productionnode_modules/+ manifest + icon, all zipped per MCPB v0.3). Claude Desktop picks it up from "Browse extensions" → users fill a two-field form (Gemini API key optional + sensitive; "Load all 29 modules" toggle default off) and AirMCP is live. Nonpm, noclaude_desktop_config.jsonediting, no shell. CI runsbuild:mcpb:checkon every push; 16 new manifest-shape tests pin MCPB v0.3 contract (required fields,${__dirname}-anchored args,${user_config.KEY}substitution drift guard, Node-engine canary vspackage.json). Install guide lives atdocs/mcpb.md. Codesign + notarization + Swift-bridge pre-build land as follow-ups..well-known/mcp.jsontool + module inventory for registry crawlers — existing discovery card (v2.10:network_policy+allowed_origins) extended with the fields registry crawlers actually want.tools: { count, names }— the full tool inventory read fromtoolRegistryat request time so alistChangeddoesn't leave the card stale.modules: [...]— the live enabled-module list (reflects OS gates + user config, not justMODULE_NAMES).license,homepagefrompackage.json.schema_version: "2025-11-25"pins the MCP spec revision.buildServerCardextracted tosrc/server/well-known-card.tsas a pure function with 18 unit tests; the Express handler stays a 3-lineres.jsonwrapper. Anthropic MCP Registry / Smithery / PulseMCP / Glama crawlers can now surface "AirMCP: 270+ tools across calendar, notes, mail, …" without opening a session.- Codegen helper extraction to scripts/lib/codegen-helpers.mjs — 134 unit tests — every pure function in
scripts/gen-swift-intents.mjs(the 1200-LOC codegen that produces all 229 Swift AppIntent structs + 17 AppEnum types + 50 snippet views) now lives in a shared lib:toPascalCase,swiftIdent,swiftLit,humanizeKey,enumCaseName/enumCaseDisplayLabel/enumTypeName,intentStructName,intentActionNameFor,swiftTypeFor,swiftDefaultLiteral,enumDefaultLiteral,wireExpr,isNullableUnion/nonNullType,outputTypeNameFor/snippetViewNameFor,detectSnippetShape,systemImageFor+SYSTEM_IMAGE_BY_PREFIX,collectEnums,renderAppEnum,swiftParamDecl,buildArgsBlock,resolveFollowUpMap+deriveFollowUpFactorySpecs,isCodableSafe,swiftOutputType,renderStruct,hasTypedOutput.gen-swift-intents.mjsshrinks from 1219 → 752 LOC; the generator becomes thin orchestration (manifest load, CLI wrappers for throwing lib fns,generateIntent+ snippet-view render composing pure helpers, source assembly, write/check). Every refactor step verified byte-stable viagen:intents:checkagainst the 229-intent checked-in output. Catches helper-level regressions with a specific error message instead of a multi-thousand-line diff on the codegen golden. - Tool manifest structural contract check —
scripts/verify-tool-manifest.mjsfails CI on shape regressions (missingannotationsfields, new ineligibility reason codes, per-tool cross-check against histogram counts) — a silent schema regression upstream of the drift check now surfaces with a specific error. Intentionally independent fromscripts/lib/codegen-helpers.mjsso a codegen widening can't sneak through. - RFC 0007 Phase A completion — 229 auto-generated AppIntents — the bridge grows from A.1's 154 read-only intents to a full write-capable surface. Non-destructive writes (75 tools like
create_reminder,update_event,update_note) land directly; the 48 destructive tools (delete_*,send_*,trash_*,quit_app, etc.) stay off by default and requireAIRMCP_APPINTENTS_DESTRUCTIVE=trueat codegen time. When enabled, each destructive intent runsrequestConfirmation(actionName:dialog:)before the router call (iOS 18+/macOS 15+ gate on the struct so older OS simply doesn't see the destructive surface). Action verb derives from the tool name:.sendforsend_*/reply_*/post_*,.goelsewhere —ConfirmationActionNameonly exposes those two members as of iOS 26 (verified withswiftc -typecheck). Destructive semantic is carried by the dialog text, not the button label. - AppEnum codegen for 17 enum parameters — tool inputs with
z.enum([...])now render as SwiftAppEnumtypes instead of plainStringcarrying an "Allowed: a, b, c" description. Shortcuts / Siri users get a native dropdown picker with readable labels (nextTrack→ "Next Track"). 17 enums across 16 tools includingplayback_control.action,memory_*.kind,capture_screenshot.region,get_directions.transportType. Per-tool enum type names (<Tool><Param>Option) avoid cross-tool collision;nonisolated(unsafe) static varmirrors the pattern on intent static props. - Interactive Snippet tap-through follow-ups — 13 list tools wrap their row rendering in
Button(intent: …)so tapping an item dispatches a follow-up AppIntent without leaving the host surface. Coverage: events/notes/reminders/contacts (list + search variants) → corresponding read tool;list_messages→read_message;list_chats/search_chats→read_chat(list outputid→ target paramchatId, via the new itemField/targetParam split inFOLLOW_UP_MAP).ForEach(id: \.id)key replaces the old\.offsetso SwiftUI diffs correctly across re-enters. - Snippet views wired into
perform()— typed result + view — 50 typed AppIntents now return.result(value:, view: MCP…SnippetView(data: decoded))on iOS 26+/macOS 26+, rendering the codegen'd Interactive Snippet view inline. Older OS paths keep the plain.result(value:)and still run the JSON decode as a drift guard. Scalar snippet views (get_current_weather,get_battery_status,get_clipboard, etc. — 31 tools) now render with humanized labels (stepsToday→ "Steps Today"), type-aware value formatting (Yes/No booleans,.formatted()numbers, abbreviated dates from ISO strings), and?? "—"for nil optional fields..lineLimit(1)+.truncationMode(.tail)prevents long strings from blowing out the card. - Tool manifest describes why tools drop out —
isAppIntentEligiblereplaced withappIntentEligibilityreturning{ appIntentEligible, ineligibleReason }. Manifest top-level carriesineligibleCount+ineligibleByReasonhistograms; per-tool entries record their specific reason (record-inputforadditionalProperties: true,object-param:<key>/array-of-object:<key>for composite schemas). The dump script's one-line summary prints the histogram inline. - Golden-sample CI regression check —
scripts/verify-golden-intents.mjsfulfills the RFC 0007 §3.6 promise. Extracts everyrunAirMCPTool("<name>", …)call site fromapp/Sources/AirMCPApp/AppIntents.swiftand asserts each tool exists in the manifest (or is registered in src but hardware-gated likehealth_summary) AND has a matchingpublic struct <PascalName>Intentin the generated Swift. Tolerance-based, not byte-compare — codegen refinements don't break CI; the check's floor is "did codegen drop a tool the macOS app depends on?". - MCPIntentRouter unit tests — 5 XCTest cases covering happy-path roundtrip, empty-args zero-parameter tools,
handlerNotInstallederror when no handler is set, handler-throw propagation, and "lastsetHandlerwins" semantics. The router was previously zero-test — a Swift 6 concurrency regression or handler-wiring bug would only surface on a real device. - Description source fix for Shortcuts UI —
AIRMCP_COMPACT_TOOLS(default on,src/shared/tool-filter.ts) truncates tool descriptions to ~80 chars with…to save tokens intools/listfor LLM consumers. Right tradeoff for MCP clients, wrong tradeoff for Swift codegen whose output is rendered as user-facing text in Shortcuts / Siri / Spotlight — truncations like"… Foundation Model and repo…"leaked into the UI.scripts/dump-tool-manifest.mjsnow setsAIRMCP_COMPACT_TOOLS=falseso the manifest feeding codegen has full descriptions. - CI trigger on every PR —
pull_request: branches: [main]dropped so stacked PRs (PR → PR → main chains, common with multi-axis RFC work) get full CI feedback before the chain merges sequentially. Previously only the bottom PR of a stack reported checks. - outputSchema Wave 3 — 10 additional read tools now declare typed
outputSchemawith matchingstructuredContent:list_chats,read_chat,search_chats,list_participants(messages),health_today_steps,health_heart_rate,health_sleep(health),list_shortcuts,search_shortcuts,get_shortcut_detail(shortcuts). This raises the exact three-wave runtime-contract set from 17 to 27 tools. Messages tools keep theokUntrusted*wrappers so external-content markers still guard against prompt injection, now layered withstructuredContent. Health tools graduate fromokLinkedtookLinkedStructuredso the primary text block,_links, and the typed payload all round-trip cleanly.tests/output-schema-wave3.test.jsadds strict-parse drift guards for each new schema, andoutput-schema-structured.test.jspicks up the 10 fixtures so the exhaustive contract check stays green. - RFC 0005 draft — OAuth 2.1 + Resource Indicators — new RFC targeting v2.11.0 that lays out the migration from the legacy
AIRMCP_HTTP_TOKENBearer path to the MCP 2025-06-18 OAuth 2.1 + RFC 8707 Resource Indicators flow. Coverswith-oauth/with-oauth+originnetwork policies,.well-known/oauth-protected-resourcediscovery, scope design (mcp:read/mcp:write/mcp:destructive/mcp:admin), and a 5-step rollout that keeps the existing Bearer path working until v3.0.
safari.add_bookmarkgated on macOS 26+ — Safari removed bookmark scripting in macOS 26; the tool now skips registration on macOS 26+ instead of registering and returning an error at call time. Agents no longer see a "tool exists but always fails" entry in their plans on Tahoe hosts. Legacy hosts (macOS ≤ 25) keep the tool with the existing deprecation message. Test expectation updated from 12 → 11 registered Safari tools in the non-Darwin test env (add_bookmarkgated off, other 11 always present).- Prettier run across 9 drifted files —
src/safari/scripts.ts,src/shared/esc.ts, and the 7 skill built-in YAMLs (daily-journal,favorites-digest,focus-block-planner,project-digest,sender-to-tasks,weekly-digest-note,weekly-review). No behaviour change.
- Hono / @hono/node-server overrides —
package.jsonoverridesforceshono@^4.12.13+@hono/node-server@^1.19.13to address CVE-2026-29045, CVE-2026-39407, CVE-2026-29087, and CVE-2026-39406 (middleware bypass via repeated slashes + authorization bypass via encoded slashes inserveStatic). AirMCP does not use Hono directly — both are transitive via@modelcontextprotocol/sdk— butnpm audit --omit=devnow reports 0 findings instead of 2 moderate.
- Swift menubar onboarding — module coverage + v2.10 surface —
OnboardingViewmodule picker grew from 15 to 25 items, reorganised into four readable clusters (Everyday · Media · System + automation · Intelligence + introspection · Context sensors · Integrations) instead of a flat list. The v2.10 introspection modules — Context Memory and Audit — get dedicated entries so first-run users see them alongside Notes/Calendar instead of having to discover them in config later. Localisable.strings gainsmodule.memory.*andmodule.audit.*keys in both English and Korean. Swift build green; no Node-side changes. - Registry submissions tracker +
server.jsonrefresh — newdocs/REGISTRY_SUBMISSIONS.mdtracks status across Anthropic MCP Registry, Smithery, Glama, MCP Market, Cline Marketplace, and PulseMCP, with a resubmission checklist that requiresstats:syncgreen before any registry UI is touched.server.json(Anthropic schemastatic.modelcontextprotocol.io/schemas/2025-12-11) gets a v2.10-era description ("269 tools across 29 modules with YAML skills, context memory, queryable audit log, and declarative HTTP network policy") and joins thenpm run stats:syncfleet so its counts stay truthful automatically. - Browser-MCP guide (Claude in Chrome et al.) — README's Client Setup and the docs site's Configuration page now cover the HTTP-transport path that extension-based MCP clients need. Concrete steps: generate a token, set
AIRMCP_ALLOWED_ORIGINS=https://claude.ai, switch policy towith-token+origin, bind with--bind-all, paste URL + Bearer token into the extension. Includes a/.well-known/mcp.jsonverification curl and a short "security gotchas" list (proxy headers, Origin allow-list, emergency-stop path). TheAIRMCP_ALLOW_NETWORK+AIRMCP_ALLOWED_ORIGINSenv vars are documented alongside the existing HTTP settings. - Landing page: "Beyond Siri" section — five concrete cards on
docs/index.htmlpitching what AirMCP does that Siri / Shortcuts cannot: newsletter-inbox-to-tasks loop, screen-lock-triggered journal, fused timeline view, clipboard URL auto-save, and a queryable audit trail with one-command emergency stop. Nav gains a#beyond-sirianchor. All 14 new i18n keys added across all 9 locales (Korean authored, others copy the English source socheck-i18nstays green — translation polish tracked separately). - README refresh + demo tape update — Features block now reflects v2.10's actual footprint (270+ tools / 29 modules / 14 skill built-ins / 9 event triggers / 3 interactive apps). Two new top-level sections land above the existing client-setup docs: a Skills DSL walkthrough with a ready-to-lift
sender-to-tasksexample (parallel + loop + retry + inputs +on_error: continue) and a Safety & Operations section covering HITL, rate limit, kill switch, audit log, and HTTP network policy. The "Why AirMCP?" comparison table picks up rows for Automations, HTTP transport, and the expanded safety story, and now lists the correct active version.scripts/demo.tapegets a new story arc (--version→--help→doctor— highlighting the new Compatibility + HTTP policy sections — → banner) and a roomier 1000×680 canvas so the widened doctor output doesn't clip. Tools-section counts auto-sync viacount-stats --sync;MODULE_NAMESis no longer missing thememory/auditmodules. - Memory module polish — outputSchema +
memory://recentresource +daily-journalskill — the v2.8 context-memory module graduates from scaffold to "first-class consumable". All four tools (memory_put,memory_query,memory_forget,memory_stats) now declare typedoutputSchemawith matchingstructuredContent, using a sharedmemoryEntrySchemaso put/query stay in lockstep. New MCP resourcememory://recentreturns the 20 most recently updated entries for AI clients that prefer polling over the tool surface (gated on thememorymodule being enabled). New showcase built-indaily-journalcombinesinputs+parallelreads +retryonsummarize_text+memory_putto persist a day's activity as a taggedepisode— the reference pattern for "skills that remember". Built-in count: 13 → 14. - Skill prompt arguments — skills with
expose_as: promptnow support the sameinputsblock that tool-exposed skills got in the prior release. When declared, the skill registers via MCP'sregisterPromptwith anargsSchema(string-only per MCP spec), and the generated prompt text folds the bound values into anInputs:block so the LLM picks them up as already-resolved. Skills without inputs keep using the legacyserver.prompt()registration — no change to existing built-ins' wire format. 3 new unit tests cover the legacy-path fallback,registerPrompt+argsSchemasynthesis, and no-args callback behaviour. - RFC 0002 Phase 2 — proxy header detection +
doctorHTTP policy section — the HTTP transport now soft-warns once-per-process when aloopback-onlyserver seesX-Forwarded-For,X-Forwarded-Host,X-Real-IP, or a non-loopbackHostheader — the motivating threat of RFC 0002 was a reverse proxy silently exposing a loopback server without auth. Warning hits stderr + audit log (__proxy_signal_detected) so an operator has a timestamped trail.npx airmcp doctorgrows anHTTP network policysection that runs the sameresolveAllowNetworkthe server uses and surfaces the effective policy, token presence (not value), origin allow-list, plus explicit red flags forunauthenticatedmode or--bind-all+loopback-onlyconfig conflicts. 2 new tests (resolver export surface + full four-policy roundtrip). - MCP App:
timeline_today— third interactive UI view (aftercalendar_week_viewandmusic_player). Fuses today's calendar events and dueDate-carrying reminders onto a single 6am–10pm vertical axis, with an "Unscheduled" rail for items that don't fit a time slot (all-day events, reminders without a due time). First built-in App that merges two data sources in one UI. Gathers both sides viaPromise.allSettledso a slow/failing source (e.g. Reminders permission not granted) doesn't block rendering — the other rail still shows. Registered only whencalendarandremindersmodules are both enabled. - Skill DSL runtime
inputs— skills exposed as tools can now declare typed runtime arguments in YAML. Inputs reach the template scope as{{name}}identically to prior-step results; loader rejects skills where an input name collides with a step id so resolution is always unambiguous. Converts cleanly to an MCPinputSchema(string/number/boolean withdescription,default, andrequiredhonoured). Existing showcasesender-to-tasksnow accepts{ query, mailbox, limit }at call time instead of a hardcoded "newsletter" literal — one skill now covers newsletter triage, a specific sender, or any ad-hoc query. 8 new unit tests across loader (accept, step-id collision rejection, invalid identifier rejection), register (schema synthesis, arg forwarding, no-args fallback), executor (input seeding, input + step interaction, no-inputs equivalence). ai_plan_metricstool + expanded golden set — new intelligence tool that samples N cases fromGOLDEN_PLANS, runs each through the real on-device planner, and returns aggregate scores (parseRate, averageScore, expectedCoverageAvg, leakedForbiddenTotal) plus per-case breakdowns. Intended for users/maintainers to catch planner regressions after macOS / Apple Intelligence updates. Seedable case sampling (LCG Fisher–Yates) so before/after runs compare on identical slices.GOLDEN_PLANSexpanded from 24 → 31 cases: 4 newmustAvoidnegatives (read-only goals must not reach for destructive tools), 2 audit-introspection goals (audit_log/audit_summary), 1read_note-by-id case.DEFAULT_PLAN_TOOLSgrew from 12 → 29 so the tool pool covers mutation tools the negatives test against, plus the new audit + read tools. 5 new unit tests on the tool handler (bridge gate, aggregation, per-case failure tolerance, seed reproducibility).- Event bus:
screen_locked/screen_unlockedtriggers — two new SwiftDistributedNotificationCenterobservers (com.apple.screenIsLocked/com.apple.screenIsUnlocked) surface lid-close / lock-screen state changes through the same event pipeline as calendar/focus/pasteboard. No TCC or Automation permission needed — both notifications are public. Wired end-to-end:EventObserver.Eventenum,AirMcpBridgeserialization, NodeAirMCPEventTypeunion + validator set, Skillstrigger.eventenum,mcp-setup.tsresource-invalidation listeners, and themonitoringfield ofevent_subscribe. New showcase skillevening-winddown(drafts an on-lock "day in review" note viaparallelreads +retryonsummarize_text) demonstrates the trigger. Event-type total: 7 → 9. Built-in skills: 12 → 13. - Skill DSL
retry+retry_backoff_ms— per-step retry policy that runs BEFOREon_error. Up to1 + retryattempts with exponential backoff (base + ±25% jitter, capped at 60s).isErrorresponses are treated as failures so both thrown errors and declared-error tool responses get the same retry treatment. Applied per-iteration insideloopsteps so a single flaky item doesn't fail the whole batch. Two existing showcase skills now use it:weekly-digest-notewrapssummarize_text(Foundation Models occasionally times out) andsender-to-taskswrapssearch_messages(Mail.app sometimes returns empty results right after launch). 5 new unit tests.retry: 0(default) keeps existing behaviour unchanged. - Agent rate limit + emergency kill switch — two-tier token-bucket caps the damage a runaway agent or buggy plan can do without user intervention. Defaults: 60 tool calls/minute (global) and 10 destructive calls/hour, overridable via
AIRMCP_MAX_TOOL_CALLS_PER_MINUTE/AIRMCP_MAX_DESTRUCTIVE_PER_HOUR. Denied calls short-circuit before the handler runs and are logged to audit withstatus: error, so the deny trail stays queryable viaaudit_log. Emergency stop: creating~/.config/airmcp/emergency-stopblocks every destructive tool immediately (1-second probe cache, no restart needed) — a one-command panic button for live incidents. Bucket atomicity is preserved: a destructive-bucket denial does not consume a global-bucket token, so retries don't erode unrelated budgets. Newsrc/shared/rate-limit.ts, integrated intotool-registrypre-handler gate; 7 new unit tests covering capacity, atomicity, kill-switch semantics. audit_log/audit_summarytools — consumption path for the on-device audit log.audit_logreturns paginated JSONL entries filterable by tool name / status / time window (walks current + rotated siblings; tolerates malformed lines).audit_summaryaggregates call count, error rate, and the busiest tools over a configurable window — useful for weekly reviews and for spotting runaway agents. Args remain PII-scrubbed at write time; both tools are read-only. Newsrc/audit/module, 7 new end-to-end tests undertests/audit-tools.test.jscovering time/tool/status filters, rotated-file walk, and malformed-line tolerance. Module count 27 → 28.- RFC 0002 Phase 1 — declarative HTTP
allowNetworkpolicy — network exposure is now expressed as a policy (loopback-only/with-token/with-token+origin/unauthenticated) instead of implied by CLI flags. Startup invariant check inhttp-transport.tsrefuses to boot a misconfigured server (e.g.--bind-allwithout a token, orwith-token+originwithout an allow-list) so a reverse-proxy footgun can't turn a loopback server public. New--unsafe-no-authflag opts into theunauthenticatedmode explicitly and flags.well-known/mcp.jsonwithsecurity: insecure.AIRMCP_ALLOW_NETWORKenv overrides the inferred policy..well-known/mcp.jsonnow exposesnetwork_policy+allowed_originsso Managed Agents / discovery clients can reason about exposure before connecting. 12 new unit tests onresolveAllowNetwork/validateNetworkPolicy. - Skill DSL showcase built-ins (5) — five new built-in skills that exercise the features added in v2.9.0 (
on_error,loop,parallel, event triggers):weekly-digest-note(parallel fan-out + summarise + compose note),focus-block-planner(loop reminders → calendar blocks with per-iterationon_error: continue),clipboard-url-to-reading(pasteboard_changedtrigger + defensive URL add),favorites-digest(photo metadata loop + terminal note),sender-to-tasks(mail search → reminder loop with error tolerance). Each skill is under 30 lines and README-demo-ready. Brings the built-in skill count from 7 to 12 and gives external users a concrete reference for every DSL feature. - outputSchema Wave 2 — 14 additional read tools now declare typed
outputSchemawith matchingstructuredContent:read_note,list_folders,list_reminder_lists,read_reminder,list_calendars,read_event,list_messages,list_accounts,list_groups,list_bookmarks,list_reading_list,list_directory,list_playlists,list_tracks. Together with Wave 1's 3 tools, this establishes 17 exact runtime contracts and makes these tools directly chainable from the skill executor without re-parsing the text block.tests/output-schema-wave2.test.jsadds strict-parse drift guards for each new schema andoutput-schema-structured.test.jsenforces fixture presence so any future tool that declaresoutputSchemawithout a matching fixture breaks the build. - Dev MCP scripts —
npm run dev:mcp,dev:mcp:watch,dev:connectfor running the local checkout through the samesrc/index.tsentrypoint used by the CLI without hand-editing config paths. - release-please workflow (PR-only mode) — auto-generates
chore(release): v<next>PRs bumpingpackage.json+CHANGELOG.mdfrom Conventional Commits, preventing TODO ↔ CHANGELOG drift. Tag creation stays in the existing auto-release + cd.yml chain. (.github/workflows/release-please.yml,release-please-config.json,.release-please-manifest.json). - RFC 0001 foundation —
src/shared/error-categories.tswithERROR_CATEGORIESenum +ToolErrorPayload+toolErr()/errNotFound()/errJxa()/errDeprecated()helpers inresult.ts. Backward compatible with legacyerr()/toolError(). (21 new unit tests.) - RFC 0004 foundation —
src/shared/compatibility.tswithresolveModuleCompatibility()+summarizeCompatibility();ModuleRegistration.compatibilityfield threaded throughMANIFEST. Annotatedintelligence(beta + apple-silicon) andhealth(apple-silicon + healthkit). Runtime behaviour unchanged; data is informational only for now. (21 new unit tests.) - RFC 0001 Wave 0 — toolError delegation — the legacy
toolError(action, e)helper now wrapstoolErr()and classifiesnot_found/permission_denied/upstream_timeout/rate_limited/internal_errorfrom the thrown message. Every existing tool that catches withtoolError()automatically gainsstructuredContent.error; text wire format is unchanged. - RFC 0001 Wave 1 — notes migration — share-guard blocks in
src/notes/tools.tsnow emit typedpermission_deniederrors viaerrPermission()with a[permission_denied]text prefix +structuredContent.error. - RFC 0004 runtime activation —
mcp-setup.tsroutes every module throughresolveModuleCompatibility()instead of checkingminMacosVersionalone; banner now surfacesdeprecated:andbroken:module groups alongsideunavailable:.airmcp doctorgains a[Compatibility]section that shows the resolver's decision per module for the current host. getCompatibilityEnv()factory —src/shared/config.tsexposes a plainCompatibilityEnvsnapshot (osVersion,cpu,healthkitAvailable) for the resolver.print-compat-reportscript —npm run compat:report(text or--json) renders the manifest × host decisions.MODULE_MANIFESTis now exported so doctor / scripts can read compat metadata without loading modules.- RFC 0003 Phase 1 — moderate audit advisory —
scripts/summarize-audit.mjssummarises moderate+npm auditfindings; CI runs it as a non-fatal step right after the hardnpm audit --audit-level=highgate.SECURITY.mdgets a new Dependency Advisory SLAs table. - outputSchema Wave 1 drift guards —
tests/output-schema-wave1.test.jsrunslist_notes,list_reminders,list_eventsagainst mocked runtimes and validates the returnedstructuredContentagainst each tool's ownoutputSchemaviaz.object().strict().safeParse(). - Release checklist & RFC process docs —
docs/RELEASE_CHECKLIST.md,docs/rfc/README.md, RFCs 0001/0002/0003/0004 (Draft). - Quality diagnosis report —
QUALITY_DIAGNOSIS_2026-04-17.mdwith maturity snapshot, risk matrix, KPI proposals.
TODO.mdre-synced to the v2.7.3 baseline — checked off work completed in v2.7.0–v2.7.3, refreshed coverage numbers (46.9%, gate 46%).BannerInfogains optionalmodulesDeprecated/modulesBrokenfields. Existing fields are unchanged.
--version/-vflag — prints version and exits- Unknown command rejection —
npx airmcp typonow exits with error instead of silently starting stdio server NO_COLORsupport — respectsNO_COLORenv var across banner, help, doctor, init (no-color.org)- TTY guard for
init— exits with helpful message in non-interactive environments (CI, Docker, pipes) - First-time user hint — banner shows
"First time? Run: npx airmcp init"when no config exists - Config validation warnings — unknown module names, invalid HITL levels, wrong boolean types now logged
- Doctor version comparison — local version ahead of npm no longer falsely shown as "update available"
- Config parse errors — actual JSON error now shown (was silently falling back to defaults)
- Init config write — caught with try/catch, corrupt JSON warned before overwrite
- Config double file-read — eliminated redundant
readFileSync+JSON.parseon startup path
- Node.js not found — shows error state with install instructions (was: silent failure)
- Server crash detection —
terminationHandlerwith auto-restart (max 3 within 5 minutes) - Graceful shutdown — polls for server exit up to 5 seconds (was: hardcoded 0.5s)
- HITL notification denied — logs warning when permission denied (was: silently ignored)
- Widget "All day" — now localized via
NSLocalizedString
- Block
javascript:anddata:URL schemes inrun_javascript— prevents XSS via crafted tab URLs. Extends the existingfile:/about:/blob:blocklist added in v2.7.1. escJxaShell()control character stripping — now strips\x01-\x1f(except\t,\n,\r) matchingesc()andescAS(). Previously, control characters passed through to shell arguments inside JXA strings.- Extract shared
RE_CTRLregex constant — the control-character regex was duplicated acrossesc(),escAS(), andescJxaShell(); now defined once.
resetTriggers()now resetslistenerInstalledflag — previously, callingeventBus.stop()followed by a restart would permanently disable skill trigger dispatch because the singleton guard was never cleared.cross/tools.tsJSON.parse fallback — wrapped in try/catch so a malformed snapshot doesn't crash the daily briefing tool; falls back to raw text.
- safety-annotations.test.js — validates all 262 tools have correct
readOnlyHint/destructiveHint/idempotentHint/openWorldHintannotations - executor.ts — 54% → 99% (conditionals, loops, parallel steps, template resolution, error paths)
- hitl-guard.ts — → 100% (elicitation, managed clients, telemetry, env vars)
- hitl.ts — → 100% (socket errors, timeouts, buffer overflow, chunked responses, reconnection)
- swift.ts — 10% → 95% (NDJSON parsing, prototype pollution defense, single-shot fallback)
- skills engine — 0% → 99% (loader, register, triggers, index)
- tool-registry.ts — 3% → 80% (SDK integration, search, callTool)
- event-bus, esc, safari-scripts, server-init, http-transport edge cases expanded
- Coverage thresholds raised to statements 46% / branches 40% / functions 42% / lines 46%
- Silent error swallowing in usage tracker —
loadFromDisk/flush/flushSync/timer all surfaced viaconsole.errorinstead of emptycatch. ENOENT on first run is still silenced; everything else (corrupt JSON, ENOSPC, EACCES) now reaches stderr so disk-full / permission issues no longer hide for weeks. - Audit flush timer fire-and-forget — Added top-level
.catchlogging to cover unforeseen rejections outside the inner retry path (e.g. ESM dynamic import failure during the swap window).
- Hardcoded constants centralized in
shared/constants.ts:API.OLLAMA(was inline default inlocal-llm.ts) — env overrideAIRMCP_OLLAMA_URLEXT_APPS.CDN_URL(was hardcodedesm.shURL in two places inapps/tools.ts) — derivedEXT_APPS_ORIGINkeeps the CSPresourceDomainslist in sync with the import URL automaticallyBUFFER.SWIFT_LINE_MAX(was magic1_048_576literal inswift.ts)PATHS.TEMP_DIR(was hardcoded/tmp/inscreen/scripts.tsandshortcuts/scripts.ts) — usesos.tmpdir()by default, env overrideAIRMCP_TEMP_DIR. Sandboxed runtimes can now redirect intermediate captures.AUDIT.MAX_ARG_LENGTH/MAX_ENTRY_SIZE/MAX_FILE_SIZE/MAX_FLUSH_FAILURES/FLUSH_INTERVAL(was module-local inaudit.ts)
- Test-only helpers refuse to run in production —
audit._testReset()andtoolRegistry.reset()now throw unlessNODE_ENV=testorAIRMCP_TEST_MODE=1is set. Without this guard, any caller importing the production module could wipe in-memory audit entries before flush, or clear every registered MCP tool/prompt at runtime. Verified at runtime in addition to unit tests.
- Restored CHANGELOG entries for v2.7.0 and the four follow-up fixes (#49–#52) that landed on main without being recorded.
- Claude Managed Agents compatibility — prefix-match
"claude"covers all Anthropic clients (Desktop, Code, Cowork, Managed Agents) — no more exact-match maintenance AIRMCP_MANAGED_CLIENTSenv var for third-party managed clients in enterprise deployments- Server card —
.well-known/mcp.jsonexposesauthorization: { type: "bearer" }when token is configured, enabling Managed Agents auto-discovery - OpenTelemetry instrumentation (optional) —
@opentelemetry/apipeer dependency, zero overhead when not installed- Tool execution spans:
tool.{name}withmcp.tool.name,mcp.tool.arg_count - HITL approval spans:
tool.approvalwithmcp.approval.{decision,channel,destructive,managed_client}— correlates with Enterprise Compliance API for SIEM platforms (Splunk, Cribl) - Enable with
AIRMCP_TELEMETRY=trueorconfig.json → features.telemetry: true
- Tool execution spans:
- +16 new tests (874 total): managed client prefix match, telemetry no-op path, approval spans, config telemetry flag
- Skip elicitation for Claude Desktop/Cowork (
claude-aiclient) — fixes silent timeout causing tool denial in agentic contexts (issue #28) - iWork PDF export path guarding (#52) — mark destructive, validate output path against guard list
- Audit redaction for location/health tools (#51) — fully redact args for
get_current_location,get_location_permission,health_*instead of relying on key-name patterns cross/prompts.tsuser input quoting (#50) — wrap user inputs inq()consistently to prevent prompt-injection drift- Comprehensive security audit across 35 modules (#49) — input validation, command injection guards, path traversal protection
- outputSchema/structuredContent consistency —
okLinkedStructurednow emits_linksas a separate content block so primary JSON andstructuredContentboth conform to the declaredoutputSchema(#28 follow-up) - JXA batch array safety — Calendar list/search/upcoming/today scripts guard against sparse arrays from batch property access (
Math.min+ null checks) - Notes createFolder implicit return — Rewrote if-else to use explicit variable, eliminating JXA implicit-return ambiguity
- Mail message ID validation — Added
regex(/^\d+$/)to all message ID inputs, preventingNumber(id) = NaNon non-numeric strings - Mail listMessages array guard — Batch property access now uses
Math.minsafety bound like calendar - Finder stat parsing —
trim().split(/\s+/)withisNaNfallbacks for robustness across macOS versions - Error classification —
toolErrorauto-classifies "not found" errors as[not_found]; newerrInvalidParamsanderrNotFoundhelpers for explicit error typing
- outputSchema test hardening — Tests now validate
structuredContentAND primary text JSON against ZodoutputSchemavia.safeParse()(52 new assertions); fixed weather fixture missingweatherDescriptionandunitsfields - Module count synced to 27 across all docs, locales (9 languages), landing page, and legal documents
- TypeScript upgraded to 6.0.2; GitHub Actions dependencies updated (codeql-action 4.35.1, deploy-pages 5)
- Dev test mode —
scripts/dev-test.mjslightweight in-process developer testing (npm run dev:test); MockMcpServer harness calls tool handlers directly without MCP SDK, stdio transport, or child processes — 3x faster and 10x less memory than debug-pipeline - Git-aware testing —
npm run dev:test:changeddetects modified modules viagit diffand tests only those;src/shared/changes trigger full test - Watch mode —
npm run dev:test:watchrebuilds and re-tests on file changes with ESM cache-busting - Single-tool testing —
npm run dev:test -- --tool list_notesfinds and tests a single tool across all modules with reverse index lookup - Memory reporting — dev-test reports per-module heap delta and total memory usage
- Updated CONTRIBUTING.md, docs/testing.md with dev-test workflow documentation
- Module count corrected to 27 in README.md and server.json (was 25)
- Regenerated llms.txt / llms-full.txt
- Debug pipeline —
scripts/debug-pipeline.mjsfor module-isolated debugging (npm run debug -- --module notes); prevents 262-tool simultaneous load from exhausting memory - Debug env vars —
AIRMCP_DEBUG_MODULES(whitelist) andAIRMCP_DEBUG_SEQUENTIAL(sequential loading) for targeted module debugging - Embedding cache memory cap — 256MB default limit with
AIRMCP_EMBED_CACHE_MAX_MBoverride; fast-path size estimation for numeric arrays - Audit flush interval config —
AIRMCP_AUDIT_FLUSH_INTERVALenv var (default raised from 5s to 30s) - ESLint layer boundaries —
no-restricted-importsrules enforce Core → Bridge → Services dependency direction insrc/shared/ - SDK signature validation —
tool-registry.tsvalidates callback position at runtime; logs warning and falls back gracefully on SDK mismatch - 57 new tests — SDK integration tests for tool-registry (12), config parsing (7), audit logging (30), module loading (8); total 773→830
- Module list sync —
config.tsMODULE_NAMESwas missingspeechandhealthmodules (disable/enable config had no effect on them) - Idle battery drain — audit logger and usage tracker converted from
setIntervalto event-drivensetTimeout; zero CPU wake-ups when no tools are active - Cache eviction efficiency —
evictIfNeeded()now re-checks limits after pruning expired entries, avoiding unnecessary key-snapshot allocation
- MCP SDK pinned to exact version
1.29.0(was^1.29.0) to prevent silent monkey-patch breakage @modelcontextprotocol/ext-appspinned to exact1.3.1tool-registry.tsreclassified from Bridge (Layer 2) to Services (Layer 3) to reflect actual dependencies- Validation blocks in tool-registry deduplicated via
validateCallback()helper console.warnstandardized toconsole.errorin tool-registry (MCP servers use stderr for logging)- Audit test helpers consolidated:
_testDrainBuffer+_testResetState→ single_testReset - Regenerated
llms-full.txt
- Swift bridge single-shot — replace regex-based prototype pollution check with reviver pattern (matches persistent mode)
- iWork JXA injection — add
assertValidAppName()whitelist to all iWork script generators - SSRF prevention —
open_urlnow blocksfile://,javascript://, localhost, and internal network addresses - Prompt injection defense — add
okUntrusted()to 16 additional tools returning user/external content (GWS Gmail/Drive/Calendar/Tasks, Finder, UI, Maps, Intelligence) - Prompt input sanitization — wrap user inputs in
<user_input>delimiters in cross-module prompts - Skill shadowing prevention — built-in skill names are now protected; user skills with conflicting names are skipped
- Drive query injection — strengthen sanitization by removing all punctuation from search queries
- HITL socket DoS — add 1MB buffer size limit on HITL socket data
- Symlink traversal — add
resolveAndGuard()tomove_fileandtrash_fileoperations - Config validation — add runtime type checking for
config.jsonparsing (reject malformed configs safely) - API credential masking — improve error message redaction for Gemini API key patterns
- Ollama URL validation — use proper URL parsing to prevent localhost check bypass
- Event bus type safety — add proper type guards for parsed event data
- Rate limiter memory — add 10K max bucket limit with LRU eviction to prevent unbounded growth from IP rotation
- Screenshot cleanup — delete temp file before throwing on oversized captures (prevents orphaned files)
- Screen recording timer — fix potential timer leak when recording promise rejects early
- Caffeinate tracking — use
Set<number>to track all PIDs instead of single variable - Cache eviction — use key snapshot to prevent iterator invalidation during eviction
- Skills executor DoS — add
MAX_LOOP_ITERATIONS(1000) and 1MB tool response size limit - YAML skill loading — add 256KB file size limit
- Escaping tests — fix 3 pre-existing
escJxaShelltest expectations to match correct double-escaping behavior
- HTTP health endpoint no longer exposes
uptimefield (information leakage prevention) - HTTP transport adds
X-Request-IDheader for request tracing - Audit logging added to
run_javascript,send_mail,reply_mailoperations - Skills trigger failures now retry once after 2s delay
- Jest coverage thresholds raised: statements 30→35%, branches 20→25%, functions 25→30%, lines 30→35%
- CI: Swift build artifacts cached, checkout optimized to
fetch-depth: 1 esbuildadded as explicit devDependencygws_rawparams/body size-limited (10KB/100KB)
gws_rawhardening — service whitelist (11 allowed), destructive method blocking (delete/trash/remove/purge require opt-in)- JXA injection full audit — all 20 JXA-using modules verified safe (esc/escAS/escJxaShell applied)
- Structured Tool Output ×17 —
outputSchemaadded to contacts (4), system (6), mail (2), safari (2), music (1), finder (1) tools (total 12→29) - 73 new tests — esc.ts (57), automation.ts (5), gws_raw security (6), jxa (1), server-init (2), http-transport (1), modules (1)
- Test coverage 21.6% → 36.1% (exceeds 30% threshold)
Promise<any>replaced with typed responses in weather/api.ts and maps/api.tsgws_rawservice description derived fromGWS_ALLOWED_SERVICESconstant (prevents drift)- Contacts
zContactSummaryZod schema extracted to shared constant (DRY)
- Path traversal defense —
assertSafePath()blocks..in all Finder script functions - Embedding cache keys hashed with SHA-256 to prevent PII exposure
- Rate limit bucket cleanup shortened (5min→1min) to mitigate IP rotation
- Audit flush race condition fixed with flushing lock
safeInt()strengthened withNumber.isSafeInteger(blocks extreme values)- Health endpoint no longer exposes session count
- Gemini API error messages now redact API key fragments
escAS()now escapes\u2028/\u2029line separators (parity withesc())
envInt()returns fallback on NaN parse (was returning NaN)- Inflight promise memory leak — safety timeout cleans up entries that never settle
TtlCache.clear()now also resets inflight promise map- Session cleanup timer properly
.unref()'d to prevent process hang
- Messages send scripts deduplicated via shared
buildSendScript()helper - Embedding cache key construction extracted to
embedCacheKey()helper compactDescription()recognizes!and?as sentence terminators- Resource cache TTLs tuned for
event_subscribeinvalidation - Rate bucket prune interval uses
RATE_WINDOW_MSconstant - Node.js minimum bumped from 18 to 20
- Dependencies: MCP SDK 1.27→1.28, ext-apps 1.2→1.3.1
- Swift 6.2 upgrade — all 3 packages (AirMCPKit, AirMCPServer, AirMCPApp) bumped to swift-tools-version 6.2
- 42 unit tests — XCTest suites for AirMCPKit (Types, ISO8601, EventKit recurrence, errors) and AirMCPServer (JSON-RPC parsing, AnyCodable, MCPServer dispatch)
- CI Swift pipeline —
swift build+swift testfor both swift/ and ios/ packages in GitHub Actions - Shared authorization helper — extracted
authorize(store:flag:request:errorMessage:)in EventKitService, eliminating copy-paste between event/reminder auth
- Concurrency safety —
nonisolated(unsafe) varauthorization flags replaced withOSAllocatedUnfairLockfor proper thread-safe access - ISO 8601 formatters — migrated from
nonisolated(unsafe)ISO8601DateFormatterglobals to cachedDate.ISO8601FormatStyle(Sendable value type, no per-call allocation) - ServicesProvider —
@unchecked Sendablereplaced with@MainActorisolation - FoundationModels guard —
#if canImport(FoundationModels) && compiler(>=6.3)prevents build failures on toolchains lacking the FoundationModelsMacros plugin - iOS minimum version — unified from iOS 16 to iOS 17, removing legacy
#availablebranches for EventKit authorization persistentMode— changed fromnonisolated(unsafe) vartolet(computed once from CLI args)- Safety rationale comments added to all
@unchecked Sendabletypes (LocationFetcher, BluetoothManager, AnyCodable, ToolBox)
- SpeechService
sendingerror (Swift 6.2 stricter data race checking) health-heart-ratenil output — replaced[String: Any?]Encodable error with properHeartRateOutputstruct- HealthService
var readTypes→let(unused mutation warning)
- Deduplicated
runAppleScript/runJxaInner— extracted sharedhandleOsascriptErrorandparseOsascriptOutputhelpers - JXA semaphore now lazy-initialized (created on first use after config parse, not at import time)
- CONCURRENCY lazy getters use
??=(read env once, not on every access) - Stale
applescript:prefix comment removed from messages/scripts - Simplified unnecessary cast in
evaluateCondition
- Stats synced across all docs, locales (9 languages), landing page, server.json, llms.txt: 253 tools, 32 prompts, 25 modules
- Privacy policy version updated to v2.3.0, bug report placeholder to 2.3.0
- Hero h1 restyled: Air (light) + MCP (bold)
- Removed TODO.md from tracking and git history (contained internal roadmap and security notes)
- Added
coverage/andqa-sequential-report-*.mdto .gitignore
- Sequential QA test runner (
npm run qa:seq) — tests each module in isolation, one at a time, to avoid overloading the machine - Expanded QA coverage: 207/247 tools (84%) across sequential + CRUD tests
- QA coverage TODO tracking for remaining 40 tools with documented exclusion reasons
- Upgraded
zodfrom~3.24.0to~3.25.76— fixes server startup crash caused by@modelcontextprotocol/sdk@1.27.1andext-apps@1.2.2requiringzod ^3.25 || ^4.0 - JXA→Swift dual-path architecture (
runAutomation) for reminders, photos, contacts, calendar — Swift preferred when available, JXA fallback preserved index.tssplit intoserver/init.ts,server/mcp-setup.ts,server/http-transport.ts- Build system switched to esbuild (resolves tsc OOM crash)
- Module registration via dynamic
MANIFESTinshared/modules.ts(no more manual imports) - CONTRIBUTING.md updated with sequential test instructions and current module addition guide
- Race conditions, hangs, and double-resume crashes in server lifecycle
- TypeScript typecheck OOM resolved with lightweight
McpServerinterface - Prompt injection defenses hardened
- Security fixes for input validation and escaping
generate_image— on-device image generation via Apple ImageCreator API (macOS 26+)scan_document— OCR text extraction via Apple Vision frameworkgenerate_plan— on-device AI planner using Foundation Models tool callingspotlight_sync/spotlight_clear— push/clear data in macOS Spotlight for Siri discoverysemantic_clear— delete all vector store data (GDPR/privacy), also clears Spotlightquery_photos— PhotoKit queries with date/type/favorites filtersclassify_image— Vision-based image classification with confidence labelsai_planrenamed togenerate_plan(verb_noun convention)- App Intents for companion app (SearchNotes, DailyBriefing, CheckCalendar, CreateReminder)
- MCP Sampling 3-tier fallback: Sampling → Foundation Models → raw snapshot
llms.txt/llms-full.txtfor AI discovery- OpenSSF Scorecard, CodeQL, dependabot, stale bot workflows
count-stats.mjs— auto-count tools/prompts/resources from source (CI-verified)check-i18n.mjs— verify locale key sync (CI-verified)- commitlint + husky for conventional commit enforcement
- GitHub Discussions, 6 good-first-issue tickets, GOVERNANCE.md, CODEOWNERS
- README badges (CI, npm, license, downloads, node)
- Centralized all hardcoded constants into
constants.ts - All subprocess runners (JXA, Swift, GWS) now use shared
Semaphoreclass - Module imports parallelized via
Promise.all(faster startup) - Module registration isolated with try-catch (one broken module doesn't crash server)
- HTTP session cleanup now closes McpServer instances (fixes memory leak)
- Privacy policy rewritten with full data flow disclosure (FM, Spotlight, Siri, Gemini)
- Podcasts module rewritten from JXA (never worked) to SQLite + URL scheme
- CI workflows pinned to SHA (OpenSSF Scorecard compliance)
- CI permissions locked to
contents: readminimum
AIRMCP_FULL=truenow properly overrides config file'sdisabledModules- JXA error codes mapped to human-readable messages (-1743, -1728, -600, etc.)
zFilePathresolves~/to$HOME(JXA/AppleScript don't expand tilde)- Path traversal regex tightened (no longer rejects
file..name.txt) - Swift bridge
pngData()nil guard (prevents false success) - Swift semaphore double-release prevented via
releasedflag - Weather API fetch now has timeout (
AbortSignal.timeout) - GWS CLI errors now include timeout/failure-specific messages
- Messages Tahoe compatibility (service type fallback for macOS 26)
allowSendMail/allowSendMessagesdefault changedtrue→false. Users must explicitly enable sending via config or env var.update_reminderparameternamerenamed totitleto matchcreate_reminder.add_bookmarkdeprecated — Safari removed bookmark scripting in macOS 26. Returns error with guidance to useadd_to_reading_list.gws_rawnow hasdestructiveHint: trueand blocks Gmail send/delete/trash whenallowSendMailis false.run_shortcutand dynamic shortcut tools now havedestructiveHint: true(Shortcuts can execute shell commands).- Init wizard now sets
allowSendMail: false(wastrue).
execSync→execFileSynceverywhere (prevents shell injection)- Gemini API key moved from URL query to
x-goog-api-keyheader - Path validation (
zFilePath) applied to all 15+ file path parameters gws_gmail_sendgated byallowSendMail- Startup fails fast if
HOMEenv var is not set
--helpcommand with usage guide- Polished CLI UX with spinner animations and shared styles
npx airmcp doctordiagnostic overhaul
- 244 MCP tools across 25 modules
- Full Apple ecosystem integration
- Semantic search with Gemini embeddings + on-device Swift embeddings
- Human-in-the-loop (HITL) approval system with SwiftUI companion app
- Interactive setup wizard (
npx airmcp init) - Skill engine with YAML-based workflows
- Cross-module prompts (30 prompts)
- MCP resources (11 resources)
- HTTP/SSE transport mode
- Internationalization (9 languages)