createVendo. Open the
MCP door instead and an agent you do not run acts in
your product as the person who signed in, through the same guard, approvals,
and audit your own surfaces use.
Nothing to build on the agent’s side: it reads the door’s tools/list and
calls what it finds — your host actions, and Vendo’s own vendo_make.
1. Open the door
Three things, all host decisions, none of them scaffolded byvendo init:
mcp: true, an OAuth adapter, and the origin-root discovery route.
npx vendo doctor checks the wiring. The adapter contract and the consent page
are on MCP door.
2. Connect any client
Hand the client one URL:https://your-app.example.com/api/vendo/mcp/connect — your product’s name, the
URL to paste, and per-client steps for Claude, ChatGPT, and Cursor. Link users
to it from your settings.
3. Claude Code: install the plugin
Claude Code users can skip the paste. The Vendo plugin is a manifest, the connection, and one skill:examples/claude-code-plugin.
What the agent gets
surfaces.mcp in .vendo/overrides.json; the
vendo_* tools are never curated away. See
Curate the tool menu.
vendo_make
The receipt law
vendo_make answers with four fields of words, never UI:
say is one line in the person’s voice, written to be said close to verbatim.
status is "ready", "building", or "failed", and "building" is an
honest answer rather than an error — an escalated build outlives the call, and
there is nothing to poll. The agent never receives the tree, the components, a
payload, or a URL, which is exactly what stops it narrating a screen it cannot
see. Pixels go server → your page; words go to the agent.
Where the screen lands
By default, the person’s own apps list in your product. Passslot and it
lands in that spot on your page instead — the same <VendoSlot> your
in-product surfaces fill:
@vendoai/ui as a direct dependency. Empty, the slot renders your own
children; targeted, it shows a build skeleton, then the live screen or the
failure with a retry — no blank hole in any state. One app per slot per person:
placing a second one evicts the first.
Slot ids are yours, and nothing enumerates them for an outside agent: tell the
agent the id, or let the person say it. The plugin skill is taught never to
invent one — an invented id puts the screen where nobody is looking, and that
does not look like an error.
vendo_apps_pin and vendo_apps_unpin move an app the person already has in
and out of a slot. Both are writes, judged like any other write: under a
cautious policy the first one parks in your approvals queue. The routes
behind all three are in HTTP routes.
<VendoToolResult> — see
Embeds and envelopes. Over MCP the slot is the
destination, and the outside agent’s own chat stays text.Walk through it
What the whole thing looks like end to end, run against thedemo-bank
host (Maple) with an outside MCP client. Every screenshot below is that run.
Sign the agent in
tools/list then carries your host actions and the vendo_*
tools together. Nothing about the connection is your code.Show the agent where a view can land

The host's own hero slot before anything is placed — Maple's markup, not Vendo's.
Ask for a view with a destination

The same slot, filled. The figures come from the host's own spending endpoint, read at render time.
Or ask with no destination, and place it after
slot out and the screen goes to the person’s apps list. To move it
onto the page later, the agent calls vendo_apps_pin with the app — by id,
or by the name the person used.That is a write, so under a cautious policy it parks. The person sees the
tool, the app, the slot, and the exact arguments before anything moves:
The parked pin in Maple's approvals queue. The agent is told an approval is pending; nothing has changed yet.

The pinned app in the hero slot, replacing what held it.
Take it back out
vendo_apps_unpin clears the slot. The app itself is untouched — it stays
in the person’s apps and can go back any time — and the slot returns to
your own children:
After unpinning. This is the same screenshot as step 2, because the page really is identical again.
Check it is per person

A second user's dashboard. The first user's pinned app is not there, and their apps list is their own.
Two things that are not bugs
A build can come backfailed, with a reason. The checks floor rejects a
screen whose bindings claim data your host does not return, or whose props do
not type-check — nothing is painted and whatever held the slot stays. The agent
gets that sentence and should retry once on the same app with a narrower
request rather than rebuilding from scratch.
Placing is a write, and writes park. Reading is not: on Maple’s policy the
agent’s host_* reads answer straight away, while every vendo_apps_pin and
vendo_apps_unpin waits for the person. Your policy decides where that line
sits — the door adds no exemption to it either way.
The perimeter does not move
- Every door call runs the guard-bound registry — policy, approvals, audit — under the OAuth’d subject. The door adds no second authority path.
- A parked call waits in your approvals queue. The agent is told an approval is pending and moves on; the person approves in your UI, and the agent learns the outcome on a later turn if it matters.
- Door calls reach host routes through the
actAsseam, never by forwarding the MCP bearer. Without a grant or a consent record, the call fails closed. - Returning
nullfromoauth.principal(subject)kills every live session for that person on the next request.
Verify
GET /status reports blocks.mcp. For an end-to-end proof
with no Vendo code in the agent’s process:
say, and asserts the receipt law itself: if a
tree, components, a machine, or a snapshot ref ever appear in a receipt, it
exits non-zero and says which one.
Next
MCP door internals
remoteAs, federation, and
revocation.