# Connect an AI agent (MCP)

> Connect Claude, ChatGPT, Claude Code, Cursor or the Claude API to Brix over MCP. Sign in from the app or use an API key. Endpoint, setup and tools.

Source: https://brixsignage.com/developers/mcp/

Brix runs a Model Context Protocol (MCP) server. Connect an agent once and it can find your screens, explain why one is blank, cast content, build playlists and pull proof-of-play and uptime reports.

## Connection details

| Setting | Value |
| --- | --- |
| Endpoint | `https://api.brixsignage.com/v1/mcp` |
| Transport | Streamable HTTP. `POST` one JSON-RPC 2.0 message; the reply is a JSON response. No SSE stream. |
| Auth | Sign in with OAuth 2.1 (claude.ai, ChatGPT and other connector apps), or `Authorization: Bearer <api key>` with the same keys as the REST API. |
| Protocol versions | `2024-11-05`, `2025-03-26`, `2025-06-18` |
| Rate limit | 30 requests per minute per caller, inside the workspace's 600 per minute. Over it: HTTP 429 with `Retry-After`. |
| Methods | `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/templates/list`, `resources/read`, `prompts/list`, `prompts/get` |

There are two ways to connect:

- **Sign in (claude.ai, ChatGPT).** Add the endpoint as a custom connector. The app opens a Brix page where you sign in and choose what it may do. No key to copy.
- **API key (Claude Code, Cursor, Claude Desktop config file, the Claude and OpenAI APIs).** Create a key and put it in the client's config.

Either way, the permissions you pick decide what the agent can do. See [Authentication and scopes](/developers/authentication/).

## claude.ai and ChatGPT: sign in

1. Copy the endpoint: `https://api.brixsignage.com/v1/mcp`.
2. Add it as a custom connector.
   - **Claude:** Settings > Connectors > Add custom connector. Claude Desktop uses the same connectors as claude.ai.
   - **ChatGPT:** Settings > Apps and Connectors > Advanced settings, turn on Developer mode, then Create.
3. The app opens a Brix page. Sign in, then pick the workspace, a location if you want to limit the app to one, and the permission areas. Click **Allow**.

The app acts as you, with only the areas you picked. Viewing screens is always included, so if you pick no areas, the app can only view screens. You cannot give the app a permission you do not hold.

Whatever areas you pick, an app connected by signing in cannot use billing, payment, invoice, API key, single sign-on, user or role routes. A person does these in the CMS. API keys are not affected.

Every connection is listed in the CMS under **Settings > API & MCP > Connected apps**, with who connected it and when it was last used. **Revoke** ends it at once: the app's next call is refused and it has to be connected again.

For client developers: the server follows the MCP authorization spec. A call without a token gets `401` with a `WWW-Authenticate` header that points to the protected-resource metadata at `/.well-known/oauth-protected-resource/v1/mcp`. The authorization server is `https://api.brixsignage.com` (metadata at `/.well-known/oauth-authorization-server`). It supports authorization code with PKCE (S256), dynamic client registration and client ID metadata documents. Access tokens last an hour. Refresh tokens rotate on every use. An access token from this flow works only at `/v1/mcp`.

## API key clients

### 1. Create a key for the agent

In the CMS: **Settings > API & MCP > New key**. Give it only the areas the agent needs. For a first try, a view-only key (Screens, Playlists, Schedules, Files, Proof of Play) is safe: the agent can answer questions but cannot change what plays.

The same page has a **Model Context Protocol (MCP)** section with the endpoint and **Copy config** and **Copy command** buttons.

### Claude Code

```bash
claude mcp add --transport http brix https://api.brixsignage.com/v1/mcp \
  --header "Authorization: Bearer $BRIX_API_KEY"
```

Then run `claude` and ask: "Which of my Brix screens are offline, and why?" Check the connection with `/mcp`.

### Claude Desktop

To connect by signing in, add Brix as a custom connector (see above). To use a key instead: Claude Desktop's config file starts local (stdio) servers, so connect through the `mcp-remote` bridge. It needs Node.js. Open **Settings > Developer > Edit Config** and add:

```json
{
  "mcpServers": {
    "brix": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://api.brixsignage.com/v1/mcp",
        "--header", "Authorization:${BRIX_AUTH}"
      ],
      "env": { "BRIX_AUTH": "Bearer ak_us_live_..." }
    }
  }
}
```

Restart Claude Desktop. The header value goes through `env` because some platforms split arguments that contain spaces.

### Cursor

Add to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):

```json
{
  "mcpServers": {
    "brix": {
      "url": "https://api.brixsignage.com/v1/mcp",
      "headers": { "Authorization": "Bearer ${env:BRIX_API_KEY}" }
    }
  }
}
```

`${env:BRIX_API_KEY}` reads the key from your environment, so it stays out of the file. You can also paste the key in place of it. Open **Cursor Settings > MCP** to check that `brix` is connected.

Other clients that accept a remote URL with custom headers use the same shape. This is the config that **Copy config** in the CMS gives you:

```json
{
  "mcpServers": {
    "brix": {
      "url": "https://api.brixsignage.com/v1/mcp",
      "headers": { "Authorization": "Bearer <YOUR_API_KEY>" }
    }
  }
}
```

### OpenAI API

The OpenAI Responses API sends your key in a header to a remote MCP server:

```bash
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5",
    "tools": [{
      "type": "mcp",
      "server_label": "brix",
      "server_url": "https://api.brixsignage.com/v1/mcp",
      "headers": { "Authorization": "Bearer '"$BRIX_API_KEY"'" },
      "require_approval": "never"
    }],
    "input": "Which of my screens are offline?"
  }'
```

`"require_approval": "never"` lets the model call tools without a pause. Use it with a view-only key, or set it to `"always"` and approve each call.

### Claude API

The Messages API connects to remote MCP servers with the MCP connector:

```bash
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: mcp-client-2025-11-20" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "max_tokens": 4096,
    "mcp_servers": [{
      "type": "url",
      "name": "brix",
      "url": "https://api.brixsignage.com/v1/mcp",
      "authorization_token": "'"$BRIX_API_KEY"'"
    }],
    "tools": [{ "type": "mcp_toolset", "mcp_server_name": "brix" }],
    "messages": [{ "role": "user", "content": "Which of my screens are offline?" }]
  }'
```

## Test the endpoint with curl

```bash
curl https://api.brixsignage.com/v1/mcp \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'
```

A tool result comes back as a text block plus `structuredContent`. A failed tool call returns `isError: true` with the error text, so the agent can correct itself.

## How the tools behave

- Start with `whoami` in an unfamiliar workspace. It returns the key, the workspace and its permissions.
- `search_screens` and `search_media` resolve names to real ids. Never guess an id.
- `cast_content` shows existing content for a limited time (default one hour). `assign_content` changes what a screen plays by default. `quick_post` writes an announcement and casts it in one step.
- `list_api_routes` finds any documented REST endpoint the key allows, for anything without a dedicated tool. `api_get` reads it. Writes are separate tools: `api_create` (POST), `api_update` (PATCH or PUT) and `api_delete`. Key clients set up before this split can still call `call_api`; connections made by signing in cannot.
- Every tool declares MCP annotations: read-only, destructive, idempotent, and whether it reaches outside Brix. Clients use them to decide when to ask you first.
- Content created over MCP lands as a draft where the location requires approval.
- Changes made over MCP are recorded in the workspace audit log. Reads are not.

## Things to ask

- "Which of my screens are offline, and why?"
- "Why is the lobby screen blank? Show me what it's displaying right now."
- "Put the Summer Menu playlist on every screen at the Downtown location for the next two hours."
- "How many times did the Burger Promo play last week, and on which screens?"
- "Give me this month's uptime report, worst screens first."

Your AI app asks before it runs a tool that changes what a screen shows.

## Privacy, disconnecting and support

- The app sees only what the tools return for the workspace, location and permission areas you approved. Brix does not receive your conversation with the app, only the tool calls it makes.
- Disconnect any time in Brix under Settings > API & MCP > Connected apps. Revoke ends the connection at once: the app's next call is refused. Removing the connector in the app stops the app using it.
- A connection record is deleted 30 days after the connection ends (revoked, refresh token expired, or sign-in never completed). The audit log entries stay for the audit log's normal retention period.
- Brix does not use data accessed through connected apps to train AI models. The app's provider handles what the app receives under its own terms.
- How Brix handles data is in the [Privacy Policy](https://brixsignage.com/privacy#connected-ai-apps). For help, email [hello@brixsignage.com](mailto:hello@brixsignage.com).
- Service status is at [status.brixsignage.com](https://status.brixsignage.com).
- To report a security problem, see [Reporting a vulnerability](https://brixsignage.com/security/#report).

## Tool catalog

47 tools. Each needs the permission shown on the API key. Read-only tools never change anything.

### capture_device_frame

Ask a screen to capture a screenshot now and return the newest stored frame — `latestScreenshotId`, `latestFrameUrl`, `latestFrameAt`, and `fresh` (true when the frame post-dates this request). To see the picture, pass `latestFrameUrl` to api_get: the image comes back as { contentType, totalBytes, bodyBase64 }. Use to see what's actually on the panel.

Permission: `screen.edit`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `screenId` | string | yes |  |

### get_screen_why

Explain why a screen is showing what it's showing, in plain language. Returns the resolution chain (deactivated / emergency / cast / scheduled / default).

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `screenId` | string | yes | The screen ID, e.g. scr_aurora_42 |

### diagnose_screen

"What might be wrong" — a likely-cause troubleshooting verdict for a screen that looks blank/wrong, reasoned by ELIMINATION from verifiable signals (online/offline, render-truth, resource pressure, hardware early-warning). Returns one likely cause + a guided next-step checklist. Note: the player cannot read TV/HDMI/input state, so when the stick is healthy and outputting content the verdict is the `downstream` layer with `guided: true` and a TV/input/cable CHECKLIST — a guided guess, NOT a detection. A definitive CEC `display` read (tv-off/disconnected) is the one real display signal.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `screenId` | string | yes | The screen ID, e.g. scr_aurora_42 |

### list_open_alerts

List currently-open alert events, newest first. Optionally filter by severity, screen or org node. Paginate with offset; the response reports total + hasMore.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `severity` | info \| warning \| critical | no |  |
| `screenId` | string | no |  |
| `nodeId` | string | no | Limit to alerts raised at (or under) this org node. |
| `offset` | integer | no | Skip this many matching alerts (default 0). |

### assign_content

Push a playlist, schedule, app, media or layout to a screen — or to MANY screens at once via screenIds. Requires `screen.cast`.

Permission: `screen.cast`. Behavior: changes existing data, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `screenId` | string | no | Single target screen. |
| `screenIds` | array | no | Bulk alternative: assign the same content to every listed screen (max 200). |
| `contentKind` | playlist \| schedule \| app \| media | yes |  |
| `contentId` | string | no |  |

### send_command

Send a runtime command (reboot, refresh, screenshot, clear-cache, cec-on, cec-off) to one screen or many (screenIds). Reboot interrupts what's on screen — confirm with the operator when unsure.

Permission: `screen.edit`. Behavior: changes existing data.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `screenId` | string | no |  |
| `screenIds` | array | no | Bulk alternative: command every listed screen (max 200) in one call. |
| `kind` | reboot \| refresh \| screenshot \| clear-cache \| cec-on \| cec-off | yes |  |

### trigger_emergency_template

Fire a pre-armed emergency template (Lockdown, Severe weather, Fire drill, …) across its in-scope screens — an immediate takeover. List ids first with the brix://emergency-templates resource; confirm scope with the operator.

Permission: `screen.edit`. Behavior: changes existing data.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `templateId` | string | yes |  |

### upload_media_from_url

Download a file from a public URL into Brix media. Returns the new asset id + URL. Use for migration — pull from a Drive share, S3, the customer's old-CMS export, etc. Max 500 MB; use mint_upload_url for larger.

Permission: `media.create`. Behavior: creates, reaches outside Brix.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Publicly fetchable http(s) URL. |
| `name` | string | yes | Display name for the asset. |
| `folderId` | string | no |  |
| `tags` | array | no |  |

### mint_upload_url

Get a one-shot upload URL for a large file. The model's host PUTs bytes to it then calls confirm_media_upload with the same `uploadId`.

Permission: `media.create`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `contentType` | string | yes |  |
| `name` | string | yes |  |

### confirm_media_upload

Finalize an upload after PUTting bytes to a mint_upload_url. Creates the media_assets row.

Permission: `media.create`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `uploadId` | string | yes |  |
| `folderId` | string | no |  |
| `tags` | array | no |  |

### bulk_create_playlists

Create many playlists at once. Max 500 per call. Items reference media or app-instance ids that must already exist in this workspace.

Permission: `playlist.create`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `playlists` | array | yes |  |

### bulk_create_org_nodes

Create many org-tree nodes in one call. Max 1000. Each node hangs off `parentId` (an existing node), `parentExternalId` (the Location ID of an existing node or of an EARLIER node in this same call — build regions then stores in one call), or the Space root when both are omitted. `externalId` sets the node's Location ID (store number / region code; unique per workspace) — SSO claim mapping matches IdP location claims against it. A node whose externalId already exists is skipped, so re-running an import is safe.

Permission: `org-unit.create`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `nodes` | array | yes |  |

### search_screens

Filter the screen fleet. All criteria optional and AND-ed. Returns up to 200 per page; the response carries total + hasMore so you can page with `offset` instead of re-querying.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | no | Substring match on screen name. |
| `status` | online \| offline \| pairing | no |  |
| `nodeId` | string | no | Limit to screens at (or under) this org node. |
| `tag` | string | no |  |
| `offset` | integer | no | Skip this many matches (default 0). |

### bulk_create_schedules

Create many schedules in one call. Max 200; up to 50 blocks each. Times are HH:MM 24-hour in the SCREEN's timezone.

Permission: `schedule.create`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `schedules` | array | yes |  |

### assign_screen_to_node

Move a screen to a different org node — useful for post-migration cleanup. Requires `screen.edit`.

Permission: `screen.edit`. Behavior: changes existing data, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `screenId` | string | yes |  |
| `nodeId` | string | no | null to clear; otherwise the destination node id. |

### create_creatives

Author new creatives (data-bound canvases) from scratch. This is how you BUILD templates programmatically. Each: { name, stage?:'landscape'|'portrait', backgroundUrl?, dataSourceId?, nodeId?, boxes:[…] }. A box is { kind, x, y, w, h, … } where x/y/w/h are 0–1 fractions of the stage and kind is one of text|field|list|image|shape|celebrations|event-board|signage|group|slot. Shapes use { shape:'rect'|'circle'|'line'|'triangle', fill, stroke, strokeWidth, cornerRadius }; text uses { text, color, fontWeight, align, fontSize? }; field/list bind via { field:'dot.path' }; image uses { url }; slot is a transparent hole the screen fills with real content, like a layout zone — { slot: { kind:'media'|'playlist'|'app', id, name } } naming a file / playlist / app in THIS workspace (or slot:null for an empty hole the customer fills later; borderRadius clips what plays there); signage is a full Signage Template Engine block — { signage: { archetype, style, mode, industry, orientation, content, anim } } — rendered 1:1 (layout + animations), usually one full-bleed box (x:0,y:0,w:1,h:1). Boxes are validated + clamped server-side; an invalid box rejects the whole call. Max 100 creatives per call. Designs also pass a quality gate (the Brix template standard): REJECTED with named findings when text pins under 24px (1920w scale), an authored contrast pair computes <3:1, or one layer fully hides another; WARNED as structured diagnostics for busy layouts (>36 content layers), >5 font sizes, >2 font families, hand-frozen list rows that should be ONE list box, static clock/date copy instead of {{now.*}} tokens, and competing loop animations. Treat warnings as defects: fix and re-emit.

Permission: `creative.create`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `creatives` | array | yes |  |

### whoami

Who this connection is and what it may do. Useful at the start of work in an unfamiliar workspace. Returns the actor (an API key with the org node it is pinned to, or the signed-in user), the workspace id + name, and the flat permission set. A pinned key simply cannot see or write outside its branch, so this is usually the answer to an unexplained 403 or an empty list. The permission set is a planning hint: node-precise checks still run on every write.

Permission: `screen.view`. Behavior: read-only.

### list_api_routes

Lists the Brix REST API (https://brixsignage.com/developers/api/): every documented endpoint as { method, path, description }. Everything the CMS UI can do lives behind one of these routes. A GET route is read with api_get; POST, PUT/PATCH and DELETE routes go through api_create, api_update and api_delete. Narrow with `prefix` (e.g. '/v1/screens') or free-text `q` (matched against path + description). One page is 200 routes and the surface is larger than that: while the reply carries `nextOffset`, call again with that `offset` to see the rest.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `prefix` | string | no | Path prefix filter, e.g. '/v1/playlists'. |
| `q` | string | no | Case-insensitive substring match on path or description. |
| `offset` | number | no | Skip this many matching routes — pass the `nextOffset` from the previous reply to page through the whole surface. |

### api_get

Sends a GET request to the Brix REST API (reference: https://brixsignage.com/developers/api/) with this connection's own permissions and returns { status, body }. Find routes with list_api_routes. Query strings may be included in `path`. A route that answers with bytes (a media file, a poster, an invoice PDF, a screenshot) comes back as { contentType, totalBytes, bodyBase64 }. A 4xx/5xx returns the API's error body.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | string | yes | Absolute API path starting with /v1/ with real ids in place of :params, optionally with ?query. |

### api_create

Sends a POST request to the Brix REST API (reference: https://brixsignage.com/developers/api/) with this connection's own permissions: creates a record or runs an action (some POST routes act on screens, e.g. commands). Find POST routes with list_api_routes. Pass a JSON `body`, or `bodyBase64` + `contentType` for the few routes that take raw bytes; whole media files upload better with mint_upload_url. Pass `idempotencyKey` on a create you might retry and the API dedupes on it. Returns { status, body }; a 4xx/5xx returns the API's error body.

Permission: `screen.view`. Behavior: changes existing data.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | string | yes | Absolute API path starting with /v1/ with real ids in place of :params, optionally with ?query. |
| `body` | any | no | JSON request body. Sent as application/json. Ignored when `bodyBase64` is present. |
| `bodyBase64` | string | no | Raw request body, base64-encoded, for the routes that take bytes (POST /v1/media/:id/poster, /captions/upload, /captions/transcribe, PUT /v1/media/upload/:uploadId). Requires `contentType`. Capped at ~6 MB encoded. |
| `contentType` | string | no | Content-Type for `bodyBase64`, e.g. image/jpeg, text/vtt, audio/wav. Required with bodyBase64. |
| `idempotencyKey` | string | no | Forwarded as the `idempotency-key` header so a retried create cannot duplicate. Use any stable unique string per logical create. |

### api_update

Sends a PATCH (default) or PUT request to the Brix REST API (reference: https://brixsignage.com/developers/api/) with this connection's own permissions, changing an existing record. Find PATCH/PUT routes with list_api_routes. Pass a JSON `body`, or `bodyBase64` + `contentType` for PUT /v1/media/upload/:uploadId. Returns { status, body }; a 4xx/5xx returns the API's error body.

Permission: `screen.view`. Behavior: changes existing data.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `method` | PATCH \| PUT | no | PATCH (the default) or PUT, as the route lists it. |
| `path` | string | yes | Absolute API path starting with /v1/ with real ids in place of :params, optionally with ?query. |
| `body` | any | no | JSON request body. Sent as application/json. Ignored when `bodyBase64` is present. |
| `bodyBase64` | string | no | Raw request body, base64-encoded, for the routes that take bytes (POST /v1/media/:id/poster, /captions/upload, /captions/transcribe, PUT /v1/media/upload/:uploadId). Requires `contentType`. Capped at ~6 MB encoded. |
| `contentType` | string | no | Content-Type for `bodyBase64`, e.g. image/jpeg, text/vtt, audio/wav. Required with bodyBase64. |

### api_delete

Sends a DELETE request to the Brix REST API (reference: https://brixsignage.com/developers/api/) with this connection's own permissions. Most content deletes move the item to the recycle bin for 30 days (see list_recycle_bin); a purge route deletes permanently. Find DELETE routes with list_api_routes. Returns { status, body }; a 4xx/5xx returns the API's error body.

Permission: `screen.view`. Behavior: changes existing data, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | string | yes | Absolute API path starting with /v1/ with real ids in place of :params, optionally with ?query. |

### search_media

Search the media library by name, kind, folder or tag. Resolves media names to asset ids before building playlists, casting or assigning. Returns id/name/kind/tags/state/duration per asset; results are node-scoped to this key.

Permission: `media.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | no | Case-insensitive substring match on asset name. |
| `kind` | string | no | Asset kind filter, e.g. image \| video \| audio \| pdf \| web \| rss. |
| `folderId` | string | no | Only assets filed in this folder. |
| `tag` | string | no | Exact tag match (case-insensitive). |
| `limit` | integer | no | Page size (default 50). |
| `offset` | integer | no | Skip this many matches (default 0). |

### list_emergency_templates

List the pre-armed emergency takeover templates (Lockdown, Severe weather, Fire drill, …) with their ids, scope and severity. Pass an id to trigger_emergency_template — never guess one.

Permission: `emergency-override.view`. Behavior: read-only.

### list_pending_approvals

List approval requests (content awaiting an approve/reject decision before it airs). Defaults to pending; pass state to see decided history. Content created over MCP lands as 'draft' when the location requires approval — check here to see what's waiting.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `state` | pending \| approved \| rejected \| withdrawn | no | Filter by decision state (default pending). |
| `limit` | integer | no | Page size (default 50). |

### list_recycle_bin

List soft-deleted items across every content kind within the 30-day restore window, with what used them before deletion. Pair with restore_recycle_bin_item for 'undo that delete' requests. Deleted SCREENS appear here too — DELETE /v1/screens/:id soft-deletes into the same 30-day window.

Permission: `screen.view`. Behavior: read-only.

### update_playlist

Edit an EXISTING playlist: rename/describe it, set its play window, toggle shuffle, append items, remove items, patch item duration/fit, or reorder. Compose any subset in one call; each change runs through the same validation + approval invalidation as the CMS editor. Requires `playlist.edit` at the playlist's node.

Permission: `playlist.edit`. Behavior: changes existing data.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `playlistId` | string | yes |  |
| `name` | string | no | New display name. |
| `description` | string | no |  |
| `shuffle` | boolean | no |  |
| `fullscreen` | boolean | no | When this playlist plays inside a layout, EVERY item fills the whole screen and the other zones hide. Per-item: patchItems[].fullscreen. |
| `startsAt` | string | no | ISO datetime the playlist starts airing; null clears. |
| `expiresAt` | string | no | ISO datetime the playlist stops airing; null clears. |
| `appendItems` | array | no |  |
| `patchItems` | array | no |  |
| `removeItemIds` | array | no |  |
| `reorderItemIds` | array | no | The COMPLETE item order after the move — unknown ids are dropped, the rest keep this sequence. |

### update_media

Update a media asset's metadata: rename it, retag it, move it to another folder, set alt text, set how it fills the screen, or give it an expiry (+ optional auto-archive into the recycle bin). Bytes are never touched. Requires `media.edit`.

Permission: `media.edit`. Behavior: changes existing data, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `mediaId` | string | yes |  |
| `name` | string | no |  |
| `tags` | array | no | Replaces the whole tag list. |
| `folderId` | string | no | Destination folder in THIS workspace; null moves to the library root. |
| `altText` | string | no |  |
| `fit` | contain \| cover \| fill \| blur-fill | no | How this file fills any screen or zone it plays in: contain (whole picture, black bars), blur-fill (whole picture, blurred edges), cover (fills, crops the edges), fill (fills, distorts). Applies everywhere the file plays; a playlist slot can override it. |
| `expiresAt` | string | no | ISO datetime after which the asset stops airing; null clears. |
| `autoArchiveOnExpiry` | boolean | no | Soft-delete the asset into the recycle bin when it expires. |

### create_web_link

Add a public web page or dashboard to the media library so it can be cast, scheduled or added to a playlist. Returns the new mediaId — pass it to configure_web_link to crop the page, set a refresh, or dismiss cookie banners. For a page only the screen's own network can reach, set onScreenNetwork: the address is stored but Brix's cloud never fetches it and the screen opens it directly. Requires `media.create`.

Permission: `media.create`. Behavior: creates, reaches outside Brix.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | What operators will see in the library. |
| `url` | string | yes | http(s) address. A private/loopback address is refused unless onScreenNetwork is true. |
| `folderId` | string | no | Library folder; omit for the root. |
| `tags` | array | no |  |
| `onScreenNetwork` | boolean | no | The page lives on the screen's own network (an intranet dashboard, a device web UI). |

### configure_web_link

Change how a web link is rendered for screens: show only one part of the page, auto-dismiss cookie banners, auto-scroll a long page, set the refresh cadence, or set the browser size it is rendered at. Only the fields you pass are changed; a recorded sign-in is preserved. `showOnly` takes a CSS selector — everything else on the page is hidden, so a timetable or one dashboard panel fills the screen. Prefer an id (`#schedules`): class names change whenever the site is republished. Cropping needs a server-rendered snapshot, so setting showOnly switches the link to snapshot rendering. Requires `media.edit`.

Permission: `media.edit`. Behavior: creates, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `mediaId` | string | yes |  |
| `showOnly` | string | no | CSS selector of the one element to show. Empty string shows the whole page again. |
| `acceptCookies` | boolean | no | Click Accept on common cookie / consent / age-gate banners on every load. |
| `autoScrollPxPerSec` | number | no | Scroll a long page top-to-bottom at this speed. 0 stops auto-scrolling. |
| `refreshSeconds` | number | no | How often the page is re-loaded or re-rendered. 0 means never. |
| `renderWidth` | number | no | Browser width the page is rendered at. Match the screen; 1920 by default. |
| `renderHeight` | number | no | Browser height the page is rendered at. 1080 by default. |

### render_web_link

Force a fresh server-side snapshot of a web link instead of waiting for its next scheduled refresh. Use it right after configure_web_link to see whether a crop selector actually matched. Only meaningful for snapshot-rendered links — a live page is loaded by the screen itself. Requires `media.edit`.

Permission: `media.edit`. Behavior: creates, idempotent, reaches outside Brix.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `mediaId` | string | yes |  |

### restore_recycle_bin_item

Restore ONE soft-deleted item from the recycle bin (kind from list_recycle_bin). Playlists/screens referencing it resume automatically where they still do. Each kind's own permission applies (e.g. playlist.delete restores playlists). Purging (permanent deletion) is not part of this tool.

Permission: `screen.view`. Behavior: creates, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | screen \| media \| playlist \| schedule \| layout \| creative \| app \| data-source \| signage \| banner \| media-folder | yes |  |
| `id` | string | yes |  |

### decide_approval

Approve or reject a pending approval request (from list_pending_approvals). Rejecting requires a reason. Only works when this key's identity is actually an approver for the request's chain — the API enforces that, not this tool.

Permission: `screen.view`. Behavior: changes existing data.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `approvalId` | string | yes |  |
| `decision` | approve \| reject | yes |  |
| `reason` | string | no | Required for reject. |

### cast_content

Show EXISTING content on screens as a TIMED takeover (a cast) — screens return to their normal schedule when it expires. Scope it to everything (all), an org node subtree, or explicit screenIds. This is the tool for 'put the lunch menu on the lobby screens until 2pm'. Requires `screen.cast`.

Permission: `screen.cast`. Behavior: changes existing data, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `contentKind` | media \| playlist \| app \| creative \| schedule | yes |  |
| `contentId` | string | yes |  |
| `scopeKind` | all \| node \| screens | no | Default all — every screen this key can reach. |
| `scopeNodeId` | string | no | With scopeKind=node: the org-node subtree to cast across. |
| `screenIds` | array | no | With scopeKind=screens: explicit targets (max 500). |
| `expiresAt` | string | no | ISO datetime the cast ends. Default: one hour from now. |
| `headline` | string | no | Display name recorded for the cast. |

### end_cast

End an active cast early — its screens return to scheduled content immediately. Requires `screen.cast`.

Permission: `screen.cast`. Behavior: changes existing data, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `castId` | string | yes |  |

### quick_post

Author a headline (+ optional body) announcement and put it on screens in ONE step — the agent twin of Quick Post. Creates a branded takeover creative and casts it across the chosen scope. Needs creative.create AND screen.cast.

Permission: `screen.cast`. Behavior: changes existing data.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `headline` | string | yes |  |
| `body` | string | no |  |
| `tone` | announcement \| alert \| celebrate \| info | no | Picks the background treatment. Default announcement. |
| `scopeKind` | all \| node \| screens | no | Default all. |
| `scopeNodeId` | string | no |  |
| `screenIds` | array | no |  |
| `expiresInSec` | integer | no | How long the message stays up (default 3600). |

### create_screen_group

Create a named, saved cohort of screens (optionally seeding members). Groups are the reusable target for bulk commands, casts and assignments. Requires `screen.edit`.

Permission: `screen.edit`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `description` | string | no |  |
| `screenIds` | array | no | Initial membership; foreign/out-of-scope ids are skipped silently. |

### add_screens_to_group

Add screens to a saved screen group. Idempotent; out-of-scope ids are skipped, never fatal. Requires `screen.edit`.

Permission: `screen.edit`. Behavior: creates, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `groupId` | string | yes |  |
| `screenIds` | array | yes |  |

### remove_screens_from_group

Take screens out of a saved screen group — only the label drops, the screens themselves are untouched. Requires `screen.edit`.

Permission: `screen.edit`. Behavior: changes existing data, idempotent.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `groupId` | string | yes |  |
| `screenIds` | array | yes |  |

### sync_data_source

Pull fresh data through one connected feed (CSV, Google Sheets, POS…) now, so data-bound apps and creatives show current values. Requires `integration.edit`.

Permission: `integration.edit`. Behavior: creates, reaches outside Brix.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `dataSourceId` | string | yes |  |

### refresh_all_data_sources

Sync EVERY connected data feed in this workspace once. Use before big announcements so menus/prices are current. Requires `integration.edit`.

Permission: `integration.edit`. Behavior: creates, reaches outside Brix.

### get_proof_of_play

Proof-of-play rollup for a time window: plays, watch time, skips and failures per piece of content, plus which screens reported. Answers 'how did the promo actually do?'. Requires `playback-log.view`.

Permission: `playback-log.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | no | ISO datetime; default 7 days ago. |
| `to` | string | no | ISO datetime; default now. |
| `screenId` | string | no | Restrict to one screen's report. |

### get_screen_outages

One screen's outage history over the last N days (default 30, capped at the offline retention window): every outage with when / how long / a plain-language CAUSE (Wi-Fi dropped, power cut, Brix unreachable, …) and its evidence, plus a rollup sentence ('went down 6 times in the last 30 days, all Wi-Fi drops'). Planned darkness (operating hours) is listed but not counted. Requires `screen.view`.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `screenId` | string | yes |  |
| `days` | number | no | Window in days; default 30. |
| `limit` | number | no | Max rows; default 50, max 200. |

### get_screen_display_history

What the TV itself has been doing, which the outage history cannot show because the player is online and playing through all of it: every time the panel was switched off, switched to another HDMI input, lost its HDMI connection, or came back — newest first, each with how long the previous state lasted — plus its current state. Only a device that can read its panel (a Signage Stick over CEC) produces rows; a web or Windows player returns none and no current state. Default window is the telemetry retention (14 days). Requires `screen.view`.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `screenId` | string | yes |  |
| `days` | number | no | Window in days; default and cap = the telemetry retention (14). |
| `limit` | number | no | Max rows; default 50, max 500. |

### get_uptime_report

Uptime/SLA summary for a window: online % per screen, downtime windows (each with its cause), worst offenders first. Every screen, location and the fleet carry an `outageSummary` — cause counts, the top cause and a plain-language insight such as 'went down 6 times in the last 30 days, all Wi-Fi drops'. Requires `screen.view`.

Permission: `screen.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | no | ISO datetime; default 7 days ago. |
| `to` | string | no | ISO datetime; default now. |
| `screenId` | string | no |  |

### search_audit_log

Read the workspace audit log, newest first — who changed what, including changes made by MCP tools themselves. Keyset-paged via cursor. Requires `audit-log.view`.

Permission: `audit-log.view`. Behavior: read-only.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Page size (default 200). |
| `cursor` | string | no | nextCursor from the previous page. |

### bulk_create_celebration_entries

Import birthdays/anniversaries/etc. for the Celebrations app in bulk — the classic migration job. Max 100 per call; each needs at least a name plus month/day. Requires `integration.create`.

Permission: `integration.create`. Behavior: creates.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `entries` | array | yes |  |

## Resources

- `brix://screens`: Every screen in the workspace, with status + location + assigned content.
- `brix://playlists`: Playlists with their items (refKind/refId/duration), approval state and org node.
- `brix://schedules`: Time-of-day schedules and which screens they target.
- `brix://alerts/open`: Currently open alert events — anything offline / storage-critical / playback-failing.
- `brix://org-tree`: The hierarchical org-node structure (Spaces / districts / schools / rooms).
- `brix://media`: Images, videos, PDFs, web links and fonts — id, name, kind, folder, tags, state.
- `brix://creatives`: Canvas creatives (data-bound designs) with stage + box count.
- `brix://apps`: Installed app instances (weather, RSS, menus…) with their app key + node.
- `brix://layouts`: Multi-zone layouts a screen can be pointed at, each with its `theme` (the Brand Kit look — { source: "brand", mode?, radius? } — or null for a bare canvas). Zone geometry, bindings and per-zone `frame` / `radius` / `role` come from GET /v1/layouts/:id.
- `brix://data-sources`: Connected data feeds (CSV, Sheets, POS…) with connection + last-sync status.
- `brix://screen-groups`: Named saved cohorts of screens — the unit for bulk commands and casts.
- `brix://emergency-templates`: Pre-armed emergency takeover recipes; pass one's id to trigger_emergency_template.
- `brix://approvals/pending`: Content awaiting an approve/reject decision before it can air.
- `brix://screens/{screenId}/why`: The resolution chain (deactivated / emergency / cast / scheduled / default) for one screen.
- `brix://playlists/{playlistId}`: One playlist WITH its ordered items.
- `brix://media/{mediaId}/usage`: Where a media asset is used (playlists/creatives/layouts) and when it last played.

## Prompts

- `fleet_health_report`: A daily ops digest: fleet status, open alerts, worst uptime offenders and what to do about each.
- `diagnose_offline_screen`: Walk the likely-cause diagnosis for one screen and hand back a guided next-step checklist. Arguments: `screen_id` (optional).
- `onboard_new_location`: Set up a new org location end-to-end: create the node, home its screens there and assign starter content. Arguments: `location_name`.
- `weekly_content_review`: Proof-of-play rollup for the last 7 days: top content, skipped/failed plays, unused media worth pruning.
