# MCP endpoint

A REST API (RAPI) instance can also speak the **Model Context Protocol (MCP)**: one more route, `POST …/instance/{instanceKey}/mcp`, that any MCP-capable agent connects to with the URL and the instance's API key. For you as a developer it means an agent reads your deployed application the way a client uses the REST API — same data, same authentication, nothing to install or host. For the agent it means a set of read-only tools: the data models, a guide to the instance, filtered queries, aggregations, facets, pivots, suggestions, and one tool per [scripted endpoint](/console/rapi/scripted-endpoints) of the template. What the agent sees and how it should start: [MCP for agents](/console/rapi/mcp-agents).

```mermaid
flowchart LR
  A[Agent — Claude Code, Cursor, VS Code …] -->|POST /instance/key/mcp| R[Runner]
  R -->|models, query, aggregate …| D[Deployed application]
  R -->|endpoint_get_orders_id| S[Scripted endpoint]
```

## Prerequisites

- A deployable REST API template — see [REST APIs](/console/rapi)
- A paid plan with MCP: dcupl turns it on for your project; ask your dcupl contact (see [Turning it on](#turning-it-on))
- An app runner that supports MCP (the runners dcupl hosts do)
- An MCP client that supports **Streamable HTTP** servers with custom headers: Claude Code, Cursor, VS Code with GitHub Copilot, Claude Desktop and claude.ai (as a custom connector), the MCP Inspector, or your own MCP SDK client

## Turning it on

MCP is opt-in twice, and the route exists only once the template that turns it on is deployed:

1. **Your plan.** MCP is part of a paid plan that dcupl turns on for your project; ask your dcupl contact. It is off for new projects, and you cannot turn it on from the Console. Until it is on, deploying a template with MCP on is refused with `400 MCP is not enabled for this project …`.
2. **The template toggle.** With MCP on for your project, the API template form shows an **MCP** switch, its own section below _v-guard_. It writes `mcp.enabled: true` into the file:

```json filename="api/orders-api.api.json"
{
  "key": "orders-api",
  "name": "Orders API",
  "vGuardEnabled": false,
  "auth": [{ "type": "api-key", "value": "your-secure-api-key" }],
  "mcp": { "enabled": true },
  "dcuplConfig": {
    "projectId": "your-project-id",
    "apiKey": "your-sdk-readonly-key",
    "processOptions": { "applicationKey": "production" }
  }
}
```

3. **Deploy.** Deploy (or redeploy) the template as described in [REST APIs](/console/rapi#step-2-deploy-to-runner). The runner reads the toggle from the deployed template only: flipping the switch changes nothing on a running instance until you redeploy, and an instance deployed from a template without `mcp.enabled: true` — including every template deployed before the toggle existed — answers `404` on `/mcp`.

Only the literal `true` counts. `"mcp": {}`, `"mcp": true`, `"enabled": "true"` or `1` leave MCP off.

## The endpoint

```
POST {runner-url}/instance/{instanceKey}/mcp
```

The URL is the instance's base URL plus `/mcp`; the Console's MCP panel shows it ready to copy. When the runner is reached through the dcupl runner proxy, the route is `{proxy-url}/{runnerKey}/instance/{instanceKey}/mcp` — the proxy forwards it untouched.

`{instanceKey}` is the key in the runner tab's **Instance** column — the template key prefixed with your project ID (`<projectId>-orders-api`); a runner set up without the prefix uses the bare template key. See [Access your API](/console/rapi#step-3-access-your-api). The examples below write it as `your-project-id-orders-api`.

The endpoint is a **stateless Streamable HTTP** MCP server: every JSON-RPC `POST` is self-sufficient, there are no sessions and no server-sent events. `GET` and `DELETE` answer `405` with `Allow: POST`. The MCP transport requires an `Accept` header listing both `application/json` and `text/event-stream`; MCP clients send it on their own, only hand-written `curl` calls need to add it.

## Authentication and the version guard

The MCP route sits behind the same guards as the instance's data routes:

- **API key.** If the template has an `api-key` auth entry, every request carries that key as the `api-key` header (or the `api-key` query parameter). A template without an auth entry deploys an **open** instance: no header is needed, and anyone who has the URL can query it — add a key to the template and redeploy to protect it.
- **Version guard.** If `vGuardEnabled` is `true`, every request must also carry the instance's current `v` as the `v` header or query parameter, exactly like the data routes ([v-Guard caching](/console/rapi#v-guard-caching)). `v` changes with every update of the instance; read it from `/status` after a redeploy. A request without it is refused with a plain HTTP `400`, which most MCP clients only report as a connection failure — so turn v-Guard off for instances agents should connect to, or put the current `v` into the client's headers.

Everything the agent can do is read-only: the data tools never write, and an endpoint tool runs the script with the same read-only `dcupl` facade as the HTTP route. Treat connection snippets that contain the API key like the key itself.

## Connecting an agent

Pick a server name (the Console uses `dcupl-<template key>`), the URL and the header. Leave the `api-key` header out for an open instance; add a `v` header when the version guard is on.

### Claude Code

```bash
claude mcp add --transport http dcupl-orders-api "https://runner.example.com/instance/your-project-id-orders-api/mcp" \
  --header "api-key: your-secure-api-key"
```

`claude mcp list` shows the server as connected; `claude mcp get dcupl-orders-api` prints its configuration. Remove it again with `claude mcp remove dcupl-orders-api`.

### JSON config

Cursor, Claude Code's `.mcp.json` and most other clients read an `mcpServers` block:

```json filename=".mcp.json"
{
  "mcpServers": {
    "dcupl-orders-api": {
      "type": "http",
      "url": "https://runner.example.com/instance/your-project-id-orders-api/mcp",
      "headers": { "api-key": "your-secure-api-key" }
    }
  }
}
```

VS Code with GitHub Copilot takes the same entry under `servers` in `.vscode/mcp.json`. Claude Desktop and claude.ai connect without a file: add the URL as a **custom connector** and put the `api-key` header under its request headers. Claude reaches the server from Anthropic's cloud, so the instance has to be reachable from the public internet.

---

The server announces itself as `dcupl-app-runner`, with a title and description that name the instance (by its template's name where it has one) and the dcupl SDK version it runs. Its instructions tell the agent to call `models` first and state the query conventions; see [MCP for agents](/console/rapi/mcp-agents). Nothing else is needed on the client side.

## The tools

### The built-in tools

Every MCP-enabled instance exposes eight read-only tools: seven data tools and `guide`. Each data tool is a thin adapter over the same operation the REST route uses, and returns the raw dcupl SDK result as one JSON text block — REST and MCP return identical data for identical options (`models` adds an `instance` block with the instance's key, name and description). `guide` answers with markdown.

| Tool        | What it does                                                                                                                                  |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `models`    | The data models of the application with their item counts. The recommended first call: it yields the `modelKey` values every other tool takes |
| `guide`     | How to query this instance, as markdown: models, attributes, reference paths, endpoints, worked examples and conventions (also a resource)    |
| `query`     | A filtered query against a model — filter conditions, pagination (`start`, `count`), projection and sorting                                   |
| `aggregate` | Statistics over an attribute (`min`, `max`, `sum`, `count`, `avg`, `distinct`, …), optionally filtered                                        |
| `facet`     | The distinct values of an attribute with their occurrence counts, optionally in the context of a filter                                       |
| `pivot`     | A pivot table: row and column attributes with aggregations per cell                                                                           |
| `metadata`  | Metadata of a model, e.g. its total and filtered item counts                                                                                  |
| `suggest`   | Values of an attribute matching a partial input (autocomplete), to resolve a term to the exact stored values before filtering                 |

`guide` is the same text as the instance's one resource, `dcupl://instance/{instanceKey}/guide`, for clients that do not surface resources; see [The guide resource](/console/rapi/mcp-agents#the-guide-resource).

`query` returns **100 items by default and at most 1000** per call (`count`); agents page with `start` or reach for `aggregate` and `facet` instead of reading whole models — a tool result lands in the agent's context as one block. Every tool except `models` takes a `modelKey`; `aggregate`, `facet`, `pivot`, `metadata` and `suggest` also accept an optional `query` in the dcupl filter shape (`{ queries: [{ attribute, operator, value }], groupType }`).

### One tool per scripted endpoint

Every entry of the deployed template's `endpoints` array ([Scripted endpoints](/console/rapi/scripted-endpoints)) becomes one more tool, listed after the data tools. The name is derived from the route:

```
endpoint_<method>_<path>
```

- the method lower-cased (`get`, `post`)
- the path with `/` replaced by `_`, the `:` of a parameter dropped, and any character outside `A-Z a-z 0-9 _ -` replaced by `_`; the path's case is kept
- the prefix `endpoint_` is fixed, so an endpoint can never shadow a data tool
- names are at most 64 characters; two endpoints that normalise to the same name get `_2`, `_3`, … appended

Examples:

- `GET orders-summary` → `endpoint_get_orders-summary`
- `GET orders/:id` → `endpoint_get_orders_id`
- `GET orders/:id/summary` → `endpoint_get_orders_id_summary`
- `POST orders/export` → `endpoint_post_orders_export`
- `GET v1.2/orders~latest` → `endpoint_get_v1_2_orders_latest`

In `tools/list` the tool's `title` is the route (`GET orders/:id/summary`) and its description is the endpoint's `description` from the template followed by a fixed sentence about the route and its parameters — write the description for an agent, it decides when to call the tool. The input is flat:

- one required string per `:name` segment of the path (`id` for `orders/:id`)
- for `GET`, an optional `query` object — the values the script reads as `request.query` (strings, numbers, booleans or arrays of those)
- for `POST`, an optional `body` — any JSON, read by the script as `request.body`

The call takes the HTTP route's own path: the same matcher, the same [time budget](/console/rapi/scripted-endpoints#timeout), the same error mapping. The script sees the same `request` as over HTTP, minus HTTP headers — no headers and no credentials ever reach it from an MCP call. The result is the script's return value as JSON; a script error or timeout is a tool error such as `EndpointTimeout: endpoint script did not settle within 5000ms` or `TypeError: …`, and an endpoint that has left the template reads `endpoint not found`.

The tool list follows the template: a redeploy that changes `endpoints` changes the next `tools/list`. A template with endpoints but without `mcp.enabled: true` still answers `404` on `/mcp` while its `endpoints/*` routes keep working.

## What the answers mean

The checks run in a fixed order — the API key, then the version guard, then the MCP toggle — so the first refusal tells you what is missing:

| Answer                                  | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP `403`                              | No or wrong `api-key`, or an unknown instance key. Checked first, before anything else — the route is not discoverable without the key                                                                                                                                                                                                                                                                                                                                                                                        |
| HTTP `400`                              | The version guard is on and the request carries no (or a stale) `v`. Checked before the MCP toggle                                                                                                                                                                                                                                                                                                                                                                                                                            |
| HTTP `404`                              | The deployed template does not set `mcp.enabled: true` — never set, or deployed before the toggle existed. Flip the switch and redeploy                                                                                                                                                                                                                                                                                                                                                                                       |
| HTTP `405`                              | A `GET` or `DELETE` on `/mcp`; only `POST` is served                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| HTTP `406`                              | The `Accept` header does not list both content types — usually a proxy rewriting it                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| JSON-RPC `-32002` (HTTP `200`)          | The instance is not ready: `Instance "…" is not ready (status: waiting / processing / error / stopped)`. Wait for `ready`; an `updating` instance keeps answering                                                                                                                                                                                                                                                                                                                                                             |
| JSON-RPC `-32600`                       | A JSON-RPC batch (an array) was sent; batches are not supported                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| Tool result with `isError` (HTTP `200`) | The reason is in the result's text, never a JSON-RPC `error`. Rejected before the instance is called: an unknown tool name (`MCP error -32602: Tool <name> not found`) or arguments that fail the tool's schema (`MCP error -32602: Input validation error: Invalid arguments for tool <name>: …`, e.g. `expected string, received number at id` for a path parameter sent as a number). Failed in the runner or the instance: `instance not ready`, `endpoint not found`, or a script error / timeout as `<name>: <message>` |

## Testing

### In the Console

Open the instance's **MCP endpoint** panel (the **MCP** tag in the instance table or on the template card, or the row menu). It shows the URL, the `api-key` header when the template has one, the `v` note when the version guard is on, copy-ready snippets for Claude Code, a JSON config and `curl`, and the tools the runner really serves (`live · N`, the endpoint tools grouped separately).

**Open in API tester** opens the tester on the `mcp` entry: a `POST` that sends `tools/list` with the right headers. **Send** must answer `200` with `result.tools` listing `models`, `guide` and the six other data tools plus one `endpoint_*` tool per scripted endpoint; a `404`, `403` or JSON-RPC `-32002` means what the table above says. The tester's cURL export reproduces the same request in a shell.

### With the MCP Inspector

The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a real MCP client. Its CLI lists and calls the tools from a terminal:

```bash
npx @modelcontextprotocol/inspector --cli "https://runner.example.com/instance/your-project-id-orders-api/mcp" \
  --transport http --header "api-key: your-secure-api-key" --method tools/list

npx @modelcontextprotocol/inspector --cli "https://runner.example.com/instance/your-project-id-orders-api/mcp" \
  --transport http --header "api-key: your-secure-api-key" \
  --method tools/call --tool-name query --tool-arg modelKey=Order --tool-arg count=2

npx @modelcontextprotocol/inspector --cli "https://runner.example.com/instance/your-project-id-orders-api/mcp" \
  --transport http --header "api-key: your-secure-api-key" \
  --method tools/call --tool-name endpoint_get_orders_id --tool-arg 'id="42"'
```

Without `--cli`, `npx @modelcontextprotocol/inspector` opens the Inspector UI: choose transport **Streamable HTTP**, paste the URL and add the `api-key` header. Path parameters are strings — quote the value (`'id="42"'`) so the Inspector does not send a number.

### With curl

```bash
curl -X POST 'https://runner.example.com/instance/your-project-id-orders-api/mcp' \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'api-key: your-secure-api-key' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"models"}}'
```

## Monitoring

MCP calls show up in the instance's invocation log as `mcp:<tool>` actions (`mcp:guide` for the guide tool, `mcp:resource:guide` for a read of the guide resource) next to the actions they trigger (`query`, `facet`, `endpoint:<path>` …): `GET /instance/{instanceKey}/invocations?action=mcp:query` isolates the agent's queries, an endpoint tool is counted as `mcp:endpoint:<configured path>` (`mcp:endpoint:orders/:id`). Because the triggered action is counted as well, summing every action double-counts agent traffic.

## Troubleshooting

| Symptom                                                                                                                     | Cause                                                                                          | Fix                                                                                                                                   |
| --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| The MCP switch is missing from the template form                                                                            | MCP is not turned on for your project                                                          | MCP is part of a paid plan: ask your dcupl contact to turn it on. A template that already has `mcp.enabled: true` shows a yellow note |
| Deploy fails with `400 MCP is not enabled for this project …`                                                               | The template turns MCP on, but MCP is not turned on for your project                           | Ask your dcupl contact to turn it on, or switch MCP off in the template                                                               |
| `404` on `/mcp`, the instance is `ready`                                                                                    | The deployed template does not set `mcp.enabled: true`                                         | Flip the switch and redeploy                                                                                                          |
| The client says "connection failed", `curl` gets `400`                                                                      | `vGuardEnabled` is on and no `v` is sent                                                       | Add the current `v` from `/status` as a header, or disable the version guard                                                          |
| The client connects but shows no `endpoint_*` tools                                                                         | The deployed template has no `endpoints`, or the runner does not support endpoint tools yet    | Add endpoints and redeploy                                                                                                            |
| The tool result has `isError` and reads `MCP error -32602: Input validation error … expected string, received number at id` | A path parameter was sent as a number; the MCP server rejects it before the instance is called | Send it as a string                                                                                                                   |
| The agent reads whole models and runs out of context                                                                        | `query` with a large `count`                                                                   | Keep `count` small, page with `start`, prefer `aggregate` / `facet`, or add a scripted endpoint that computes the answer              |

## Next steps

- [MCP for agents](/console/rapi/mcp-agents) — the guide, the conventions and the first calls an agent makes
- [Scripted endpoints](/console/rapi/scripted-endpoints) — the routes that become `endpoint_*` tools
- [REST APIs](/console/rapi) — templates, deployment, authentication and v-Guard
- [Scripts and script modes](/sdk/guides/scripts) — what an endpoint script can do
- [Build with agents](/agentic) — dcupl for AI agents beyond a single instance
- [CLI quickstart](/cli) — let an agent work on the project locally
