Skip to content

The MCP service for AI assistants

An assistant should work with your inventory: address, authentication, tool list, confirmation requirement, limits, and errors.

16 minute readAPIUpdated: 10/25/2026

On this page

Before you start

For
Anyone who connects an assistant that speaks the Model Context Protocol, or wants to know what such an assistant can do at all.
Prerequisite
the address of the service in your environment and a credential: an oat_… access token from delegated access or an ak_… API key

The MCP service gives an AI assistant a curated selection of tools for your inventory: viewing scenes and media, checking displays and locations, proposing drafts, and, after explicit confirmation, setting a program. This page lists the address, the authentication, the tools, the limits, and the errors.

What the service is

It is a thin adapter in front of the same API that the portal and the command line use. It holds no data, knows no secret, and does not validate a credential: Your Bearer token travels unchanged on to the platform, and permission resolution remains that of the API. There is no route, field, or rule there that exists only there.

What it contributes is four things: a selection instead of a mapping of the whole API, a confirmation before every effect, limits for result size and runtime, and the guarantee that a tool deletes only one thing: a display that runs in the browser.

The service speaks English. Tool names, input fields, response fields, status values, error kinds, descriptions, and hints are English, so that an assistant reads the same names in any language. This page quotes the responses of the service verbatim.

Connect

What Value
Transport Streamable HTTP per the MCP specification
Address https://mcp.<your-domain>/mcp
Authentication Authorization: Bearer oat_… or Authorization: Bearer ak_…
Resource metadata GET https://mcp.<your-domain>/.well-known/oauth-protected-resource (RFC 9728)
Health checks GET /health/live, GET /health/ready

An assistant that supports OAuth needs only the address: Without a credential, /mcp answers 401 with the WWW-Authenticate header, which points to the resource metadata. From there, the client finds the authorization server and starts the round trip described in Delegated access with OAuth 2.1. A tool without this path enters an API key instead.

Right at the handshake, the service checks the credential once against GET /v1/me. If it is rejected, the service answers 401 with the same challenge. The client then starts authentication instead of ending up in a conversation in which every tool fails.

The credential belongs on every request, even within a session. That applies equally to GET /mcp, which opens the event stream of a session, and to DELETE /mcp, which ends it. The service checks both against GET /v1/me on every request before it even looks at the session. Without a credential, they answer 401 with error="invalid_request", and with a rejected credential 401 with error="invalid_token". The session identifier alone therefore opens nothing.

The session that the handshake opens lives for 24 hours, sliding: Every call resets the deadline. Anyone who exceeds it gets a 404 with {"error": "no_session"} on the next message, which is the signal in the MCP specification to send a new initialize. If the Mcp-Session-Id header is missing entirely or contains characters that an identifier never contains, the service answers 400 with {"error": "no_session_id"}. That is not an expiry but a malformed request.

The permissions the client asks for

The resource metadata lists under scopes_supported the permissions that the tools of this service need, exactly those and no others. The same list appears as scope in the WWW-Authenticate header, so that a client that does not fetch the metadata has it too. A client passes them on unchanged into its authentication. Without scope, the authorization server rejects the request.

On the consent page, these permissions then appear bundled into areas (“Scenes”, “Playlists & schedules”, “Media”, “Displays & locations”, “Reports”), each with one sentence and one toggle. All requested areas are switched on at the start, and a person may switch individual ones off: The consent may be a subset. Your access token then carries only the granted permissions, and the token response names them under scope.

If a tool lacks a permission, it answers permission_denied and names the area that the consent would have to include, in the words of the consent page, here in the English version (“Displays & locations”, where a German consent page says “Displays und Standorte”):

{
  "error": "permission_denied",
  "hint": "The credential lacks the permission for this action. This tool works in the area \"Displays & locations\". The consent of this connection must include it.",
  "correlationId": "0198f2c1-…"
}

With that, an assistant can tell its human which toggle to switch on in the next round trip. A consent cannot be extended afterward. The way is to revoke it and allow it again.

If a tool is added that needs a permission that no tool needed before, this list grows, and an existing access token does not grow with it. It carries the permissions that were granted at the time of its consent. The new ones are not included, and the tools behind them answer permission_denied.

whoami tells you this before anyone tries it out: It compares the permissions of your credential with those that the tools of this service need and names the missing ones under missingPermissions.

{
  "account": { "…": "…" },
  "permissionCount": 14,
  "missingPermissions": ["location.create"],
  "hint": "These tools need a new consent: disconnect the connection in the client and connect again, new permissions only appear then. If a permission is still missing afterwards, the account itself does not carry it."
}

The second sentence is no ornament: Delegated access never gets more than the account behind it has. If a permission is still in the list after allowing again, it is missing from the role of the account, and then only someone who assigns roles can help. With an API key, you instead issue a new one with the matching permission.

The permission catalog for delegated access includes all permissions that this service names, including those for displays and analytics. Every tool is therefore reachable in both ways: through delegated access and through an API key that carries the same permissions. What a tool changes remains bound to your explicit confirmation anyway (see below).

The tools

The tools fall into three risk classes, and a client shows them in this order.

Class Tools What they do
Read whoami, list_scenes, get_scene, get_scene_document, list_scene_templates, list_widgets, get_widget_schema, list_graphics, list_assets, list_players, get_player, get_player_screenshot, list_display_groups, list_locations, get_location, list_playlists, get_playlist, list_schedules, get_hygiene, validate_scene View the inventory: who the credential is, which scenes, widgets, graphics, media, displays, video walls, locations, playlists, and programs exist, what is in a scene, what can be seen on a display (including its most recent capture as an image), what is left lying around, and whether a scene document follows the rules
Propose create_scene_from_template, create_scene, update_scene, render_scene, create_playlist, add_to_playlist Create and change drafts that appear on no display as long as nobody publishes them, and view a published version as an image
Write publish_scene, set_player_program, set_display_group_program, set_player_override, clear_player_override, send_player_command, assign_player_location, create_location, set_location_opening_hours, create_web_display, issue_web_display_link, delete_web_display Publish, set a program, override and release again, issue a remote maintenance command, assign a display to a location, create a location, set its opening hours, and create a display that runs in the browser, give it a new connection URL, or clean it up again

Deletion exists exactly once, and only for displays that run in the browser. delete_web_display removes a web display together with its browser access from the inventory and frees the booked seat again. The device in the browser leaves the organization in the process. A display with a device is rejected, with an error code of its own and no detour. No other tool removes a scene, a media item, a display, or an account.

The exception has a narrow reason. Everything else that is reachable here has a counterpart in the world: a device in a room, a scene that someone has worked on, a playlist that a branch has been running for months. A web display has none. It is an address in a browser, created in a second and gone in a second. An assistant that can create one but not clean one up leaves an orphan entry with a booked seat behind it after every trial run.

Likewise missing are account and role administration, the issuing of keys, billing, and everything the platform operator does. These areas are not wired into the service, not just hidden.

A display that shows something

create_web_display is the way an assistant can show anything at all. Without it, every other tool presupposes a display that someone has connected at some point. It answers with the identifier of the display and a connection URL. The URL belongs in the answer to the person: Opening it in a browser on the target device is the one step of this process that no assistant can do itself.

Until that happens, the display appears in the fleet as “Not connected” ("status": "not_connected" in the response), with its name, its location, and, if desired, its program already set. It occupies a booked seat from the moment it is created. If all seats are taken, the service answers no_seat_available instead of a technical error. As soon as a browser has opened the URL, the display shows every further change by itself, without needing a reload.

issue_web_display_link gives the same display a new URL if the browser behind it has lost its storage. Whoever opens it takes over the display, and the browser that showed it until then loses it. Name, location, program, and the booked seat remain untouched.

The cleanup questions are not tools of their own but filters on the lists that exist anyway: unused media, published scenes without an assignment, displays without a program or without a check-in for a long time. get_hygiene counts them and names for each number the tool that lets you view the rows behind it.

Two tools of the Propose class carry confirm nonetheless: create_scene and update_scene, because content is created or replaced there that nobody has seen beforehand. render_scene carries no confirmation: It produces a preview image and nothing else.

Build a scene

The way from idea to image is a chain of six steps. Every tool description names the next one, so an assistant does not have to guess the order.

1. View the widgets. list_widgets names the widgets a scene is made of: the core widgets text, image, video, shape, graphic, and document, which are always there, and the widgets of the apps that your organization has installed. Each entry shows the identifier, app, name, a sentence, the required fields, and whether the app is installed. A widget of an app that is not installed can be written into a document but runs on no display. If your organization has booked no app, all app widgets show installed: false, and the response also says so in plain text instead of leaving you to guess.

2. Get the rules. get_widget_schema returns, for a widget, the complete JSON Schema document of its configuration (Draft 2020-12): every field with title and description, the required fields under required, and under examples at least one complete example configuration. Take an example and swap the values: That is the shortest guide there is. The initial configuration and the preview variants of the widget are included as well.

One field the schema does not name with values: the graphicId of the core widget graphic. It is a fixed identifier from the graphics library, and list_graphics lists it, with category and keywords, searchable by an intent such as “pfeil” (arrow) or “kaffee” (coffee), because the search terms are German words. It is not guessed. The schema validates it but knows no list that it could show you.

3. Build the document. A scene document has four fields:

{
  "canvas": { "width": 1920, "height": 1080, "backgroundColor": "#ffffff" },
  "layoutTemplateId": null,
  "widgets": [
    {
      "id": "wgi_01K7Z9M4T0W14MCPTEXT000001",
      "appId": "core",
      "widgetType": "text",
      "appVersionPin": "0.0.0",
      "frame": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
      "zIndex": 0,
      "config": { "content": "Welcome" }
    }
  ],
  "transitions": {
    "in": { "kind": "fade", "durationMs": 600 },
    "out": { "kind": "fade", "durationMs": 600 }
  }
}

layoutTemplateId: null is the free scene: Every widget sits solely by its frame in pixels of the canvas. With a template come named slots to which a widget can be bound through slotName, at most one widget per slot. config is the part that comes from step 2.

4. Dry run. validate_scene checks the document without saving anything: the structure, the layout bindings, the referenced media, and every configuration against the schema of its widget. The response names the widget instance and the field path for each finding:

{
  "valid": false,
  "findings": [
    {
      "severity": "BLOCKING",
      "type": "ConfigSchemaIncompatible",
      "message": "widget 'wgi_…' (text): config/content must NOT have fewer than 1 characters",
      "widgetId": "wgi_…",
      "field": "/content"
    }
  ]
}

BLOCKING prevents publishing later, WARNING does not: A widget that sticks out past the edge of the canvas is reported and stops nothing.

What the dry run reports can also appear later as an open issue on the scene, for example when a media item was deleted or an app was uninstalled. get_scene returns it under openIssues with payload and a plain-text hint that names the remedy: “Widget wgi_… is bound to the slot "main", which the scene does not have. Either it has no layout template or the template does not know this slot. Remove slotName from the widget or set layoutTemplateId to a template with this slot.”

Exactly one tool covers deletion. Only a display that runs in the browser is deleted (delete_web_display, with confirm). No tool deletes displays with a device, scenes, or media.

5. Create or change. create_scene creates a draft from the document, and update_scene replaces the document of an existing scene. Both run the dry run once more themselves and write nothing at all if a finding blocks: You get the findings back, but no half draft in the inventory. Both need confirm: true. Without it, they only describe what they would do.

Changing an EXISTING scene therefore starts one step earlier: get_scene_document returns the document of the draft and its version. Change this document, check it with validate_scene, and send it whole to update_scene, together with expectedVersion from the same response, so that someone else’s change in between is not overwritten. There is no way to change a single widget. Anyone who reinvents the document instead of reading it loses every configuration they did not know about.

6. View and refine. render_scene renders a published version and returns the image as a PNG in the result, as content and not as an address. A pure draft has no version. Publish it first with publish_scene, which does not make it appear on a display as long as none is assigned to it. The image is the only way to answer questions that are in no schema: whether the text stays in its box, whether something is covered, whether the colors work. If something is off, continue with update_scene and another publish_scene.

If your environment needs no preview image or no snapshot service is set up, render_scene says so in plain text instead of throwing an error.

What is actually on a display, in contrast, is answered by no render but only by the device itself: get_player_screenshot returns its most recent capture, likewise as an image in the result and not as an address, together with capturedAt. The capture can be a few minutes old, because devices capture at a regular interval and after every content change. If none exists yet, that is not an error: The response carries status: "no_screenshot" and the way to get one. send_player_command with command: "screenshot" and confirm: true requests one, which the device files with its next check-in. With that, you check what an assignment has actually done on the display instead of deducing it from the program.

Confirmation

Every writing tool carries the field confirm. Without confirm: true, nothing happens: The tool reads the current state and answers with a preview: which targets it would affect, what they are called, whether they are online, what would be replaced.

{
  "status": "preview",
  "executed": false,
  "hint": "Nothing was changed. Show the effect to the human, then call the same tool again with the same values and confirm: true to execute it.",
  "effect": { "…": "…" }
}

Only the second call with the same values and confirm: true executes and answers with "status": "executed", "executed": true, and the result under result. The preview is explicitly not a reservation: It holds nothing. If something changes between the two calls, the concurrency check of the API applies, not the service.

In addition, all tools carry the hints readOnlyHint and destructiveHint, so that a client can ask its own follow-up question. Every writing call travels with an Idempotency-Key that is derived from session, tool name, and arguments: The same intent takes effect once, even if it arrives twice.

Limits

Limit Default
Runtime of a tool call 30 seconds
Runtime of a single call to the API 15 seconds
Length of a result 20,000 characters, anything beyond that is visibly truncated
Entries in a list 50 per response. The number also caps the requested page size
Idle session 24 hours, sliding. Every call resets the deadline

The operator of your environment can set these values differently. Results contain no signed download URLs, and names and texts from a response are data for the assistant, not instructions. Every tool description says so explicitly.

If something goes wrong

A failed tool answers with three fields: error (one of the kinds below), hint (one sentence in English), and correlationId (the identifier with which the operations team can find the call again). The field paths and identifiers from the error body of the API are deliberately retained.

error Meaning Solution
unauthenticated The platform rejected the credential (401). Sign in again. An access token oat_… is valid for one hour.
permission_denied The credential lacks the permission (403). Include the permission in the consent, or issue the key with the matching permission.
not_found There is nothing for this identifier in this organization (404). Take the identifier from a list fetched with the same credential.
conflict The object has changed since it was read (409). Read again and decide again.
invalid_input The platform rejected the values (422). Check the values.
no_seat_available All booked seats of the organization are occupied (402). A person books an additional seat in the portal.
rate_limited The rate limit is exhausted (429). Wait, see Throttling and rate-limit headers.
platform_unreachable The API does not answer. Try again later. The service can do nothing without it.
timeout The allotted time has run out. Ask a narrower question, for example with a filter or a smaller list.
canceled The call was canceled, for example because the connection dropped. Call again.
unexpected The platform answered in a way that the service cannot classify. Try again later and give the correlationId to the operations team.

Another tenant is invisible: The service has no identity of its own with which a client could reach more than its credential carries.