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 of the template. What the agent sees and how it should start: MCP for agents.
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
- A paid plan with MCP: dcupl turns it on for your project; ask your dcupl contact (see 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:
- 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 …. - 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: trueinto the file:
{
"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" }
}
}- Deploy. Deploy (or redeploy) the template as described in REST APIs. 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 — answers404on/mcp.
Only the literal true counts. "mcp": {}, "mcp": true, "enabled": "true" or 1 leave MCP off.
The endpoint
POST {runner-url}/instance/{instanceKey}/mcpThe 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. 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-keyauth entry, every request carries that key as theapi-keyheader (or theapi-keyquery 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
vGuardEnabledistrue, every request must also carry the instance's currentvas thevheader or query parameter, exactly like the data routes (v-Guard caching).vchanges with every update of the instance; read it from/statusafter a redeploy. A request without it is refused with a plain HTTP400, which most MCP clients only report as a connection failure — so turn v-Guard off for instances agents should connect to, or put the currentvinto 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 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.
Cursor, Claude Code's .mcp.json and most other clients read an mcpServers block:
{
"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. 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.
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) 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 outsideA-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-summaryGET orders/:id→endpoint_get_orders_idGET orders/:id/summary→endpoint_get_orders_id_summaryPOST orders/export→endpoint_post_orders_exportGET 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
:namesegment of the path (idfororders/:id) - for
GET, an optionalqueryobject — the values the script reads asrequest.query(strings, numbers, booleans or arrays of those) - for
POST, an optionalbody— any JSON, read by the script asrequest.body
The call takes the HTTP route's own path: the same matcher, the same time budget, 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 is a real MCP client. Its CLI lists and calls the tools from a terminal:
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
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 |