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

# Authentication and Grants

> Authenticate MCP clients with your identity provider and limit what each agent may do

<Warning>MCP endpoints are in beta.</Warning>

Every request to an MCP endpoint must carry a bearer token, and every token must
be backed by a grant that your app issued. The token proves who is calling; the
grant says what that caller may do through this endpoint.

Bijection doesn't issue tokens or run an OAuth authorization server for MCP
endpoints. Clients get tokens from the identity provider you already use for
[authentication](/auth/overview).

## Accepting tokens

Configure your identity provider in `bijection/auth.config.ts` as usual, for
example as a [custom JWT provider](/auth/advanced/custom-jwt). Then tell the
endpoint which resource it is and which authorization servers issue its tokens:

```ts bijection/mcp.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const resource = process.env.BIJECTION_SITE_URL!;
const authorizationServers = ["https://auth.example.com"];

export const mcpServer = defineMcpServer(async () => ({
  name: "tasks",
  resource,
  authorization_servers: authorizationServers,
  // ...
}));
```

* **`resource`** identifies this endpoint. Use your deployment's
  `.bijection.site` URL. Browser requests whose `Origin` differs from the
  resource's origin are refused with `403 origin_refused`.
* **`authorization_servers`** lists the issuers your clients get tokens from.
  At least one is required.

Both must be `https://` URLs without credentials, a query string or a fragment.
Plain `http://` is accepted only for `localhost`, `127.0.0.1` and `[::1]`, so you
can develop locally.

Clients send the token in the `Authorization` header:

```
Authorization: Bearer <token>
```

A request without a token receives `401 authentication_required` with a
`WWW-Authenticate` header that points to the endpoint's OAuth protected-resource
metadata:

```
WWW-Authenticate: Bearer resource_metadata="https://happy-animal-123.bijection.site/.well-known/oauth-protected-resource"
```

The `metadata` HTTP action you mounted at that path returns:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "resource": "https://happy-animal-123.bijection.site",
  "authorization_servers": ["https://auth.example.com"],
  "bearer_methods_supported": ["header"]
}
```

## Grants

A valid token isn't enough on its own. Your `authorize` query returns the grant
for the calling identity, and the endpoint accepts the request only when:

* the caller is authenticated,
* the grant's `principal` equals the caller's
  [`tokenIdentifier`](/auth/functions-auth#user-identity-fields),
* the grant's `audience` equals the endpoint's `resource`,
* the grant has a non-empty `id`, and
* the grant's `expires_at` (milliseconds since the epoch) is in the future.

Otherwise it responds with `403 access_refused`. The grant's `tools` list is an
allowlist: `tools/list` shows only the published tools named in it, and calls to
any other tool fail with `tool_unavailable`. Publishing a new tool grants
nothing until you add it to a grant.

The full grant shape is the `McpGrant` type:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
type McpGrant = {
  id: string;
  organization: string;
  application: string;
  customer: string;
  principal: string;
  audience: string;
  expires_at: number;
  tools: string[];
};
```

The endpoint itself checks `id`, `principal`, `audience`, `expires_at` and
`tools`. `organization`, `application` and `customer` are yours to define and
use, for example in your admission limits or access rules.

### Storing grants

Keep grants in a table your app owns:

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
mcpGrants: defineTable({
  principal: v.string(),
  organization: v.string(),
  application: v.string(),
  customer: v.string(),
  audience: v.string(),
  expires_at: v.number(),
  tools: v.array(v.string()),
}).index("by_principal", ["principal"]),
```

Then look up the caller's grant in an internal query:

```ts bijection/mcp.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
export const authorize = internalQuery({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (
      identity === null ||
      !authorizationServers.includes(identity.issuer)
    ) {
      throw new Error("Access refused");
    }
    const grant = await ctx.db
      .query("mcpGrants")
      .withIndex("by_principal", (q) =>
        q.eq("principal", identity.tokenIdentifier),
      )
      .unique();
    if (grant === null || grant.expires_at <= Date.now()) {
      throw new Error("Access refused");
    }
    const { _id, _creationTime, ...fields } = grant;
    return { id: _id, ...fields };
  },
});
```

<Tip>
  Protect the grants table with [access rules](/access/overview) so an identity
  can read only its own grants, and so only your administrators can write them.
</Tip>

### Issuing grants

Issue one principal for each agent interaction you want to authorize, write its
grant, and give the agent's host three things: the endpoint URL, a way to obtain
that principal's token, and the grant `id`. Clients that call operation tools
must send the grant `id` with each call; a call made under a different grant
fails with `request_scope_mismatch`.

To revoke access, delete the grant, shorten its `expires_at` or remove tools from
its allowlist. A token refresh doesn't change the grant, since the principal
stays the same.

## When grants are checked

The grant is checked when the request arrives, again inside the transaction
that runs each tool call, and once more before the response is released. If a
grant is revoked or expires while a request is running, the response is withheld
and the client receives `403 request_refused`.

Revocation fences future work only. An operation that was already accepted stays
accepted, and an external call it already made is not undone.

## Permissions for tools

Grants decide which tools an agent may call through this endpoint. They don't
replace the permissions of the functions behind those tools. Query tools run as
the caller, so your access rules decide which rows it can read. Operation tools
need the same permission to invoke the operation that any other client would.
See [Access](/access/overview) and [Operations](/operations/overview).
