Skip to main content

The picture

Acme pastes an MCP URL and a token into your admin screen. From the next request, Acme’s users have Acme’s tools. Globex’s users never see them — not because a filter hid them, but because Globex is served a different tool registry. There is no Vendo-hosted UI here. The API is server-side, and the screen around it is yours.

Registering

register is save-and-test in one call: it validates by actually connecting, and answers with the tools the server really advertised.
Paste the server’s Streamable HTTP URL — usually /mcp, never /sse. The connector POSTs every JSON-RPC message to that one URL and reads a text/event-stream answer to that same POST. It speaks no legacy HTTP+SSE — there is no GET-opened stream and no second message URL — so a /sse endpoint refuses the POST and the registration comes back unavailable, carrying the server’s own MCP HTTP <status> as its message.
A refusal is typed, never a thrown string:
An OpenAPI tenant passes a spec instead of an MCP URL. url then names the API’s base URL.

How you wire it

The screen is yours and so is the route behind it. Vendo supplies the registration API and nothing else — who may administer an org is a question your own auth answers. Start with a form that has no org field, because the org is not the browser’s to name:
app/settings/connectors/page.tsx
The route behind it does three things, in this order: authorize the caller with your own auth, read the org off the session, then register.
app/api/tenant-connectors/route.ts
Render both branches. A refusal is the other half of this API: the org pasted a URL that does not answer, or a spec that is not usable, and the admin who pasted it is the only person who can fix either.
Take org from the session, never from the request body. A body field any signed-in user can set lets that user register a connector against an org they do not belong to — the token they paste is then vaulted under that org and handed to its members on their next request.
Nothing binds until your deployment asserts memberships: a registration reaches only a request whose memberships seam names the org that owns it.
app/api/vendo/[...vendo]/route.ts
See Orgs & memberships for the rest of that seam.

Who sees what

Visibility follows the orgs your host asserts for the request — the same memberships seam the rest of Vendo reads. A run that asserts acme is served a registry carrying your tools plus Acme’s; a run that asserts nothing is served the shared registry, unchanged. The isolation is structural. Another tenant’s connector is not withheld from that registry, it was never in it — so there is no filter that can be got wrong, and no listing that can leak a name.
Without a memberships seam no request can name an org, so nothing here can ever bind. The boot block says so: the tenants row appears only once the seam is wired.

The token

The pasted token is vaulted in your store’s encrypted secrets under a tenant-scoped name, and it is never readable back on any surface. list answers metadata only.
The vault belongs to whichever store composes. If Vendo composes it, VENDO_STORE_ENCRYPTION_KEY (base64, 32 bytes) is what turns it on. If you pass your own store, you configure it there yourself — createStore({ url, encryption: { key: ... } }) — because an explicitly passed store owns its own secrets config, and the environment variable never reaches it. With no key at all, what happens next depends on which store composed, and the two answers differ. A store Vendo composes gets the development allowance automatically. A store you pass does not: it owns its own secrets config, so it refuses until you give it a key — or opt in explicitly with createStore({ url, allowUnencryptedSecrets: true }), which is a development convenience and nothing more. A tokenless registration is unaffected either way: it stores no secret, so it needs no vault.

Checking and removing

test re-runs the same live handshake register did, which is what an admin screen’s status dot should call.
remove drops the registration and its vaulted token, and the tools are gone from the next request onward.
Registrations are stamped with the org that owns them, so erasing that org through the store’s erase API takes them with it.

What this is not

Not per-user credentials

One token per tenant, shared by that tenant’s users. For a credential per person, use connected accounts.

Not an OAuth flow

A pasted token, not a consent screen against the tenant’s server.