One MCP server per user: how to give each customer's agent its own tools
#The short answer
When an agent acts for many customers, you can hand it tools over MCP in two ways. One shared MCP server can work out the tenant from the token and check it inside every tool. Or each customer can get their own endpoint, with a token that only reaches that customer's resources.
Both work. The second is easier to reason about, because isolation stops depending on every tool remembering to filter by tenant: the endpoint has nothing else it could reach. Below is the trade-off, then how Burrowbox does it, with one machine per customer and an MCP endpoint and token for each.
#Two ways to build a multi-tenant MCP server
With a shared server, one URL serves everyone. The server reads the bearer token, maps it to a tenant, and every tool filters its queries by that tenant. It's one deployment and one set of logs, which is why most teams start here. The weak point is that isolation lives in application code. Each new tool, and each query inside it, is another place where a missing WHERE tenant_id = … lets one customer's agent read another customer's data.
With an endpoint per tenant, the URL and token identify one customer's resources and nothing else. A bug in a tool can still break that customer's own work, but it can't cross into another tenant, because the token can't address one. You do manage more endpoints, though if an API creates them for you, each one is a row in your database.
The MCP specification doesn't choose for you. Its authorization page (revision 2026-07-28) says authorization is optional, and that HTTP-based implementations that support it should follow the page. Two of its rules apply to either design:
- The client sends the token as
Authorization: Bearer <token>on every HTTP request, never in the query string. - A server must only accept tokens issued for it, and must not pass other tokens on.
A per-tenant token meets the second rule by construction, since it was only ever issued for one endpoint.
#How Burrowbox splits it
Burrowbox has two kinds of MCP endpoint, each with its own token:
| Platform MCP | Machine MCP | |
|---|---|---|
| Endpoint | POST /mcp |
POST /api/machines/{id}/mcp |
| Token | API key, tmk_… |
Machine token, tmm_… (the machine's mcpToken) |
| Reaches | Every machine on your account, plus billing, pools and event webhooks | One machine |
| Tools | machine_create, machine_get, machine_destroy, usage_by_customer, … |
browser_navigate, browser_login, shell_run, file_write, vault_list, … |
| Who holds it | Your server | The customer's agent session |
You run the fleet through the platform MCP. The machine MCP works one computer: its browser, desktop, shell, files and vault. The authentication docs say which one the agent gets: the machine token "is the token to hand to an agent, including agents acting for your customers."
Each customer gets a persistent Linux machine. A stopped machine keeps its files, installed apps and browser logins, so the endpoint is the same from one session to the next. (The post on what a machine keeps between runs covers what survives a stop.)
#Wire it up
The pattern from Machines for your customers is short. Create a machine the first time a customer needs an agent, store its id and mcpToken next to the customer, and wake it for later sessions.
// server only: BURROWBOX_KEY is your tmk_ API key
const api = (path, init = {}) =>
fetch(`https://burrowbox.dev${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.BURROWBOX_KEY}`, "Content-Type": "application/json" },
}).then((r) => r.json());
async function machineFor(customer) {
if (!customer.machineId) {
const m = await api("/api/machines", {
method: "POST",
body: JSON.stringify({ name: customer.slug, size: "tiny", ttlMinutes: 30, externalId: customer.id }),
});
await db.customers.update(customer.id, { machineId: m.id, machineToken: m.mcpToken }); // your database
return { id: m.id, token: m.mcpToken };
}
// Wake it for a new session; it resumes where it left off.
await api(`/api/machines/${customer.machineId}/start`, {
method: "POST",
body: JSON.stringify({ ttlMinutes: 30 }),
});
return { id: customer.machineId, token: customer.machineToken };
}
externalId tags the machine with your customer's id, which the usage reports group by. ttlMinutes turns the machine off after 30 minutes and keeps its state.
Then give the agent session that machine's endpoint and token and nothing else. Any MCP client that supports Streamable HTTP works. Here it is with the AI SDK's MCP client:
import { createMCPClient } from "@ai-sdk/mcp";
export async function toolsFor(customer) {
const { id, token } = await machineFor(customer);
const mcp = await createMCPClient({
transport: {
type: "http",
url: `https://burrowbox.dev/api/machines/${id}/mcp`,
headers: { Authorization: `Bearer ${token}` }, // tmm_ token, this machine only
},
});
return { mcp, tools: await mcp.tools() }; // pass tools to your model call, then mcp.close()
}
The API key never enters the agent session. If a prompt injection talks the model into trying something outside its lane, the only credential within reach is a token for that customer's own machine.
The Claude Agent SDK takes the same two values as an http server with a headers field, and the Claude API's MCP connector takes them as url and authorization_token. Burrowbox isn't involved with Vercel, Anthropic or the MCP project.
#What a machine token can't do
Per the authentication docs, a machine token only works on POST /api/machines/{id}/mcp for its own machine, and it can't list, create, stop or bill anything. It can't reach any other machine or your account.
On its own machine it can do everything the machine MCP offers. It can run shell commands, use the logins in that machine's vault through browser_login, and create scheduled jobs and webhooks scoped to that machine. It can also create a live-view link for its machine, so the agent can share its screen. Treat it as the key to that customer's computer: keep it on your server and don't show it to the end user.
An agent that creates and manages machines itself needs the platform MCP and your API key, and it can reach every machine on your account. Keep that agent out of customer-facing sessions.
#Running it for many customers
Every machine carries an externalId, so GET /api/usage?groupBy=externalId reports spend per customer. The platform MCP tool usage_by_customer returns the same table (Billing). A tiny machine costs $0.07 an hour while running and $0.001 an hour while stopped.
A customer's logins go in that machine's vault. The agent calls browser_login, which fills the form without returning the password to the model, and vault_list returns names, URLs and usernames but never secrets.
When a customer leaves, DELETE /api/machines/{id} (or the platform MCP tool machine_destroy) permanently deletes the machine: its files, installed apps, vault, browser profile, snapshots, scheduled jobs and webhooks. It can't be undone (Machines).
If creating a machine on first use is too slow for your product, warm pools keep machines booted and hand one over when you claim it. The claim response includes the mcpToken, so the wiring above doesn't change. The Windows 365 for Agents post goes through that check-out model.
#Key takeaways
- A shared MCP server puts tenant isolation in every tool. An endpoint per customer puts it in the token.
- On Burrowbox, the API key (
tmk_) stays on your server and manages the fleet. The machine token (tmm_) goes to the agent and reaches one machine. - Store
idandmcpTokenper customer, tag machines withexternalId, and destroy the machine when the customer leaves.
#FAQ
#Do I need OAuth for an MCP server per user?
Not according to the spec. The MCP authorization page makes authorization optional and says HTTP implementations that support it should follow its OAuth-based flow. Burrowbox issues the machine token through its own API when you create the machine, and your client sends it as a bearer token in the Authorization header, the same header the spec uses.
#Can I put the API key and the machine token in the same agent session?
You can, but then the session can reach every machine on your account. Keep the API key in your server code, where you create and start machines, and give the agent only the machine token.
#Is a per-user endpoint expensive to keep around?
A stopped machine is billed at $0.001 an hour, or $0.72 per 30 days, and it keeps its files, apps and logins. You pay the running rate ($0.07 an hour for tiny) only while it's on.
#What if one customer needs several agents at once?
Give each concurrent agent its own machine and tag them all with the customer's externalId. GET /api/machines?externalId=… lists that customer's machines, and usage still adds up per customer. For one long-lived assistant per user, see a cloud computer for every user.
Create an account to try it, or start with Machines for your customers.
#Sources
- Model Context Protocol, Authorization (revision 2026-07-28): https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- AI SDK, MCP tools: https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools
- Claude Agent SDK, Connect to external tools with MCP: https://code.claude.com/docs/en/agent-sdk/mcp
- Burrowbox docs: MCP, Authentication, Machines for your customers, Billing