# MCP server

> Connect an AI agent to your workspace's records through MCP tools: every API read, and with a read-and-write key, adding counterparties, invoices, milestones and payee links.

Vestiarion runs a remote [MCP](https://modelcontextprotocol.io) server. Connect an AI agent to it with a workspace API key, and the agent can answer questions from your workspace's own records: which payments are held and why, whether the ledger is intact, what is due to contractors.

Each tool is one of the API's operations. A tool call runs that operation with your key and returns its JSON exactly as the API does. With a read-only key, an agent connected here can read and explain, and nothing more. With a [read-and-write key](https://www.vestiarion.xyz/docs/get-started/authentication#scopes), it can also add counterparties, invoices and milestones, which the agent in Vestiarion then decides as it decides any other, and make payee links. No tool verifies work, approves or pays.

## The server

```text
https://www.vestiarion.xyz/api/mcp
```

- **Transport:** Streamable HTTP. The server keeps no session, so a client sends every message as a `POST`. A `GET` or a `DELETE` with a valid key answers `405`: there is no event stream to open and no session to end.
- **Protocol versions:** the 2026-07-28 revision of MCP, and clients on the earlier revisions, such as 2025-06-18 and 2025-11-25, at the same URL.
- **Surface:** tools only. The server has no resources and no prompts.

## Authentication

Send a [workspace API key](https://www.vestiarion.xyz/docs/get-started/authentication) in the `Authorization` header, exactly as for `/api/v1`:

```http
Authorization: Bearer <key>
```

The key is checked before any MCP message is read:

- A missing, malformed, unknown or revoked key answers `401` with the API's own error body and a `WWW-Authenticate: Bearer realm="vestiarion"` header.
- A key without the `read` scope answers `403`, with `error="insufficient_scope"` in `WWW-Authenticate`. Every key issued today has that scope.
- The header is the only place a key is read. A key in the query string or in an MCP message's `_meta` is ignored: without the header, the request answers `401`.
- If the key cannot be looked up at all, the answer is `500`, not `401`: retry later, and keep the key. See [When authentication fails](https://www.vestiarion.xyz/docs/get-started/authentication#when-authentication-fails).

Every tool call reads the key's own workspace and no other. Asking `get_counterparty` for another workspace's counterparty answers `not_found`, as the API does.

## Tools

One tool per [API operation](https://www.vestiarion.xyz/docs/api). Its name is the operation's id in snake case, its arguments are the operation's parameters, or for a write the fields of its request body and an optional `idempotencyKey`, with their types, allowed values and bounds, and its description, which is what the agent reads, is the operation's summary and description from its reference page. A read's tool is marked read-only and idempotent. The four write tools are marked as adding records: not read-only, and not idempotent.

- `create_counterparty`, `create_invoice` and `create_milestone` are not destructive. A repeat adds another record, unless it passes the same `idempotencyKey`.
- `create_payee_link` is marked destructive, because a new link revokes the payee's unused one. It takes no `idempotencyKey`: the link is in its answer only, so a repeat makes a new link.

| Tool | What it answers | Reference |
| --- | --- | --- |
| `get_status` | Get workspace status | [`GET /api/v1/status`](https://www.vestiarion.xyz/docs/api/get-status) |
| `list_ledger_entries` (arguments: `limit`, `cursor`, `domain`, `actor`) | List ledger entries | [`GET /api/v1/ledger`](https://www.vestiarion.xyz/docs/api/list-ledger-entries) |
| `verify_ledger` | Verify the ledger | [`GET /api/v1/ledger/verify`](https://www.vestiarion.xyz/docs/api/verify-ledger) |
| `list_invoices` (arguments: `limit`, `cursor`, `direction`, `status`, `counterpartyId`) | List invoices | [`GET /api/v1/invoices`](https://www.vestiarion.xyz/docs/api/list-invoices) |
| `create_invoice` (arguments: `direction`, `counterpartyId`, `amount`, `currency`, `dueDate`, `memo`, `poReference`, `goodsReceived`, `earlyPayDiscount`, `idempotencyKey`) | Add an invoice | [`POST /api/v1/invoices`](https://www.vestiarion.xyz/docs/api/create-invoice) |
| `list_counterparties` (arguments: `limit`, `cursor`, `role`, `riskLevel`) | List counterparties | [`GET /api/v1/counterparties`](https://www.vestiarion.xyz/docs/api/list-counterparties) |
| `get_counterparty` (arguments: `id`) | Get a counterparty | [`GET /api/v1/counterparties/{id}`](https://www.vestiarion.xyz/docs/api/get-counterparty) |
| `create_counterparty` (arguments: `name`, `role`, `address`, `chain`, `jurisdiction`, `paymentLimit`, `noticeEmail`, `idempotencyKey`) | Add a counterparty | [`POST /api/v1/counterparties`](https://www.vestiarion.xyz/docs/api/create-counterparty) |
| `create_payee_link` (arguments: `counterpartyId`) | Create a payee link | [`POST /api/v1/payee-links`](https://www.vestiarion.xyz/docs/api/create-payee-link) |
| `list_milestones` (arguments: `limit`, `cursor`, `status`, `contractorId`) | List milestones | [`GET /api/v1/milestones`](https://www.vestiarion.xyz/docs/api/list-milestones) |
| `create_milestone` (arguments: `contractorId`, `title`, `amount`, `verificationSource`, `idempotencyKey`) | Add a milestone | [`POST /api/v1/milestones`](https://www.vestiarion.xyz/docs/api/create-milestone) |
| `get_treasury` | Get the treasury | [`GET /api/v1/treasury`](https://www.vestiarion.xyz/docs/api/get-treasury) |
| `get_insights` | Get insights | [`GET /api/v1/insights`](https://www.vestiarion.xyz/docs/api/get-insights) |

What a call returns:

- **A success** is the API's JSON, as text and as structured content.
- **An API error** is a tool error (`isError`) whose text is the API's error body, such as a `400 invalid_request` for a cursor from another endpoint, or a `404 not_found` for an unknown id. The agent reads the message and corrects its call. A read-only key calling a write tool gets the API's `403 forbidden` the same way. The error codes are on [Errors](https://www.vestiarion.xyz/docs/get-started/errors).
- **A collection** is one page, 50 items unless the agent passes `limit`, up to 200. The agent passes `page.nextCursor` back as `cursor` for the next page, as described in [Pagination](https://www.vestiarion.xyz/docs/get-started/pagination).
- **Nothing is cut.** A tool returns the whole response, `get_insights` included, as the API documents it.

## Setup

Keep the key in the `VESTIARION_API_KEY` environment variable. None of the blocks below writes the key itself into a file.

### Claude Code

```bash
claude mcp add --transport http vestiarion https://www.vestiarion.xyz/api/mcp \
  --header "Authorization: Bearer $VESTIARION_API_KEY"
```

Your shell puts the key into the command, and Claude Code saves the header in its own configuration, `~/.claude.json`. To share the server with a project instead, without the key, add it to `.mcp.json` at the project root. Claude Code expands `${VESTIARION_API_KEY}` from the environment when it starts:

```json
{
  "mcpServers": {
    "vestiarion": {
      "type": "http",
      "url": "https://www.vestiarion.xyz/api/mcp",
      "headers": {
        "Authorization": "Bearer ${VESTIARION_API_KEY}"
      }
    }
  }
}
```

### Cursor

Add the server to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` for every project. Cursor resolves `${env:VESTIARION_API_KEY}` from the environment:

```json
{
  "mcpServers": {
    "vestiarion": {
      "url": "https://www.vestiarion.xyz/api/mcp",
      "headers": {
        "Authorization": "Bearer ${env:VESTIARION_API_KEY}"
      }
    }
  }
}
```

### Codex

Add the server to `~/.codex/config.toml`, or to `.codex/config.toml` in a trusted project. `bearer_token_env_var` names the variable Codex reads the key from and sends as `Authorization: Bearer`:

```toml
[mcp_servers.vestiarion]
url = "https://www.vestiarion.xyz/api/mcp"
bearer_token_env_var = "VESTIARION_API_KEY"
```

### Clients that only start local servers

A client that runs MCP servers as local commands over stdio can reach this one through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), which relays between stdio and the remote server:

```json
{
  "mcpServers": {
    "vestiarion": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://www.vestiarion.xyz/api/mcp",
        "--header",
        "Authorization: Bearer ${VESTIARION_API_KEY}"
      ]
    }
  }
}
```

`mcp-remote` replaces `${VESTIARION_API_KEY}` from its own environment, so the key never appears in the arguments. If your client does not pass `VESTIARION_API_KEY` on to the servers it starts, set the variable in that server's `env` entry.

Some clients do not escape spaces inside `args` when they start `npx`; the `mcp-remote` README names Cursor, the Codex CLI and Claude Desktop on Windows. There, write the header as `Authorization:${VESTIARION_AUTH_HEADER}`, with no spaces, and set `VESTIARION_AUTH_HEADER` to `Bearer ` followed by the key.

## Questions to ask

Once the server is connected, ask in plain language. The agent picks the tools:

- "Is our ledger intact, and are any payments held? Why was each one held?" uses `verify_ledger`, then `list_invoices` and `list_milestones` with `status: "held"`, whose entries carry the agent's reasoning.
- "Which contractor milestones are verified but not paid yet?" uses `list_milestones`.
- "Which counterparties are high risk, and what did their latest screening find?" uses `list_counterparties` with `riskLevel: "high"`, then `get_counterparty`.
- "How much do we hold in reserve, and what does the latest liquidity forecast say?" uses `get_treasury`.
- "Are payments on this workspace live or simulated?" uses `get_status`.
- With a read-and-write key, "Add Quill Studio's invoice for 420 USDC against PO-4012, due October 31; the work was delivered" uses `list_counterparties` to find Quill Studio, then `create_invoice`.
- With a read-and-write key, "Pay Mona 150 USDC for https://github.com/acme/app/pull/42 once it is merged" uses `list_counterparties` to find Mona, then `create_milestone`.

## What it does not do

- **It does not decide.** Approving or rejecting a payment, confirming an address, pausing the agent, and managing members, API keys or webhooks are a person's decisions, made in the console. No tool can make them, and the write tools only add records.
- **It has no OAuth.** The server authenticates with a key in a header and has no authorization server, so it cannot be added as a claude.ai web connector, which needs OAuth. Use a client that sends a header, like the ones above.
