Companion to FleetAlert AI

An MCP server with safety tiers

fleetalert-mcp exposes the live FleetAlert demo to Claude Code, Claude Desktop and any MCP client: alerts, investigation traces, usage, and the ability to start an investigation or confirm a fix. FleetAlert’s own safety tiers decide which tools exist on the wire, confirming a fix asks the human through elicitation, and the server adds the auth (bearer tokens, or OAuth for Claude Desktop) and rate limits the public demo API does not have itself.

Read the source and decision record, the FleetAlert write-up, the live demo it fronts, or the hosted endpoint's health.

Tiers, and where the human sits

Claude Code · Claude Desktop · any MCP clientstdio locally · Streamable HTTP + bearer tokenfleetalert-mcp — MCP server (Python SDK v2)tools registered by tier; anything above the server's tier is absent from the wireone middleware: caller's tier · token buckets · JSON audit line · /metricsa token's tier can narrow the server's, never widen itread_onlylist_alerts · get_alertget_trace · get_raw_spansget_usage · get_activityalways registeredproposes_actionstart_investigationsimulate_inbound_emailrun_telemetry_detectorspends the demo's budgetexecutes_actionconfirm_fix · reject_fixthe only tier that makessomething run upstreamoff by defaultelicitation: approve?The human, in their clientsees fix, confidence, root causeGETPOSTtoken fetched only after approvalFleetAlert API — API Gateway (public, no auth, daily cost guard)

A server at the default tier registers only the six read tools; the other two tiers do not exist on the wire until an operator raises it. Even at the top tier, confirm_fix cannot complete without a person answering in their own client, and the confirmation token moves server-to-server only after they do.

Connect your Claude to the demo

The server runs as a Lambda in the FleetAlert account. This token is read-only: your Claude can list the alerts, summarise an investigation and check the daily budget, and cannot start or confirm anything.

claude mcp add --transport http fleetalert \
  https://xx8eypbgv2.execute-api.us-east-1.amazonaws.com/mcp \
  --header "Authorization: Bearer d2067598688a6ca159dec88d3b0911805d09a2940513e869"

Then ask: “List the FleetAlert alerts and summarise the trace for the one awaiting confirmation.”

Claude Desktop and claude.ai need no token: add a custom connector with the same URL and the server’s own consent page signs it in as the read-only visitor. The server is its own OAuth 2.1 authorization server, with no database behind it (ADR-0007). An investigation started from either client shows up in FleetAlert’s Activity feed tagged mcp.

The five-minute version

01

Tools exist by tier, or not at all

FleetAlert tags every capability read_only, proposes_action or executes_action, and its model never receives the last one. The MCP server keeps the same three names. It runs at one tier, read-only by default, and tools above that tier are not registered: a client cannot list them, so a model cannot be talked into calling them. That is FleetAlert's own standard, unreachable rather than refused, carried through the protocol.

02

Confirming a fix asks the human, not the model

confirm_fix is the one tool that makes something execute upstream. It takes an alert id and nothing else. Its second parameter is resolved on the server through MCP elicitation: the client puts the proposed fix, the agent's confidence and its root-cause summary in front of the person, and only an explicit approval makes the server fetch the confirmation token and call the API. The token never appears in a tool result, and a client that cannot ask is refused before any upstream call.

03

The server is where identity starts

The demo API has no authentication; FleetAlert bounds it with fixed scenarios and a daily cost cap. Over Streamable HTTP the server requires bearer tokens, compared by digest in constant time through the SDK's TokenVerifier protocol, so a real authorization server is a one-class swap. Each token carries a tier that can narrow what the server allows but never widen it. Over stdio there are no tokens: the process boundary is the boundary, as the spec intends.

04

Refusals the model can read

One server middleware wraps every tool call: who is calling, whether their tier allows the tool, and three token buckets (per caller, a tighter one for tools that spend budget, and a global one in front of the upstream API). A refusal comes back as a tool result marked as an error, with the reason and a retry time, rather than a protocol error the model never sees. The same middleware writes a JSON audit line per call and feeds the counters /metrics reports.

05

A trace summary a model can afford

A single FleetAlert round writes a dozen or more spans with full inputs and outputs. get_trace folds them into one entry per round: entry point, outcome, proposed fix, model calls, tokens, cost and an ordered list of compact steps. get_raw_spans is there for the detail. Both carry JSON Schema output, generated from the same pydantic models the server validates against.

06

Tested without a network, then hosted for real

The SDK's in-memory client drives the real server against a mocked FleetAlert API, including the elicitation round trip for confirm_fix: approve sends exactly the token upstream, decline sends nothing, a client with no elicitation support is refused. Then the same server ran as a stdio subprocess, over HTTP with a bearer token being rejected and accepted, and finally as a Lambda behind API Gateway in the FleetAlert account, in stateless JSON mode with the elicitation state sealed by a shared key (fleetalert-mcp ADR-0005, fleetalert-ai ADR-0025). v0.3.0 added the OAuth sign-in, driven end to end through the same shim by the SDK's own OAuth client: register, consent, code, token, then tool calls as the visitor with reject_fix refused. The box above is that deployment.

Key decisions, from the record

ADR-0001
A standalone repo on the Python SDK v2. Installable with one command, versioned apart from the backend, coupled only through the public API. The v2 middleware, structured output, resolver-based elicitation and in-memory test client are each load bearing.
ADR-0002
Tiers decide which tools exist; tokens can only narrow. Registration, not call-time refusal, keeps action tools unreachable at the default tier. A read-only token on an executes-action server sees every tool and is refused on anything above its tier.
ADR-0003
Human approval through elicitation. The status model has no confirmation-token field. The token is fetched separately, only after the human's approval, and never reaches a tool result. A real build failure, deferred annotations breaking the resolver, is recorded with it.
ADR-0004
The auth and rate-limit boundary; refusals as tool results. Static bearer tokens behind the SDK's TokenVerifier. Buckets protect the upstream daily cap. A protocol error goes to the client application; an error result is text the model reads.
ADR-0007
A stateless OAuth 2.1 authorization server, so Claude Desktop can connect. Claude Desktop and claude.ai follow the MCP authorization spec, not a bearer header. The server is its own authorization server with no database: client registration, consent, code and tokens are each AES-GCM sealed under a key derived from the shared state key. The consent page signs a connector in as the read-only visitor, or as an operator's tier with their key, and scopes are tier names, so the guard reads an OAuth token exactly as a static one.

By the numbers

11
Typed tools across three safety tiers, pydantic in and out
1
Tool that can make something execute, and it asks the human first
3
Ways to run it: stdio locally, Streamable HTTP, and a Lambda behind API Gateway
37
Tests, including the elicitation round trip and the OAuth sign-in through a local API Gateway shim
0
Confirmation tokens visible to the model, by construction
7
Architecture decision records

Why an MCP server, and why this one

An MCP server is the current unit of integration for agent tooling, and most examples stop at wrapping an API. This one treats the protocol edge as a security boundary: what a model can discover, what a token can do, how often anyone can call, and which single action needs a human in the loop. The choices are written down as ADRs next to the code that enforces them, the way FleetAlert itself is documented.

It was built with Claude Code as a pair against the SDK v2 docs, which changed enough from v1 that recalled patterns were wrong; the server middleware, resolver-based elicitation and in-memory client were learned from the current reference and verified with tests before anything was written up.