--- title: 'MCP' description: 'Four tools at /mcp that list, describe and call the box API under your identity, with a confirmation step before anything irreversible.' position: 8 --- # MCP > Your **box** (the container running your workspace, on your laptop under > `stackbone dev` or deployed in your cloud) serves the Model Context Protocol at > `/mcp`. Point Claude Code, Codex or any other MCP client at it and > the agent gets four tools that reach the box's own HTTP API: find an operation, > read its exact shape, call a read, call a write. Every call re-enters the box > as an ordinary request, so the role check and the audit trail are the ones > Studio and the CLI already go through. Nothing passes through Stackbone. ## Four tools, not one per route | Tool | What it does | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `list_operations` | Lists the operations this box exposes, as compact entries: id, method, path, tag, summary, and whether it is a read or a write. Filter with `query`, `tag` or `kind`. | | `describe_operation` | One operation in full: its parameters, its JSON request body and its JSON responses, with only the schema components it references. Says whether the operation asks for your confirmation before it runs. | | `call_read_operation` | Calls one read operation by id, with its declared parameters and an optional JSON body. | | `call_write_operation` | Calls one write operation by id, the same way. Some writes pause for your confirmation first (see below). | The catalog is a projection of the same OpenAPI document the box serves at [`/api/openapi.json`](/docs/home/features/api-reference). The box keeps an operation out when it cannot project it safely: a route that answers a credential (revealing a secret), one that streams (the chat wires, the event stream, the log tail), one that takes a file upload, or a write with no role check on it. The agent gets exactly the operations left. A route added without meeting those rules does not appear. The list is also filtered per caller. An operation your role cannot reach is absent from `list_operations` and unknown to `describe_operation`. Naming its id anyway reaches the route and gets the route's own `403`. ## Reads and writes are two tools An MCP client lets you approve tools by name. With one `call_operation` covering everything, approving it once to read a log would approve deleting a trigger. So the box splits the call in two: `call_read_operation` is marked read-only, `call_write_operation` is marked destructive, and you can allow the first without the second. Whether an operation is a read or a write comes from the permission its route checks, not from the HTTP verb. `POST /api/db/query` checks a read permission, so it is a read. Listing secrets, API keys, dynamic config, prompts or connections checks a write permission on this box, so those `GET` routes are writes and only `call_write_operation` reaches them. `list_operations` tells you which is which. Calling an operation through the wrong tool is refused with `wrong_operation_kind` before anything runs. ## Some writes ask you first Deleting something, rotating a secret, revoking an API key, cancelling a run, starting an eval run: the box does not run these on the first call. `call_write_operation` answers with a question instead, the client shows you what is about to happen, and only your acceptance runs it. Decline or cancel and nothing happens. The box checks the call before it asks. Parameters, body, size and encoding are all validated first, so a call it was going to refuse anyway comes back as that refusal instead of as a question. The sentence you are shown then names the arguments that passed those checks, which is the call the box would send. The mechanism is MCP elicitation. The box keeps no state between the question and the answer. Everything the retry needs travels in an opaque token bound to you and to the exact call, and it expires after 90 seconds. A token from another user, another call or an earlier minute is refused. A client the box cannot ask does not get to call these operations at all. The answer is `confirmation_unavailable` and nothing runs. Two kinds of client land there: one that does not support elicitation, and one connected on the older MCP revision, where the box keeps nothing open to carry your answer back on. Either way the operation is refused rather than run unasked, so make that one change from Studio or the CLI. Claude Code supports elicitation and shows the dialog on its own. `describe_operation` reports the requirement in advance, in the `confirm` field, so the agent can tell you before it calls. ## Connect a client The address is your box address plus `/mcp`. The credential depends on where the box runs: | Where your box runs | Address | Credential | | ----------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | On your laptop, under `stackbone dev` | `/mcp` | Your Stackbone account, as on a deployed box. The tunnel address is new on every `stackbone dev`, so the client signs in again after each restart. With no tunnel, `/mcp` answers not ready. | | Self-hosted, with the Docker Compose package | `https:///mcp` | The operator token, sent as `Authorization: Bearer `. The same value Studio takes on that box. | | Deployed in your cloud and registered in Studio | `https:///mcp` | Your Stackbone account. The client discovers the sign-in from the box, you sign in and grant it, and it receives a five-minute token that is valid at this box's `/mcp` and nowhere else. Your role's permissions apply on every call. | On a box deployed in your cloud you hand the client the address and nothing else. It reads from the box where to sign in, identifies itself, and sends you through sign-in and consent. There is no client id or secret for you to obtain, paste or keep in sync. Approve it once, and the client holds a short-lived token that works at that box's `/mcp` and nowhere else. In Claude Code: ```sh # a local box claude mcp add --transport http my-box http://127.0.0.1:4242/mcp # a self-hosted box claude mcp add --transport http my-box https:///mcp \ --header "Authorization: Bearer " ``` In Cursor (`.cursor/mcp.json`) and most clients that read a JSON config: ```json { "mcpServers": { "my-box": { "type": "http", "url": "https:///mcp", "headers": { "Authorization": "Bearer " } } } } ``` Drop `headers` for a local box. The field names differ by client; the [coding agents](/docs/home/get-started/coding-agents#as-an-mcp-server) page lists the differences for the clients the CLI knows. ### Turning MCP on and off MCP is on unless somebody turned it off. Studio's **MCP** screen carries the switch beside the address, and only an owner or an admin can move it — the same seats that hold the other settings a member is kept away from. Off means off for everyone. A client that already holds a valid credential — your account's token, the operator bearer, or nothing at all on a local box — is refused the same way as one that holds none: ```json { "code": "mcp_disabled" } ``` with HTTP `503`, and no sign-in challenge: the credential is not the problem, so signing in again will not help. Ask an operator to turn MCP back on. It takes effect on the next call — nobody has to restart the agent — and it changes nothing else: Studio, the assistant and your own API calls keep working while MCP is off. "Enabled" describes the setting and nothing more. It is not a claim that the box is reachable or that your credential will be accepted; a box that was never registered and armed is enabled and still answers `mcp_not_ready`. Three things to know about the wire: - The box serves two MCP revisions on the same address: the current `2026-07-28`, and the older `initialize` handshake most shipped clients still open with. Your client negotiates one of them, so there is nothing to configure. A client that asks for a revision newer than `2026-07-28` gets one of these two back: the current transport refuses it with a `400`, the older handshake answers with the newest revision the box does speak. Neither revision opens a session, so every call stands on its own credential. On the older one the box cannot ask you to confirm, which is the limit described above. - A request from a browser carries an `Origin`. One outside the box's CORS allowlist is refused with `403` before anything else. Non-browser clients send no `Origin` and are not affected. - `/mcp` takes `POST` only. Anything else is `405`. ## Limits | What | Limit | When you cross it | | ----------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Request body | 1 MiB | `413` at the door, before the body is read. One operation's JSON body has the same ceiling (`request_too_large`), so over `/mcp` the door always answers first | | Response of one operation | 256 KiB | `response_too_large`. The answer is refused whole, never truncated. | | One `list_operations` answer | 256 operations or 64 KiB | `catalog_too_large`. Narrow it with `query`, `tag` or `kind`. | | Bodies | JSON in, JSON out | An operation with any other body type is not in the catalog | | Request headers you may set | `accept-language`, `if-none-match`, `if-modified-since`, when the operation declares them | `header_not_allowed` | | Response headers you get back | `content-type`, `etag`, `last-modified`, `retry-after`, `location` | Everything else is dropped | The box never follows a redirect or walks an operation's pages for you. An operation that pages answers one page, with whatever cursor the route returns. ## What the box answers A call comes back as one JSON envelope: the operation id, the inner HTTP status, the safe response headers, the content type and the body. An inner `4xx` or `5xx` keeps the envelope and is flagged as an error result, so the agent sees the route's own answer (the `403` for a permission you lack, the `404` for a run that is gone). When the box refuses before the route runs, the result carries a `code`: | Code | Meaning | | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `operation_not_found` | No such id on this box. `describe_operation` also answers it for an id your role cannot reach, and `list_operations` leaves that id out of its answer. A call to that id reaches the route and gets the route's own `403` | | `wrong_operation_kind` | A read called through `call_write_operation`, or the reverse. The message names the right tool | | `invalid_parameter`, `missing_parameter`, `undeclared_parameter` | A parameter that fails its schema, is required and absent, or is not declared by the operation | | `invalid_body`, `missing_body`, `body_not_allowed` | The JSON body fails its schema, is required and absent, or was sent to an operation that takes none | | `header_not_allowed` | A header outside the list above | | `confirmation_unavailable` | The operation needs your confirmation and this client cannot be asked. `details.reason` says which: `no_elicitation` (your client declared none) or `legacy_protocol` (the older MCP revision, where the box keeps nothing open to carry your answer back) | | `confirmation_declined`, `confirmation_expired`, `confirmation_invalid`, `confirmation_mismatch` | You declined, the 90 seconds passed, the token does not verify, or it belongs to a different call or user | | `confirmation_unreadable` | The retry carried the token but no answer the box could read. Call the operation again and answer the new prompt | | `request_too_large` | See Limits | | `catalog_too_large` | See Limits | | `response_too_large`, `unsupported_response_content_type`, `invalid_response_json` | The route answered more than 256 KiB, something other than JSON, or JSON that does not parse | At the door, before any tool runs: `403 invalid_origin` for a browser origin the box does not allow, `413` for an oversized request, `401 invalid_token` for a missing or wrong credential, and three `503`s that mean different things and should be read carefully: | Code | Meaning | | ----------------- | ------------------------------------------------------------------------------------------------------------------- | | `mcp_disabled` | An operator turned MCP off for this installation. Your credential is fine; ask them to turn it back on | | `mcp_not_ready` | A deployed box that has not been registered and armed yet. Retry once it has; do not sign in again | | `mcp_unavailable` | The box could not read whether MCP is enabled, so it refused rather than guess. Retry shortly; do not sign in again | None of the three carries a sign-in challenge. ## What is not there - MCP resources and prompts. The box serves tools only. - The chat wires, the event stream and the log tail. They stream, and the catalog projects bounded JSON only. Use them as described on the [API](/docs/home/features/api) page. - File uploads and binary downloads. - A tool per route, or a separate permission model. The role you hold decides, the same as in Studio. ## What's next - [API](/docs/home/features/api): the routes the tools project. - [API reference](/docs/home/features/api-reference): the OpenAPI document behind the catalog. - [Security and auth](/docs/home/features/security-auth): how each caller proves itself to the box. - [Studio copilot](/docs/home/features/studio-copilot): the same tools, in a panel beside every Studio screen. - [Coding agents](/docs/home/get-started/coding-agents): wire the same agent to this documentation.