vibe coded with ❤️

Documentation

Deploying and controlling a local MCP server with MDM.

Concept app. A working exploration of an idea, built and shared to show what's possible — not a supported commercial product. Expect rough edges, no SLA and no roadmap guarantees. It has been tested in a demo environment only, not in production. Try it on test Macs first; feedback is very welcome.

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
  1. Package on the Macs.
  2. Background items: macmcpctl profile background-items --org "Acme" > bg.mobileconfig keeps the agent or daemon enabled and hides the “Background item added” notice. The bundle ships a ready one: Profiles/MacMCP-BackgroundItems.mobileconfig.
  3. Organisation key for encrypted secrets (see below). Skip it if you already use one for Gimle or Mimir.
  4. Settings profile for the domain com.spectrechen.macmcp.
  5. 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
KeyTypeDefaultMeaning
EnabledbooltrueMaster switch. false: no tools are offered
RunModestringagentagent (per user) or daemon (whole Mac, profile credentials only)
HTTPEnabledbooltrueHTTP endpoint POST /mcp
ListenAddressstring127.0.0.1Other addresses need AllowRemoteClients
Portint73851024–65535
AllowRemoteClientsboolfalseAllow a non-loopback address (a token is always required then)
RequireClientTokenbooltrueHTTP clients send Authorization: Bearer <token>
ClientTokenstring—Fixed token (MACMCP-ENC); empty = a random per-user token shown in the app
CatalogURLstringGitHub latest releaseSigned catalog (https)
CatalogPublicKeysarray—Extra Ed25519 keys (base64) for your own catalog
CatalogPinnedVersionstring—Stay on this catalog version
CatalogUpdateHoursint24Catalog check interval
AllowUserConnectorsboolfalseUsers may add connectors themselves (agent mode)
AllowPlaintextSecretsboolfalseAccept unencrypted secrets (labs only)
Connectorsarray—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.

KeyMeaning
IDLowercase, at most 16 characters; prefix of every tool name
ConnectorCatalog ID: jira-dc, confluence-dc, jamf-pro
DisplayNameOptional label
Enabledfalse switches the connector off at runtime
BaseURLhttps://…, may include a context path (https://intranet/jira)
AllowInsecureHTTPAccept an http:// base URL
AuthCatalog auth ID (pat, basic, apiclient); empty = the connector's first
CredentialsFields of the auth method: token; username + password; clientID + clientSecret; value. Secrets as MACMCP-ENC:v1
AllowUserCredentialsUsers may store their own credentials (default true, only for auth methods the catalog marks perUser). They win over the profile's
AllowWriteToolsOffer tools that change data (default false)
EnabledToolsOnly these catalog tools
DisabledToolsNever 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.

ConnectorRead toolsWrite tools (need AllowWriteTools)
Jira Data Centersearch_issues, get_issue, list_projects, get_transitionsadd_comment, create_issue, transition_issue
Confluence Data Centersearch, get_page, list_spaces, get_childrencreate_page
Jamf Prosearch_computers, get_computer, search_mobile_devices, list_scripts, list_computer_groups, list_policies, get_policyredeploy_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.

  1. Create the key once: make-org-key.sh ~/macmcp-key "Acme macMCP Key" (macmcp-org.cert.pem to share, a .p12 for MDM, .key.pem offline in a vault).
  2. Deploy it: Jamf's Certificate payload with the .p12 (Allow all apps access on, Allow export off), or make-key-profile.sh … macmcp-orgkey.mobileconfig.
  3. Encrypt: macmcpctl encrypt --cert macmcp-org.cert.pem (secret on stdin), or the offline Tools/encrypt.html with the public key from macmcpctl key-info.
  4. 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:

RuleEffect
Relative paths onlyNo .., //, ://, ? or # in a tool path
Base-URL pinningThe final URL must have the configured scheme, host, port and path prefix
Reserved headersA catalog can't set Authorization, Cookie, Host …
Write gateNon-GET or non-read-only tools need AllowWriteTools
No cross-host redirectsAuthorization never follows a redirect to another host
Declared placeholdersEvery 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

ThreatMitigation
Plist on disk readable by any userSecrets encrypted; plaintext refused by default
Another local process uses the HTTP port with the service accountClient token (per-user random or MDM), loopback only
A web page talks to localhostOrigin check + token
A forged catalog exfiltrates credentialsSignature + base-URL pinning + reserved headers
Catalog downgrade to a vulnerable versionRollback protection
An AI client is tricked into destructive callsWrite tools off unless allowed per connector; read-only annotations
A root daemon holding fleet credentialsAgent 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).