Skip to content

MCP Server

Neil Martin edited this page Sep 11, 2026 · 13 revisions

MCP Server

jamf-cli mcp serve exposes the CLI command tree to any Model Context Protocol (MCP) client: Claude Desktop, Cursor, IDE assistants, and custom agents. The server ships two generic tools, and the connecting model composes CLI invocations from the live command catalog of 1,700+ commands.

For the curated Claude Code experience, see the Claude Code Skills plugin; mcp serve is the open transport that lets any MCP-capable client drive jamf-cli.

Requirements

  • jamf-cli v1.20.0+ installed and on your PATH
  • At least one profile configured (see Setup Guide)
  • An MCP-capable stdio client (Claude Desktop, Cursor, OpenAI Codex CLI, etc.). ChatGPT needs a bridge (see ChatGPT (needs an HTTPS bridge) below).

Transport and Tools

The server speaks JSON-RPC over stdio and registers two tools:

Tool Purpose
list_commands Returns the full command catalog as JSON. The model calls this first to discover what it can run. It shells out to jamf-cli commands -o json on the same binary, so the client sees the entire catalog record, every field listed below. Takes no arguments.
run_command Executes any jamf-cli command. The model passes the command and flags as an args array, e.g. ["pro","computers","list"] or ["pro","classic-policies","get","--name","My Policy"]. Output defaults to JSON.

Each run_command call re-invokes the same binary as a child process, so auth, output formatting, gateway routing and version checks all run in the child's normal startup path.

Catalog fields

list_commands returns commands -o json verbatim, so an agent can settle these before running anything:

Field Use
command, description, flags, aliases Compose a valid invocation
product, group Scope a search to one product
destructive Know in advance that --yes is required
api Know which API serves the command: pro, pro-classic, platform-gateway, or radar. It matters most under security, where the last two sit side by side and take different credentials
gateway, gatewayBasis, gatewayDetail Know whether the command is refused on the profile the server was launched with. "gateway": "unserved" means a platform gateway credential cannot reach it and the CLI refuses it before sending (exit 8); gatewayBasis and gatewayDetail carry the evidence
privileges The Jamf Pro API-role privileges a Pro command needs, or the capability permissions for a Platform command
gatewayPrivileges The Jamf Account capability permissions the gateway requires for a Pro or Classic endpoint, in a different vocabulary from privileges
gatewayPermissions The same requirement in the words Jamf Account's picker prints, e.g. Inventory > Device groups: Read (device-groups:read); the actionable form, since the picker shows no slugs. Present for Platform commands too, whose own privileges are already capability slugs
scopes The Jamf Platform API scope levels the published spec declares for a Platform command: environment, tenant, or both. A 403 names a capability permission and says nothing about the level, so this field is where the level is recorded. Reported, never enforced, since the gateway does not follow the specs everywhere

Each field is positive-only: an absent field means "nothing recorded". An absent gateway is no promise that the endpoint is served, and an absent privileges no claim that none are needed. Use the fields to skip a doomed call, and read the error when one comes back.

A refusal is also explained in the command's own --help and in the runtime error. All three name a replacement command where one ships, and the catalog carries it as gatewaySuccessor. No row carries it in v1.29.0, so the runtime message points at an instance profile instead; have the model read the refusal message.

Security Model

  • Credentials never cross the protocol. The server captures the profile it was launched with (-p/--profile or JAMF_PROFILE) and injects it into every child invocation. The connecting model sees command output alone, never tokens or secrets.
  • The server is pinned to its launch profile. run_command rejects five model-supplied flags in args: --profile/-p (and with it any clustered short token carrying p, such as -pProd or -np Prod), --url, --token-file, --tenant-id, and --out-file. A rejected call fails with an error naming the disallowed flag. The list is those five, so treat the credential layer as the security boundary (see Is there a read-only mode? below).
  • Children run with --no-input. Commands that would prompt (setup, unconfirmed destructive operations) fail fast instead of hanging. The server strips any model-supplied --no-input from args and re-applies it itself.
  • Destructive commands require explicit --yes. The model passes --yes in the args array to confirm a delete or other destructive operation, or the command refuses to run. list_commands marks each one "destructive": true, so the model can tell which commands need it.

Note: the server runs against a single profile, so scope what the connecting client can touch by pointing it at a profile with limited API privileges.

Is there a read-only mode?

There is no read-only flag on mcp serve. The server exposes the whole command tree, and the built-in guardrails gate destructive operations (delete, erase) behind an explicit --yes. The server passes create, update and apply through.

For a read-only session, enforce it at the credential layer: point the server at a profile whose Jamf API role has read-only privileges. The MCP server inherits the permissions of its launch profile, so a read-only role leaves every run_command read-only however the model composes it, and Jamf Pro enforces that server-side.

# Launch the server against a profile backed by a read-only API role
jamf-cli -p my-readonly-profile mcp serve

Configuration

Claude Desktop

Add jamf-cli to your claude_desktop_config.json:

{
  "mcpServers": {
    "jamf-cli": {
      "command": "jamf-cli",
      "args": ["-p", "my-profile", "mcp", "serve"]
    }
  }
}

Replace my-profile with the profile name you want the client to target. Restart the client to pick up the new server.

OpenAI Codex CLI

Codex CLI is a stdio MCP client, so it drives jamf-cli mcp serve the same way Claude Desktop does. Add a server block to ~/.codex/config.toml:

[mcp_servers.jamf-cli]
command = "jamf-cli"
args = ["-p", "my-profile", "mcp", "serve"]

Or register it from the terminal (Codex writes the same config entry for you):

codex mcp add jamf-cli -- jamf-cli -p my-profile mcp serve

The tokens after -- are the launch command as you would type it. Start Codex and it discovers the list_commands and run_command tools. Codex separates stdio servers, configured with command/args as above, from remote streamable_http servers configured with a url; jamf-cli is a stdio server.

Other MCP clients

Any client that supports stdio MCP servers works the same way. Configure it to run:

jamf-cli -p my-profile mcp serve

The -p/--profile flag (or the JAMF_PROFILE environment variable) selects the instance. Omit it and the server uses the default profile resolution from Configuration & Profiles.

ChatGPT (needs an HTTPS bridge)

ChatGPT's Developer mode (available on Pro, Plus, Business, Enterprise, and Education accounts on the web) takes custom MCP servers over HTTPS with the Streamable HTTP or SSE transport. jamf-cli mcp serve speaks stdio, so ChatGPT cannot launch or connect to it the way Claude Desktop, Cursor, or Codex CLI do.

Reaching it from ChatGPT takes a stdio-to-HTTP bridge (e.g. mcp-proxy) in front of mcp serve, with that HTTPS endpoint exposed through a public tunnel such as ngrok or Cloudflare Tunnel. This is not recommended: it publishes a server holding live Jamf credentials to the public internet, and ChatGPT Developer mode itself warns that write actions "can inadvertently destroy, alter, or share data." If you pursue it, put the bridge behind authentication and point mcp serve at a read-only profile (see Is there a read-only mode? above). For an interactive assistant, use a local stdio client (Claude Desktop, Cursor, or Codex CLI).

Example Session

Once connected, the client:

  1. Calls list_commands to discover the catalog
  2. Calls run_command with ["pro","computers","list"] to list computers
  3. Calls run_command with ["pro","classic-policies","get","--name","My Policy"] to fetch a policy by name
  4. For a destructive change, calls run_command with ["pro","buildings","delete","--name","HQ","--yes"]

Output comes back as text (JSON by default). A non-zero exit reaches the model as an error result with the captured output, so the model can react to failures.

Launched against a platform gateway profile, the server refuses a Jamf Pro or Classic command outside the gateway's published API before it is sent, with exit code 8 and a message explaining why (see Error Handling & Exit Codes#Refused by Policy (exit 8)). The refusal covers everyday device actions, so a model driving MDM commands over MCP wants either an instance profile or a list_commands check on the gateway field first:

list_commands  →  select rows where gateway == "unserved"  →  don't call those

Troubleshooting

Client can't find the server. Make sure jamf-cli is on the PATH of the environment the client launches from. Use an absolute path in command if needed (e.g. /opt/homebrew/bin/jamf-cli).

Commands fail with an auth error. The profile passed in args must exist and resolve. Test it in your terminal first: jamf-cli -p my-profile pro computers list.

A command "refuses to run". Destructive commands need an explicit --yes in the args array. Setup commands cannot run over MCP, since they require interactive prompts (see the Credential Input Policy).

Model passes a command as one string. Each command and flag is a separate array element: ["pro","computers","list"], and not ["pro computers list"].

A result that will not parse as JSON, with a warning: line at the top. run_command returns the child's combined stdout and stderr, so anything the CLI writes to stderr is prepended to the output the model gets. The usual cause is a retired pro resource name: 103 were renamed in v1.29.0, 100 still run, and each prints one line on stderr first.

warning: `icons` is a deprecated name for `icon` and stops working after 2027-03-09. Use `pro icon`.
{
  "id": 1,
  ...
}

Neither --quiet nor --no-hints suppresses that line, so use the current name. list_commands carries the current spelling for every resource, which is one more reason for the model to call it before composing anything. Full tables: Pro Command Renames.

A command fails with "exitCodeName": "unsupported" (exit 8). The launch profile's credentials cannot reach the API that serves the command. Point the server at a profile that can (an instance profile for Jamf Pro and Classic endpoints outside the gateway's published API; a platform profile for Platform API commands), or have the model skip those commands using the gateway and api fields from list_commands.

See Also

jamf-cli Wiki


Products

  • Jamf Pro: jamf-cli pro
  • Jamf Platform API: jamf-cli pro (blueprints, benchmarks, DDM reports)
  • Jamf Platform: jamf-cli platform (AI Governance, Jamf Account, audit)
  • Jamf Protect: jamf-cli protect
  • Jamf School: jamf-cli school
  • Jamf Security Cloud: jamf-cli security

Clone this wiki locally