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.

    Searching the menu, over REST
    POST /api/v1/tools/menu/search
    Authorization: Bearer cc_live_...
    Content-Type: application/json
    
    {
      "query": "something warm",
      "exclude": { "allergens": ["peanut"] },
      "limit": 5
    }
    What comes back
    {
      "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.

    A scenario: the nut-free search must never return the kaju katli
    search          { "exclude": { "allergens": ["tree nuts"] } }
    must never      Kaju katli
    must return     Dal tadka
    at least        1 result

    Webhooks: 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.

    Checking a delivery in Node: the header is t=<seconds>,v1=<hex>
    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. 1

      query

      Words, up to 200 characters. Ranked by meaning and by matching text together.

    2. 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. 3

      filters

      Ordinary narrowing: a value, a list, or a {gte, lte} range on numbers and dates.

    4. 4

      exclude

      Safety exclusions, enforced in the query. Offered only on safety fields. Read the next section.

    5. 5

      sort

      Up to three fields. Missing values always sort last, either direction.

    6. 6

      limit and cursor

      Page size, and the opaque cursor from next_cursor. Send the same other parameters with it.

    7. 7

      include_receipts

      On by default. Turn it off for a smaller payload once you trust the pipeline.

    Adding it to Claude Code
    claude mcp add --transport http menu https://cloudcrane.ai/api/mcp/menu \
      --header "Authorization: Bearer cc_live_..."
    Adding it to any client that takes a JSON list of servers
    {
      "mcpServers": {
        "menu": {
          "url": "https://cloudcrane.ai/api/mcp/menu",
          "headers": { "Authorization": "Bearer cc_live_..." }
        }
      }
    }
    1. 1

      Claude Desktop, Cursor, Windsurf

      The JSON above. Some clients spell the block servers rather than mcpServers, and some want a type of http alongside the url.

    2. 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. 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. 4

      Dify and Flowise

      Add the endpoint as an MCP server with a Bearer token.

    5. 5

      LangChain

      MultiServerMCPClient with transport streamable_http and a headers dict.

    6. 6

      OpenAI Agents SDK

      MCPServerStreamableHttp with 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.