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

# Connecting Clients

> Call an MCP endpoint from an MCP client and make agent writes recoverable

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

To connect, an MCP client needs three things from you: the endpoint URL, a way
to get a token for its principal, and its grant `id`. See
[Authentication and grants](/mcp/authentication) for how you issue them.

The endpoint URL ends with the current publication revision. Read it after each
push:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection run mcp:revision
```

The URL is then `https://<your deployment name>.bijection.site/mcp/<revision>`.
When you push a change, give clients the new URL.

## Trying the endpoint

The endpoint is a stateless JSON-RPC endpoint over HTTP `POST`. You can list its
tools with `curl`:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl "https://happy-animal-123.bijection.site/mcp/$REVISION" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Method: tools/list" \
  -H "Mcp-Protocol-Version: 2026-07-28" \
  -d '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "tools/list",
    "params": {
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientInfo": { "name": "curl", "version": "1" },
        "io.modelcontextprotocol/clientCapabilities": {}
      }
    }
  }'
```

The response lists only the tools in your grant.

## Read-only clients

Any MCP client that can send a bearer token to a remote HTTP server can discover
tools and call query and status tools. Give it the endpoint URL and a token.

Operation tools are different. Each operation call needs a request key that
stays the same across retries, and a model can't be trusted to invent one. A
client that doesn't supply the key and grant `id` can't call operation tools;
those calls fail with `request_scope_mismatch`.

## Recoverable writes

The `bijection/mcp/node` module gives an agent host what it needs to call
operation tools safely. It runs in your agent's Node.js process, not in your
deployment. Don't import it from your `bijection/` folder.

```ts agent/connection.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { connectMcp, McpRequestJournal } from "bijection/mcp/node";

const connection = connectMcp({
  publicationUrl: "https://happy-animal-123.bijection.site/mcp/3f9c…e81a",
  deploymentUrl: "https://happy-animal-123.bijection.cloud",
  grantId: process.env.MCP_GRANT_ID!,
  getToken: async () => await fetchTokenFromYourIdentityProvider(),
  store: new McpRequestJournal("/var/lib/agent/mcp-journal"),
});
```

* **`publicationUrl`** is the endpoint URL. It must end with the revision and use
  HTTPS, or HTTP on a loopback address.
* **`deploymentUrl`** is your deployment's `.bijection.cloud` URL. It is used for
  recovery and status, which call your operation's recovery functions directly
  and keep working after the endpoint URL changes.
* **`getToken`** is called for every HTTP request, so tokens can be refreshed
  freely. Tokens are never written to the store.
* **`store`** persists each write's request before it is sent.

The connection has these methods:

| Method | What it does |
| - | - |
| `discover()` | Lists the granted tools and records each operation tool's recovery binding. |
| `fetchForCall(id)` | Returns a `fetch` for one tool call. Operation calls get a request key from the store, keyed by `id`, outside the model's arguments. |
| `recover(id)` | Returns the original result of the write recorded for `id`, under the caller's current permissions. It never resubmits. |
| `status(id)` | Returns the execution status of the write recorded for `id` without releasing its business result. |

The call `id` must stay the same when your host replays a step, for example a
persisted workflow step identity. Calling an operation tool again with the same
`id` returns the same invocation.

<Warning>
  A timeout, dropped connection or `5xx` response doesn't tell you whether a
  write was accepted. Call `recover(id)` or `status(id)` with the original call
  `id`. Never retry with a new `id`: that is a new request.
</Warning>

### Request stores

`McpRequestJournal` is a small file-based store that syncs each record to disk
before the request is sent. Put its directory on durable storage. For
production hosts, implement the `McpRequestStore` interface on your own
database:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
interface McpRequestStore {
  prepare(
    callId: string,
    input: Omit<McpRequestRecord, "request_key">,
  ): Promise<McpRequestRecord>;
  load(callId: string): Promise<McpRequestRecord | null>;
}
```

`prepare` must atomically keep one immutable record per call ID, return the same
request key when called again with the same input, and reject a call ID reused
with different arguments, grant or endpoint. `load` must survive a process
restart. Never store credentials in it.

### Mastra

Pass the connection's `fetch` to Mastra's MCP client. Create one client per tool
call ID:

```ts agent/mastra.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { MCPClient } from "@mastra/mcp";

const client = new MCPClient({
  id: callId,
  timeout: 10_000,
  servers: {
    tasks: {
      url: new URL(publicationUrl),
      fetch: connection.fetchForCall(callId),
    },
  },
});
const tools = await client.listTools(); // e.g. tasks_list_tasks, tasks_complete_task
```

### Eve

Eve persists its own session and call identity. Use `eveMcpRequest` as a
provided argument; Eve hides it from the model and adds it to each call:

```ts agent/connections/tasks.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineMcpClientConnection } from "eve/connections";
import { eveMcpRequest } from "bijection/mcp/node";

export default defineMcpClientConnection({
  url: publicationUrl,
  description: "Task lookups and completions.",
  instanceKey: `${grantId}:${publicationUrl}`,
  auth: { getToken: async () => ({ token: await getToken() }) },
  tools: { allow: ["list_tasks", "complete_task", "task_status"] },
  toolCall: {
    providedArguments: { __bijection_request: eveMcpRequest(grantId) },
  },
});
```

Keep each connection bound to one grant and one endpoint URL.

### Other clients

If you build your own client, send the request identity in the tool call's
`_meta`, never in the model's arguments:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "jsonrpc": "2.0",
  "id": "call-1",
  "method": "tools/call",
  "params": {
    "name": "complete_task",
    "arguments": { "task": "…" },
    "_meta": {
      "bijection/request": {
        "request_key": "<stable key, 1–256 characters>",
        "grant_id": "<grant id>"
      }
    }
  }
}
```

Add `"mode": "recover"` to look up an earlier call with the same key and
arguments without invoking the operation. Persist the key before sending the
request.

## Errors and deadlines

Each discovery, `recover` and `status` attempt, including getting a token, has
a 10-second deadline. Set your MCP client's own timeout for tool calls, as in
the Mastra example above. Failures are thrown as `McpRequestError` with a `kind`
of `deadline`, `cancelled`, `unavailable`, `authorization` or
`request_refused`. None of them establishes whether a write committed.

## Presenting status

`presentMcpOutcome` from `bijection/mcp/outcome` turns a status tool's result
into a fixed outcome and message for your users, such as `pending`,
`awaiting_review` or `unknown`. Its `external_effect_confirmed` field is always
`false`: delivery records alone don't prove that an external system applied a
change. Show these facts directly rather than a model's summary of them.
