Skip to main content
MCP endpoints are in beta.
You define an MCP endpoint with defineMcpServer from bijection/mcp. It takes an async function that returns the tools you publish and references to a few ordinary functions that you write in the same app: a grant query, two admission mutations and two dispatch functions. The function runs for each request, so it can await operation references. This page builds a complete endpoint in bijection/mcp.ts. The grant query is covered in Authentication and grants.

Selecting tools

A tool never contains business logic of its own. Each one selects a function you already have and derives its MCP input and output schemas from that function’s validators.

Query tools

mcpQuery publishes a public query. Pass the function reference, the registered query itself, and a description for the agent:
bijection/tasks.ts
bijection/mcp.ts
The query must declare a returns validator; mcpQuery throws if it does not. The query runs as the calling agent, so ctx.auth and your access rules see the agent’s identity, just as they would for a query called from a client.

Operation tools

mcpOperation publishes a business operation defined with defineOperation. Pass an operation reference and a description:
bijection/mcp.ts
Operation calls are deduplicated by a request key that the MCP client supplies outside the model’s arguments. When a call arrives, the endpoint first recovers any invocation already accepted for that key and returns its original receipt. Only when none exists does it invoke the operation. Repeating a call with the same key therefore returns the same invocation instead of doing the work twice. Connecting clients shows how a client supplies the key. An operation tool returns one of these results:
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.

Operation status tools

mcpOperationStatus publishes a read-only tool that reports the execution state of an accepted invocation:
bijection/mcp.ts
It takes { invocation_id } and returns invocation_id, the review state, each external call’s delivery and publication state, and is_external_outcome_unknown. It never returns the operation’s business result, reviewer identities or internal diagnostics. When is_external_outcome_unknown is true, the outcome must be reconciled; the tool description tells the agent not to report completion or resubmit.

Mutations and actions

You can’t publish a mutation or an action as a tool directly. To let an agent change data, define an operation and publish it with mcpOperation; operations give each write the stable identity, deduplication and recovery that an agent retrying over the network needs.

Tool names, descriptions and values

Tool names are the keys of the tools object. A name starts with a letter and contains only letters, digits and underscores, up to 64 characters. A description is at most 4,096 characters, and an endpoint 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 that 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.

Dispatch functions

Tool calls run through two internal functions that you export. Their handlers are the endpoint’s dispatch.read and dispatch.write, which check the publication revision and the caller’s current grant inside the same transaction as the tool’s work:
bijection/mcp.ts
Query tools, status tools and operation recoveries run through read. Operation invocations run through write.

Admission

Every request to the endpoint, including discovery, first acquires a lease from your admission.acquire mutation and releases it through admission.release when it finishes. You own these limits and their tables, so rate and concurrency counters commit transactionally with the rest of your app. acquire receives grant_id, tool (the tool name, or the MCP method for requests that aren’t tool calls), a fresh request_id, and is_poll, which is true for status tools and recovery requests. It returns either { lease_id } or { retry_after_ms }. Returning retry_after_ms refuses the request with 429 capacity_exhausted and a Retry-After header. This example keeps a per-minute counter and a list of active leases for each grant:
bijection/schema.ts
bijection/mcp.ts
A lease limits capacity only. It never owns the business work: if release fails, the lease simply expires, and an operation that was accepted stays accepted.
You can keep counters at several levels, for example per grant and per application, by updating several rows in the same acquire mutation. Use is_poll to give status checks their own budget.

Serving the endpoint

defineMcpServer returns a handler HTTP action for MCP requests, a metadata HTTP action for OAuth protected-resource metadata, and a publication() function that returns the current revision. Mount both actions in bijection/http.ts:
bijection/http.ts
The handler accepts a request only when the last path segment equals the current revision. Export a query that returns it:
bijection/mcp.ts
With the route above, the endpoint URL is https://<your deployment name>.bijection.site/mcp/<revision>. The revision covers your whole deployed program, not just mcp.ts: when bijection dev or bijection deploy pushes code, the CLI computes a digest of every bundled module, schema, auth configuration and component, and the endpoint combines it with its name, resource, authorization servers, tool descriptions, bindings and schemas. Any change produces a new revision and a new URL. Environment variables and data are not part of the revision; current grants and access rules are checked on every call instead.
An app that defines an MCP endpoint must bundle all of its dependencies. Pushing fails if the app installs external packages through node.externalPackages, because installed packages are not part of the digest.

Tool results

tools/list returns each granted tool with its inputSchema and an outputSchema of the form { result: ... }. Query tools are annotated readOnlyHint and idempotentHint; operation tools are annotated destructiveHint and carry a bijection/operation entry in _meta that clients use for recovery. A successful tools/call returns the value as structuredContent.result and as the same JSON in a text content block. A failed call returns isError: true with an error code such as tool_refused or read_unavailable. Error messages thrown by your own functions are not passed to the agent. See Limits for every code.