# MCP server: operate your account from your coding agent

nisebookings runs a hosted [Model Context Protocol](https://modelcontextprotocol.io) server on top of the
[Management API](README.md). Connect Claude Code, Codex, Cursor or VS Code to it and the agent can read
your catalogue, find free times, plan and change appointments, and configure the account with tool calls —
the same operations, scopes and validation as `/api/v1`, without writing HTTP code.

| | |
|---|---|
| Endpoint | `https://app.nisebookings.com/api/mcp` |
| Transport | Streamable HTTP, stateless: `POST` one JSON-RPC message, `application/json` response (no SSE, no session id) |
| Auth | `Authorization: Bearer nk_…` — a Management API key. OAuth is not supported yet. |
| Protocol versions | `2025-06-18` (default), `2025-03-26`, `2024-11-05` |

## 1. Get a key

In the admin, go to **Settings → API → New key** and pick the scopes the agent may use (table in the
[Management API quickstart](README.md#1-get-a-key)). The key is shown **once**. Every tool call is checked
against the key's scopes exactly like the REST call it maps to: a missing scope comes back as a tool
error with status `403`, not as a connection failure, so a read-only key is a safe way to start.

| You want the agent to… | Scopes |
|---|---|
| look around (catalogue, schedules, settings, appointments) | `catalog:read`, `settings:read`, `customers:read`, `bookings:read` |
| plan, move and cancel appointments | add `customers:write`, `bookings:write` |
| set up services, staff, schedules, opening hours, coupons | add `catalog:write`, `settings:write` |
| look at or complete payments | `payments:read` / `payments:write` |

**Just trying it?** `curl -s -X POST https://app.nisebookings.com/api/sandbox` (no auth) returns a 24-hour
[sandbox account](https://guide.nisebookings.com/08-sandbox.html) with an all-scopes `apiKey` — connect MCP with that. At most **5 per hour per IP**; over that you get `429 {"error":{"code":"rate_limited"}}` and both the `Retry-After` header and the message say how long to wait (a sliding window, so usually a minute or two, not a full hour). Its mail is
never delivered; `list_outbox` shows what would have been sent.

Keep the key out of version control: every snippet below reads it from an environment variable or a
prompt. Revoke it in **Settings → API** when you no longer need it; every write that reaches the
database is audited per key (rate-limited rejections and the sandbox delete are not).

## 2. Connect

### Claude Code

```sh
claude mcp add --transport http nise-booking https://app.nisebookings.com/api/mcp \
  --header "Authorization: Bearer $NISE_API_KEY"
```

The shell expands `$NISE_API_KEY` once, so the key is stored in your user config (`~/.claude.json`), not
in the project. To share the server with a team, commit a `.mcp.json` that expands the variable at
start-up instead, so each member uses their own key:

```json
{
  "mcpServers": {
    "nise-booking": {
      "type": "http",
      "url": "https://app.nisebookings.com/api/mcp",
      "headers": { "Authorization": "Bearer ${NISE_API_KEY}" }
    }
  }
}
```

Check with `claude mcp list`, or `/mcp` inside a session.

### Cursor (`.cursor/mcp.json` or `~/.cursor/mcp.json`)

```json
{
  "mcpServers": {
    "nise-booking": {
      "url": "https://app.nisebookings.com/api/mcp",
      "headers": { "Authorization": "Bearer ${env:NISE_API_KEY}" }
    }
  }
}
```

### Codex (`~/.codex/config.toml`)

```toml
[mcp_servers.nise-booking]
url = "https://app.nisebookings.com/api/mcp"
bearer_token_env_var = "NISE_API_KEY"
```

Codex sends `Authorization: Bearer <value of NISE_API_KEY>`. Export the variable in the shell that starts
Codex. A trusted project can use `.codex/config.toml` instead.

### VS Code / GitHub Copilot (`.vscode/mcp.json`)

```json
{
  "inputs": [
    { "type": "promptString", "id": "nise-api-key", "description": "nisebookings API key (nk_…)", "password": true }
  ],
  "servers": {
    "nise-booking": {
      "type": "http",
      "url": "https://app.nisebookings.com/api/mcp",
      "headers": { "Authorization": "Bearer ${input:nise-api-key}" }
    }
  }
}
```

VS Code asks for the key once and stores it securely; the file itself holds no secret and can be committed.

### Any other client

Any MCP client that supports Streamable HTTP with a custom header works. To check the endpoint by hand:

```sh
curl -s https://app.nisebookings.com/api/mcp -H "Authorization: Bearer $NISE_API_KEY" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## 3. Tools

Tool names are snake_case; arguments are camelCase and go to the Management API unchanged, so the field
names, defaults and error codes in [`openapi.yaml`](openapi.yaml) and the [quickstart](README.md) apply.
Read-only tools carry `readOnlyHint`; deletes and whole-object replacements carry `destructiveHint`, so
clients can auto-approve reads and ask before anything changes. `whoami` and `find_slots` work with any
valid key. The client gets each tool's full description and JSON Schema from `tools/list`.

Everything else under `/api/v1` — extras, compound-service parts, notification templates, events and
ticket types, packages, coupon edits, payment completion and refunds, the Mollie key, webhook edits,
secret rotation and delivery retries — goes through
**`api_request`** `{method, path, query?, body?}` with `path` relative to `/api/v1` (for example
`{"method":"POST","path":"/services/<id>/extras","body":{"title":"Wassen","price":5}}`). Its description
lists every route.

<!-- tools:start (generated from the server's tool list) -->
| Tool | Does | Scope | Kind |
|---|---|---|---|
| `whoami` | Who am I | any key | read |
| `list_services` | List services | catalog:read | read |
| `get_service` | Get service | catalog:read | read |
| `create_service` | Create service | catalog:write | write |
| `update_service` | Update service | catalog:write | write |
| `list_categories` | List categories | catalog:read | read |
| `list_staff` | List staff | catalog:read | read |
| `get_staff` | Get staff member | catalog:read | read |
| `create_staff` | Create staff member | catalog:write | write |
| `update_staff` | Update staff member | catalog:write | write |
| `get_staff_schedule` | Get staff schedule | catalog:read | read |
| `set_staff_schedule` | Replace staff schedule | catalog:write | write (destructive) |
| `list_staff_services` | List services of a staff member | catalog:read | read |
| `assign_staff_service` | Assign service to staff member | catalog:write | write |
| `unassign_staff_service` | Unassign service from staff member | catalog:write | write (destructive) |
| `get_business_hours` | Get business hours | settings:read | read |
| `set_business_hours` | Replace business hours | settings:write | write (destructive) |
| `list_holidays` | List days off | catalog:read | read |
| `add_holiday` | Add day off | catalog:write | write |
| `delete_holiday` | Delete day off | catalog:write | write (destructive) |
| `get_settings` | Get settings | settings:read | read |
| `update_settings` | Update settings | settings:write | write |
| `list_customers` | List customers | customers:read | read |
| `get_customer` | Get customer | customers:read | read |
| `create_customer` | Create customer | customers:write | write |
| `update_customer` | Update customer | customers:write | write |
| `list_appointments` | List appointments | bookings:read | read |
| `get_appointment` | Get appointment | bookings:read | read |
| `create_appointment` | Create appointment | bookings:write | write |
| `update_appointment` | Reschedule / update appointment | bookings:write | write |
| `delete_appointment` | Delete appointment | bookings:write | write (destructive) |
| `add_customer_to_appointment` | Add customer to appointment | bookings:write | write |
| `update_booking` | Update booking status | bookings:write | write (destructive) |
| `find_slots` | Find free slots | any key | read |
| `list_outbox` | List sent e-mails | settings:read | read |
| `list_payments` | List payments | payments:read | read |
| `get_payment` | Get payment | payments:read | read |
| `list_events` | List events | catalog:read | read |
| `list_coupons` | List coupons | catalog:read | read |
| `generate_coupons` | Generate coupons | catalog:write | write |
| `list_webhooks` | List webhook endpoints | settings:read | read |
| `create_webhook` | Create webhook endpoint | settings:write | write |
| `delete_webhook` | Delete webhook endpoint | settings:write | write (destructive) |
| `test_webhook` | Send test webhook | settings:write | write |
| `list_webhook_deliveries` | List webhook deliveries | settings:read | read |
| `api_request` | Raw management API request | per route | write (destructive) |
<!-- tools:end -->

## 4. Results and errors

- A tool result is the API's JSON response as text (and as `structuredContent` when it is an object).
  A `204` comes back as `ok`.
- An API error is a tool result with `isError: true` and the API's error body plus its HTTP `status`:
  `403 forbidden` (scope missing), `404 not_found`, `409 slot_unavailable` / `capacity_exceeded`,
  `422 validation` (see `fields`) and `422 warnings`.
- **Warnings and `force`.** Creating or moving an appointment runs the admin's checks (overlap, duration,
  outside the schedule or in a break, day off). Any of these returns `422 warnings` with the reasons in
  `fields`. Show them to the human; resend with `force: true` only when they agree.
- **`notify`** defaults to `false` on appointment and booking writes, so the agent never mails a customer
  unless asked.
- Lists take `limit` (1–100, default 50) and `offset` and return `{data, total}`.
- **Webhooks.** `create_webhook` returns the signing `secret` (`whsec_…`) once — hand it to the human or
  store it where the receiver reads it. Then `test_webhook` and, a second later, `list_webhook_deliveries`
  to see whether the receiver answered 2xx. Payloads and verification:
  [Webhooks](https://guide.nisebookings.com/10-webhooks.html).
- A missing or invalid key fails the whole HTTP request with `401` and `WWW-Authenticate: Bearer`; the
  client shows the server as failed to connect.

## 5. Verify the connection

Ask the agent: *"Use nise-booking: who am I, and what are the next free times for the first service?"*
It should call `whoami`, `list_services` and `find_slots`. If the client reports the server as failed,
the key is missing, wrong or revoked (401). If a tool returns `403`, the key lacks that scope.

The whole flow (initialize, tool list, slot search, customer + appointment create/update/delete, scope
check) is covered by an automated end-to-end test.

## Limits

- API keys only; OAuth (and therefore "Connect" buttons in hosted assistants such as claude.ai) is not
  supported yet.
- No resources, prompts or server-sent notifications — tools only, and the tool list does not change.
- One JSON-RPC message per request (no batches). Browser origins other than the app's own are refused.
- MCP calls run through the same handlers in process: `/api/v1` tool calls share the key's
  **120 requests/min** budget (per server process) and always speak the latest API version, while the
  public-API tools (`find_slots`, …) have no client address and are counted per tenant, not per key.
- Not available through MCP either: creating accounts (a sandbox is created with `POST /api/sandbox`, no key)
  and dashboard statistics. Deleting a sandbox early: `api_request` `{"method":"DELETE","path":"/sandbox"}`.
