# MCP server

Muvia runs an MCP (Model Context Protocol) server so that AI assistants and
agents can use Muvia directly: look up projects, read data, run queries and
propose changes. It is the richest way in; the [REST API](/reference) is the
simple, read-only one.

## Endpoint

`https://api.muviabi.com/mcp/v1`

The transport is JSON-RPC 2.0 over HTTP `POST`. Send
`Content-Type: application/json` and
`Accept: application/json, text/event-stream`; the answer arrives as a
server-sent event with one `data:` line holding the JSON-RPC response. The
protocol version is `2025-06-18`. A complete exchange is in the
[Agent quickstart](/guides/agent-quickstart).

## Authentication

| Who | How |
| --- | --- |
| A person's assistant (Claude, ChatGPT, an IDE) | OAuth. The person adds the URL as a connector, signs in to Muvia and grants scopes. The assistant acts as that person. |
| An integration or agent without a person | An `mvk_` API key, sent as `Authorization: Bearer $MUVIA_API_KEY`. It acts as the key, in the key's company, within the key's scopes. |

API keys are the same keys the REST API uses (`X-Api-Key`); see
[Authentication](/guides/authentication).

## Listing tools

Send `tools/list`. Every tool has a `name`, a `description`, an `inputSchema`
(JSON Schema) and `annotations`. Call `whoami` first, then `list_projects`:
every other tool acts inside one project. If your client cannot search a long
tool list, `find_tools` names the tools of an area (datasets, signals, nodes,
alarms, dashboards, analyses, reports, documents, predictors).

[MCP tools](/mcp-tools) lists every tool of the server, by area: what it does,
whether it reads or writes, its write mode and its arguments. `tools/list`
answers the same tools, narrowed to what your key or connection may use.

Tools return their result as `structuredContent` (an object) and as text in
`content`. Tables are `{"columns": [...], "rows": [[...]]}`. A failure is a
result with `isError: true`, not an HTTP error.

## Annotations: reads and writes

| Annotation | Meaning |
| --- | --- |
| `readOnlyHint: true` | The tool only reads. Safe to call freely. |
| no `readOnlyHint` (with `destructiveHint: false`) | The tool writes: it creates or changes something. |
| `destructiveHint: true` | The tool writes and deletes or cannot be undone. Ask the person before calling it. |

All tools carry `openWorldHint: false`: they act on Muvia only.

## Writes

A write tool does not change Muvia on the spot. It files a **proposal**, and
what happens next is the company's policy for that kind of change:

- **Ask for confirmation**: the answer is `{"status": "pending", "action_id",
  "approve_url", "summary", "needs_admin"}`. Nothing has changed. A person
  opens `approve_url` in Muvia and approves or discards it. Give them the link;
  do not retry.
- **Run immediately**: the answer is `{"status": "applied", "action_id", "id",
  "url"}`. The change is made and `url` opens it in Muvia.

Either way, `get_action` with the `action_id` tells what became of the proposal:
`pending`, `applied` (with its `result`), `discarded`, `stale`, `expired` or
`failed` (with a `reason`). Anything that needs an administrator waits for one,
whatever the policy.

### Writes by a key

A key writes only when an administrator has turned on «Can write through
assistants» on it, in Settings › Company › API keys. It is off by default, and
the key's scopes still apply: a key without `analysis_annotate` cannot annotate,
whatever the policy says. Without the switch, a write tool answers with
`isError: true` and this text, and reads are unaffected:

> This API key can only read. To propose or make changes through an assistant, an
> administrator must turn on «Can write through assistants» on the key, in
> Settings › Company › API keys.

Some tools stay a person's even for a writing key and answer «Only a person can
do this: connect the assistant with your own account.» They are: a person's own
notes and the browser drop link. Changes that belong to the person who makes
them are always proposed by a key, whatever the policy, and the person who
confirms them applies them and owns the result: creating, editing, sharing or
deleting an analysis; a notebook made from an analysis; public links to an
analysis, a dashboard or a notebook; annotations and event notes; creating a
project; Athena's memory; running a notebook; acknowledging, shelving,
unshelving or noting an alarm. Writes that need an administrator always wait for
one. Everything else (dashboards, datasets, flows, channels and the rest)
follows the company's policy.

A change made by a key appears in Muvia as authored by «API key» and the key's
name. A proposal that a person confirms is applied as that person. A person's
OAuth assistant writes within the scopes the person granted.

## whoami

The REST `GET /v1/whoami` answers the same `writes` for a key, as
`mcp.writes`. This tool answers who the connection acts as and what it may do:

| Field | Meaning |
| --- | --- |
| `kind` | `api_key` for a key, `user` for a person's assistant. |
| `company` | `{id, name}` of the company. |
| `key` | For a key: `{name, created_by}`. |
| `user` | `{id, email}` of the person, or `null` for a key. |
| `client` | `{client_id, name}` of the assistant application, or `null` for a key. |
| `scopes` | The scopes the connection holds. |
| `permissions` | The permissions those scopes resolve to. |
| `writes` | A boolean: whether this connection may write through MCP. For a key, whether «Can write through assistants» is on. |

```json
{
  "kind": "api_key",
  "company": { "id": "6aa95e431b5b18b1850a2a21", "name": "ACME srl" },
  "user": null,
  "client": null,
  "key": { "name": "Reporting agent", "created_by": "Anna Rossi" },
  "scopes": ["datasets_read", "settings_read"],
  "permissions": ["datasets_read", "settings_read"],
  "writes": false
}
```

## Limits and errors

Each key has its own limits. Reads share the REST per-key per-minute limit (the
same number as REST; see [Rate limits](/guides/rate-limits)); writes are limited
to **30 per minute per key**. Past a limit the tool answers `isError: true` with
«This key made too many requests in a minute: try again in N seconds.» or «This
key made too many changes in a minute: try again in N seconds.» Wait that long.

A call that cannot run answers `isError: true`; a malformed JSON-RPC
request answers a JSON-RPC `error`. A missing, wrong or expired key answers
HTTP `401`.
