Skip to main content
One bearer arrives, one user is named, and every call after that answers to the same guard your own UI answers to.

Who is calling

The bearer on the request is a JWT your broker signed, and the door verifies it against the broker’s published keys. The sub claim is one of your own user ids, in your own spelling. The door then asks your auth preset to resolve that subject to a live principal, on every request. Return null there and the token is dead on the next call, sessions included. An anonymous or ephemeral principal is never served a session. The door persists grants and audit rows, so it needs a durable subject.

What the agent sees

With no menu authored, tools/list answers with your merged, enabled tools whose audience is end-user or unset. Operator and internal tools stay off it. Author a surfaces.mcp menu and that list is the menu instead — see Curate the menu below. Names, descriptions, and input schemas are served verbatim from your registry. A tool’s title is what a client shows a person instead of the wire name. Vendo’s own vendo_* tools ride along: vendo_make for a generated screen; the saved-apps viewer, which is vendo_apps_list, vendo_apps_open, and vendo_apps_call; and the caller’s own files, which is vendo_user_files_list, vendo_user_files_read, and vendo_user_files_put. They are runtime plumbing and are never curated away — a surfaces.mcp menu filters your tools, and the vendo_* prefix bypasses it. The file tools are scoped to the bearer’s subject and take no subject argument, so a token for user_1904 reaches that person’s files and no one else’s. See User data for what saves, what reads back, and the cap. Each listing carries MCP annotations derived from the tool’s Vendo risk label, so a client can warn before a write without re-reading prose. An ungraded tool asserts neither hint. MCP’s own default for destructiveHint is true, so leaving both out keeps the client on the spec’s conservative reading.

Curate the menu

A shorter, deliberate menu is named in .vendo/overrides.json under surfaces.mcp.
.vendo/overrides.json
The door then lists exactly those tools, and a call to any other name returns the not-found error an unknown name returns. The menu is a filter, not a checked list, so a tool a later add() registers is never stranded. The file itself resolves once at boot: edit it and restart. A menu is curation, not a permission boundary. Your policy, approvals, audit, disabled, and audience exclusions decide what may run, and none of them read this block.

How a call reaches your host tools

The agent has no session in your product, so the inbound bearer is never forwarded to your host routes. Door tool calls reach your API through the actAs seam. The door records the caller’s consent for that client id and projects it onto every context it mints. When a call resolves to a host route, Vendo hands actAs(principal, grant) that projection and uses the auth material it returns. Without actAs configured the tool returns a not-implemented error. A grant whose subject is not the current principal fails closed with act-as-subject-mismatch. The authenticated user is the authority. Scopes on the token buy no exemption, and the door adds none.

What comes back

Five outcomes, all of them in-band so a retry never re-executes a committed write. vendo_make answers with an id, a title, a status, and one line to say out loud. The screen itself lands in your product, never in the agent’s transcript. That line is say, and it is written for the person rather than for the model. An agent relays it close to verbatim and reports nothing else.

Rendering the app in your own chat

React only — there is no web component and no MCP Apps UI resource. If the chat calling vendo_make is your own frontend, <VendoAppEmbed> is the documented way to put the app inline in it, off the receipt’s own id.
status on the ref is always the literal "building", whatever the receipt actually said — the embed polls your own wire for the real state, so a receipt that already came back "ready" just resolves on its first poll. Skip the mount on "failed": nothing painted, and say already says why. While the build streams, the embed paints the app assembling step by step, then swaps to the live app in place. <VendoProvider> rides your product’s own session, not the MCP token — the same wrapper Embeds in your chat uses, because this polling never goes through the door.

Where a screen lands

A new screen goes to the person’s own list of views. Pass slot on vendo_make and it takes a named place on your page instead.
tools/call arguments
Slot ids belong to your product. Your surfaces report the ones they mount, so an agent passes an id it was told rather than one it invented. slot is for something new. Moving an app the person already has is a different pair of tools. A slot holds one app, so pinning replaces. Both are writes, and on the cautious preset they park like any other write. vendo_make refuses app and slot together, by name. An edit that also moved the app would evict whatever held that slot off the back of a change nobody aimed there.
Maple's home page with an empty hero slot reading This space builds itself, next to the total balance chart

A pin, start to finish. The slot is the caller's own, so the second person's home is untouched.

The same Maple home page after a pin, the hero slot now holding a savings goals table with a goal and target column for five goals
Maple's home page signed in as Mia Nakamura, the same hero slot still empty and reading This space builds itself

Approvals wait for the person

On the cautious preset, reads answer straight away and writes park. The agent gets an error naming the approval and where it is waiting. Approving does not resume the call. The agent has to call again, and it has to be on the same MCP session.
The door mints a fresh call id per tools/call, except for an identical call still parked in the same session, and the one-off approval is pinned to that exact id. A client that reconnects between attempts mints a new id and parks again.

The discovery your broker owns

The first unauthenticated request answers 401 with a WWW-Authenticate challenge naming the door’s protected-resource metadata. That document names your broker as the authorization server. A broker-fronted door serves no /authorize, /token, or /register of its own. Those live at the broker, which is why the exchange in Service keys & broker posts there. Broker mode is declared, never discovered. Nothing is registered anywhere and nothing is fetched at boot, so no deploy can repoint a live door behind its back.