Per-user MCP sessions
Mint a scoped, revocable MCP endpoint for each of your end-users or conversations — one URL that serves exactly the tools that user may use, with their credentials.
Scenario: Acme's assistant runtime speaks MCP. Instead of wiring one MCP client per app per user, Acme mints one session per conversation: a single MCP URL scoped to the merged, curated tool surface for that user — Gmail, HubSpot, and Acme's own private connector alike — and dies when the conversation ends. (What tools/list literally serves on that URL depends on the preset — see below.)
This is also the shape for "bring your own agent": your power users paste a personal MCP URL into Claude Desktop or Cursor, scoped to exactly what they connected.
Mint a session
const session = await protobox.sessions.create({
userId: 'user_123', // your end-user id
toolsetSlug: 'support', // optional: restrict to one curated toolset
});
// session.token is returned ONCE — store it server-side, never log it.
// session.mcpUrl is the endpoint to hand to the agent.Two serving presets decide what tools/list returns on the URL:
'meta'(the default) — three meta-tools (search_actions,get_action_schema,execute_action) that search and execute across the scope. Scales past ~40 tools; the toolset still bounds whatexecute_actionmay run.'direct'— the toolset's tools, flat: what most MCP clients expect for a small curated surface. Requires SDK ≥ 2.3.0:
const session = await protobox.sessions.create({
userId: 'user_123',
toolsetSlug: 'support',
preset: 'direct', // tools/list serves the toolset flat
});Wire any MCP-capable runtime to it:
{
"mcpServers": {
"acme": {
"url": "<session.mcpUrl>",
"headers": { "Authorization": "Bearer <session.token>" }
}
}
}get and list never return the token again. If it's lost, revoke the session and mint a new one.
Scope with toolsets
toolsetSlug restricts the session to one curated selection — the right tools, your names, your policies. A session with no toolset serves the user's full connected surface. Curate first, scope always: an agent that can see 200 tools is slower and riskier than one that sees 8.
Sessions are live-rebindable — update the binding and the MCP endpoint picks it up on the next call, no re-mint:
await protobox.sessions.update(session.id, { toolset: 'support-tier-2' });The same session, without an MCP client
Backend code that already has the session doesn't need to speak MCP — a session handle exposes the same scoped tools over plain method calls:
const handle = await protobox.sessions.use(session.id);
const tools = await handle.tools({ format: 'anthropic' }); // or openai/vercel/langchain/google-genai/raw
const result = await handle.execute('HUBSPOT_SEARCHCONTACTS', {
arguments: { query: 'initech.com' },
});execute addresses tools by their served (possibly overridden) names — the same names the model sees — and returns a structured result (completed, pending_approval, or failed). One session, two transports, identical tools, policies, and traces.
Connect-from-chat
When a tool fails because the user never connected the app, drive the fix from inside the conversation:
const { redirectUrl } = await handle.authorize('hubspot');
// send redirectUrl to the user: "Connect HubSpot to continue"Lifecycle
| Moment | Call |
|---|---|
| Conversation starts | sessions.create({ userId, toolsetSlug }) |
| User signs out / conversation ends | sessions.revoke(id) — kills the URL immediately |
| Audit "what did this agent do?" | sessions.listEvents(id) — see below |
| Find a user's sessions | sessions.list({ search: 'user_123' }) — never returns tokens |
// Offboarding a user: revoke everything they hold
const sessions = await protobox.sessions.list({ search: 'user_123' });
await Promise.all(sessions.map((s) => protobox.sessions.revoke(s.id)));Activity on the session
Every search, execution, and connection event is recorded for 90 days:
let cursor: string | undefined;
do {
const page = await protobox.sessions.listEvents(session.id, { cursor, limit: 100 });
for (const e of page.items) {
console.log(e.createdAt, e.type, e.tool, e.status, e.latencyMs);
}
cursor = page.nextCursor;
} while (cursor);sessions.get(id) additionally returns a server-computed summary — tool calls, errors, toolkits used — handy for a per-conversation debug panel.
Production notes
- One session per conversation beats one long-lived session per user: revocation is scoped, traces map 1:1 to conversations, and a leaked token has a short blast radius.
- Store the token next to your conversation record, encrypted at rest, and treat it like a password.
- Sessions are for identified callers. For an endpoint your customers configure once in their own AI clients — durable, named, key- or OAuth-guarded — publish a server instead: Publish an MCP server.
Related
- Embed Protobox in your product — the full partner runbook for this pattern
- Observability & audit
Publish an MCP server
Ship a durable, named MCP endpoint your customers add to Claude, Cursor, or any MCP client — backed by a toolset you curate, guarded by OAuth or API keys.
Automations
Run tools from workflows and background jobs — stateless execution with the same credentials, policies, and traces as your agents — and react to platform events.