---
cuppy_hub: cuppy
cuppy_path: guides/sdk-and-transport.md
cuppy_section: docs
cuppy_doc: EDD6-DR36
cuppy_doc_uuid: 153e35cd-539b-4bed-bb62-a78cc8014a79
cuppy_permalink: https://cuppy.io/cuppy/d/EDD6-DR36
cuppy_version: 1
cuppy_updated: 2026-10-17T06:32:45.511Z
cuppy_canonical: https://cuppy.io/cuppy/docs/guides/sdk-and-transport
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 582d868c8c23ec3debb42d9eea4437721321006cab241aec01efaa261f857ac3
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---

# Cup.py and the transport

This guide is for a client: an agent, a script or a library that has to talk to this
surface and wants to know what is actually required. It has three parts — the protocol,
the client contract, and the credential question — and it marks every claim as measured
on a run, read from the specification, or not built yet.

## 1. Two eras, one endpoint

The protocol this surface speaks changed shape in 2026. The relevant difference, for a
client:

| era | what it looks like |
|---|---|
| **legacy** | an `initialize` handshake, a protocol session identifier on a header, a long-lived server-push stream, elicitation over that stream |
| **modern** | no handshake and no protocol session: each request carries its protocol version, an up-front discovery method replaces trial negotiation, results declare a result type, a cache scope and a time to live, and multi round-trip requests replace server-initiated calls |

**One endpoint serves both.** That is this platform's choice and not a protocol
guarantee, and the reason it was made is worth stating: no claim about which era a
given client speaks is better than second-hand, the cost of serving both is one handler,
and the cost of guessing wrong is a dead integration. The client matrix in the
specification marks every row as unverified, deliberately.

**What a run measured, and why you should still not assume it.** On the release this hub
was tested against, **with the modern-transport flag on**, the server answered protocol
version `2025-06-18` with a stable list key across the whole run, and both of the method
rules below held — the harness sheet's rows M3, M6, M17 and M18, measured in
`docs/commander/RUNS/2026-10-15-public-staging.md`. The specification's older reading,
that the surface pins 2024-11-05, describes the handler a **flags-off** deployment
serves, and the cutover is chartered flags-off, so that version may still be the answer
on a given host. **Read the version out of the response and send that back; do not
assume either value.**

Two endpoint rules a client meets immediately:

- **The tool list is a pure function of the protocol era and the registry version.** Not
  of the endpoint, the caller, the credential, the plan or the tier. It is returned in
  registry order, and the hub-scoped endpoint forms differ only in which hub the calls
  resolve against by default.
- **POST only.** A GET and a DELETE on an MCP path answer `405`. That is this platform's
  endpoint hygiene, not a protocol requirement: the modern era removed the GET stream
  and the session lifecycle a DELETE used to end.

**The list key**, which is how caching works here, is a hash of the era and the registry
version. It is returned in the metadata of every cacheable result and as the HTTP entity
tag, and a client may send it back to receive a not-modified answer. Because the key has
no hub input, every endpoint shares one cached list per era. A call naming a tool outside
the registry answers with a method-not-found and the current list key, so **a stale list
degrades to one round trip and never to a wrong answer**.

## 2. What a client needs

The minimum, and it is genuinely this short:

1. **POST** to the endpoint with a JSON body, `content-type: application/json`, and an
   `accept` header naming both `application/json` and `text/event-stream`.
2. After the first call, send the **protocol version** the server returned on every
   later call.
3. **No credential for a read.** Anonymous is a first-class caller on the read surface.
   It is never a caller on a write.
4. **Identify yourself.** Send a session label on every call that takes one, and your
   model name and start time where an agent-info argument is accepted. This is how a run
   is matched on the server's own rows afterwards; an unlabelled run cannot be.
5. **Mint a handle and pass it back.** The modern transport has no connection state, so
   a session is an opaque handle a tool returns and later calls carry as an argument.
   The boot call mints nothing and reads nothing by design.
6. **Read the refusal.** Every refusal names what was refused, why, and the call that
   recovers. Do not retry a refusal unchanged.

Two things not to build. Do not hold a stream open: this surface opens none and declares
no subscription of any kind. Do not batch JSON-RPC arrays: they are refused with a
message. A long-running command returns a run handle immediately and you poll it.

**The compatibility pair.** Two tools, `search` and `fetch`, exist under exactly those
names because some client families require them: `search` returns a list of hits with an
id, a title, a URL and a snippet, and `fetch` returns one document by that id. They are
the plainest possible retrieval pair and they are core on every endpoint.

**A caveat about `search` on the tested release, measured and recorded.** The tool named
`search` reads a different corpus from the hub's own documents: on the run above, five
queries about this server's own documentation returned hits from another product's
corpus, with working ids, and nothing in the answer named the corpus; the same question
asked through the documents tool returned twelve hits from this hub. It is recorded as
a finding (`tracking/backlog/hub.md` item 34.3) and the fix is the owner's call. Until
it lands: **to read this hub, use the documents tool, not `search`.**

## 3. The eighteen tools, and what the kit wraps

The registry declares **eighteen** tools, counted from the normative command registry in
the specification: four single-call tools (`_hello`, `help`, `search`, `fetch`) and
fourteen umbrella tools that take a `command` argument (`account`, `hubs`, `session`,
`decisions`, `insights`, `docs`, `layers`, `skills`, `knowledge`, `memory`, `ops`,
`hub_admin`, `tenant_admin`, `redaction`).

**Six of the eighteen are on the wire on the tested release**, and this is the single
most useful fact for a client author: `_hello`, `docs`, `fetch`, `help`, `search` and
`session`, with nine reachable command rows between them. That is the harness sheet's
rows M7, M8 and M9, measured on the same run. A step that asks for anything else is
answered honestly as not served, and a client that improvises a route around a missing
capability has stopped measuring this surface.

**The software development kit is Cup.py**, and it is specified rather than built. The
Python distribution is `cup.py`, the import name is `cuppy`, the command line is
`cuppy`; the TypeScript package is `@cuppy/sdk`. What the kit is for: wrapping the
eighteen tools as functions, scaffolding a hub from the skeleton, validating front
matter and conformance, importing and exporting bundles, and a thin client for scripts.
The program rule attached to it is the one that matters — the kit is the only client the
platform's own interfaces use, so it can never fall behind the surface.

**Nothing of it exists in this tree.** Searching the repository for either package name
at the base commit in this document's front matter returns design documents and no
implementation; the kit and its validator are later-phase work. Treat every sentence in
the paragraph above as a specification, and treat `cuppy validate` as a proposal with a
defined shape. What exists today is the MCP surface itself, which any client can call
over plain HTTP with the six steps in section 2.

## 4. Where credentials stand today

Three doors are specified, and they are not interchangeable.

**Anonymous** is the read door, and it works. The run in section 1 made every call
anonymously, against a public hub, and reached the hub's boot document that way.

**A key is a machine credential**, issued by an account owner for an app talking to
cuppy. It is explicitly **not the user door**: a person signing in is not supposed to
paste a key, and the key classes the hub mints are scoped by purpose — a bearer key, a
continuous-integration signing key, and a publisher key whose reach is a closed
enumeration of three write commands and nothing else. A publisher key may write
documents under its own layer, attach notes and report outcomes; every other command is
refused for it on every hub including its own.

**OAuth is the third door, not the only one**, and today it is built and closed. The
resource-server half is implemented behind a flag that defaults off: protected-resource
metadata, the challenge that starts discovery, and bearer validation against the
authorization server's published keys. With the flag off there is nothing to discover.
And even with it on, the door is inert for a person, because the authorization server
mints no token whose audience is cuppy and whose subject is a human being yet; the
change that would is the owner's open decision D-13 and belongs to another system's
release process. So: **the OAuth door is behind a flag until the identity side ships**,
and a client should not plan a user login against it.

**A write needs a resolved principal.** A row the registry marks as a write, a
destructive operation or a long one, reached by a caller for whom no principal resolves,
answers `principal_required` **before any write** and names the credential kind that
would resolve one. That is a different answer from "known identity, insufficient rung"
and a different answer again from "no such hub", and the distinction is the point: each
tells the client a different thing to do next.

Two rules that are frozen and will not be relaxed for convenience. **A role comes from
the credential, never from a header** — a development-mode header once granted the owner
rung to anyone for two days, and that is why the rule is written this way. And **a
shared published key is not a principal**: it resolves a rung and names no identity.

## 5. If something goes wrong

- **A 405** means you used GET or DELETE. Use POST.
- **A method-not-found** naming a tool means your list is stale: list the tools again,
  and the answer carries the current list key.
- **A refusal naming a command** means the tool is served and the command is not. Ask
  `help` for the tool: the refusal names that call.
- **A not-found about a hub or a document** is deliberately the same answer whether the
  thing is absent or invisible to you. Do not read it as a hint that something exists.
- **An authentication refusal on a write** is the control working. Resolve a principal
  or stop; do not look for another route to the same write.

## Next reads

- `guides/connect.md` — how to connect from each client, with the deep links.
- `reference/cuppy/mcp-architecture.md` — the surface in full: the tools, the umbrella
  shape, the error envelope.
- `guides/agent-ergonomics.md` — what a call promises you and how it names your next
  action.
- `guides/test-loop.md` — how a client agent tests a hub, and what the first run of this
  one found.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 18 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/cuppy/
<!-- /cuppy -->
