MCP
MCP lets an AI tool (Claude Desktop, or anything else speaking the Model Context Protocol) drive a robot on a signed-in person’s behalf: read its datapoints, call its services and actions, watch its jobs, take a camera snapshot. An org admin reaches the console’s own operations the same way.
One server for the whole platform
There is a single MCP server, and one URL to hand out:
https://mcp.fleetless.dev
No path, no client id. Paste it into an AI tool, sign in with your email, done — the tool discovers everything else from the bare host, registers itself, and asks for nothing more.
https://api.fleetless.dev/mcp is the canonical resource identifier for
the same server: Fleetless mints every OAuth issuer, resource and token
audience from that base URL and checks a token against it, never against the
host a request arrived on. Both addresses reach the same process. Use the
canonical one where a tool insists on a full endpoint path, or when you are
chasing an audience mismatch; hand out the short one to people.
It is not per app, and nothing publishes it. Earlier versions gave each
app its own server at /mcp/<app>, switched on in App Settings. Both are
gone: the endpoints were deleted, and the switch beside them — which had
outlived them by a release — was removed from the app itself. Nothing on
an app configures MCP. Who may reach the one server is decided under
Settings → Groups (see below); an app’s MCP tab is a preview of what one
role would be able to do, not a setting.
What a person finds at that one URL depends entirely on who they are.
Three things this deliberately does not do:
- No live camera as a tool. A camera becomes a single “take a snapshot now” tool, not a persistent video session.
- No MCP “resources” or “prompts.” Tools only.
- No login of its own. An AI tool connecting to
/mcpgoes through the same Fleetless-hosted login as every other sign-in — see Apps, End Users & Roles.
Who is allowed in
Access is decided per person, and re-decided on every single call — not once at login. Turning access off takes effect at a caller’s next request, not at their next token refresh.
- Org admins always have access. A member of your org’s Org Admins group is let in unconditionally; no flag and no override can shut them out.
- Everyone else is gated by their group, with a per-person override.
Each customer group carries an MCP enabled flag, and each user in one
carries an override of
default,allowedordenied. The override wins in both directions:deniedrefuses someone whose group is switched on,allowedadmits someone whose group is switched off,defaultfollows the group. The group’s flag lives under Settings → Groups → the group; the per-person override is on that person’s own detail page, reached from Team or Users depending on which group they belong to.
A caller who is gated out is refused with mcp_access_denied, which says so
plainly — unlike a role refusal, this is something the caller can act on by
asking an admin.
Authentication: OAuth 2.1, identifier-first
The person only ever types in the server URL. The AI tool runs the standard OAuth 2.1 flow (discovery, PKCE, dynamic client registration) against the Fleetless-hosted login page.
Dynamic client registration means there is no client id to configure.
The authorization server advertises a registration_endpoint (RFC 7591), so
a tool registers itself the first time it connects. If a tool asks about
OAuth at all, the answers are: auto-register, no client id, no client secret
— Fleetless registers public clients only and refuses a registration that
asks to be confidential rather than silently downgrading it. A tool that
does not speak DCR can still be configured by hand with the well-known
client id fleetless-central-mcp; that path is unchanged and keeps working.
Registering buys nothing on its own. A freshly registered client still has to complete the whole login below and still has to clear the access gate above before it holds a token.
Because there is one server for every org, the login asks for an email address first and works out the rest from it — the same pattern Google and Microsoft use. One address belongs to at most one person platform-wide, so the email resolves to a user, their org, their group, and therefore which identity provider (if any) that group uses:
- the group has an OIDC provider → the browser is handed off to it, and comes back federated;
- the group has none — which includes every Org Admins group → the Fleetless password step.
An address nobody recognises gets the password step too, and then the same 401 as a wrong password: same page, same message, same cost. The server will not tell a stranger whether an address exists.
One consequence worth knowing: a brand-new federated user cannot start here. Provisioning-on-first-login needs to know which provider to use, and at a single address-first door there is no user row yet to say. Such a person signs into one of your apps once — where the group is known — and can use the MCP server by email from then on.
Roles and audit apply automatically, with the real identity behind the token.
Two catalogs, one door
Every session is offered the same tools. Fourteen robot tools, and — for
an org admin — seventy-nine console_* tools on top. Nothing is assembled per
person and nothing is left out of the list. What differs from one caller to
the next is what a call answers: which robots robots_list names, which
exposures robot_describe lists, and which calls come back refused.
This is a change from earlier releases, and it is worth stating plainly
because the old shape is the one most people still expect. A tool used to be
generated per robot and per exposed service, which made the catalog itself the
permission boundary: a model could not tell “I may not do that” from “there
is nothing here to do”, and a role change silently rewrote a client’s tool
list in the middle of a conversation. Now the list is stable and the refusals
do the talking — forbidden, naming the robot and the slug.
The fourteen robot tools
| Tool | What it does |
|---|---|
robots_list |
The robots you reach, with bridge state and published configuration version. Call it first; every other tool takes a robot_id from here. |
robot_describe |
The datasheet for one robot — see below. Consult it before calling, and again after a validation error. |
datapoint_read |
The latest value of one datapoint, with the bridge’s own capture timestamp. |
datapoint_history |
The recorded series for one datapoint over a time range. |
service_call |
Calls a service and waits for its result. |
action_start |
Starts an action. Returns the result if it finishes inside the patience window, otherwise the running job. |
jobs_list |
What is running on a robot now; with history: true, what has run — that half needs the action_history capability. |
job_get |
The job on one slug. |
job_cancel |
Stops what is running on a slug. |
publish |
Sends one message to a publisher. One message, never a stream. |
camera_snapshot |
The most recent snapshot as an image, with its capture age. |
assets_list |
The robot’s meshes and textures. Needs the assets capability, as do the two below. |
asset_get |
One asset’s metadata plus a signed download link. |
urdf_get |
A signed download link to the robot’s URDF, its mesh and texture references rewritten to signed links a loader can follow. |
service_call and action_start are one implementation: the slug decides
which kind it is, so calling the wrong one is not an error, and the pair
exists to give a model an honest description of what it is about to get back.
One of the fourteen answers under a single assignment rather than the union
of all of them. jobs_list reads running jobs across every app you hold the
robot through, but its history half reads under the one assignment whose
role grants the most on that robot — so a past job on a slug that only another
of your assignments grants is not listed. The running list has no such limit.
Ten of the fourteen carry MCP’s readOnlyHint. The four that reach the
machine — service_call, action_start, job_cancel, publish — carry
destructiveHint instead, stated rather than left to the protocol’s defaults,
because that annotation is what a client reads to decide whether a call may
run unattended.
robot_describe is the datasheet
One robot, one answer: every datapoint, service, action, publisher and camera
the caller’s role grants, each with its slug, its kind and its description;
a datapoint’s unit and decimals; and for an action, service or publisher
the parameter JSON Schema built from the configuration’s own
min/max/enum/pattern rules. decimals is the datapoint’s own
numeric.decimals — how many fraction digits the console shows the value
with, so a client can present it the same way — and is null both for a
datapoint that sets none and for every kind that has no numeric value at all. It also names the two capabilities that gate whole tools rather than
slugs — action_history and assets.
Everything the role grants is listed, described or not. A missing
description is reported as description: null; it no longer removes the
exposure. The tool still works, the model just has less to go on — which makes
writing descriptions a quality decision rather than a switch, and makes a
granted slug visible to the developer who granted it.
A robot with nothing published yet answers with an empty exposure list rather than a refusal. “I may act on this robot and it has not been configured” is a real state, and the caller is entitled to it.
The console_* catalog
An org admin is additionally offered one tool per console operation, named
from resource and verb, each one calling the same service the console’s own
REST route calls — so it validates identically and audits as the admin.
A caller who is not an org admin is offered none of them, and is refused
forbidden for calling one anyway.
| Family | Tools | Covers |
|---|---|---|
| Robots | 10 | Create, rename, describe, set details, delete (with a preview of what a deletion takes), exposures, asset sync and its status |
| Configuration | 8 | Read and replace the draft, publish, list and restore versions, slug usage and renames |
| Introspection | 4 | The cached ROS graph and its refresh; stored message types and fetching more |
| Apps | 9 | Apps, their group, their branding |
| Roles | 5 | Roles, their permissions, and the per-role MCP preview |
| Server keys | 4 | List, create, rotate, revoke |
| App OAuth clients | 4 | The clients that may send an app’s end users through login, and consent withdrawal |
| Groups | 6 | Groups and a preview of what deleting one costs |
| Group identity providers | 3 | Read, set and remove a group’s OIDC provider |
| Users | 9 | Users, their group, their app assignments |
| Invitations | 4 | List, invite, re-issue a link, revoke |
| Org | 4 | The org itself, its quotas and its metered usage |
| Federation | 2 | The org’s federation policy |
| Audit | 1 | The org’s audit events |
| Fleet health | 6 | Firing alerts, job runs and their summary, bridge latency, resource health |
Annotations follow the same discipline as the robot half: readOnlyHint on
every read; destructiveHint on every delete, revoke, move, publish and
restore, and on the two puts that can withdraw access — a role’s permissions
and a group’s identity provider; idempotentHint on the put-shaped ones.
Two things worth knowing before you drive a configuration through this:
console_config_draft_put replaces the whole fleetless.yaml — there are
no targeted edit operations, on purpose — and console_org_get is the one
console tool with no REST twin, because the console reads the org off its own
session endpoint and an MCP session has no browser session to describe.
Signed asset links
An MCP session token is refused on the REST API by design, so an agent holding
one cannot fetch /api/robots/:id/assets/:assetId itself. asset_get and
urdf_get therefore mint a link at /api/asset-links/:token that needs no
sign-in at all: a signed payload naming robot, asset and expiry, valid for
15 minutes. A URDF served through such a link has its mesh and texture
URIs rewritten to links of the same expiry, so a loader can follow them.
The residual, stated rather than implied: a link is a bearer capability for
its lifetime. Whoever holds it reads that asset. The capability check — the
assets permission on the caller’s role — runs when the link is minted, not
when it is fetched, and there is no revocation list; the 15 minutes are the
whole of the containment. A mint is audited with robot, asset and expiry. A
fetch is not, because nothing identifies the fetcher.
What the server tells a client at connect time
The protocol hands a client instructions once, before any tool is called,
and it is the only place an ordering can be stated — a tool description says
what one tool does, and nothing in a fourteen-tool catalog says “start here,
then that”. What Fleetless sends: start at robots_list, then
robot_describe; an empty robots_list is a normal answer and not an error;
a validation_error names field and rule, so consult robot_describe and
retry; an action that outlives its patience window returns a job to poll;
signed links expire after 15 minutes; before editing a configuration read the
introspection snapshot and refresh it if stale; a draft put replaces the
whole document; secrets come back once.
Not tools, deliberately
- Deleting an org, and promoting someone to owner — the two operations the design excludes outright. Neither has a tool under any name.
- Self-account routes — reading or editing your own account, changing your own password. Those belong to the login surface; the account a session acts as is not something that session should edit.
- The audit CSV export, which answers a file attachment for a human to
keep.
console_audit_listis the readable half of the same data. - Live camera sessions, and streams of any kind. A camera is a snapshot tool; a model cannot hold a video session open.
Configuring a group and configuring its identity provider are tools, which they were not in the previous release. Client registration and provider configuration are app and group configuration, not the authorization flow itself, and the console operations for them are ordinary admin work.
Secrets are returned once
console_robot_create returns the robot’s bridge token, and
console_server_key_create and console_server_key_rotate return the key.
Each comes back once, in the tool result, and no tool reads it back
afterwards; a lost secret is replaced by rotating, never by looking it up.
The listing tools carry metadata only. Per-app OAuth clients are the
exception that needs no rule: they are public clients with no secret at all,
so there is nothing for console_app_oauth_client_create to hand over once.
Where to go next
The MCP server recipe has the full developer-facing detail: writing descriptions well, with worked examples per exposed kind; how a model works its way through the catalog; and the end-user side of pasting a URL into an AI tool. Both catalogs are listed tool by tool, with their REST twins, in the API reference.