Skip to main content
Glama
README.md
# Apple Business Manager MCP server

A [FastMCP](https://gofastmcp.com) server that exposes the
[Apple School & Business Manager API](https://developer.apple.com/documentation/apple-school-and-business-manager-api)
as MCP tools. It is **strictly read-only** — it lists and reads devices, MDM
servers, blueprints, configurations, apps, packages, users, user groups,
organizational units, audit events, AppleCare coverage, and assignment/activity
status. It never creates, updates, deletes, or reassigns anything.

Read coverage of the Apple Business API is complete (34 tools). The write
operations (assign/unassign devices; create/update/delete for blueprints,
configurations, MDM servers; blueprint membership changes) are intentionally
not implemented.

Works with both Apple **Business** Manager (`business.api`) and Apple **School**
Manager (`school.api`).

## Tools

**Read — devices, MDM services, activities (always available):**

| Tool | What it does |
| --- | --- |
| `list_mdm_servers` | List all MDM servers in the org (id, name, type, timestamps). |
| `get_mdm_server` | Details for one MDM server by id. |
| `list_devices_for_mdm_server` | Device ids/serials assigned to a given MDM server (bulk-friendly). |
| `list_org_devices` | Devices enrolled via Automated Device Enrollment. |
| `get_org_device` | Full details for one device by serial number. |
| `get_device_assigned_server` | Which MDM server a device is assigned to (full info). |
| `get_device_assigned_server_id` | Assigned MDM server id (linkage only). |
| `get_device_applecare_coverage` | AppleCare coverage for one device. |
| `get_device_activity` | Status of a device-management activity by id. |
| `list_mdm_enrolled_devices` | Devices enrolled in Apple device management services (`mdmDevices`). |
| `get_mdm_enrolled_device_details` | Details for one MDM-enrolled device. |

**Read — blueprints (always available):**

| Tool | What it does |
| --- | --- |
| `list_blueprints` | List device blueprints in the org. |
| `get_blueprint` | Details for one blueprint by id. |
| `list_blueprint_device_ids` | Org-device ids assigned to a blueprint. |
| `list_blueprint_app_ids` | App ids attached to a blueprint. |
| `list_blueprint_configuration_ids` | Configuration ids attached to a blueprint. |
| `list_blueprint_package_ids` | Package ids attached to a blueprint. |
| `list_blueprint_user_group_ids` | User-group ids attached to a blueprint. |
| `list_blueprint_user_ids` | User ids attached to a blueprint. |

**Read — configurations, apps, packages (always available):**

| Tool | What it does |
| --- | --- |
| `list_configurations` | List MDM configurations in the org. |
| `get_configuration` | Details for one configuration by id. |
| `list_apps` | List apps available in the org (Apps and Books / VPP). |
| `get_app` | Details for one app by id. |
| `list_packages` | List packages in the org. |
| `get_package` | Details for one package by id. |

**Read — users, user groups, organizational units, audit (always available):**

| Tool | What it does |
| --- | --- |
| `list_users` | List managed Apple Account users in the org. |
| `get_user` | Details for one user by id. |
| `list_user_groups` | List user groups in the org. |
| `get_user_group` | Details for one user group by id. |
| `list_user_group_user_ids` | User ids belonging to a user group. |
| `list_organizational_units` | List organizational units in the org. |
| `get_organizational_unit` | Details for one organizational unit by id. |
| `list_organizational_unit_user_ids` | User ids belonging to an organizational unit. |
| `get_audit_events` | Org audit events in a time window (start/end timestamps required). |

## 1. Create API credentials in ABM/ASM

1. Sign in to Apple Business (or School) Manager.
2. Click your name (bottom-left) → **Preferences** → **API**.
3. Click **+** to create an API account, give it a label, **Create**.
4. Click **Generate Private Key** — the `.pem` downloads once. Keep it safe;
   it cannot be re-downloaded (you can revoke and regenerate).
5. Click **Manage** on the account to copy the **Client ID** and **Key ID**.

## 2. Install

```bash
cd abm-mcp
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
```

## 3. Configure credentials

The server reads four settings from environment variables:

```
ABM_CLIENT_ID=BUSINESSAPI.9703f56c-....
ABM_KEY_ID=d136aa66-0c3b-....
ABM_PRIVATE_KEY_PATH=/absolute/path/to/private-key.pem
ABM_SCOPE=business.api          # or school.api
```

There are two ways to supply them — **pick one, you don't need both:**

- **`env` block in Claude Desktop's config** — recommended when running inside
  the Claude Desktop app (see step 5, Option B). Claude Desktop injects these
  when it launches the server, so **no `.env` file is needed.**
- **`.env` file** — used when you run the server yourself from a terminal
  (`python server.py`) or with the one-command installer (step 5, Option A).
  Copy the template and fill it in:

  ```bash
  cp .env.example .env
  ```

  The server loads `.env` automatically (via `python-dotenv`).

You can inline the key with `ABM_PRIVATE_KEY_PEM` instead of a path in either
case. Whichever file holds your credentials, keep it readable only by your own
user account.

### Credential field details

Copy the three identifiers from the ABM/ASM API account's **Manage** screen
using the copy icons (avoid retyping — the Client ID wraps across lines in the
UI and is easy to mistype):

| Setting | Where it comes from | Format / example |
| --- | --- | --- |
| `ABM_CLIENT_ID` | Manage screen → "Client ID" | Starts with `BUSINESSAPI.` (or `SCHOOLAPI.`) + a UUID, ~48 chars, e.g. `BUSINESSAPI.9703f56c-10ce-4876-8f59-e78e5e23a152` |
| `ABM_KEY_ID` | Manage screen → "Key ID" | A UUID, 36 chars, e.g. `d136aa66-0c3b-4bd4-9892-c20e8db024ab` |
| `ABM_PRIVATE_KEY_PATH` | The `.pem` downloaded when the key was generated | Absolute path to a **PKCS#8** EC P-256 key (see below) |
| `ABM_SCOPE` | Must match the client type | `business.api` for a `BUSINESSAPI.` client; `school.api` for `SCHOOLAPI.` |

The key must be an **EC P-256** key (Apple's API signs with ES256). The curve
is non-negotiable; the *encoding* is what needs attention.

**Apple issues the key as SEC1 (`-----BEGIN EC PRIVATE KEY-----`) with explicit
EC parameters, and this does NOT load in Python's `cryptography` library** — it
fails with an ASN.1 / "EC curves with explicit parameters" error. You must
convert the downloaded key once to a named-curve PKCS#8 key:

```bash
openssl pkcs8 -topk8 -nocrypt -in abm-api-private-key.pem -out private-key-pkcs8.pem
```

Then point `ABM_PRIVATE_KEY_PATH` at `private-key-pkcs8.pem` (`-----BEGIN
PRIVATE KEY-----`). The conversion re-encodes the *same* key material to the
named-curve form the library accepts, so the Key ID registered in ABM stays
valid. (A named-curve SEC1 key would also load fine — but Apple's specific
download uses explicit parameters, so conversion is required in practice.)

Lock down whatever holds the secret, and don't sync the folder anywhere public:

```bash
chmod 600 .env private-key-pkcs8.pem
```

### Verify credentials without Claude Desktop

`test_token.py` performs a live token request and prints a masked report
(field lengths, the decoded JWT claims, and Apple's exact response) so you can
confirm auth before wiring up the app:

```bash
python test_token.py            # uses .env
# or pass inline:
ABM_CLIENT_ID='...' ABM_KEY_ID='...' \
ABM_PRIVATE_KEY_PATH='/abs/path/private-key-pkcs8.pem' ABM_SCOPE='business.api' \
python test_token.py
```

A `✅ SUCCESS` line means the credentials and signing are correct.

### Troubleshooting `invalid_client`

If Apple's token endpoint returns `{"error":"invalid_client"}`, check, in order:

1. **JWT audience must include `/v2/`.** The `aud` claim has to be
   `https://account.apple.com/auth/oauth2/v2/token` even though the token
   request is POSTed to the non-`v2` URL `https://account.apple.com/auth/oauth2/token`.
   This is set in `abm_client.py` (`TOKEN_AUDIENCE`) and is the single most
   common cause. `test_token.py` warns if it's missing.
2. **Client ID / Key ID match the key.** Confirm all three come from the same
   API account's Manage screen, with no stray whitespace or truncation (watch
   the field lengths in `test_token.py`).
3. **Scope matches the client type** (`business.api` ↔ `BUSINESSAPI.`).
4. **Key not revoked / freshly created** — a brand-new key can take a few
   minutes to become active; a revoked key fails permanently.

## 4. Run (optional — to test standalone)

```bash
python server.py                 # stdio transport
# or
fastmcp run server.py:mcp        # via the FastMCP CLI
```

## 5. Use it in the Claude Desktop app

Claude Desktop launches the server itself over stdio, so you don't run it by
hand — you just register it once.

### Option A — one command (recommended)

Requires [`uv`](https://docs.astral.sh/uv/) and `fastmcp` installed as CLI
tools (`brew install uv` and `uv tool install fastmcp`). This path uses the
`.env` file, so create it first (step 3). With your `.env` filled in:

```bash
./install-claude-desktop.sh
```

This runs `fastmcp install claude-desktop`, which builds an isolated
environment for the server (installing `requirements.txt`), passes your `.env`
values through, and adds the entry to Claude Desktop's config automatically.
Fully quit and reopen Claude Desktop afterward.

### Option B — edit the config by hand

Open Claude Desktop's config file:

- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

Add an entry under `mcpServers`, pointing `command` at the Python inside the
virtualenv you created in step 2 (so the dependencies are available):

```json
{
  "mcpServers": {
    "apple-business-manager": {
      "command": "/absolute/path/to/abm-mcp/.venv/bin/python",
      "args": ["/absolute/path/to/abm-mcp/server.py"],
      "env": {
        "ABM_CLIENT_ID": "BUSINESSAPI.9703f56c-....",
        "ABM_KEY_ID": "d136aa66-0c3b-....",
        "ABM_PRIVATE_KEY_PATH": "/absolute/path/to/private-key.pem",
        "ABM_SCOPE": "business.api"
      }
    }
  }
}
```

Use **absolute paths** for `command`, `args`, and the key path — Claude Desktop
does not run inside your project folder. On Windows, the interpreter is
`.venv\Scripts\python.exe` and paths use `\\`. With the `env` block filled in
here, you do **not** need a `.env` file.

It's JSON: comma-separate multiple servers under `mcpServers` and don't leave a
trailing comma. Save, then fully quit and reopen Claude Desktop.

### Verify

In a new chat, open the tools menu (the tools/hammer icon in the message box) —
you should see **Apple Business Manager** with its 8 tools. Try asking:
"List the MDM servers in Apple Business Manager." If nothing appears, check
Claude Desktop's MCP logs:

- **macOS:** `~/Library/Logs/Claude/mcp*.log`

## How auth works

Apple uses a two-step OAuth flow (see
[Implementing OAuth for the AxM API](https://developer.apple.com/documentation/apple-school-and-business-manager-api/implementing-oauth-for-the-apple-school-and-business-manager-api)):

1. The server signs a **client assertion** JWT (ES256, using your EC P-256
   private key) with the standard claims (`iss`/`sub` = Client ID, `aud` =
   Apple's token endpoint).
2. It exchanges that assertion for a bearer token via the `client_credentials`
   grant, then calls `https://api-business.apple.com/v1/...`
   (or `api-school.apple.com`).

The token lives ~1 hour and is cached and refreshed automatically.
`abm_client.py` holds all of this; `server.py` just wraps GET endpoints as
tools.

## Test (offline)

```bash
python test_smoke.py
```

Verifies JWT signing/verification, scope→host resolution, and that all tools
register — no live API calls.

## Notes & limits

- **Strictly read-only.** No write tools are implemented, so the server cannot
  assign/unassign devices or create/update/delete any resource.
- AppleCare has **no bulk endpoint** — one serial per call — so avoid looping
  `get_device_applecare_coverage` over a large fleet.
- Pagination follows JSON:API `links.next` cursors; use the `limit` argument on
  list tools to cap results for big orgs.
- Behind a proxy, set `HTTPS_PROXY` / `NO_PROXY` in the environment.
```