FleetlessFleetlessdocs
Concepts/MCP

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 /mcp goes 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, allowed or denied. The override wins in both directions: denied refuses someone whose group is switched on, allowed admits someone whose group is switched off, default follows 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.

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_list is 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.