docs: document agent access over MCP and token tag restrictions

This commit is contained in:
2026-09-09 08:20:42 +00:00
parent c4bc4b68bf
commit a0c20299a5
3 changed files with 236 additions and 0 deletions
+11
View File
@@ -83,6 +83,17 @@ one, create a replacement and revoke the old key.
Restricted keys show their tags as chips beside their scopes in the list.
Unrestricted keys show nothing there, which is the common case.
The restriction applies to every API token, not only ones handed to an
[MCP agent](../vantage/mcp.md). A CI token, a monitoring script's token and
an agent's token are all held to the same tag scope check wherever the
service layer resolves servers — the mechanism does not know or care what
kind of caller is holding the token.
Nor can a token created under a restricted token reach further than its
creator: minting a new key from an already-restricted key can only narrow
the tag set, never widen or drop it. A `env=staging` token cannot mint a
token that also sees production.
## Reading the key list
Each key is one record rather than a row of bare strings:
+224
View File
@@ -0,0 +1,224 @@
---
id: mcp
title: Agent access (MCP)
sidebar_label: Agent access (MCP)
---
The Model Context Protocol is how an LLM agent — Claude, or any other MCP
client — talks to a tool server over a defined wire format instead of a
one-off integration. Connecting Vantage to an agent this way lets it answer
questions about your fleet and, if you choose to let it, act on it: run a
workflow, apply updates, or draft a step for you to review.
:::info Requires the MCP feature on your licence
Agent access is a per-instance feature you enable on a paid plan. Without it,
`/api/mcp` answers `feature_unavailable` and the token form refuses to mint a
token carrying an `mcp:*` scope. See
[Licensing and entitlements](../hq/licensing-and-entitlements.md).
:::
## Minting a token
An agent authenticates the same way any script does: with an
[API token](../reference/api-tokens.md). Two scopes govern what it can reach
on the MCP endpoint specifically, on top of whatever resource scopes
(`servers:read`, `workflows:write`, and so on) it also needs:
- **`mcp:read`** — permission to reach the MCP endpoint at all and use its
read-only tools.
- **`mcp:write`** — permission to use its write tools: running a workflow,
applying updates, assigning a key, or creating a step, workflow or monitor.
Without `mcp:write`, write tools are not merely refused when called — they
are left out of the tool listing entirely, so a read-only agent never learns
they exist. A tool it cannot see is not one it can talk a user into asking
for.
Each tool additionally needs the ordinary resource scope for what it touches
`run_workflow` needs `workflows:write`, `list_servers` needs
`servers:read`, and so on, exactly as the [scopes table](../reference/api-tokens.md#scopes)
describes for any other token. `mcp:read` or `mcp:write` decides whether the
MCP surface is open to this token at all; the resource scope decides what it
can do once there.
## Restricting a token by tag
An agent token benefits from the same [tag
restriction](../reference/api-tokens.md#tag-restrictions) every API token
can carry, and it is the main lever for keeping an agent's blast radius
small. A token restricted to `env=staging` sees only staging hosts from
`list_servers`, gets a not-found error asking about a production host by ID,
and cannot run a workflow that would touch one.
**Worked example: a staging-only agent token.** Create a token with
`mcp:read`, `mcp:write`, `servers:read`, `workflows:read` and
`workflows:write`, restrict it to `env=staging`, and give it to your agent.
It can list and inspect staging servers and run a workflow already
configured to target them. Point it at a workflow whose saved targets
include even one production host, though, and `run_workflow` refuses the
call outright — see [what an agent cannot do](#what-an-agent-cannot-do)
below for why that refusal is all-or-nothing rather than "run on the
servers it can see."
## Connecting a client
Agent access lives at `/api/mcp` on your instance, over the streamable HTTP
transport, and takes a bearer token the same way the REST API does. It has
no separate authentication of its own — a browser tab with a signed-in
session does not count, only a real API token does.
Point an MCP client at it with a configuration block like this:
```json
{
"mcpServers": {
"vantage": {
"type": "http",
"url": "https://YOUR-INSTANCE/api/mcp",
"headers": { "Authorization": "Bearer vt_your_token_here" }
}
}
}
```
The endpoint is stateless and answers POST requests only: it holds no
per-connection session, so each call is self-contained and there is nothing
to resume. A GET request — a client probing for the endpoint, or a browser
navigating to it directly — gets a `405`, by design, rather than a page. That
is the protocol-correct response from a server that does not offer the
GET/SSE half of MCP, not a misconfiguration.
## The tool list
Every tool below calls the same service code the REST API and the Vantage
UI call. Nothing in this list has its own path to the database — the MCP
surface is a presentation layer over existing authority, not a new one.
| Tool | What it does | Scope required |
| --- | --- | --- |
| `list_servers` | List servers in the fleet, optionally filtered by tags. | `mcp:read`, `servers:read` |
| `get_server` | Get one server's OS, online state and tags. | `mcp:read`, `servers:read` |
| `list_monitors` | List monitors and their current state. | `mcp:read`, `monitors:read` |
| `get_monitor_status` | Get one monitor's up/down/pending state, last check and last error. | `mcp:read`, `monitors:read` |
| `list_incidents` | List monitor incidents (outages), most recent first. | `mcp:read`, `monitors:read` |
| `get_monitor_samples` | Get one monitor's recent raw check results. | `mcp:read`, `monitors:read` |
| `list_workflows` | List workflows with step count, target count and whether each is scheduled. | `mcp:read`, `workflows:read` |
| `get_workflow` | Get one workflow's ordered steps, targets and schedule. | `mcp:read`, `workflows:read` |
| `get_run` | Get one workflow run's status and per-server counts. | `mcp:read`, `workflows:read` |
| `get_run_logs` | Get the ordered log lines for one server within one run. | `mcp:read`, `workflows:read` |
| `list_pending_updates` | List outstanding package updates, fleet-wide or for one server. | `mcp:read`, `servers:read` |
| `list_vulnerabilities` | List known CVEs affecting the fleet, one row per CVE/package pair. | `mcp:read`, `vulns:read` |
| `get_server_packages` | List installed packages on one server, optionally filtered by name. | `mcp:read`, `vulns:read` |
| `search_fleet` | Search every server's installed packages by name across the fleet. | `mcp:read`, `vulns:read` |
| `list_audit_events` | List recent audit log events. | `mcp:read`, `settings:read` |
| `list_secret_names` | List secret group and key names — metadata only, never a value. | `mcp:read`, `secrets:read` |
| `run_workflow` | Run a workflow against the servers it is already saved to target. | `mcp:write`, `workflows:write` |
| `cancel_run` | Cancel an in-progress workflow run. | `mcp:write`, `workflows:write` |
| `apply_updates` | Apply pending OS package updates on real servers. | `mcp:write`, `servers:write` |
| `update_agent` | Trigger the Vantage agent on real servers to update itself. | `mcp:write`, `servers:write` |
| `assign_key` | Assign an SSH key to real servers. | `mcp:write`, `keys:write` |
| `create_step` | Create a reusable workflow step. | `mcp:write`, `workflows:write` |
| `create_workflow` | Create a workflow from existing step IDs. | `mcp:write`, `workflows:write` |
| `create_monitor` | Create a monitor, saved disabled. | `mcp:write`, `monitors:write` |
A known limitation worth calling out on `search_fleet`: its `version_below`
argument is not implemented. Passing it gets you an error explaining that
correct version ordering depends on each host's own package manager
(dpkg, rpm, apk each order versions differently), which the tool has no way
to resolve, so it refuses rather than guess with a naive string comparison.
Omit `version_below` and it returns every matching install instead, for you
or the agent to filter.
## Running a workflow: targets are fixed, not chosen
`run_workflow` does not take a target list. A workflow runs the servers and
tags it was already saved with — an agent cannot redirect it at a different
part of the fleet on the fly. Before starting the run, Vantage checks that
those saved targets sit entirely inside the calling token's tag
restriction. If even one of them falls outside it, the whole call is
refused; there is no partial run against just the servers the token happens
to be able to see. A token scoped to `env=staging` cannot use a
production-and-staging workflow to touch only the staging half — it has to
be pointed at a workflow whose saved targets are staging-only from the
start.
## What an agent can create
Three tools let an agent propose new work rather than only look at existing
work: `create_step`, `create_workflow` and `create_monitor`. Nothing they
create is armed:
- A created **workflow** cannot be given a schedule. `create_workflow`
refuses outright if a `schedule` argument is present at all, so scheduling
stays a separate, later decision a human makes in the UI.
- A created **monitor** is always saved disabled, regardless of any
`enabled` argument. It checks nothing and alerts nobody until a human
turns it on.
- A created **step** may not reference a secret. `create_step` refuses a
`secret_refs` argument, because a script an agent wrote is not something
you want run with credentials attached before a person has read it.
Steps an agent creates are marked with a `source` of `mcp`, which the
Vantage UI uses to badge them, so a human reviewing the step library can
tell at a glance which scripts came from an agent rather than a person.
There is no tool to update or delete an existing step, workflow or monitor.
An agent can add to your instance; it cannot change or remove anything that
was already there.
## What an agent cannot do
This is the section to read before turning the feature on.
- **It cannot see a secret's plaintext.** `list_secret_names` returns group
and key names only; no tool anywhere in the MCP surface returns a
secret's value to a model.
- **It cannot open a console or a shell.** There is no MCP tool for the
browser console, SSH, RDP or VNC. An agent can trigger a workflow step
that runs a script, but it cannot open an interactive session on a
machine.
- **It cannot exceed its tag restriction.** Every tool that resolves
servers — by ID, by tag, or through a saved workflow — resolves them
through the token's own scope. A server outside that scope is invisible
to `list_servers` and `get_server`, not-found to anything asking about it
by ID, and excluded from what a write tool will act on.
- **It cannot act at all without `mcp:write`.** Every write tool needs it,
on top of its own resource scope, and a token that lacks it never even
sees those tools in the listing.
- **It cannot touch more than 25 servers in one call without saying so
explicitly.** Any write tool that would affect more than 25 servers is
refused with a message asking for `confirm:true` before it will proceed —
cheap insurance against a mis-parsed tag selector reaching the whole
fleet on one badly phrased instruction.
- **It cannot edit or delete anything that already exists.** The creation
tools only add; there is no MCP tool that updates or removes a step,
workflow or monitor a person defined.
A monitor whose runner server sits outside a token's scope is not hidden
from that token — it still appears in `list_monitors` — but the runner
field is left out, the same redaction the REST API already applies. A
workflow's target list is filtered the same way: servers outside the
token's scope are dropped from what `get_workflow` reports, and a
`targets_restricted` flag says that some were held back, without saying how
many.
## Every call is audited
Every tool call reaches the audit log, whether it read something or changed
something:
- A successful call — read or write — is recorded as `mcp.tool_call`.
- A call refused by policy — missing scope, an out-of-scope target, or a
fan-out over 25 servers without confirmation — is recorded as
`mcp.tool_denied`, naming the specific gate that refused it, so a human
reading the log can tell "the agent was stopped" from "the agent tried
and the underlying action failed."
- A write tool that reached the underlying service and that service
rejected is recorded as `mcp.tool_failed`.
- Creating a step, workflow or monitor is recorded separately as
`mcp.created`, naming what was made and its ID — a question like "what
has this agent added to my instance" should not require digging through
hundreds of read rows to answer.
See the [audit log](./audit-log.md) for how to read these alongside
everything else on the instance.
+1
View File
@@ -32,6 +32,7 @@ const sidebars: SidebarsConfig = {
"vantage/status-pages",
"vantage/secrets",
"vantage/browser-console",
"vantage/mcp",
"vantage/audit-log",
"vantage/settings",
],