Your API as MCP Tools
You give us REST endpoints; we run the MCP server. Your users' agents connect to it, and every call it makes to your API is stamped with the end-user it's acting for. There is no MCP server for you to build or host, and no per-user credential for you to collect.
That's what makes an embedded agent useful: it reads their orders, updates their settings, files their tickets — as them, in your product.
The shape, once:
- One service token. Your API already knows how to authorize your users; it just needs to know which one. m8tes calls your API with a token you issue us and stamps the end-user's id on every request.
- One tool row per end-user, all carrying that same token. The row's scope is what becomes the identity we send.
- One mate per end-user, holding that row.
- The proxy pins the mate per request, so each of your users lands on their own.
Already host your own MCP server? Point us at it instead with
kind="mcp_http"— we connect out and discover its tools. Note that the end-user identity header below is not sent on that path, so a remote MCP server has to carry its own per-customer scoping (one server per customer, or identity in its own URL/credential). This page covers the common case: you have a REST API and no MCP server.
1. Wrap your API as a tool
A tool def is a method, a path, and a description. That's the whole contract — m8tes synthesizes the MCP tools for you.
2. Provision per end-user, at signup
Run this once per customer, wherever you already create their account. Both calls take the same user_id — that pairing is the part that matters (see Scope pairing).
One row per customer sounds heavy; it isn't. Slugs and the per-account tool quota are both scoped per end-user, so every customer can hold the same acme-api slug and none of them count against each other. The token is stored encrypted, once per row, and never reaches the agent or the sandbox.
3. Pin the mate per request
Requires
@m8tes/react@0.1.0-alpha.2or later.resolveAgentIdis a new option; on an older build it is silently ignored, no mate is pinned, and the browser's ownteammate_idwins — the exact hole pinning exists to close. Runnpm i @m8tes/react@alphato upgrade, and checkM8TES_REACT_VERSIONif you're unsure what you have.
Each customer has their own mate now, so a single static agentId no longer works — it would send everyone to one customer's mate, and the API would reject the mismatch. Resolve it per request instead:
resolveAgentId is handed the already resolved userId. Key your lookup off that, never off a query param or a readable cookie — otherwise the browser is choosing the mate again. It fails closed: throw or return nothing and the request 401s rather than starting an unpinned run. See Embed a UI for the rest of the proxy.
4. What your API receives
Every tool call arrives from m8tes' servers (never the browser, never the sandbox) as a plain HTTPS request:
Authorize it exactly as you would a logged-in session for cust_123:
Trust basis: the identity header is a claim; the service token is what makes it trustworthy, because only m8tes holds that token. Two rules follow, and both are on you:
- Authorize on the token, every request. Never expose an endpoint that honours
X-M8tes-End-User-Idwithout verifying the credential first. The header says who we're acting for, not that we're allowed to. - Treat a missing header as "no end-user", not as "any end-user". Reject or fall back to a safe default — never to an admin scope.
We send the header only on servers configured with real auth (an auth_type other than none, with a secret). On an unauthenticated server it is omitted rather than left forgeable. It is also omitted, with a warning logged, in the rare legacy case where a server's own credential header is named X-M8tes-End-User-Id — the credential always wins that collision, and new servers can't be configured that way.
The value is percent-encoded, so any id transmits safely as a header. Ordinary ids (cust_123, a UUID) are unchanged by that; if yours can contain non-ASCII or spaces, URL-decode before you look it up.
Your API stays the authority. m8tes never assumes an id grants access — if cust_123 may not see an order, return your usual 403 and the agent will tell them so.
Scope pairing
A tool resolves only when the RUN's user_id matches the tool's. That is the whole rule — there is no fallback to account-level data in either direction:
The mate matters because it constrains which runs are possible. A mate scoped to cust_123 only ever runs as cust_123, so pairing it with that customer's tool is the configuration you want, and it can't drift. An account-level mate has no fixed scope: its runs carry whatever user_id you pass, so an end-user tool attached to it still resolves — on that end-user's runs only. That works, but it puts the burden on every caller to pass the right user_id; one omission silently yields no tools.
A mismatch is silent — the agent simply has no tools and answers as best it can, with no error in the run. If an embedded agent says it can't do something it clearly should, check this first: confirm the run carried the user_id you expect, then that a GET on the mate lists the slug you expect in tools. Account health checks flag attachments that can never resolve (a mate pinned to one end-user holding another's tool); they deliberately don't flag the account-level case above, which is legitimate.
Two more constraints worth knowing up front:
- Custom slugs attach to an agent only. Passing one in a per-task or per-run
toolslist is rejected. GETtools never prompt for approval, in any permission mode. Writes do, unless the server is marked trusted. See Take Actions.
Limits
Calls are capped at 120 per minute per end-user, per server — the budget covers a server's whole tool set, not each tool separately. One runaway agent or abusive customer therefore can't burn the API quota everyone else shares through your service token. A throttled call returns a rate_limited error carrying retry_after to the agent, which backs off; it never fails the run.
Rotating the token
The token is stored per row, so rotation is a sweep:
Issue the new token before revoking the old one so in-flight runs don't fail mid-call.
Next: Embed a UI · Multi-tenancy · Take Actions · Human-in-the-Loop
