> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Endpoints with Sessions

> Declare an MCP endpoint as a list of tools, and give each conversation a short-lived, scoped session

<Warning>Endpoints with sessions are in beta.</Warning>

`defineEndpoint` declares an [MCP endpoint](/mcp/overview): a list of tools and
how its sessions start. You declare it once; Bijection serves it at
`/mcp/<name>` and handles its grants, limits and tokens. The AI agent itself
(the model, the conversation, voice) runs in your own host.

Three things take part:

* **An endpoint** is code. It names its tools and how a session starts: the
  service that starts it, the resource type one session acts for and the role
  a session holds.
* **A service** is your backend, declared in the access model. A person
  authorizes it; one of its **credentials**, a key installed on the
  deployment, authenticates it. It reads no data.
* **A session** is one conversation. It holds the endpoint's role on one
  object, such as one customer, until it expires.

Because a session holds ordinary grants of ordinary roles of your
[access model](/access/access-model), your access rules decide everything it
reads and changes. Its token works on its endpoint only: Bijection refuses it
your app's regular API.

Use `defineMcpServer` instead when your app writes its own access rules:
sessions need an access model.

## Declaring an endpoint

Give a session a role, give the service a role whose only business permission
is the power to grant that role, and declare the service:

```ts bijection/access.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineAccessModel } from "bijection/server";

export const access = defineAccessModel({
  resources: {
    operation: { permissions: ["read", "invoke", "preview"] },
    customer: { permissions: ["see", "read.risk"] },
    refund: { parent: "customer" },
  },
  roles: {
    "root:owner": ["root:*", "operation:*", "customer:*", "refund:*"],
    "operation:invoker": ["operation:read", "operation:invoke"],
    "operation:reader": ["operation:read"],
    "operation:issuer": [
      "operation:grant.invoker",
      "operation:grant.reader",
    ],
    "root:supportIssuer": ["customer:see", "customer:grant.supportAgent"],
    "customer:supportAgent": {
      permissions: [
        "customer:see",
        "customer:read",
        "refund:read",
        "refund:create",
      ],
      maxDuration: 30 * 60 * 1000,
    },
  },
  services: {
    "support-desk": {
      description: "Starts support sessions for customers it has verified.",
      ceiling: ["root:supportIssuer", "operation:issuer"],
    },
  },
});
```

A service is granted only roles of its `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.

The `operation` resource is how your access model decides
[who may use an operation](/access/access-model#deciding-who-may-use-operations),
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 is what lets it give its sessions those roles. An endpoint
with no operation tools needs none of the three.

Then declare the endpoint and export the module:

```ts bijection/mcp.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineEndpoint, endpointsModule, tool } from "bijection/mcp";
import { access } from "./access";
import { context, hours, refund } from "./support";

export const support = defineEndpoint("support", {
  description: "Customer support for verified customers.",
  sessions: {
    issued: {
      by: "support-desk",
      for: "customer",
      role: "customer:supportAgent",
      serviceRole: "root:supportIssuer",
      beforeVerification: ["opening_hours"],
    },
  },
  tools: {
    get_context: tool.query(
      "support:context",
      context,
      "Read the verified customer's name, tier and remaining refundable amount.",
    ),
    opening_hours: tool.query(
      "support:hours",
      hours,
      "Read the support desk's opening hours.",
    ),
    request_refund: tool.operation(
      "support:refund",
      refund,
      "Request a refund for the verified customer.",
    ),
    refund_status: tool.status(
      "support:refund",
      refund,
      "Check an accepted refund request.",
    ),
  },
  limits: { callsPerMinute: 30 },
});

export default endpointsModule({ access, endpoints: [support] });
```

Each tool takes the definition's `module:export` path, the definition itself
and the description the model reads. A tool must be a public query or
[operation](/operations/overview) with explicit validators: publishing it adds
a description, never a way in.

Keep the default export in `bijection/mcp.ts`. To export it from another
file, pass that file's path as `module` to `endpointsModule`.

Add the tables to your schema, and mount the routes:

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineSchema } from "bijection/server";
import { endpointTables } from "bijection/mcp";
import { access } from "./access";

export default defineSchema({
  // ...your tables
  grants: access.protectGrants(),
  ...endpointTables(),
});
```

```ts bijection/http.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { httpRouter } from "bijection/server";
import { mountEndpoints } from "bijection/mcp";
import endpoints from "./mcp";

const http = httpRouter();
mountEndpoints(http, endpoints);
export default http;
```

Adding an endpoint changes nobody else's permissions. Your access model keeps
deciding who may use each operation; a session is one more principal holding
grants, 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.

## 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.

```ts bijection/mcp.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
tools: {
  order_details: tool.query(
    "desk:order",
    order,
    "Read one order of the verified customer by its order number.",
    { bind: "customer" },
  ),
  // requestRefund declares `target: { argument: "customer" }`.
  request_refund: tool.operation(
    "desk:requestRefund",
    requestRefund,
    "Request a refund on one delivered order of the verified customer.",
  ),
},
```

```ts bijection/desk.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
export const order = query({
  args: { customer: v.id("customers"), order_number: v.string() },
  returns: v.union(v.null(), v.object({ status: v.string() })),
  handler: async (ctx, { customer, order_number }) => {
    const found = await ctx.db
      .query("orders")
      .withIndex("by_customer", (q) =>
        q.eq("customer", customer).eq("number", order_number),
      )
      .unique();
    return found && { status: found.status };
  },
});
```

The model is offered `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 `endpointSession(ctx)` from `bijection/mcp`.

Three rules shape 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 `on` before 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.

An operation tool answers with its request's receipt:
`{ kind: "accepted", invocation_id, result }`, where `result` is what your
operation returned.

## Tenants and staff directories

Sessions work with any access model, including one whose staff are a
[workspace directory](/access/access-model) and one that restricts objects to
a tenant. A session principal is never a person of the directory: it holds
only what its own grants give it.

When the object a session acts for is restricted to a tenant, the session
must also pass that tenant. Name the role that carries the pass in
`ancestorRoles`: it is granted with the session, on the object's own tenant,
and ends with it.

```ts bijection/mcp.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
export const support = defineEndpoint("support", {
  description: "Answers one verified customer.",
  sessions: {
    issued: {
      by: "support-desk",
      for: "customer",
      role: "customer:supportAgent",
      ancestorRoles: ["store:supportSeat"],
      serviceRole: "store:supportIssuer",
    },
  },
  tools: { /* ... */ },
});
```

```ts bijection/access.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
"store:supportIssuer": [
  "store:pass",
  "store:read",
  "store:grant.supportSeat",
  "customer:see",
  "customer:grant.supportAgent",
],
"store:supportSeat": { permissions: ["store:pass"], maxDuration: 30 * 60 * 1000 },
```

The service's role is granted per tenant, so it starts sessions only in the
tenants a person authorized it for. A service is one principal: every one of
its credentials acts with all its grants, so tenants whose hosts must not reach
each other need a service each. The service's role must be able to read the
field that names the object's parent (`customer:see` above covers a customer's `store`).
Deployment validates the coordination tables, required indexes, public tool
definitions, operation authorization and mounted routes before publishing the
program. An incomplete installation names the missing declaration.

## Seeing what a session may do

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection mcp describe support
bijection mcp inspect <session>
bijection mcp check <session> request_refund '{"amount":"1200"}'
```

`describe` reads the declaration. `inspect` reads an existing session, its
termination history and the current state of each recorded tool contract.
Neither creates a session or needs its signing key; both use deployment
administration. Every command prints its report as JSON with `--json`.

`check` validates session admission, the tool contract and the arguments
without executing the tool or consuming its rate allowance. A definite refusal
returns `denied` and exit status 1. Otherwise it returns `requires_execution`:
operation permission, data access, business conditions, request ownership and
rate admission are decided for the session when the tool runs. Deployment
administration reads none of your access model's grants, so a check never
predicts those decisions. A check is an observation, not a reservation or a
promise that a later call succeeds.

`bijection mcp try support --key support-desk.voice-prod.servicekey --for <customer id>`
starts a one-minute session and lists its tools. Naming a tool and arguments
executes it through the ordinary endpoint, then ends the session.

## Authorizing the service and creating a credential

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection mcp authorize support
bijection access credential create support-desk --name voice-prod
```

`authorize` grants, as your verified business identity, the service its role
where it may start the endpoint's sessions, so you need the power to grant
that role, and then the issuing role on each operation the endpoint's tools
use, so you need the power to grant that too. Use `--on <key>` when the
service's role is granted on one object, such as one organization, rather
than on the whole deployment.

`credential create` is deployment administration: it installs the
credential's public key on the deployment and writes the private key to
`support-desk.voice-prod.servicekey`. Bijection keeps no copy. Store the
file's one line as a secret in your backend, then delete the file. The key
confers nothing by itself; the service's grants are its authority.

A credential belongs to one deployment. Create one for each of development,
staging and production. If your identity provider already issues your backend
a machine client, such as a WorkOS M2M application, trust it instead:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection access credential add support-desk --issuer <issuer> --client <client id>
```

To revoke a credential:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection access credential revoke <credential>
```

Every token the credential signed, and every session it started, is refused
from then on.

## Starting a session

In your backend, after you have verified who is calling, start a session and
hand the agent's host its endpoint and token:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { serviceClient } from "bijection/services/node";

const desk = serviceClient({ credential: process.env.SUPPORT_DESK_KEY! });

const session = await desk.startSession("support", {
  actsFor: customerId,
  channel: "voice",
  evidence: { method: "otp" },
});

// For a host that takes a URL and a bearer token:
const { url } = session;
const token = session.token();

// For a host that calls operation tools through bijection/mcp/node:
const mcp = session.connect({ store });
```

Bijection records your `evidence` and does not check it: only the service,
through one of its credentials, can start a session, and you decide when a caller is verified.

A session never changes whom it acts for. Before you know who is calling,
start a session with `actsFor: null`: it holds no grant and only the tools in
`beforeVerification`. Once the caller is verified, start another session.

`bijection/services/node` runs in your own Node.js process. Don't import it
from your `bijection/` folder.

Call `session.end()` when the conversation ends: its grants are deleted and
its token is refused from then on. Otherwise the session ends when it expires.

## What a session can and cannot do

* It may call the tools it was offered when it started. A tool you add later
  reaches new sessions only.
* It may read and change what its role permits on its one object, through
  its tools.
* Its token calls nothing but its endpoint. A direct call to one of your
  app's functions is refused before the function runs, even a function behind
  one of its tools: the tool list, the bound arguments, the limits and the
  call log always apply.
* It is never a person: it cannot approve, grant permissions, delegate, or
  take any decision that names who decided.
* Its requests are limited each minute by `limits`.

When you deploy while a conversation is running, the session keeps every tool
whose definition you left unchanged. A tool whose name, description or schema
changed answers `tool_changed` for that session.

When a tool call reads something the session may not, such as a field its
role lacks, the request fails as a whole rather than returning a tool error.

A host that was given only the endpoint and a token can call operation tools:
within one session, the same operation with the same arguments is one
request, so a repeated call never runs twice. A host that needs two identical
requests uses `session.connect({ store })`, which gives each call its own
identity.

## Watching and revoking sessions

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection mcp sessions support
bijection mcp calls support --since 1h
bijection mcp calls --session <session> --failed
bijection mcp revoke <session>
```

`bijection mcp calls` lists the tool calls sessions made and whether each
completed, from your deployment's audit log. It shows which session called
which tool for which object, never the arguments. A call that did not complete
was refused by your rules, failed, or was outside the session's tools or
limits.

The console's **MCP endpoints** page shows the same: each declared endpoint and
the service that starts it, and recent sessions, with a button to revoke a live
one.

A session is `live`, `expired`, `ended` (its host finished it with
`session.end()`) or `revoked` (an operator revoked it, or the credential that
started it was revoked). An ended or revoked session keeps that state, and when it happened,
after its original expiry passes.

Revoking a session ends it at once. Bijection refuses its token everything
from then on, and a tool call still running is refused where it next reads or
writes. An operation the session already requested, such as a refund awaiting
review, stays requested.

## Limits

* 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.
* Up to four `ancestorRoles` in an endpoint.
* Up to 16 endpoints in a module, 32 tools in an endpoint and 256 credentials
  in a deployment, revoked ones included.
* A session cannot keep working for a signed-in person after that person's own
  session ends.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.