defineMcpServer from bijection/mcp declares one MCP server:
its tools, the instructions that come with them and how its sessions start.
mcpModule serves every server you declare, mcpTables adds the tables
sessions need, and mountMcp mounts the routes.
Declaring a server
A session holds a role of your access model on the one object it acts for, and is granted nothing. Mark that rolesessions: true, give the service that starts sessions a role carrying the power to
issue it (customer:issue.supportAgent), and declare the service:
bijection/access.ts
ceiling. see covers a customer’s
identifier alone, so the service can tell that a customer exists and reads
none of its fields. The session’s role has no read.risk, so the fields behind
it never reach the agent. maxDuration is the longest a session can last.
A session holds its role only while its service holds the matching issue
permission on that customer or above it. Revoke the service’s role, or let it
expire, and every session it started loses its access at the same moment. A
role marked sessions cannot carry grant.* or issue.* permissions.
The operation resource is how your access model decides
who may use an operation,
for your staff and for sessions alike. A session holds operation:invoker on
each operation its tools request and operation:reader on each it may only
follow, until it ends. The service holds operation:issuer on those
operations, which lets it issue those roles to its sessions. A server with no
operation tools needs none of the three.
Then declare the server and export the module:
bijection/mcp.ts
bijection/mcp.ts. To export it from another
file, pass that file’s path as module to mcpModule.
Add the tables to your schema, and mount the routes:
bijection/schema.ts
bijection/http.ts
/mcp/<name> on your deployment’s
.bijection.run host, with its OAuth protected-resource metadata beside it.
Deployment checks the tables, their indexes, the public tool definitions,
operation authorization and the mounted routes before it publishes your
program, and an incomplete installation names what is missing.
Adding a server changes nobody else’s permissions. Your access model keeps
deciding who may use each operation; a session is one more principal holding
roles, for exactly what its tools do: an operation tool reads and invokes its
operation, a status tool only reads its requests, and no session previews.
Instructions
instructions tell the agent how to use the server’s tools together: what to
read first, which tool answers what, what never to promise. Each tool’s
description says what that tool does; the instructions say how the tools fit
together for this server’s work.
Every client receives the instructions when it connects (initialize and
server/discover), before it calls any tool. An agent your deployment
runs reads them before its own instructions. They are at most
8 KiB of text, and the same for every session of the server.
Tools
Each tool takes the definition’smodule:export path, the definition itself,
whose validators become the tool’s schema, and the description the model
reads. A tool must be a public query or operation with
explicit validators: publishing it adds a description, never a way in.
tool.querypublishes a public query with areturnsvalidator. It runs as the session, so your access rules decide which rows it reads.tool.operationpublishes a business operation defined withdefineOperation. A call is a request of that operation through its ordinary invocation path.tool.statuspublishes the safe status of accepted requests of an operation: its review state and each external call’s delivery and publication state, never its business result.
Acceptance is a local receipt. If your operation makes external calls, an
accepted result does not mean an external system has applied the change.
Publish a status tool so the agent can check.Binding a tool to the session’s object
A tool’s function takes the object it is about as an ordinary argument, so your app and your staff call it too. For a session,bind names that
argument: the model never sees it and cannot supply it, and the session fills
it in.
An operation that declares the object as its target needs no bind: its
tool binds the target on its own.
bijection/mcp.ts
bijection/desk.ts
order_details with order_number alone. A bound
argument is a required top-level ID or string. A tool with no arguments can
read the session instead, with mcpSession(ctx) from bijection/mcp, which
returns the session’s server, the object it acts for and its channel, or
null for every other caller.
Writing a tool’s function
- Select only what the role may read. Reading a whole document asks for every field. A tool that reads a field the session’s role lacks fails as a whole request, not as a tool error.
- Keep an operation’s object type to what the session reads. An operation
reads its target through the view or table it is declared
onbefore it runs. Declare a view that selects only fields the session’s role may read; a view over the whole table is refused for a role that lacks one field. - Write under what you read. A function may write into the table it read, the tables below it in your access model, and grants. An operation that reads an order and writes a refund works when a refund belongs to its order; it is refused when both only belong to the customer.
- Answer refusals as data. Return
{ outcome: "refused", reason }from an operation for the model to read. An error thrown after the function read protected data reaches the caller as a fixed refusal with no reason.
Names and values
Tool names are the keys of thetools object: a letter followed by letters,
digits or underscores, up to 64 characters. A description is at most 4,096
characters, and a server publishes between 1 and 32 tools.
Every tool’s arguments must be an object validator. Tool inputs and outputs
use JSON, with these encodings for values JSON can’t represent exactly:
Unknown fields and non-finite numbers are rejected. Publishing fails when a
tool uses
v.any(), or a union whose variants look the same on the wire, such
as v.union(v.string(), v.int64()). Use an object union with a required
v.literal discriminator instead.
When you deploy
A session records each tool’s contract when it starts: its name, description, schemas and binding. When you deploy while a conversation is running, the session keeps every tool whose contract you left unchanged, at the same URL. A tool whose contract changed answerstool_changed for that session, and a tool
you add reaches new sessions only. The session’s host still reads back the
requests it made through a changed operation tool. Changing the instructions
changes no tool.
Tenants and staff directories
Sessions work with any access model, including one whose staff are a workspace directory and one that restricts objects to a tenant. A session principal is never a person of the directory: it holds only the roles its service may issue. In a workspace directory a service also acts only while a member sponsors it. When the object a session acts for is restricted to a tenant, the session passes that tenant exactly as its service does, so the service’s role must carry the tenant’spass. A session that also reads the tenant itself, such
as a store’s opening hours, names a role for that in ancestorRoles. It holds
that role on the tenant the customer belongs to, while its service may issue
it there.
bijection/mcp.ts
bijection/access.ts
Limits
- Up to 16 servers in a module, 32 tools and four
ancestorRolesin a server. - Instructions are at most 8 KiB, a description at most 1,024 characters.
- A session is never given a marking’s pass: an object that carries a marking refuses the session unless one of its roles confers that pass.
- In an app with a staff directory, a session cannot read people, so a tool cannot return a staff member’s name.