Skip to content

macos mcp server AGENTS.md changelog

io.github.cyanheads/macos-mcp-server

3 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

  • 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

  • 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