Step 6: Deploy it, and call it
A deployed tool answers MCP over Streamable HTTP at POST /api/mcp/<tool> and plain REST at POST /api/v1/tools/<tool>/search. Same data, same rules, same answer. Both authenticate with Authorization: Bearer <key>, and a key opens exactly one tool.
Over MCP your agent sees two tools: search_<tool> and get_<tool>. You never write a schema for them. It is generated from your contract, so the model picks allergen values from an enum of your list and literally cannot ask for a value you never defined. Where you defined a value, its definition is in the schema too, as it stood when the release was built, so an agent can tell its user why a record was left out.
POST /api/v1/tools/menu/search
Authorization: Bearer cc_live_...
Content-Type: application/json
{
"query": "something warm",
"exclude": { "allergens": ["peanut"] },
"limit": 5
}{
"results": [
{
"id": "9f1c...",
"fields": {
"name": "Tomato soup",
"description": "Slow-roasted tomatoes finished with a swirl of cream",
"price": 6.5,
"allergens": ["dairy"]
},
"origin": { "kind": "imported" },
"unknown_safety_fields": [],
"receipts": {
"allergens": {
"method": "rule",
"confidence": 1,
"contract_version": 3,
"decided_by": null
}
}
}
],
"next_cursor": null,
"meta": {
"release": 7,
"contract_versions": { "allergens": 3 },
"applied_exclusions": { "allergens": ["peanut"] },
"total": 1,
"ranking": "query",
"warnings": []
}
}Read meta before you read the results. applied_exclusions is the server telling you what it actually enforced, which is the caller's request merged with whatever the key pins. release and contract_versions are how a logged answer stays explainable months later.
Scenarios: searches the tool must keep answering right. On the tool's *Scenarios* tab, save a search an agent would send, the records it must never return and the ones it must, and optionally how many results it needs. From the first scenario on, a new release reaches the tool only once it passes them all. A release that fails is held back: the tool keeps answering from the last one that passed, the owners are emailed with what failed, and *Check again* lets it through after a fix. Releases published before the first scenario keep being served, so adding one never takes a tool offline. *Run* checks the version served now and only reports; it never changes what agents get.
search { "exclude": { "allergens": ["tree nuts"] } }
must never Kaju katli
must return Dal tadka
at least 1 resultWebhooks: tell your own systems when things happen. Under *Developers*, an owner adds a URL and picks events: release.ready, release.held (a tool's scenarios held one back), drift.alert, review.opened (once per run that left decisions for a person), tool.failing (a fifth of a tool's calls failing over 15 minutes, at most once an hour), accuracy.dropped (a safety value found less often than in the last release, once a day per drop) and credits.low (a fifth of the starter credits left, and again when none are). Each delivery is a POST with the event and its ids and numbers, never record data, signed with a secret shown once when the webhook is made. The URL has to be https and reach a public address; it's checked again at every send. A receiver that doesn't answer 2xx is retried with backoff, and the last 50 deliveries show on the webhook with the answer they got.
import { createHmac, timingSafeEqual } from "node:crypto";
function fromCloudCrane(secret, rawBody, header) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replayed
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}- 1
query
Words, up to 200 characters. Ranked by meaning and by matching text together.
- 2
similar_to
The id of a record. Returns records like it, using the anchor and facet fields from the release. The record may be one your exclusions leave out, which is how an agent offers a safe alternative.
- 3
filters
Ordinary narrowing: a value, a list, or a
{gte, lte}range on numbers and dates. - 4
exclude
Safety exclusions, enforced in the query. Offered only on safety fields. Read the next section.
- 5
sort
Up to three fields. Missing values always sort last, either direction.
- 6
limit and cursor
Page size, and the opaque cursor from
next_cursor. Send the same other parameters with it. - 7
include_receipts
On by default. Turn it off for a smaller payload once you trust the pipeline.
claude mcp add --transport http menu https://cloudcrane.ai/api/mcp/menu \ --header "Authorization: Bearer cc_live_..."
{
"mcpServers": {
"menu": {
"url": "https://cloudcrane.ai/api/mcp/menu",
"headers": { "Authorization": "Bearer cc_live_..." }
}
}
}- 1
Claude Desktop, Cursor, Windsurf
The JSON above. Some clients spell the block
serversrather thanmcpServers, and some want atypeofhttpalongside the url. - 2
n8n
MCP Client Tool node, transport HTTP Streamable, Bearer authentication. If it connects but lists no tools, check the transport really is Streamable: this endpoint offers no SSE stream, and a client that silently falls back to SSE finds nothing.
- 3
Docker MCP Gateway
Add it as a remote server with the url and an Authorization header, which the gateway passes through to the endpoint.
- 4
Dify and Flowise
Add the endpoint as an MCP server with a Bearer token.
- 5
LangChain
MultiServerMCPClientwith transportstreamable_httpand a headers dict. - 6
OpenAI Agents SDK
MCPServerStreamableHttpwith the url and headers.
Your own tool's snippet is in the app. Each of these appears on the tool's page once you deploy it, with your url and a real key already filled in, so none of it has to be copied off a web page and edited.