> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rxscale.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Management API MCP

> Connect AI clients (Cursor, Claude Desktop, ...) to the Management API over MCP

# Management API MCP

The Management API exposes an [MCP](https://modelcontextprotocol.io) (Model Context Protocol)
endpoint so AI clients — Cursor, Claude Desktop, and other MCP-compatible tools — can read and
write your organisation's data directly, without a custom integration. It is the same
Management API you already use over REST: MCP is an additional transport on the same service,
exposing a curated **allowlist** of tools rather than every REST route.

> [!NOTE]
>
> MCP is additive. Your existing API key integrations, webhooks, and REST calls are completely
> unaffected — this page only concerns connecting an AI client.

## Connect

```
https://api.rxscale.com/v1/management/mcp
```

Add this URL to your MCP client's server configuration. Cursor, for example, supports
[static OAuth client credentials](https://cursor.com/docs/mcp) for a remote server that
issues its own fixed client ID rather than supporting Dynamic Client Registration — which is
how this endpoint is set up. RxScale provisions an Auth0 application for your organisation
and gives you its client ID; put that in `mcp.json`'s `auth` block instead of a bearer token:

```json theme={null}
{
  "mcpServers": {
    "rxscale-management": {
      "url": "https://api.rxscale.com/v1/management/mcp",
      "auth": {
        "CLIENT_ID": "${env:RXSCALE_MCP_CLIENT_ID}",
        "scopes": ["order:read", "product:read", "questionnaire:read"]
      }
    }
  }
}
```

Set `RXSCALE_MCP_CLIENT_ID` in your shell environment rather than writing the client ID
literally into the file — Cursor interpolates `${env:...}` in the `auth` block the same way
it does in `headers`. Always set `scopes` explicitly to the list of permission strings (see
[Permissions](/permissions#management-api)) your RxScale account manager has confirmed are
provisioned for your client — request only what your use of the client actually needs.

> [!WARNING]
>
> Do not omit `scopes` to "get everything." Cursor's documented fallback for an omitted
> `scopes` field is to discover `scopes_supported` from the Auth0 tenant's own
> `/.well-known/oauth-authorization-server` metadata — the authorization server's tenant-wide
> list, not a list scoped to this MCP resource specifically (RxScale's Auth0 tenant hosts
> other APIs' scopes too). Cursor does not derive per-API permissions from this resource's
> RFC 9728 metadata or from Auth0 AS metadata; the only reliable way to get exactly the
> permissions your client should have is to list them explicitly in `scopes`.

> [!NOTE]
>
> Dynamic Client Registration is not enabled for this rollout — the client ID above is one
> RxScale issues to your organisation, not one Cursor registers on the fly. Before sign-in
> can work at all, two
> things must exist out-of-band: ask your RxScale account manager for a client ID, and have
> them register Cursor's fixed OAuth redirect URL(s) against it in Auth0 — Cursor documents
> fixed callback URLs per surface, not a single one:
>
> * Web and Cursor Agents: `https://www.cursor.com/agents/mcp/oauth/callback`
> * Desktop app: `http://localhost:8787/callback`
>
> Register both if your organisation's users sign in from both the web/Agents surface and the
> desktop app; register only the one you use otherwise. Sign-in fails with a redirect-URI
> mismatch until the client ID is provisioned and the redirect URL(s) you need are registered.
> Only once both of those exist does connecting prompt you to sign in through your
> organisation's Auth0 login page — there is still no API key or client secret to paste into
> the config file, but there is out-of-band setup RxScale must do first.

> [!TIP]
>
> If your client only supports a static header rather than an `auth` block for OAuth — or you
> want a non-interactive token for a script — request one yourself with the client
> credentials grant below and reference it the same way Cursor's `headers` field supports
> environment interpolation:
>
> ```json theme={null}
> {
>   "mcpServers": {
>     "rxscale-management": {
>       "url": "https://api.rxscale.com/v1/management/mcp",
>       "headers": {
>         "Authorization": "Bearer ${env:RXSCALE_MCP_TOKEN}"
>       }
>     }
>   }
> }
> ```

## Authentication

The MCP endpoint accepts the same credentials as the rest of the Management API — see
[Authentication](/authentication#management-api-api-key-or-oauth-bearer-token) for the full
details — with one important difference: **its Bearer tokens are issued for a separate OAuth
audience from the REST API**.

| | Audience |
| - | - |
| REST (`/v1/management/*`) | `https://api.rxscale.com/v1/management` |
| MCP (`/v1/management/mcp`) | `https://api.rxscale.com/v1/management/mcp` |

A token issued for one audience is rejected on the other. Requesting a token for the MCP
audience uses the same OAuth 2.0 client credentials grant described in
[Authentication](/authentication#getting-a-bearer-token), with `audience` set to the MCP
endpoint's own URL:

```bash theme={null}
curl -X POST "https://YOUR_AUTH0_DOMAIN/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "your-client-id",
    "client_secret": "your-client-secret",
    "audience": "https://api.rxscale.com/v1/management/mcp",
    "grant_type": "client_credentials",
    "scope": "order:read product:read"
  }'
```

`scope` is a space-separated list of the same permission strings used on REST (see
[Permissions](/permissions#management-api)) — request only what the calling client needs;
the resulting token's tools are limited to those, exactly like an API key's permissions.

> [!WARNING]
>
> A machine-to-machine application's token carries `sub` = `<client_id>@clients`, and
> RxScale must register that `sub` as a service principal in your organisation before it
> can call anything — scopes alone are not sufficient. Until then the endpoint answers
> `401` with `{"code": "user_not_found"}`. See
> [Getting a Bearer token](/authentication#getting-a-bearer-token).

> [!TIP]
>
> For an interactive AI client (Cursor, Claude Desktop), you generally do not need to run this
> request yourself. The client discovers the required Auth0 authorization server from the MCP
> endpoint's metadata and takes you through sign-in with the RxScale-provided Auth0
> application. Use the client credentials grant above only for a custom or headless client
> that does not implement OAuth discovery — set the resulting `access_token` as an
> `Authorization: Bearer` header (or your client's equivalent custom-header setting).

API keys (`X-API-Key`) also work on the MCP endpoint, scoped by the same permissions as on
REST — useful for a client that only supports a static credential.

### Client compatibility (OAuth discovery)

An unauthenticated request to the MCP endpoint returns `401` with a `WWW-Authenticate` header
pointing at this resource's [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) Protected
Resource Metadata document:

```
GET /.well-known/oauth-protected-resource/v1/management/mcp
```

```json theme={null}
{
  "resource": "https://api.rxscale.com/v1/management/mcp",
  "authorization_servers": ["https://auth.rxscale.com/"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": [
    "doctor:read",
    "doctor_statistics:read",
    "listing_request:read",
    "order:read",
    "patient:read",
    "prescription:read",
    "product:read",
    "product:write",
    "questionnaire:read",
    "questionnaire:write",
    "review_link:read",
    "scheduling:read",
    "statistics:read",
    "wallet_pass:read",
    "wallet_pass_template:read"
  ]
}
```

Strict RFC 9728 clients (for example VS Code's MCP support) require the metadata document's
`resource` field to be the exact canonical URL of the MCP endpoint itself — not the broader
Management API root — which is why the MCP audience above is the `/mcp` URL rather than
`https://api.rxscale.com/v1/management`. This discovery flow only gets a client to the point
of finding the authorization server and initiating sign-in; it does not remove the
prerequisite described in [Connect](#connect) above — RxScale must still provision your
organisation's client ID and register the exact redirect URL(s) your client uses in Auth0
before that sign-in can succeed.

### Browser-based clients and Origin allowlisting

A request that carries an `Origin` header — which only a browser sends — is checked against
this service's allowlist *before* authentication runs. An origin that isn't on the allowlist
is rejected outright, with `403` and no attempt to read credentials:

```json theme={null}
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Origin is not allowed"}}
```

Every non-browser MCP client — Cursor, Claude Desktop, a script using `curl` or an HTTP
library — never sends `Origin` at all and is completely unaffected by this check; it exists
to stop a web page from driving this endpoint through your browser's cookies via DNS
rebinding, an attack that has no relevance to a non-browser caller. It only matters if you
are building your **own browser-based** MCP client — a web app that calls this endpoint
directly from JavaScript running in a page. If that's your case, contact your RxScale account
manager with the exact origin (`scheme://host[:port]`, e.g. `https://app.example.com`) you
need allowlisted; it cannot be added by the client itself, and there is no self-service way to
register one.

## Allowed Tools

`tools/list` only ever returns the tools your token's permissions allow — the same
`EnumAPIKeyPermission` scopes checked on REST. Every tool dispatches through the identical
handler and serialization path as its REST equivalent, so the two surfaces cannot return
different JSON for the same data.

| Tool | REST equivalent | Required permission |
| - | - | - |
| `health_check` | `GET /health` | *(any valid credential)* |
| `list_orders` | `GET /orders` | `order:read` |
| `get_order` | `GET /orders/{order_uid}` | `order:read` |
| `get_prescription` | `GET /prescriptions/{prescription_uid}` | `prescription:read` |
| `list_products` | `GET /products` | `product:read` |
| `get_product` | `GET /products/{product_uid}` | `product:read` |
| `create_product` | `POST /products` | `product:write` |
| `update_product` | `PATCH /products/{product_uid}` | `product:write` |
| `list_doctors` | `GET /doctors` | `doctor:read` |
| `get_doctor_statistics` | `GET /doctors/{doctor_uid}/statistics` | `doctor_statistics:read` |
| `list_doctors_statistics` | `GET /statistics/doctors` | `statistics:read` |
| `get_dashboard_statistics` | `GET /statistics/dashboard` | `statistics:read` |
| `list_questionnaires` | `GET /questionnaires` | `questionnaire:read` |
| `get_questionnaire` | `GET /questionnaires/{questionnaire_uid}` | `questionnaire:read` |
| `create_questionnaire` | `POST /questionnaires` | `questionnaire:write` |
| `update_questionnaire` | `PATCH /questionnaires/{questionnaire_uid}` | `questionnaire:write` |
| `update_questionnaire_model` | `PATCH /questionnaires/{questionnaire_uid}/model` | `questionnaire:write` |
| `get_questionnaire_submission_statistics` | `GET /questionnaires/{questionnaire_uid}/statistics/submissions` | `statistics:read` |
| `find_patient` | `GET /patients?email=` | `patient:read` |
| `get_patient` | `GET /patients/{patient_uid}` | `patient:read` |
| `list_appointments` | `GET /scheduling/appointments` | `scheduling:read` |
| `list_appointment_types` | `GET /scheduling/appointment-types` | `scheduling:read` |
| `list_listing_requests` | `GET /listing-requests` | `listing_request:read` |
| `get_listing_request` | `GET /listing-requests/{uid}` | `listing_request:read` |
| `list_wallet_pass_templates` | `GET /wallet-passes/templates` | `wallet_pass_template:read` |
| `list_wallet_passes` | `GET /wallet-passes` | `wallet_pass:read` |
| `list_review_links` | `GET /review-links` | `review_link:read` |

> [!WARNING]
>
> `get_questionnaire_submission_statistics` requires `statistics:read`, **not**
> `questionnaire:read` — it wraps a statistics endpoint, so it is gated the same way its REST
> route is, even though it is grouped with the other questionnaire tools by subject.

> [!NOTE]
>
> `health_check` still requires *some* valid credential — an API key, or a Bearer token
> issued for the **MCP audience specifically** — even though it needs no permission. A
> Bearer token issued for the REST audience is rejected here just like on any other tool,
> because the MCP route validates against `MCP_API_AUDIENCE`, not the REST `API_AUDIENCE`
> (see [Authentication](#authentication) above). This differs from the REST `GET /health`
> endpoint, which requires no authentication at all: every MCP call, including
> `health_check`, first goes through this endpoint's own authentication, while `/health` is
> deliberately left open so infrastructure can probe it unauthenticated.

### Not available over MCP

Only the tools listed above exist. There is no generic "call any REST endpoint" tool, and
adding a new tool is a deliberate catalog change on RxScale's side — a tool never appears
just because a REST route exists.

**REST-only Management API operations.** These endpoints exist and behave exactly as
documented on their own REST reference pages; they are simply not on the MCP allowlist in
the current release:

* **Order intake and fulfillment** — creating an order, updating one, creating a
  fulfillment, and cancelling a fulfillment
  ([Order Intake](/api-reference/management/order-intake))
* **Prescription signing** — rendering a prescription PDF or marking one externally signed
* **Notification subscriptions (webhooks)** — the entire `/notification-subscriptions/`
  resource: listing, creating, removing, and test-sending
  ([Webhooks](/api-reference/management/webhooks))
* **Waiting room** — registering, checking the status of, or cancelling a waiting-room entry
  ([Waiting Room](/api-reference/management/waiting-room))
* **Listing request decisions** — accepting or declining a pending listing request (listing
  and getting a listing request *are* on MCP; only the accept/decline actions are REST-only)
* **Wallet pass verification and push notifications** (listing templates and passes *is* on
  MCP)
* **Patient profile field deletion**
* **Patient intent status** — `GET /patients/{patient_uid}/intent/{intent}` (listing and
  getting a patient by UID or email *is* on MCP; this narrower lookup is REST-only)
* **Review link create and revoke** — `POST /review-links` and
  `DELETE /review-links/{review_link_uid}` (listing review links *is* on MCP)
* **Doctor blacklisted SKUs** — the entire `/doctors/{uid}/blacklisted-skus` resource
* **Scheduling administration writes** — cancelling an appointment, and all appointment-type
  and doctor-availability-rule/date-override CRUD (only listing appointments and appointment
  types is on MCP)

**Not available via the Management API at all — not on REST, and therefore not on MCP
either.** These are not missing MCP tools; the underlying capability isn't exposed by this
API for any client today, so there is no REST page to link to:

* Deleting a product or a questionnaire
* Questionnaire authoring features (custom CSS, file assets, archiving, duplication) —
  Admin Tool-only
* Nested SKU attribute graphs beyond the create/patch shape documented on
  [Products](/api-reference/management/products)
* Statistics CSV exports
* Referral-scan and referral-conversion statistics

Use the [Management API REST reference](/api-reference/management/overview) for the first
group of operations.

## Calling a Tool

The MCP endpoint speaks JSON-RPC 2.0 over a single `POST` route — no separate handshake is
required. `tools/call` takes the tool `name` and its `arguments`, matching the tool's
`inputSchema` from `tools/list`:

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/management/mcp" \
  -H "Authorization: Bearer your-access-token-here" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "list_orders",
      "arguments": {"limit": 10, "search": "smith"}
    }
  }'
```

### A successful call

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"orders\": [...], \"totalRegistries\": 3, \"totalPages\": 1}"
      }
    ],
    "structuredContent": {
      "orders": [{ "uid": "order-abc123", "status": "completed" }],
      "totalRegistries": 3,
      "totalPages": 1
    },
    "isError": false
  }
}
```

### Two different kinds of failure

MCP distinguishes a request the server couldn't even attempt (a **protocol-level** JSON-RPC
error) from a tool that ran and reported a failure of its own (a **domain-level** result).
This endpoint keeps that distinction rather than collapsing both into the same shape:

**Protocol-level — a top-level JSON-RPC `error`, no `result` at all.** An unknown tool
`name`, a missing `name`, `arguments` that aren't an object, an argument key the tool's
`inputSchema` doesn't define, or arguments that fail the tool's own schema validation all
fail *before* the tool runs, as `error.code -32602` (`Invalid params`):

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Unknown tool: list_orderz",
    "data": { "name": "list_orderz" }
  }
}
```

A top-level JSON-RPC `method` this endpoint doesn't support at all (as opposed to a tool
*name* inside `tools/call`) fails the same way with `error.code -32601` (`Method not found`).

**Domain-level — a normal `result`, with `isError: true`.** The tool name and arguments were
valid and dispatch actually ran — and *that* is what failed: a permission the token lacks, or
a UID that doesn't exist or belongs to another organisation. This is not a JSON-RPC error; it
is a successful RPC call whose payload happens to report failure, shaped like the success
example above but with `isError: true`, a human-readable message in `content`, and a
machine-readable `structuredContent.code`:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      { "type": "text", "text": "The authenticated principal lacks the order:read permission required by list_orders." }
    ],
    "structuredContent": { "code": "permission_denied", "required_permission": "order:read" },
    "isError": true
  }
}
```

`structuredContent.code` is `permission_denied` when the token lacks the tool's required
permission, or `not_found` when the UID doesn't exist or belongs to another organisation.

> [!NOTE]
>
> Two JSON-RPC protocol revisions are supported on this same endpoint: a stateless
> header-routed form (`Mcp-Protocol-Version: 2026-07-28`) that most current MCP clients use,
> and the earlier `initialize`-handshake form (`2025-11-25`). Both are handled transparently —
> your MCP client selects the revision it understands, and you do not need to configure this.

## Headers for custom and headless clients

An interactive MCP client (Cursor, Claude Desktop) sets these automatically; you only need to
know about them if you're writing your own client against the `2026-07-28` stateless
revision rather than using an off-the-shelf one:

| Header | Required | Description |
| - | - | - |
| `Mcp-Protocol-Version` | For `2026-07-28` | Send `2026-07-28` to use the stateless, no-handshake form. Omit it (or send `2025-11-25`) to use the legacy `initialize` handshake instead. |
| `Mcp-Method` | For `2026-07-28` | Must exactly equal the JSON-RPC body's `method`. This is what lets infrastructure route on headers alone; the server rejects a request where the header and the body disagree rather than silently trusting one over the other. |
| `Mcp-Name` | For `2026-07-28` `tools/call` only | Must exactly equal `params.name`. Same disagreement check as `Mcp-Method`, scoped to the tool being called. |

> [!NOTE]
>
> Any of these three headers may be sent in the MCP spec's Base64 sentinel form,
> `=?base64?<base64-of-the-value>?=`, instead of a plain value — required when the value
> cannot safely travel as a raw ASCII HTTP header. The server decodes the sentinel and
> compares the *decoded* value against the JSON-RPC body; an undecodable sentinel is
> rejected the same way a mismatched plain value is.

A `2026-07-28` request missing `Mcp-Method` (or, for `tools/call`, missing `Mcp-Name`) is
rejected with `400` and JSON-RPC error code `-32020` before dispatch — on that revision,
these headers are load-bearing for routing, not optional metadata. The response still
carries the request's own `id` (a header-routing failure is not a notification) and a
`data` object naming the header and the value it was compared against:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32020,
    "message": "Mcp-Method is required for protocol version 2026-07-28",
    "data": { "header": "Mcp-Method", "bodyValue": "tools/list" }
  }
}
```

An `Mcp-Protocol-Version` header naming a version this endpoint doesn't support is rejected
with `400` and JSON-RPC error code `-32022`, with `data.requested` and `data.supported` (the
list from [Two JSON-RPC protocol revisions](#calling-a-tool) above) so the client can retry
with a version it supports:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": { "requested": "1999-01-01", "supported": ["2026-07-28", "2025-11-25"] }
  }
}
```

## Error Responses

| HTTP Status | When | Body |
| - | - | - |
| `200` | A JSON-RPC request was handled — a successful or domain-level-failed tool call (`isError: true`), an invalid-params error (`-32602`, always `200` regardless of protocol revision), or a `2025-11-25` client's method-not-found error | `{"result": ...}` or `{"error": ...}` |
| `202` | A JSON-RPC **notification** (a request sent with no `id`) was accepted | *(empty body)* |
| `400` | Unparsable JSON; a malformed JSON-RPC envelope; `Mcp-*` routing headers that disagree with the body, with each other, or are missing where required (`-32020`); an unsupported `Mcp-Protocol-Version` (`-32022`); or an undecodable Base64 sentinel header value | `{"error": {"code": ..., ...}}` |
| `401` | Missing or invalid credentials — see the `WWW-Authenticate` header for the metadata URL | `{"code": "...", "description": "..."}` (not a JSON-RPC envelope) |
| `403` | The request carried an `Origin` header not on this service's allowlist (browser clients only — see [above](#browser-based-clients-and-origin-allowlisting)) | `{"error": {"code": -32600, ...}}` |
| `404` | Method not found (`-32601`), **only** for a `2026-07-28` client; a `2025-11-25` client reads the same failure out of a `200` instead | `{"error": {"code": -32601, ...}}` |
| `500` | Unexpected server error (any half-applied write is rolled back) | `{"error": {"code": -32603, ...}}` |

> [!NOTE]
>
> A tool-level failure (`permission_denied`, `not_found`) is **always** `200` with
> `result.isError: true` — it never becomes an HTTP error status. Only a protocol-level fault
> (malformed JSON-RPC, unknown method or tool, invalid arguments, a disallowed `Origin`, bad
> credentials) changes the HTTP status away from `200`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.