Name the thing instead of calling it a door
One word stood for several: the MCP server, one address that accepts requests, the single way in, and the boundary a credential is validated at. Each site now names its own sense — endpoint, entry point, the MCP server, the edge, address, relay — or, where none of those fit, takes the word the surrounding code already uses. Prose and comments only, 1:1 line for line. Identifiers keep their names: CSS classes, DOM ids, struct fields, enum variants, config values and the doc comments that must open with the identifier they document.
This commit is contained in:
+10
-10
@@ -18,7 +18,7 @@ MEASURED at `hanzoai/[email protected]`: the hand-merged master carried 2093
|
||||
operations against cloud's 2333, sharing 1908 — so it described 185 operations
|
||||
nothing serves under that name and missed 425 that are served. Of the 185, 36 are
|
||||
refuted outright (real 404, nonsense-sibling control 404), 44 are unfalsifiable
|
||||
behind a prefix auth gate, 66 sit under a `{wildcardN}` relay door, and **39 are
|
||||
behind a prefix auth gate, 66 sit under a `{wildcardN}` relay route, and **39 are
|
||||
live and undescribed** — every one a hanzoai/cloud defect, itemised in LLM.md.
|
||||
|
||||
The projection is six codegen rules and nothing else. It exists because
|
||||
@@ -250,7 +250,7 @@ cross-language method rename, not cosmetics. It fell out of the resync rather
|
||||
than being chosen, and on review it stands: a route-derived id is a total
|
||||
function of the immutable `/v1` path, while a hand-authored id is owned by a
|
||||
spec that no longer describes the route and vanishes when that spec is deleted —
|
||||
the same break again, later, silently. It is also the name the MCP door already
|
||||
the same break again, later, silently. It is also the name the MCP server already
|
||||
uses. The break is once, now, and cannot recur for these operations.
|
||||
|
||||
### `mcp_Request.id` is a string — a scalar `oneOf` does not survive codegen
|
||||
@@ -261,21 +261,21 @@ carrier class for a scalar `oneOf` (Kotlin `class McpRequestId()` with no
|
||||
members) that serializes to `{}` — not a legal id anywhere. Java and C++ produce
|
||||
the same shape, so it blocked three languages on one declaration.
|
||||
|
||||
Verified on the live door before changing it, because the fix is only safe if
|
||||
Verified on the live MCP server before changing it, because the fix is only safe if
|
||||
the wire agrees: `id: "1"`, `id: 1` and `id: "abc-123"` are all accepted and all
|
||||
echoed back with the TYPE they were sent; omitting `id` returns `null`. A string
|
||||
is therefore always legal, and a client generated from this document always
|
||||
reads back the string it sent.
|
||||
|
||||
Declared `type: string` on both `Request` and `Response`, with the polymorphism
|
||||
recorded in the prose rather than in a shape — no second door, no alias, one
|
||||
recorded in the prose rather than in a shape — no second type, no alias, one
|
||||
declaration. The generated field is now `kotlin.String?`, `private String id`,
|
||||
`Id *string`; the `McpRequestId`/`McpResponseId` carriers are gone from every
|
||||
client.
|
||||
|
||||
### Declare the MCP door — `POST /v1/mcp`, the one address an MCP client speaks
|
||||
### Declare the MCP endpoint — `POST /v1/mcp`, the one address an MCP client speaks
|
||||
|
||||
The fleet's JSON-RPC door has been answering all along and was in no spec:
|
||||
The fleet's JSON-RPC endpoint has been answering all along and was in no spec:
|
||||
`POST /v1/mcp` returns 200 with 796 tools, unauthenticated. It read as missing
|
||||
because the probe was a GET — the route is POST-only, so GET is 404 — and
|
||||
because the document held only `/v1/mcp/servers`, the registry of external
|
||||
@@ -284,14 +284,14 @@ operation (`mcp_rpc`) with the JSON-RPC 2.0 request and response typed:
|
||||
`initialize`, `tools/list`, `tools/call`, a `Tool` with its `inputSchema`, and
|
||||
both failure channels that live INSIDE a 200 — `error` (`-32601` for an unknown
|
||||
method) and `result.isError` (a tool that ran and refused). Verified against the
|
||||
live door, not guessed: `serverInfo`, `protocolVersion: 2025-06-18`, and the
|
||||
live MCP server, not guessed: `serverInfo`, `protocolVersion: 2025-06-18`, and the
|
||||
`content[]` shape are all copied from its own answers.
|
||||
|
||||
The `tools` flow moves onto it, and the probe rule that hid it is corrected
|
||||
everywhere it was written: **probe the method the document declares, not GET.**
|
||||
A POST-only route and an absent route are indistinguishable from a GET.
|
||||
|
||||
What the door exposes is recorded because it cannot be derived: 796 tools
|
||||
What the MCP server exposes is recorded because it cannot be derived: 796 tools
|
||||
against 2479 operations, so about two thirds of the surface is not a tool, and
|
||||
the split is the binary's decision. The naming rule is mechanical — a tool name
|
||||
is its operationId minus the leading `<service>_` — holding for 795 of 796. The
|
||||
@@ -301,7 +301,7 @@ one identifier in every generator, while its tool is still `get_v1_pricing_polic
|
||||
|
||||
A tag whose key is a service NAME now belongs to that service's domain rather
|
||||
than to whichever spec used it first: `MCP` was tagged by four specs, the
|
||||
earliest alphabetically was `automations`, and the fleet's MCP door was being
|
||||
earliest alphabetically was `automations`, and the fleet's MCP endpoint was being
|
||||
filed under Streams.
|
||||
|
||||
### `hanzo.yaml` is THE published document — cloud's woven spec merged, and it wins
|
||||
@@ -350,7 +350,7 @@ served, so every one was re-resolved against the merged document AND probed.
|
||||
`money` and `agent` move to cloud's ids (`billing_*` and
|
||||
`cloud_AgentsController.*` no longer exist — the binary's document owns those
|
||||
routes now). `store` moves from the KV value plane to the store itself, and
|
||||
`tools` from the automations MCP door to `/v1/tools`: both of the originals
|
||||
`tools` from the automations MCP endpoint to `/v1/tools`: both of the originals
|
||||
reply 404 to GET and 405 to PUT/POST/DELETE, which is what a GET-only wildcard
|
||||
answers when nothing is routed there. `hello` and `chat` were already right —
|
||||
`ai_getAccount` returns 200 with the owner and name to print.
|
||||
|
||||
@@ -60,15 +60,15 @@ Each was probed at `api.hanzo.ai` with **its own method** (a POST-only route
|
||||
answers 404 to GET, so a GET sweep cannot tell "absent" from "wrong verb"), and
|
||||
each probe carries a CONTROL: the same address with `-zzq9` appended to its last
|
||||
literal segment. 119 are addresses no live route pattern covers; 66 sit under a
|
||||
`{wildcardN}` relay door, which by construction answers identically for a real
|
||||
`{wildcardN}` relay route, which by construction answers identically for a real
|
||||
path and an invented one and therefore decides nothing.
|
||||
|
||||
| verdict | n | reading |
|
||||
|---|---:|---|
|
||||
| **ABSENT** — real 404, control 404 | **36** | refuted. Nothing serves this. |
|
||||
| **UNFALSIFIABLE** — real ≠ 404, control identical | **44** | a gate or a door answered, not a route |
|
||||
| **UNFALSIFIABLE** — real ≠ 404, control identical | **44** | a gate or a relay answered, not a route |
|
||||
| **SERVED** — real ≠ 404, control 404 | **39** | live, and cloud's document lacks it |
|
||||
| absorbed by a `{wildcardN}` door | 66 | unfalsifiable by construction |
|
||||
| absorbed by a `{wildcardN}` relay | 66 | unfalsifiable by construction |
|
||||
|
||||
**A 404 on both sides IS a refutation, and it is the only way absence can ever be
|
||||
shown.** The control exists to stop a 401/403/200 being read as existence — a
|
||||
@@ -136,7 +136,7 @@ served as `DeleteSession`, and the suffix went out verbatim in the tool catalogu
|
||||
of three client distributions, naming a tool `POST /v1/mcp` answers to no such
|
||||
name. So the rename now records what it renamed: `x-id` is the id hanzo-inc/cloud
|
||||
emitted, written only where the two differ, and `tools.py`'s `wire()` is the one
|
||||
place a tool takes its name. Measured against the live door: 1299/1323 before,
|
||||
place a tool takes its name. Measured against the live MCP server: 1299/1323 before,
|
||||
1300/1323 after. The collision itself belongs upstream — two operations whose ids
|
||||
differ only in case are one name to every generator there is.
|
||||
|
||||
@@ -310,7 +310,7 @@ separate functions here (`refuted()` fetches; the caller decides).
|
||||
make a document GENERATABLE; a doc page needs none of them, because it reads
|
||||
paths, tags, summaries and schemas, every one of which cloud emits itself. So the
|
||||
projection bought the reference nothing and cost it currency — its pin sat 19
|
||||
cloud releases back, rendering four relay-door products with twelve operations
|
||||
cloud releases back, rendering four relay products with twelve operations
|
||||
each where the document has one, one, one and two. A consumer belongs here only
|
||||
while it needs what `publish.py` adds. Codegen does; prose does not.
|
||||
|
||||
@@ -350,7 +350,7 @@ Nothing below is fixable in this repo, and every item is measured at
|
||||
`wecom-bot`, `traffic`, `provider-flags`, `docs`, `health`, `feedback`,
|
||||
`documents`) that are that seam's hand-declared shrapnel rather than products.
|
||||
- **`POST /v1/mcp`** — 200 with the tool list, control `/v1/mcp-zzq9` 404, in no
|
||||
document. The fleet's one MCP door is undeclared, which cost `flows.yaml` its
|
||||
document. The fleet's one MCP endpoint is undeclared, which cost `flows.yaml` its
|
||||
`tools` example. Note it answers 404 to GET, which is why a GET-only sweep has
|
||||
twice concluded it does not exist.
|
||||
- **`GET /v1/{world,evals,referrals}/health`** — 200, controls 404, undescribed.
|
||||
@@ -360,12 +360,12 @@ Nothing below is fixable in this repo, and every item is measured at
|
||||
registers `/v1/s3/{name}` (provisioning) and `/v1/s3/buckets/…` (storage);
|
||||
`manifest/apps.go:83` already notes those two owners share one prefix.
|
||||
|
||||
### 2. Relay doors that can only ever emit `{wildcardN}` — 66 addresses
|
||||
### 2. Relay routes that can only ever emit `{wildcardN}` — 66 addresses
|
||||
|
||||
`bot` (32), `dns` (16), `tasks` (8), `collections` (5), `kms` (2), and one each
|
||||
of `download`, `exec`, `files`. Each is a single `app.All("/v1/<x>/*")`
|
||||
registration, so there is no per-operation site and the document can only show
|
||||
the door. The fix is projecting the mounted registry, exactly as for `apps/ai`;
|
||||
the relay. The fix is projecting the mounted registry, exactly as for `apps/ai`;
|
||||
until then these addresses are unknowable from outside, and the honest document
|
||||
says so rather than guessing a list.
|
||||
|
||||
@@ -532,7 +532,7 @@ Every SDK also renders `flows.yaml` into its `examples/` — the same six journe
|
||||
from the same operationIds, in every language. An SDK does not choose its own
|
||||
examples any more than it chooses its own methods. Two of the six moved in this
|
||||
change, both because a hand-authored operationId vanished with its hand-authored
|
||||
spec: `hello` was `bot_authMe` (`/v1/bot/auth/me`, now behind a relay door) and
|
||||
spec: `hello` was `bot_authMe` (`/v1/bot/auth/me`, now behind a relay route) and
|
||||
is `get_v1_keys`; `tools` was `mcp_rpc` (`POST /v1/mcp`, undeclared) and is
|
||||
`get_v1_tools`. Both replacements were probed with controls — see `flows.yaml`.
|
||||
|
||||
@@ -542,7 +542,7 @@ The `<svc>_` prefix `merge.py` applied is gone. It existed to keep 52 authored
|
||||
specs from colliding, and with one document there is exactly **one** collision in
|
||||
2284 operations. `cloud_get_v1_billing_balance` is now `get_v1_billing_balance`;
|
||||
`gateway_createChatCompletion` is `post_v1_chat_completions`. A prefix invented
|
||||
here was a second naming authority, and the bare id is the one the MCP door
|
||||
here was a second naming authority, and the bare id is the one the MCP server
|
||||
already uses — a tool name is its operationId. Pin against the document, not
|
||||
against a remembered method name.
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ move upstream into cloud's emitter; see [LLM.md](LLM.md).
|
||||
the API's own version is `/v1`. Three things, three places.
|
||||
- operationIds are the document's own (`get_billing_balance`) — no `<svc>_`
|
||||
prefix, because a prefix invented here is a second naming authority and the
|
||||
bare id is what the MCP door already uses as a tool name.
|
||||
bare id is what the MCP server already uses as a tool name.
|
||||
- No `deprecated: true`. No `/api/` prefix. No dates in filenames.
|
||||
- Security: every operation uses cloud's `bearer` scheme. No service has its own login.
|
||||
- Errors: 4xx client, 5xx server, JSON body with `status` and `msg`.
|
||||
|
||||
+4
-4
@@ -46,7 +46,7 @@ domains:
|
||||
role: "models · agents · evaluation — the mind of the cloud"
|
||||
# The bare-/v1 inference surface (chat, completions, embeddings, messages,
|
||||
# rerank, models, images, audio, videos, responses) is Hanzo AI's OpenAI- and
|
||||
# Anthropic-compatible door: there the PATH is the compatibility contract,
|
||||
# Anthropic-compatible surface: there the PATH is the compatibility contract,
|
||||
# because every vendor SDK hard-codes it. It is served by cloud like
|
||||
# everything else — measured with nonsense-sibling controls: GET /v1/models
|
||||
# 200 while /v1/models-zzq9 404s, POST /v1/chat/completions 401 while its
|
||||
@@ -55,7 +55,7 @@ domains:
|
||||
# The long tail here (query, query_multiple, install-patch, dev-bridge,
|
||||
# wecom-bot, traffic, docs, health, feedback, documents) is SHRAPNEL from one
|
||||
# seam, not ten products: apps/ai relays a whole embedded router through one
|
||||
# door and hand-declares the operations behind it, so its declarations tag
|
||||
# endpoint and hand-declares the operations behind it, so its declarations tag
|
||||
# themselves. Those names disappear from this file the release apps/ai
|
||||
# projects the mounted registry instead — `provider-flags` already has.
|
||||
tags: [ai, router, agent, agents, ml, finetune, evals, benchmark,
|
||||
@@ -87,8 +87,8 @@ domains:
|
||||
# that share the carrier with it. It files here rather than under
|
||||
# Applications because a call is a message reaching a person over somebody
|
||||
# else's network, not a surface of its own.
|
||||
# `git-webhook` is the door the forge pushes through, not a thing anybody
|
||||
# opens: one POST that a push fires, which starts a build. Its family is
|
||||
# `git-webhook` is the endpoint the forge pushes to, not something a person
|
||||
# calls: one POST that a push fires, which starts a build. Its family is
|
||||
# `webhooks` and `event` — inbound async — rather than `git`, which is the
|
||||
# surface a person uses.
|
||||
tags: [tasks, pubsub, mq, notify, tel, automations, auto, flow,
|
||||
|
||||
+5
-5
@@ -26,7 +26,7 @@
|
||||
# live handler says "not found" too.
|
||||
# • PROBE THE METHOD THE DOCUMENT DECLARES, not GET. A POST-only route also
|
||||
# 404s on GET: POST /v1/mcp answers 200 with 796 tools while GET /v1/mcp is
|
||||
# 404, and reading that 404 as absence is how the fleet's one MCP door
|
||||
# 404, and reading that 404 as absence is how the fleet's one MCP endpoint
|
||||
# stayed undeclared. One verb is never a liveness probe.
|
||||
#
|
||||
# And a 200 is not proof either — see `hello`: /v1/ai/account answers 200 with
|
||||
@@ -64,7 +64,7 @@ flows:
|
||||
Verified with a nonsense-sibling control: GET /v1/keys answers 403
|
||||
{"code":"forbidden","error":"sign in to manage API keys"} with NO key and
|
||||
with a bogus one, while GET /v1/keys-zzq9 answers 404 — so the 403 is this
|
||||
route refusing, not a wildcard door. With a real key it returns the
|
||||
route refusing, not a wildcard relay. With a real key it returns the
|
||||
caller's own keys. An SDK's hello must be the call that says no.
|
||||
|
||||
|
||||
@@ -154,8 +154,8 @@ flows:
|
||||
activated. Assert the count is greater than zero and print three names.
|
||||
|
||||
|
||||
This flow named `mcp_rpc` (POST /v1/mcp, the JSON-RPC door) until the
|
||||
document became the only authority on what exists. That door IS live —
|
||||
This flow named `mcp_rpc` (POST /v1/mcp, the JSON-RPC endpoint) until the
|
||||
document became the only authority on what exists. That endpoint IS live —
|
||||
POST /v1/mcp answers 200 with the tool list while POST /v1/mcp-zzq9
|
||||
answers 404, so the address is real and the control refutes nothing — but
|
||||
cloud's emission does not declare it, so it has no operationId and no
|
||||
@@ -167,4 +167,4 @@ flows:
|
||||
|
||||
Note for whoever writes that declaration: JSON-RPC reports failure INSIDE a
|
||||
200, so a client reads `error` before `result` and `result.isError` before
|
||||
`result.content`. The same door with method=tools/call runs one.
|
||||
`result.content`. The same endpoint with method=tools/call runs one.
|
||||
|
||||
+3
-3
@@ -248,8 +248,8 @@ def project(doc, groups):
|
||||
`deleteSession` vs `DeleteSession`). The later one by path order takes a
|
||||
suffix, which is what a generator does for the duplicates it can see —
|
||||
and it keeps the id cloud emitted as `x-id`, because an operationId is
|
||||
ALSO the wire name of an MCP tool. The suffix exists for Go; the door
|
||||
does not know it. Whoever needs the name a caller can use reads `x-id`
|
||||
ALSO the wire name of an MCP tool. The suffix exists for Go; the MCP
|
||||
server does not know it. Whoever needs the name a caller can use reads `x-id`
|
||||
first. The collision itself belongs upstream: two operations whose ids
|
||||
differ only in case are one name to every generator on earth.
|
||||
|
||||
@@ -304,7 +304,7 @@ def project(doc, groups):
|
||||
# a fact about GENERATORS and the id is also the WIRE NAME: it is
|
||||
# what `POST /v1/mcp` answers to. Renaming it silently put
|
||||
# `DeleteSession_2` in the MCP catalogue of three client
|
||||
# distributions, and the door knows no such tool — a suffix
|
||||
# distributions, and the MCP server knows no such tool — a suffix
|
||||
# invented here for Go's benefit is not a thing anything serves.
|
||||
# Present only where the two differ, so its meaning is its
|
||||
# presence. `tools.py` names the tool by this.
|
||||
|
||||
@@ -730,7 +730,7 @@ sdks:
|
||||
# an interpolated value: the gemspec read `hanzo-inc/cloud's`, and
|
||||
# rubygems renders a gem description verbatim, so that is what the registry
|
||||
# page would have shown.
|
||||
gemDescription: The official Ruby library for the Hanzo API — generated from the OpenAPI document that hanzo-inc/cloud emits.
|
||||
gemDescription: The official Ruby library for the Hanzo API — generated from the API OpenAPI document.
|
||||
gemHomepage: https://github.com/hanzo-ruby/sdk
|
||||
gemLicense: Apache-2.0
|
||||
gemAuthor: Hanzo
|
||||
@@ -867,7 +867,7 @@ sdks:
|
||||
# generator also offers pubspec.yaml, analysis_options.yaml, README.md,
|
||||
# .travis.yml and git_push.sh at the root; naming lib/ leaves them where
|
||||
# the fleet `drop` list would have had to grow two Dart-shaped entries.
|
||||
# The repo owns its manifest, its lint config and its front door.
|
||||
# The repo owns its manifest, its lint config and its README.
|
||||
take: { lib: lib }
|
||||
# TWO mustache overrides, and without them there is no Dart client at all.
|
||||
#
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ output (dist/ is generated, never committed):
|
||||
exception. This is the whole contract. A generator that synthesises names
|
||||
from paths is a THIRD naming rule, and a third rule is the disease this file
|
||||
exists to prevent — as is publishing `publish.py` rule 6's codegen suffix,
|
||||
which the door does not answer to;
|
||||
which the MCP server does not answer to;
|
||||
• one name, one tool — a duplicate id makes the second tool unreachable;
|
||||
• callable verbs only — OPTIONS/TRACE are transport machinery, never tools;
|
||||
• self-contained schemas — every `$ref` a tool emits resolves inside that
|
||||
@@ -54,7 +54,7 @@ def test_name_is_the_operation_id():
|
||||
|
||||
|
||||
def test_no_codegen_suffix_becomes_a_tool_name():
|
||||
"""Rule 6's suffix is a fact about Go, and the door does not answer to it.
|
||||
"""Rule 6's suffix is a fact about Go, and the MCP server does not answer to it.
|
||||
|
||||
`publish.py` renames an operationId when two collide under the identity a
|
||||
generator uses, so `DELETE /v1/o11y/sessions` is published as
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
ONE source, and the source is the DOCUMENT. A tool name is an operation id:
|
||||
not derived from the path, not looked up in a table, not invented here. The
|
||||
fleet's own door already answers that way — every tool `POST /v1/mcp` lists is
|
||||
fleet's own MCP server already answers that way — every tool `POST /v1/mcp` lists is
|
||||
verbatim an id hanzo-inc/cloud emitted — so a catalogue built any other way would
|
||||
name tools the API cannot be told to run. Which is measurable, and was measured
|
||||
against the door: the id, not the published `operationId`. `publish.py` rule 6
|
||||
against the MCP server: the id, not the published `operationId`. `publish.py` rule 6
|
||||
suffixes an id when two collide under a generator's identity, and that suffix is
|
||||
a fact about Go rather than about the API — see `wire()`, which is the only
|
||||
place a tool gets its name.
|
||||
@@ -34,12 +34,12 @@ The projection is total and mechanical:
|
||||
machinery, not something an agent calls;
|
||||
• `inputSchema` is the operation's parameters and request body as one flat
|
||||
JSON Schema object, component `$ref`s carried along into `$defs` — the same
|
||||
shape the live door serves, so a tool generated here and a tool listed
|
||||
shape the live MCP server serves, so a tool generated here and a tool listed
|
||||
there are callable the same way.
|
||||
|
||||
WHICH operations a given deployment will actually answer for is not a fact
|
||||
about the document — it is a fact about what that deployment mounts and what
|
||||
the caller's key reaches. The door answers that at runtime. This file answers
|
||||
the caller's key reaches. The MCP server answers that at runtime. This file answers
|
||||
the other question, the only one a static artifact can: what the contract says
|
||||
exists. Filtering here would bake one deployment's answer into every client.
|
||||
|
||||
@@ -61,7 +61,7 @@ OUT = os.path.join(ROOT, "dist", "tools.json")
|
||||
SCHEMA_ID = "hanzo.mcp-tools/v1"
|
||||
|
||||
# The verbs an agent can be told to call. OPTIONS/TRACE are transport
|
||||
# machinery and are never tools; the live door omits them too.
|
||||
# machinery and are never tools; the live MCP server omits them too.
|
||||
VERBS = ("get", "post", "put", "patch", "delete")
|
||||
|
||||
REFS = "#/components/schemas/"
|
||||
@@ -74,7 +74,7 @@ def wire(op: dict):
|
||||
the identity a GENERATOR uses (punctuation stripped, case folded), so that
|
||||
Go does not declare one request type twice. That suffix is a fact about
|
||||
codegen: `DELETE /v1/o11y/sessions` is `DeleteSession_2` in the published
|
||||
document and `DeleteSession` on the wire, and the door answers to exactly
|
||||
document and `DeleteSession` on the wire, and the MCP server answers to exactly
|
||||
one of those. Publishing the other named a tool in three client
|
||||
distributions that nothing can be told to run. Where the projection had to
|
||||
change the name it records the original as `x-id`, so this is the whole
|
||||
|
||||
Reference in New Issue
Block a user