Skip to content

macos mcp server changelog

io.github.cyanheads/macos-mcp-server

6 changes recorded. MCP Harbor re-reads this listing from the official registry every six hours and asks the live server what it exposes, and keeps what changed between one look and the next.

  • AGENTS.md +21 −11

    https://raw.githubusercontent.com/cyanheads/macos-mcp-server/HEAD/AGENTS.md

    − **Version:** 0.3.0− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`− **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1−   annotations: { readOnlyHint: false, openWorldHint: false },− `instructions` is optional server-level orientation. Use it for deployment guidance — here, the permission prerequisites and the `macos_check_permissions`-first workflow — instead of repeating the same context across tool descriptions. Client adoption is uneven, but there's no downside when set.−       permission-probes.ts              # prompt-free Accessibility, Screen Recording, Automation > Finder checks−         macos-*.tool.ts                 # 13 tool definitions (one per file)− Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant f− **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.− | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |− `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was + **Version:** 0.4.0+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0+   annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },+ `instructions` is optional server-level orientation. Use it for deployment guidance — here, the permission prerequisites and what `macos_check_permissions` can and cannot see — instead of repeating the same context across tool descriptions. Client adoption is uneven, but there's no downside when set.+       permission-probes.ts              # prompt-free Accessibility, Screen Recording, Automation > Finder and Automation > Music checks+         macos-*.tool.ts                 # 14 tool definitions (one per file)+ Two skill trees live at the project root, written for different readers:+ | Tree | For | Written by |+ |:-----|:----|:-----------|+ | `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |+ | `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent by default; usually none is needed |+ Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.+ A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.+ **Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.+ | `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `.mcpbignore` coverage, plugin manifests, the published `skills/` tree (run by devcheck) |+ `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by+ - [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name (lowercase `a-z`, `0-9`, `-`, `.` only); `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.code+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass+ - [ ] If the server publishes a skill in root `skills/`: `lint:packaging` passes, and `field-test` verified every gate and identifier claim in it against live tool behavior

  • server.json 2 fields changed
    description
    Control macOS system settings, apps, windows, audio, displays, screenshots, and Focus mode via MCP. Control macOS settings, apps, windows, audio, Music, displays, screenshots, and Focus mode via MCP.
    version
    0.3.0 0.4.0

  • AGENTS.md +29 −16

    https://raw.githubusercontent.com/cyanheads/macos-mcp-server/HEAD/AGENTS.md

    − **Version:** 0.2.1− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.14`− **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0− **Zod:** ^4.6.5− **Subprocess failures.** Never let an `execFile` rejection reach the caller as-is — its message opens with `Command failed: <cmd> <args>`. Map the cases a caller can act on to a declared reason, and wrap the rest with `execFailure('<program>', err)` (`src/utils/exec-failure.ts`), which keeps the program's own error text and nothing else. `OsascriptService` does this for every script: a permission denial (recognized by its stderr text or `-25211`; a bare `-1719` "Invalid index" or `-25212` "no va− import { getSystemInfoService } from '@/services/system-info/system-info-service.js';−   description: 'Current macOS system snapshot: battery, power source, Wi-Fi SSID, hostname, version, uptime, display count.',−   output: z.object({−     hostname: z.string().describe('Machine hostname.'),−     macos_version: z.string().describe('macOS version string.'),−     uptime_seconds: z.number().describe('Seconds since last boot.'),−     display_count: z.number().describe('Number of connected displays.'),−   }),−       display-service.ts                # displayplacer list and apply-layout−       screencapture-service.ts          # screencapture + sips PNG/JPEG capture−       system-info-service.ts            # battery, Wi-Fi, hostname, uptime via system_profiler/pmset+ **Version:** 0.3.0+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1+ **Zod:** ^4.6.7+ **Subprocess failures.** Run a macOS system binary by its `SYSTEM_BINARIES` path (`src/services/binaries/binary-paths.ts`), never by bare name; add a new binary to that table. Resolve displayplacer or SwitchAudioSource with `findThirdPartyBinary()` on every call and throw the tool's not-found reason when it returns undefined. Never let an `execFile` rejection reach the caller as-is — its message opens with `Command failed: <cmd> <args>`. Map the cases a caller can act on to a declared reason, an+ **Cancellation.** Pass `signal: ctx.signal` to every `execFile` (and fs read) beside its own timeout. The first line of any catch that maps, degrades, or falls back is `if (ctx.signal.aborted) throw err;` (or `ctx.signal.throwIfAborted()` in an unbound catch), so the caller's cancellation is rethrown untouched and settles as `RequestCancelled`. Call `ctx.signal.throwIfAborted()` right before a spawn that can change state: Bun can still run a child whose signal already fired.+ **Process enumerations and NSScreen reads.** An NSScreen read starting or exiting mid-enumeration breaks a System Events process enumeration, so the two never overlap within the server. Pass `exclusion: 'process-enumeration'` to `runJxa`/`runAppleScript` for every script that enumerates System Events processes (`processes.whose(...)`, `every process whose …`, a bulk read such as `processes.unixId()`), and `exclusion: 'nsscreen-read'` for every osascript that reads NSScreen; an NSScreen read that+ **Text in `content[]`.** Render upstream or caller text in `format()` through `markdownLiteral()` (`src/utils/markdown-literal.ts`) so a CommonMark parse reads it back exactly, and quote a name inside an error message with `quotedText()` (`src/utils/quoted-text.ts`).+ **AppleScript literals.** A caller string embedded in an AppleScript (non-JXA) script goes through `appleScriptLiteral(value, argName)` (`src/services/osascript/applescript-literal.ts`), never `JSON.stringify`, whose `\u`, `\b`, and `\f` escapes AppleScript can't compile. It escapes `\` and `"`, writes every other character raw, and throws `-32602` with reason `invalid_arguments` naming `argName` for U+0000 or an unpaired surrogate; build the literal before any other lookup so that rejection com+ import {+   getSystemInfoService,+   SystemInfoSchema,+ } from '@/services/system-info/system-info-service.js';+   description: 'Current macOS system snapshot: battery, power source, Wi-Fi association and network name, hostname, version, uptime, display count. A field whose probe fails is null and named in `unavailable`.',+   output: SystemInfoSchema,+     binaries/+       binary-paths.ts                   # SYSTEM_BINARIES absolute paths; findThirdPartyBinary() PATH + Homebrew lookup+       display-service.ts                # displayplacer list and apply-layout, JXA NSScreen read for each display's index+       applescript-literal.ts            # appleScriptLiteral() — AppleScript string literal for caller text+       foreground-processes.ts           # FOREGROUND_PROCESSES_JXA — processTable / processByPid / foregroundProcesses, each process addressed uniquely+       osascript-exclusion.ts            # runExclusive() — NSScreen reads and System Events process enumerations never overlap+       resolve-app-pids.ts               # resolveAppPids() — app_name to running PIDs from NSWorkspace+     permissions/+       permission-probes.ts              # prompt-free Accessibility, Screen Recording, Automation > Finder checks+       screencapture-service.ts          # screencapture PNG capture, path checks and no-replace placement, sips JPEG preview+       system-info-service.ts            # battery (pmset), Wi-Fi (networksetup + ifconfig), version, displays (system_profiler -json); hostname/uptime via node:os+     markdown-literal.ts                 # markdownLiteral() — upstream/caller text as markdown a CommonMark parse reads back exactly+     parse-json-output.ts                # parseJsonOutput() — subprocess or file JSON through a schema, undefined when unreadable+     quoted-text.ts                      # quotedText() — a name as a JSON string literal for error messages

  • server.json 1 field changed
    version
    0.2.1 0.3.0

  • AGENTS.md +19 −16

    https://raw.githubusercontent.com/cyanheads/macos-mcp-server/HEAD/AGENTS.md

    − **Version:** 0.2.0− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`− **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0− | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |− | `ctx.inputs` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |− | `ctx.requestId` | Unique request ID. |− **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reaso−   if (!item) throw ctx.fail('no_match', `No item ${input.id}`, ctx.recoveryFor('no_match'));− throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection failed', { pool: 'primary' });− | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |− | `release-and-publish` | Push + npm + MCP Registry + GH Release. Picks up from `git-wrapup` (skip the Docker step — see Core Rules) |− `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was − **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.− **Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of w− - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; env vars added for any required API keys− - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys+ **Version:** 0.2.1+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.14`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0+ - **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |+ | `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |+ | `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |+ | `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the+   if (!item) throw ctx.fail('no_match', `No item ${input.id}`);+ throw new McpError(JsonRpcErrorCode.InitializationFailed, 'Connection failed', { pool: 'primary' });+ | `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |+ | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release. Picks up from `git-wrapup` (skip the Docker step — see Core Rules) |+ **CI is one file.** `.github/workflows/codeql.yml` (scaffolded) is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.+ `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was + **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.+ **Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewri+ - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset+ - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never wri

  • server.json 1 field changed
    version
    0.2.0 0.2.1