diff --git a/docs/reference/api-tokens.md b/docs/reference/api-tokens.md index 401dead..23ea6d0 100644 --- a/docs/reference/api-tokens.md +++ b/docs/reference/api-tokens.md @@ -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: diff --git a/docs/vantage/mcp.md b/docs/vantage/mcp.md new file mode 100644 index 0000000..264a571 --- /dev/null +++ b/docs/vantage/mcp.md @@ -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. diff --git a/sidebars.ts b/sidebars.ts index 0cab3d0..7cad904 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -32,6 +32,7 @@ const sidebars: SidebarsConfig = { "vantage/status-pages", "vantage/secrets", "vantage/browser-console", + "vantage/mcp", "vantage/audit-log", "vantage/settings", ],