Use Protobox from your own runtime (REST, no MCP client)
Drive one session per conversation entirely through @protoboxai/sdk over REST: list tools, execute actions, resolve approvals, and read the same audit trail an MCP client would, with no MCP handshake anywhere.
Scenario: Acme's agent runtime already has its own tool-calling loop. It does not embed an MCP client, and never will. It still wants everything a session gives an MCP-connected agent: one scoped identity per conversation, curated tools, human-in-the-loop on risky actions, and a full audit trail. Every call below is a plain HTTP request the SDK wraps; there is no initialize handshake, no MCP transport, anywhere in this page.
This is the same session primitive Per-user MCP sessions describes for MCP clients. This page is the runtime-integration half of that story.
1. One session per conversation
Mint a session bound to the end-user (or conversation) the moment it starts:
import { ProtoboxSDK } from '@protoboxai/sdk';
const protobox = new ProtoboxSDK({ apiKey: process.env.PROTOBOX_API_KEY! });
const session = await protobox.sessions.create({
principalId: 'conversation_c_9f2a1', // your workspace/conversation id
actorId: 'user_123', // the end-user whose credentials/memory this call uses
toolset: 'support-tools', // the curated tool surface for this conversation
preset: 'direct-tools', // tools/list serves the toolset flat, not via meta-tools
});actorId matters even when your runtime only tracks one id per conversation: it is what scopes every memory write/read and connection lookup to that specific end-user. Passing only principalId still works (the session falls back to it as the bound user), but an explicit actorId is clearer when a conversation and an end-user are different things in your system.
2. List its tools
const tools = await session.tools();
// [{ name, description, inputSchema, integrationId? }, ...], the toolset's tools, flatFeed tools straight into your own tool-calling loop, or pass { format: 'anthropic' | 'openai' | 'vercel' | 'langchain' | 'google-genai' } to get a wired, provider-shaped tool list whose own execute already routes through this session.
3. Execute an action by name
const result = await session.execute('KB_SEARCH', {
arguments: { query: 'refund policy' },
client: { name: 'chanl', version: '1.4.2' }, // see "Identify your runtime" below
});execute returns one of three shapes:
{ status: 'completed', data }
{ status: 'pending_approval', approval: { id, expiresAt } }
{ status: 'failed', error: { code, message, retryAfter? } }execute addresses tools by their served name, the same names session.tools() returned, and does not require the target tool to belong to this session's own toolset. It resolves against the workspace's full catalog, same as actions.execute.
4. A tool behind an approve policy: pause, approve, fetch
Gate a risky tool on the toolset once:
await protobox.toolsets.setToolPolicy(toolset.id, 'ISSUE_REFUND', 'approve');The next call to it pauses instead of running:
const paused = await session.execute('ISSUE_REFUND', {
arguments: { ticketId: 'T-4821', amountCents: 4900 },
client: { name: 'chanl', version: '1.4.2' },
});
// paused.status === 'pending_approval'
// paused.approval -> { id, expiresAt }Your reviewer surface decides, over the same API. See Human-in-the-loop for building the inbox:
const approved = await protobox.approvals.approve(paused.approval.id);
// approved.status === 'approved'
// approved.executionId -> set once the replay ranFetch the (now-resolved) async result:
const execution = await protobox.executions.get(approved.executionId);
// execution.outcome === 'ok' | 'denied' | 'failed'Nothing here differs from the MCP path. approvals.approve replays the exact frozen call server-side, the same as if an MCP client's tool call had paused.
5. Knowledge search and memory, for that end-user
Knowledge and memory are native tools. Reach them through the same session, so both are scoped to the session's bound end-user and show up in the same audit trail as every other call:
await session.execute('kb_search', {
arguments: { query: 'refund policy' },
client: { name: 'chanl', version: '1.4.2' },
});
await session.execute('memory_store', {
arguments: { content: 'Prefers email over phone for follow-ups.' },
client: { name: 'chanl', version: '1.4.2' },
});
const recall = await session.execute('memory_recall', {
arguments: { query: 'contact preference' },
client: { name: 'chanl', version: '1.4.2' },
});sdk.knowledge.search() and sdk.memory.store()/recall() also exist as standalone convenience methods, but calling them through session.execute() is what attributes the write/read to this conversation's end-user and session. Use the standalone methods only for workspace-level calls with no end-user or conversation to bind to.
Identify your runtime
An MCP client stamps its identity once, at initialize. A REST-only runtime has no handshake to do that in, so client: { name, version } on execute() does the same job per call: it is what shows up as the assistant on the audit log and every execution.client you read back. Send it on every call; there is no session-level place to set it once.
await session.execute('kb_search', {
arguments: { query: 'refund policy' },
client: { name: 'chanl', version: '1.4.2' },
});What's automatic
- Metered as a session call. Every
session.execute()call, including one that pauses on approval, counts toward the workspace'smcp_requestsallowance, the same as an MCPtools/callwould. - Audited. Every call, gated or not, writes an execution row with
client(from the field above),caller({ userId, kind: 'session' }),outcome, and, for a denied call,reason. Read it back withprotobox.executions.get(id)or list a caller's whole trail withprotobox.executions.list({ caller: 'user_123' }). See Observability & audit for filters and webhooks. - Policy-enforced.
allow/approve/denyon the toolset apply identically whether the call arrived over MCP or REST. Adeny-flagged tool is exposed bysession.tools()and rejected at call time, not hidden.
Production notes
- One session per conversation, not per user. Mirrors Per-user MCP sessions's guidance: revocation stays scoped to one conversation, and traces map 1:1 to it.
- Revoke when the conversation ends:
protobox.sessions.revoke(session.id). session.execute()re-resolves identity and toolset per call, with no local caching. A session rebound withsessions.update()is picked up on the very next call, no new handle needed.- Store
session.id, not a token, when driving everything server-side through the SDK. No bearer token is needed for this path, unlike handing a URL to an external MCP client (see Embed Protobox in your product).
Chanl-side integration checklist
The Protobox side of this path (session, tools, execute, approvals, executions, memory, knowledge, metering, and audit, all reachable with @protoboxai/sdk alone) is proven end to end by docs/goals/chanl-first-client-conformance.md. What's left is entirely in chanl-api, tracked there as its own cross-repo work:
- Mint one Protobox session per Chanl conversation (
principalId/actorId= the Chanl conversation/customer id) at conversation start, and revoke it at conversation end. - Store
session.idnext to the Chanl conversation record, never the session token, since this path never uses one. - Send
client: { name: 'chanl', version: <chanl-api's own release version> }on everysession.execute()call, so Protobox's audit log and metering attribute correctly. - Surface
pending_approvalresults in Chanl's own UI (or route them to whatever approves Chanl-side actions today) instead of treating them as failures. - Decide whether Chanl calls
sdk.knowledge.search()/sdk.memory.*directly (workspace-level) or through the session (conversation/end-user-scoped) per the guidance in step 5 above. Most Chanl use cases want the session-scoped form.
Related
- Per-user MCP sessions: the same primitive, for MCP-connected agents
- Human-in-the-loop: building the approval inbox
- Observability & audit: execution log, filters, webhooks
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.
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 with standing instructions.