MCP servers API
This page documents every MCP server endpoint on agent-engine-api. An MCP server hosts your Python tool code as a Model Context Protocol endpoint that agents and MCP clients can call. Every publish snapshots the whole tool set into a new, immutable version, so you can roll back to an exact past build. Set $CAI_API first — see the API overview.
For a guided introduction, start with the MCP servers overview.
Base URL
MCP servers are served by agent-engine-api, the core platform API.
kubectl -n cai-system port-forward svc/agent-engine-api 8080:8080 &
export CAI_API=http://localhost:8080
export PROJECT=<your-project-uuid> # platformctl projects list
Authentication and roles
Every route requires a bearer credential:
Authorization: Bearer <token-or-api-key>
| Role | Can call |
|---|---|
| project member | List servers, get one server, list its tools, list its versions |
| project admin | Everything a member can, plus create and delete a server, publish and delete tools, and roll back, yank, or unyank a version |
Conventions
- Error envelope. Every non-2xx response is
{"error": "<message>", "request_id": "<id>"}. - The 404 rule. A request against a project you hold no grant on returns 404 — never 403 — so project existence is not discoverable.
- Pagination. The server list takes
page_size(1–200, default 50) andpage_tokenand returnsnext_page_token. See the API overview. - Custom methods. Version actions use GCP-style
:verbsuffixes (:rollback,:yank,:unyank), always with POST. - Builds are asynchronous. Creating a server and publishing a tool both return before the build finishes. Poll the server until
statusisreadyorfailed. - Body caps. Create is capped at 4 KiB; a tool publish is capped at 1 MiB (it carries source code).
Endpoints at a glance
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/projects/{projectID}/mcpservers | admin | Create a server |
| GET | /v1/projects/{projectID}/mcpservers | member | List servers |
| GET | /v1/projects/{projectID}/mcpservers/{name} | member | Get one server (poll this for status) |
| DELETE | /v1/projects/{projectID}/mcpservers/{name} | admin | Delete a server |
| GET | /v1/projects/{projectID}/mcpservers/{name}/tools | member | List the current tool set |
| PUT | /v1/projects/{projectID}/mcpservers/{name}/tools/{tool} | admin | Publish or replace one tool |
| DELETE | /v1/projects/{projectID}/mcpservers/{name}/tools/{tool} | admin | Remove one tool |
| GET | /v1/projects/{projectID}/mcpservers/{name}/versions | member | List versions |
| POST | /v1/projects/{projectID}/mcpservers/{name}/versions/{version}:rollback | admin | Point the server at an earlier version |
| POST | /v1/projects/{projectID}/mcpservers/{name}/versions/{version}:yank | admin | Retire a version as a rollback target |
| POST | /v1/projects/{projectID}/mcpservers/{name}/versions/{version}:unyank | admin | Un-retire it |
The server object
List, get, and create all return this shape. A single-object response wraps it under "mcp_server".
{
"name": "weather-tools",
"visibility": "",
"expose": "",
"status": "ready",
"url": "http://weather-tools.cai-p-<project-short>.svc.cluster.local",
"version": 1,
"image": "localhost:30500/mcp/weather-tools@sha256:...",
"tool_names": ["get_forecast"],
"tool_count": 1,
"message": "",
"created_at": "2026-08-06T12:00:00Z"
}
| Field | Type | Meaning |
|---|---|---|
name | string | The server name you chose. Immutable |
expose | string | "" = cluster-local (reachable only inside the platform), "apps" = published on the internet |
status | string | pending, building, deploying, ready, or failed |
url | string | The endpoint MCP clients connect to. Present once the server is serving |
version | int | The version currently running |
image | string | The container image behind the running version |
tool_names | string[] | The tools in the current set |
tool_count | int | Count of the above |
message | string | Why a build or rollout failed |
created_at | string | RFC 3339 timestamp |
Create a server
POST /v1/projects/{projectID}/mcpservers
Auth: admin. Returns 201. The server starts empty — publish a tool to give it something to serve.
| Field | Type | Required | Default | Rules |
|---|---|---|---|---|
name | string | yes | — | Lowercase DNS label, 40 characters or fewer: ^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$ |
expose | string | no | "" | "" (cluster-local) or "apps" (published) |
{"name": "weather-tools", "expose": ""}
The create endpoint decodes name and expose and nothing else. Any other field you send — including scaling settings — is silently ignored. There is no API on this surface for keeping an instance warm.
Errors:
| Status | Message |
|---|---|
| 400 | missing or invalid 'name' (must be a lowercase DNS label, <=40 chars) |
| 400 | 'expose' must be "" (cluster-local) or "apps" |
| 409 | an mcp server named <n> already exists in this project |
List servers
GET /v1/projects/{projectID}/mcpservers
Auth: member. Response 200: {"mcpservers": [...], "next_page_token": "..."}.
Get one server
GET /v1/projects/{projectID}/mcpservers/{name}
Auth: member. Response 200: {"mcp_server": {...}}. This is the endpoint you poll after a create or a publish — watch status move from building to deploying to ready, or to failed with an explanation in message.
Delete a server
DELETE /v1/projects/{projectID}/mcpservers/{name}
Auth: admin. Response 200: {"name": "weather-tools", "deleted": true}.
List tools
GET /v1/projects/{projectID}/mcpservers/{name}/tools
Auth: member. Response 200:
{"server": "weather-tools",
"tools": [{"name": "get_forecast", "description": "Forecast.", "handler": "import crusoe_mcp as crusoe\n...",
"schema": {}, "credential_keys": [], "credentialKeys": [],
"updated_at": "2026-08-06T12:00:00Z"}]}
credential_keys and credentialKeys carry the same value; the second spelling exists for backward compatibility.
Publish a tool
PUT /v1/projects/{projectID}/mcpservers/{name}/tools/{tool}
Auth: admin. Body capped at 1 MiB. Returns 202 — the publish is accepted and a build starts.
The tool name in the path must match ^[a-z][a-z0-9_]{0,62}$ (a lowercase identifier, no leading underscore). It becomes the file tools/<name>.py inside the built image.
| Field | Type | Required | Notes |
|---|---|---|---|
handler | string | yes | The tool's Python source — a complete module using the @crusoe.tool() decorator |
description | string | no | Shown to agents and MCP clients |
schema | object | no | The tool's input schema |
credentialKeys / credential_keys | string[] | no | Names of credentials the tool needs. Either spelling is accepted |
A minimal handler:
import crusoe_mcp as crusoe
@crusoe.tool()
def get_forecast(city: str) -> dict:
"""Forecast."""
return {"city": city}
Response 202:
{"server": "weather-tools", "name": "get_forecast", "published": true,
"build_id": "2f6f...-uuid",
"note": "building a new immutable version from the server's current tool set"}
Errors:
| Status | Message |
|---|---|
| 400 | missing 'handler': the tool's Python source (a complete @crusoe.tool module) |
| 400 | invalid tool name (must be a lowercase identifier not starting with '_') |
Every publish snapshots the whole current tool set into a new version pinned to an image digest — not just the tool you changed.
Delete a tool
DELETE /v1/projects/{projectID}/mcpservers/{name}/tools/{tool}
Auth: admin. Returns 202 and rebuilds the server without that tool:
{"server": "weather-tools", "name": "get_forecast", "deleted": true,
"build_id": "...", "note": "rebuilding the server without this tool"}
A tool that is not in the set returns 404.
List versions
GET /v1/projects/{projectID}/mcpservers/{name}/versions
Auth: member. Response 200:
{"server": "weather-tools",
"versions": [{"version": 2, "image_digest": "sha256:...", "credential_keys": [],
"tool_names": ["get_forecast"], "tool_count": 1,
"current": true, "created_at": "..."},
{"version": 1, "image_digest": "sha256:...", "credential_keys": [],
"tool_names": [], "tool_count": 0, "current": false,
"yanked": true, "yanked_at": "...", "created_at": "..."}]}
| Field | Type | Meaning |
|---|---|---|
version | int | The version number, counting up from 1 |
image_digest | string | The exact image this version built. Present once the build succeeded |
tool_names, tool_count | — | The tool set captured in this version |
credential_keys | string[] | Credential names the version's tools declared |
current | bool | true for the version the server runs right now |
yanked, yanked_at | — | Present only on retired versions |
created_at | string | RFC 3339 timestamp |
Roll back to a version
POST /v1/projects/{projectID}/mcpservers/{name}/versions/{version}:rollback
Auth: admin. Re-points the server at that version's recorded image. There is no rebuild, so it is fast and reproduces exactly what ran before.
Response 200:
{"server": "weather-tools", "version": 1, "image": "localhost:30500/mcp/weather-tools@sha256:...",
"rolled_back": true,
"note": "re-pointed the server at version <n>'s recorded image; no rebuild"}
Errors:
| Status | Message |
|---|---|
| 400 | version must be a positive integer |
| 404 | no such version |
| 409 | version <n> is yanked (retired); unyank it or publish a new version instead |
| 409 | version <n> has no recorded image to roll back to |
Yank and unyank a version
POST /v1/projects/{projectID}/mcpservers/{name}/versions/{version}:yank
POST /v1/projects/{projectID}/mcpservers/{name}/versions/{version}:unyank
Auth: admin. Yanking retires a version as a rollback target without deleting it or its history — use it to mark a build you never want to return to. Unyanking reverses that.
Response 200: {"server": "weather-tools", "version": 1, "yanked": true} (or false for unyank).
Errors:
| Status | Message |
|---|---|
| 409 | version <n> is the one the server currently runs; roll to another version before yanking it |
| 404 | unknown method on a version. The custom methods are POST .../versions/{version}:rollback, :yank and :unyank (any other :verb) |
What a deployed server gets
A running MCP server receives these environment variables: CAI_API_URL and CAI_PROJECT_ID as plain values, and CAI_PROJECT_KEY and CAI_MCP_AUTH_TOKEN as references to Kubernetes Secrets. The per-server bearer token has the form cai_mcp_ followed by 64 hexadecimal characters; it is minted once and reused across redeploys, so a client you configured keeps working after a publish or a rollback. The request timeout on a server is 300 seconds.
See Connect agents and clients.
Audit actions
Every state change writes a row to the project audit log (GET /v1/projects/{projectID}/audit, readable by any project member):
| Action | Written by |
|---|---|
mcpserver.create | Create a server |
mcpserver.delete | Delete a server |
mcpserver.tool.publish | Publish or replace a tool |
mcpserver.tool.delete | Delete a tool |
mcpserver.version.rollback | Roll back |
mcpserver.version.yank | Yank |
mcpserver.version.unyank | Unyank |
Status codes
| Code | When |
|---|---|
| 200 | Read, delete, rollback, yank, unyank |
| 201 | Server created |
| 202 | Tool publish or tool delete accepted — a build is running |
| 400 | Validation: server name, expose, tool name, missing handler, non-positive version |
| 401 | Missing or invalid credential |
| 403 | You hold a grant but not the project admin role |
| 404 | Not found, or not yours — deliberately identical. Also unknown :verb suffixes on a version |
| 409 | Name already taken; yanked or imageless rollback target; yanking the running version |
| 429 | Per-principal rate limit |
| 500 | internal error |
Related pages
- MCP servers overview — what MCP is and when to use it
- Publish tools — the write-and-publish workflow with output
- Versions and rollback — how immutable versions behave
- Connect agents and clients — pointing an agent or a desktop client at your server
- Tutorial: MCP weather tools — end to end in one sitting
- Platform API · API overview · Platform limits