---
title: MCP server
description: Connect an AI agent to one tenant with the same key, the same scopes, and the same errors as the REST API.
icon: plug
sidebar:
  order: 6
---

The Model Context Protocol endpoint puts this API in front of an AI agent. It
speaks the same operations as the REST endpoints, authenticates with the same
key, and refuses the same things for the same reasons — an agent connected with
a read-only key can read, and nothing else.

```
https://app.bloomcount.com/api/mcp
```

## Connect

Create a key in **Settings → API** with only the scopes the agent needs, then
point a client at the endpoint with that key in an `Authorization` header.

<CodeGroup>

```bash Claude Code
claude mcp add --transport http bloomcount \
  https://app.bloomcount.com/api/mcp \
  --header "Authorization: Bearer bloomcount_..."
```

```json mcp.json
{
  "mcpServers": {
    "bloomcount": {
      "type": "http",
      "url": "https://app.bloomcount.com/api/mcp",
      "headers": { "Authorization": "Bearer bloomcount_..." }
    }
  }
}
```

```bash curl
curl https://app.bloomcount.com/api/mcp \
  -H "Authorization: Bearer bloomcount_..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

</CodeGroup>

:::warning[The key is the whole boundary]
There is no session and no second credential. Whoever holds the key holds
everything its scopes allow, so give an agent its own key rather than sharing
one with a server integration, and revoke it on its own when the agent is done.
:::

## Two servers, and which one you want

Bloomcount runs two MCP servers. They do different jobs and it is worth being
deliberate about which one an agent connects to — or connecting it to both.

| | Documentation | API |
| --- | --- | --- |
| Endpoint | `docs.bloomcount.com/mcp` | `app.bloomcount.com/api/mcp` |
| Answers with | These pages | Your tenant's data |
| Tools | `search_docs`, `get_page`, `list_pages`, `get_navigation` | The 21 below |
| Authentication | None; the docs are public | An API key, on every request |
| Writes | Never | Whatever the key's scopes allow |

An agent writing an integration wants the documentation server. An agent
operating the business — pricing an event, syncing a catalogue — wants this
one. An agent doing both wants both, and they do not conflict.

## Tools

Twenty-one tools cover all 45 documented operations. They are grouped more
coarsely than the endpoints are: one `write_supplier` rather than a create, an
update, an archive and an unarchive, because an agent choosing between four
near-identical tools chooses wrong more often than one reading a `mode`.

| Prop | Type | Default | Description |
| - | - | - | - |
| `list_products?` | `products:read` | - | Page the catalogue, with free text over names and SKUs. |
| `get_product?` | `products:read` | - | One product with its variants, prices and supplier links. |
| `upsert_products?` | `products:write` | - | Create or update up to 500 products or variants in one call. |
| `list_suppliers?` | `suppliers:read` | - | Active suppliers, with free text over names. |
| `get_supplier?` | `suppliers:read` | - | One supplier by id, archived ones included. |
| `write_supplier?` | `suppliers:write` | - | Create, update, archive or unarchive a supplier. |
| `list_price_lists?` | `priceLists:read` | - | Price lists with their dates and product types. |
| `get_price_list?` | `priceLists:read` | - | One price list and its entries. |
| `upsert_price_list?` | `priceLists:write` | - | Create or update a list and its entries together. |
| `list_tax_groups?` | `taxGroups:read` | - | Tax groups and their rates. |
| `upsert_tax_group?` | `taxGroups:write` | - | Create or update a tax group. |
| `list_events?` | `events:read` | - | Events newest first, optionally filtered by status, with their totals. |
| `get_event?` | `events:read` | - | One event with its groups, lines and pricing. |
| `upsert_events?` | `events:write` | - | Create or update event headers in bulk. |
| `write_event_groups?` | `events:write` | - | Add, change or remove an event's groups in one call. |
| `write_event_items?` | `events:write` | - | Add, change or remove an event's lines; hire dates are availability-checked. |
| `list_reference?` | `products:read, venues:read, staff:read` | - | Colours, categories, venues and staff. |
| `write_reference?` | `products:write, venues:write, staff:write` | - | Create or update any of the same. |
| `list_webhooks?` | `webhooks:manage` | - | Current subscriptions and their delivery health. |
| `subscribe_webhook?` | `webhooks:manage` | - | Subscribe a URL to event changes. |
| `delete_resource?` | `the resource's write scope` | - | Delete one record, cascade refusal and all. |

Each tool call is authorised on its own. A tool the key cannot use fails with
`missing_scope` and names what it would have needed:

```json
{
  "error": "missing_scope",
  "status": 403,
  "scopes_this_tool_may_need": ["webhooks:manage"]
}
```

## What carries over from REST

Everything, deliberately. The MCP server is a translation layer, not a second
API with its own rules.

- **Scopes** are checked per call, against the same key record.
- **Errors** come back verbatim — the same status and the same snake_case code,
  including the counted ones. A refused delete still says
  `product_on_event_items:14`, and that number is the point.
- **The rate limit** is 60 requests per minute per key, shared with REST. A busy
  agent and a busy integration on one key contend for the same budget.
- **`fields`** works on every read tool. Narrowing a projection is usually a
  better move than paging, and it is much cheaper than fetching everything.
- **`idempotencyKey`** is accepted by the write tools, so a retried call after a
  dropped connection does not double-write.
- **IDs** are the same prefixed, opaque strings the REST API returns, so an ID an
  agent found through one interface works in the other.

## Failure and refusal

A tool that fails returns a normal result with `isError: true` and the API's
JSON body as its text, rather than a JSON-RPC error. That is the protocol's
intent: the model should read a refusal and decide what to do about it, which it
cannot do if the client swallows it as a transport failure. JSON-RPC errors are
reserved for what an agent cannot act on — a malformed request, an unknown
method.

Destructive tools refuse before they cascade. `delete_resource` returns `409`
with a count of what would go, and only a second call carrying `force: true`
proceeds. Events, venues, staff, colours, categories and webhooks have no
`force` at all.

## Protocol notes

- **Transport** is a plain HTTP POST returning JSON. There is no SSE stream and
  no session: the server holds nothing between calls, so there is nothing to
  resume and nothing to terminate. `GET` and `DELETE` return `405`.
- **Protocol versions** `2025-06-18`, `2025-03-26` and `2024-11-05` are
  accepted; the client's choice is echoed back when it is one of them.
- **Methods** are `initialize`, `ping`, `tools/list` and `tools/call`. There are
  no resources or prompts.
- **Batches** work. Notifications are accepted and answered with `202`.
- **A request with no key** returns `401` with a `WWW-Authenticate: Bearer`
  header, before anything else is read.

**[Authentication](/authentication)**

Scopes, rate limits and idempotency in full.

**[Errors](/errors)**

Every code a tool can hand back to an agent.
