Repository navigation
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 three tools, and the connecting model composes CLI invocations from the live command catalog, 1,766 leaves in v1.31.0.
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.
- 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).
- For
generate_reportalone: a report directory, set once withjamf-cli config set-report-dir <dir>
The server speaks JSON-RPC over stdio and registers three tools:
| Tool | Purpose |
|---|---|
list_commands |
Browses and searches the command catalog, one JSON object per line. The model calls this first to discover what it can run. Takes two optional arguments, prefix and query (v1.32.0+). See Browsing the catalog below. |
run_command |
Executes a 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. See Security Model for what it refuses. |
generate_report (v1.31.0+) |
Writes a shareable HTML fleet report into the configured report directory and returns its path, size and warnings, never the HTML. Takes title, smart_groups and full. See Generating HTML reports below. |
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.
That also means the whole tree is exposed as it ships: the seven leaves new in v1.30.0 (pro open, protect open, school open, security open, and pro sso-settings oidc-broker-config with its get and update) need no MCP-side change. The child's stdout is a pipe, never a terminal, so open prints the console URL instead of launching a browser. A model calling ["pro","open","policies"] gets the URL back as data, and ["pro","open","--list"] returns all 139 named sections with no credentials at all.
Before v1.32.0, list_commands returned the whole catalog in one result. That ran past the 256 KiB cap on a child's output, so the model got invalid JSON cut off before the Protect, School and Security Cloud commands. Now the tool returns NDJSON, one row per line, each result under 40 KiB:
| Arguments | Result |
|---|---|
| none | The top level: the five products and the core commands. A row with "subcommands": N has N commands under it |
prefix |
One command path, e.g. "pro", then "pro computers". A runnable command carries description, destructive and flags
|
query |
The commands whose path, description or aliases contain every word, e.g. "delete policy". Add prefix to search one product. Search rows carry no flags |
{"command":"completion","description":"Generate shell completion scripts","destructive":false,"flags":"","subcommands":5}
{"command":"config","description":"Manage CLI configuration and profiles","destructive":false,"flags":"","subcommands":8}
A query that matches nothing answers {"matches":0,"hint":"no command matches; use fewer or shorter words, or browse with prefix"}. A last line carrying "truncated": true means the tool left rows out; narrow the query or add a prefix. jamf-cli commands takes the same selection as --prefix, --children and --search (see CLI Patterns).
list_commands rows carry command, description, destructive, flags and subcommands alone. The full record, with every field below, comes from the catalog command itself, which run_command runs: ["commands","--prefix","pro mobile-devices lock","-o","json"]. 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 |
preview |
Tell a Preview endpoint from a GA one. true where the spec declares x-preview on the operation; positive-only, so its absence records nothing. Thirteen commands carry it in v1.31.0: platform ai-policies × 10 and platform ai-tools × 3. A model that should not build on a changing contract reads this field rather than the Preview - prefix in description
|
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.31.0, so the runtime message points at an instance profile instead; have the model read the refusal message.
-
Credentials never cross the protocol. The server captures the profile it was launched with (
-p/--profileorJAMF_PROFILE) and injects it into every child invocation. The connecting model sees command output alone, never tokens or secrets. -
The server judges the command cobra resolves (v1.32.0+). Before,
run_commandcompared the raw argument list against a deny-list, so an alias (cfg), a leading flag (--no-color multi) or a flag inside the path (pro -q backup) got past it. Now the server resolvesargsthe way the CLI would, and the child checks again before it runs. -
The server is pinned to its launch profile.
run_commandrejects any flag naming a profile, URL, token, tenant, environment or output file:--profile(and any clustered short token carryingp, such as-pProd),--include-profile,--url,--token-file,--tenant-id,--environment-idand--out-file. Each would point the child at a different instance, swap the credentials, or write to a host path. A rejected call fails with an error naming the flag. -
The server refuses local paths unless you allow them (v1.32.0+). It refuses a flag that reads a local file (
--from-file,--file,--script-file,--mobileconfig-file,--body-file,--inputand the like) unless the path resolves inside the directory you pass tomcp serve --input-dir <dir>, symlinks followed. It always refuses--password-file, and every flag that writes a local path (--save-to,--report-dir,--dironsync, a command's own--output); the-o/--outputformat flag stays available.pro diffaccepts only this server's profile or a directory inside--input-diras a side. -
The server refuses body logging.
-vvv, or any--verboselevel of 3 or more, logs response bodies to stderr before redaction, andrun_commandreturns stderr. Use-vvor less; its header log showsAuthorization,CookieandSet-Cookieas[redacted]. -
The server refuses some command paths, since they pick their own instance, write where the model says, or print a credential:
multi,mcp,completion, the config write subcommands,config validate,doctor, everysetup, bothbackupcommands andjcds sync; everyauth token,pro api-authentication token,oauth-tokenandkeep-alive;pro sso-oauth-session-tokens; the commands that mint and print a secret (pro api-integrations client-credentials,protect api-clients apply);pro cloud-distribution-point createandpatch; the commands that set a Jamf Pro login password (pro jamf-pro-user-account-settings change-password,pro accounts create,update,apply); the writes that change who can log in to a Jamf product (v1.33.0+, listed below);protect downloads csrandwebsocket-auth; andprotect action-configs export.run_commandalso refusesdashboard; usegenerate_report.jamf-cli mcp serve --helpcarries the full list. -
The server refuses the writes that change who can log in (v1.33.0+). A write that sets no password can still grant a login: a model could create a Classic administrator account, bind an account group to an LDAP server it runs, point SSO at its own identity provider, or send password-reset mail through its own SMTP server. Each login then works outside the MCP server.
run_commandrefuses, with the reason in the error:-
create,updateand, where it exists,applyonpro classic-account-users,classic-account-groups,classic-ldap-servers,cloud-ldapandcloud-azure, andpro cloud-ldap update-mappings -
create,updateandapplyonpro api-rolesandpro api-integrations(v1.35.0), because a model could add privileges to the role its own integration is bound to, or bind the integration to a stronger role -
pro classic-smtp-server updateandpro smtp-server update -
pro sso-settings update,disable,cert create,cert updateandoidc-broker-config update, including thesso-settings-certspelling -
platform sso-connections createandupdate -
applyonprotect users,groupsandroles, and onschool usersandgroups -
protect restore, refused whole, asprotect backupalready was, because--resourcesand--excludeare the model's to choose
Reads,
delete, history notes and connection tests on these resources still run. Outside MCP nothing changes. An operator who wants a model to manage logins runs the command in a terminal. -
-
The server refuses
pro supervision-identities download(v1.35.0). It prints the identity's .p12, private key included, to stdout, which reaches the model.get,listanddeleteonapi-rolesandapi-integrationsstill run. -
The server refuses
-vtogether with--from-file(v1.35.0).-vlogs each request URL, and a lookup's URL holds lines of the file. Inside--input-dir, every message about a list entry names itsline Nand not its value, and a failed request keeps its exit code and hint but drops its text. Outsidemcp servethe messages still quote the entry. -
The server redacts some output.
config showruns with each token, client ID and client secret shown as<redacted>. Every Classicgetandlistshows each field Classic--setrefuses as a credential as<redacted>, so a Classic-o rawover MCP is not the wire bytes. It redacts configuration profile payloads by key name, and the third-party secret inprotect action-configs get,protect data-forwarding get,protect api-clients getandpro cloud-distribution-point list. The pinned tenant's own device secrets (the LAPS password, the Recovery Lock password, the FileVault personal recovery key) and blueprint configuration stay visible. -
The credential layer is the boundary. These refusals narrow what a model can reach, and a Jamf API role still decides what it can change (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-inputfromargsand re-applies it itself. -
Destructive commands require explicit
--yes. The model passes--yesin the args array to confirm adeleteor other destructive operation, or the command refuses to run.list_commandsmarks 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.
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 servegenerate_report writes a self-contained HTML fleet report and returns its
path, size and warnings. It never returns the HTML: the document is 320 to
800 KB, far too large for a conversation, which is also why run_command
refuses dashboard.
Reach for it when the administrator wants something to share or action rather
than read now; run_command still answers anything a table can.
| Input | Meaning |
|---|---|
title |
Report heading; must not begin with a dash |
smart_groups |
Smart group names to visualize |
full |
Collect the extended sections. Omit for the fast report |
The destination and file name are server-derived and cannot be set per call, since a connecting AI must not choose a host path. Designate one once:
jamf-cli config set-report-dir ~/Reportsgenerate_report refuses until it is set, and mcp serve prints a hint on
startup when it is missing. File names are derived from the profile and a
timestamp, and the file is opened O_EXCL so a collision cannot overwrite an
existing report. See Configuration & Profiles#Set the report directory (v1.31.0+).
Collection is two-tiered, and the tool description tells the model to run the
fast tier first and ask before the full one. So expect a question like "the
instance has N managed devices, would you like the full report?" rather than a
long silence. What each tier costs is in Fleet Dashboard#Two collection tiers
and in jamf-cli dashboard --help.
The model is told to get the fleet size with
["pro","computers-inventory","list","--limit","1","--field","totalCount"],
because generate_report returns the path and size alone and never the report's
own figures.
The report covers the profile mcp serve was launched with, and --include-profile
is blocked on the MCP path, so a report cannot span two profiles here. Use the
dashboard command directly for that.
A Platform profile (auth-method: platform) gives the most comprehensive
single-profile report: one set of credentials authenticates against the gateway
and collects both Jamf Pro data and Platform data (blueprints, compliance
benchmarks, DDM reports).
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.
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 serveThe 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.
Any client that supports stdio MCP servers works the same way. Configure it to run:
jamf-cli -p my-profile mcp serveThe -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'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).
Once connected, the client:
- Calls
list_commandsto discover the catalog - Calls
run_commandwith["pro","computers","list"]to list computers - Calls
run_commandwith["pro","classic-policies","get","--name","My Policy"]to fetch a policy by name - For a destructive change, calls
run_commandwith["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 check on the catalog's gateway field first:
run_command ["commands","--prefix","pro mobile-devices","-o","json","--select","command,gateway"]
→ skip rows where gateway == "unserved"
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).
flag --from-file is not available over MCP (v1.32.0+). The server has no input directory, or the model named a file outside it (the message then quotes the path and the directory). Start it with one and keep the files the model may read there, using absolute paths, since a relative one resolves from the server's working directory:
"args": ["-p", "my-profile", "mcp", "serve", "--input-dir", "/Users/me/jamf-mcp-inputs"]The directory must exist, and --input-dir "" (an unset variable, say) stops the server from starting.
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 run_command ["commands", …, "-o", "json"].
- Claude Code Skills: the curated Claude Code agent experience
- Configuration & Profiles: profile and auth setup
- CLI Patterns: apply, scaffold, name lookup, destructive ops
-
jamf-cli agent-context: a static operating guide covering the same auth/output/exit-code/destructive/MCP ground for any agent driving the CLI, over MCP or not; see CLI Patterns#Agent operating guide (v1.22.0+)
Repository · Issues · Releases
jamf-cli Wiki
- Home
- Community
- Getting Started
- CLI Reference
- Product Commands
- Workflows
- Configuration
- Reference
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