.md

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:

  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:
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" }
  }
}
  1. 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 — 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. 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). 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 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:

.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. 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 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, 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