Documentation
Deploying and controlling a local MCP server with MDM.
Overview
macMCP is a Model Context Protocol server that runs locally on a Mac. AI clients connect to it and get tools for internal systems — today Jira Data Center, Confluence Data Center and Jamf Pro. Everything an admin cares about is a managed setting: which connectors exist, which tools they offer, where the servers are, which credentials they use, whether it runs per user or for the whole Mac, on which address and port, and which privacy grants it has.
New systems arrive as data through a signed, public catalog (Spectrechen/macMCP-catalog on GitHub), which also documents how to wire each MCP client.
Not yet run against real servers: the connectors were developed and unit-tested (45 tests), and the server was smoke-tested over stdio and HTTP, but they haven't been run against a real Jira Data Center, Confluence Data Center or Jamf Pro server. Every connector and client entry in the catalog is marked experimental. Start with read-only tools on a test system.
Requirements
- macOS 26 or later, Apple silicon
- An MDM that can install custom configuration profiles
- Network access from the Mac to the configured servers, and to GitHub for catalog updates (unless you host and pin your own catalog)
Deployment
MacMCP-0.1.0.pkg is signed (Developer ID Installer) and notarized, and installs /Applications/MacMCP.app. For managed deployment, download the distribution bundle (1.4 MB): the package plus docs, example profiles, the Jamf schema, tools and the bundled catalog.
shasum -a 256 MacMCP-0.1.0.pkg
# 0b3d21e83ba48fb4cc0126a2baeab22e1583cba7240d1a055e62257b06e63fc9
spctl --assess --type install -v MacMCP-0.1.0.pkg # → Notarized Developer ID
- Package on the Macs.
- Background items:
macmcpctl profile background-items --org "Acme" > bg.mobileconfigkeeps the agent or daemon enabled and hides the “Background item added” notice. The bundle ships a ready one:Profiles/MacMCP-BackgroundItems.mobileconfig. - Organisation key for encrypted secrets (see below). Skip it if you already use one for Gimle or Mimir.
- Settings profile for the domain
com.spectrechen.macmcp. - Privacy profile, only if a connector needs it (none of the initial ones do).
The app registers the server when it first starts. With RunMode = daemon, approval comes from the background-items profile (or the user in Login Items).
About macmcpctl in the zip: the loose copy in Tools/ is signed with the same Developer ID, but no notarization ticket can be stapled to a bare command-line binary, and a quarantined download of it hasn't been tested. The package does not install macmcpctl (an open question in the project); the zip is the way to get it. The zip also contains macOS metadata files (._*).
Settings (com.spectrechen.macmcp)
In Jamf, upload Profiles/macmcp-jamf-schema.json under Application & Custom Settings → External Applications → Custom Schema. Or write a plist like Profiles/com.spectrechen.macmcp.plist, check it, and wrap it:
macmcpctl check settings.plist
macmcpctl profile settings settings.plist --name Production --org "Acme" > macmcp.mobileconfig
| Key | Type | Default | Meaning |
|---|---|---|---|
Enabled | bool | true | Master switch. false: no tools are offered |
RunMode | string | agent | agent (per user) or daemon (whole Mac, profile credentials only) |
HTTPEnabled | bool | true | HTTP endpoint POST /mcp |
ListenAddress | string | 127.0.0.1 | Other addresses need AllowRemoteClients |
Port | int | 7385 | 1024–65535 |
AllowRemoteClients | bool | false | Allow a non-loopback address (a token is always required then) |
RequireClientToken | bool | true | HTTP clients send Authorization: Bearer <token> |
ClientToken | string | — | Fixed token (MACMCP-ENC); empty = a random per-user token shown in the app |
CatalogURL | string | GitHub latest release | Signed catalog (https) |
CatalogPublicKeys | array | — | Extra Ed25519 keys (base64) for your own catalog |
CatalogPinnedVersion | string | — | Stay on this catalog version |
CatalogUpdateHours | int | 24 | Catalog check interval |
AllowUserConnectors | bool | false | Users may add connectors themselves (agent mode) |
AllowPlaintextSecrets | bool | false | Accept unencrypted secrets (labs only) |
Connectors | array | — | See below |
Changes take effect within 30 seconds — macOS sends no notification when a profile changes, so macMCP re-reads the forced preferences every 30 seconds.
Connectors
Each entry in Connectors enables one catalog connector against one server. Several entries can use the same connector (for example two Jamf Pro servers); the ID prefixes every tool name, e.g. jira_search_issues.
| Key | Meaning |
|---|---|
ID | Lowercase, at most 16 characters; prefix of every tool name |
Connector | Catalog ID: jira-dc, confluence-dc, jamf-pro |
DisplayName | Optional label |
Enabled | false switches the connector off at runtime |
BaseURL | https://…, may include a context path (https://intranet/jira) |
AllowInsecureHTTP | Accept an http:// base URL |
Auth | Catalog auth ID (pat, basic, apiclient); empty = the connector's first |
Credentials | Fields of the auth method: token; username + password; clientID + clientSecret; value. Secrets as MACMCP-ENC:v1 |
AllowUserCredentials | Users may store their own credentials (default true, only for auth methods the catalog marks perUser). They win over the profile's |
AllowWriteTools | Offer tools that change data (default false) |
EnabledTools | Only these catalog tools |
DisabledTools | Never these tools |
A tool is offered when the master switch is on, its connector is enabled and known to the catalog (not planned), credentials are available, it passes EnabledTools/DisabledTools, and it is read-only or AllowWriteTools is set. macmcpctl catalog tools catalog.json lists every tool with R/W.
| Connector | Read tools | Write tools (need AllowWriteTools) |
|---|---|---|
| Jira Data Center | search_issues, get_issue, list_projects, get_transitions | add_comment, create_issue, transition_issue |
| Confluence Data Center | search, get_page, list_spaces, get_children | create_page |
| Jamf Pro | search_computers, get_computer, search_mobile_devices, list_scripts, list_computer_groups, list_policies, get_policy | redeploy_framework |
Encrypted Secrets
Managed preferences are readable by every user on the Mac, so secrets must be encrypted. The scheme is the same as Gimle's and Mimir's (MACMCP-ENC:v1:<kid>:…: P-256 ECDH, HKDF-SHA256, AES-256-GCM); you can reuse the same organisation key, but values can't be swapped between apps because each has its own prefix and key-derivation label.
- Create the key once:
make-org-key.sh ~/macmcp-key "Acme macMCP Key"(macmcp-org.cert.pemto share, a.p12for MDM,.key.pemoffline in a vault). - Deploy it: Jamf's Certificate payload with the
.p12(Allow all apps access on, Allow export off), ormake-key-profile.sh … macmcp-orgkey.mobileconfig. - Encrypt:
macmcpctl encrypt --cert macmcp-org.cert.pem(secret on stdin), or the offlineTools/encrypt.htmlwith the public key frommacmcpctl key-info. - Paste the
MACMCP-ENC:v1:<kid>:…value into the credential field.
Check on a Mac with macmcpctl keys. To rotate, deploy the new key next to the old one, re-encrypt, update the profile, then remove the old key. Decryption uses the Security framework, so the private key never leaves the keychain; values are decrypted per request and never written anywhere. Plaintext secrets are refused unless AllowPlaintextSecrets is set.
Per-User Credentials
For Jira and Confluence, users can store their own personal access token in the macMCP app (Connectors → Your Personal access token…). It goes to their login keychain and takes precedence over a service token in the profile, so actions run with the user's own permissions. macMCP's server process stores and reads it itself, so the background agent never triggers a keychain prompt. Jamf Pro API clients are service accounts and come only from the profile. In daemon mode, only profile credentials are used.
Privacy (TCC)
macmcpctl profile privacy --service SystemPolicyAllFiles --service ScreenCapture --org "Acme" > macmcp-privacy.mobileconfig
Grants apply to the server process (identifier com.spectrechen.macmcp.server, Team ID 23VFRTF5KC). Screen Recording can't be granted silently under Apple's rules; the profile lets standard users approve it once. None of the initial connectors need privacy grants.
Components
MacMCP.app SwiftUI: status, connectors (own credentials), client snippets, keys
└─ macmcpd the MCP server (identifier com.spectrechen.macmcp.server)
macmcpctl admin CLI: encrypt, keys, status, check, catalog, clients, profiles
catalog (public) signed connectors + client wiring
macmcpd runs as a per-user LaunchAgent (HTTP), as a root LaunchDaemon (HTTP, profile credentials only), or started by an MCP client with --stdio. The app registers whichever RunMode says with SMAppService and unregisters the other. When the tool set changes, stdio clients get notifications/tools/list_changed; HTTP clients see it at their next tools/list. Starting --stdio from Claude Desktop is a separate process, so it bypasses HTTPEnabled = false (an open question in the project).
Catalog & Trust
The catalog is data only. Per connector: auth methods (bearer, basic, header, oauth2ClientCredentials; perUser says whether users may bring their own), an optional connection check, and tools. A tool is a JSON Schema for its arguments plus a request template: path segments are percent-encoded per argument, query and headers are templates, the body is a JSON template, and response.pick keeps only listed fields with a size cap.
catalog.json + catalog.json.sig (Ed25519) are accepted only with a signature from a key compiled into macMCP, or one you add in CatalogPublicKeys for your own catalog; a supported schema version; and a version not older than the cached one unless pinned. The signing key stays offline. Independently of the signature, the binary enforces:
| Rule | Effect |
|---|---|
| Relative paths only | No .., //, ://, ? or # in a tool path |
| Base-URL pinning | The final URL must have the configured scheme, host, port and path prefix |
| Reserved headers | A catalog can't set Authorization, Cookie, Host … |
| Write gate | Non-GET or non-read-only tools need AllowWriteTools |
| No cross-host redirects | Authorization never follows a redirect to another host |
| Declared placeholders | Every placeholder must be a declared input property |
HTTP Transport
POST /mcp (JSON-RPC, JSON responses only — no SSE; GET returns 405) and GET /health. One request per connection, 4 MB body limit. The bearer token is compared via SHA-256 digests. An Origin header, if sent, must be loopback (DNS rebinding). Listening on a non-loopback address needs AllowRemoteClients and always a token.
Threat Model
| Threat | Mitigation |
|---|---|
| Plist on disk readable by any user | Secrets encrypted; plaintext refused by default |
| Another local process uses the HTTP port with the service account | Client token (per-user random or MDM), loopback only |
| A web page talks to localhost | Origin check + token |
| A forged catalog exfiltrates credentials | Signature + base-URL pinning + reserved headers |
| Catalog downgrade to a vulnerable version | Rollback protection |
| An AI client is tricked into destructive calls | Write tools off unless allowed per connector; read-only annotations |
| A root daemon holding fleet credentials | Agent by default; the daemon is opt-in and has no user credentials |
These are the design's mitigations, not an audit — the design hasn't been independently reviewed.
MCP Clients
The app's Clients page shows the snippet for each client (Claude Desktop, Claude Code, VS Code, Cursor) with this Mac's URL and token. On the command line: macmcpctl clients catalog.json [client-id] --token …. Organisations that use Mimir can deploy these entries through Mimir fragments instead of user config files (Mimir's own scope for those tools is documented there).
Troubleshooting
macmcpctl status # agent; --daemon for the daemon
log stream --predicate 'subsystem == "com.spectrechen.macmcp"'
status.json lives in ~/Library/Application Support/MacMCP (agent) or /Library/Application Support/MacMCP (daemon).
What's Verified
Tested: 45 unit tests pass in 11 suites, covering the catalog trust and rollback checks, request building, credentials and the runtime. During development the server was smoke-tested over stdio and HTTP, a connector was enabled and disabled live through the config poll, the Origin check was exercised, the bundled signed catalog loaded in the built server, and encrypt.html and the Swift code decrypt each other's values.
Not verified: a run against a real Jira Data Center, Confluence Data Center or Jamf Pro server (then the connectors can leave experimental); the package installed on a managed Mac; switching between agent and daemon; the background-items and privacy profiles; an organisation key delivered as an MDM payload. There's no app icon, screenshots or test report yet. Open design questions: whether the package should install macmcpctl, an StdioEnabled key, native connectors that need TCC, and Codex support (TOML).