Skip to content

MCP for AI agents ​

An agent that wants to use your API has the same problem a developer has: what can it call, what arguments does it take, and what comes back. A Rayfold schema already answers all three, so the MCP endpoint is a translation rather than a second API — mount it and an agent sees the operations you already wrote.

Nothing about authorization changes. An agent's request runs as a viewer like any other, through the same @allow/@deny rules, the same cost limits and the same idempotency. There is no separate path into your data for agents, which is the point.

Mount it ​

ts
import { createServer } from "node:http";
import { createRayfoldServer, createHttpHandler } from "@rayfold/server";
import { createMcpHandler } from "@rayfold/server";

const server = createRayfoldServer({ schema, resolvers });
const rayfold = createHttpHandler(server, { viewer });
const mcp = createMcpHandler(server, { viewer });

createServer(async (req, res) => {
  if (await mcp(req, res)) return; // answers only its own path
  await rayfold(req, res);
}).listen(4000);
kotlin
val server = RayfoldServer(ir, resolvers)
val http = RayfoldHttp(server) { ex -> viewerOf(ex) }.start(4000) // serves /rayfold; returns the running HttpServer
RayfoldMcp(server) { ex -> viewerOf(ex) }.mount(http)              // adds /mcp beside it

The handler answers false for a path that is not its own, so it chains in front of anything else. The default path is /mcp; pass path to change it. A server serving MCP says mcp in its manifest, so a client can tell.

POST only, application/json only, and the Origin of the request is checked — without that, any web page open in a browser could drive a local or intranet server. Configure allowedOrigins as you do for the main endpoint. A body over maxBody (maxBodyBytes on the JVM, 1 MiB by default) is refused with 413, and a notification gets 202 with no body.

What an agent sees ​

RayfoldMCP
commanda tool
command with @simulatea second tool, name.simulate, that runs it as a dry run
querya tool; also a listed resource at rayfold://query/<name> when it has no required arguments (the others can still be read as rayfold://query/<name>?arg=value)
streamnot exposed
the schemaa resource at rayfold://schema

Argument schemas come from the operation's own arguments, so an agent gets the types, the defaults and the descriptions you wrote once. A tool's outputSchema describes what a call returns, which is the default view: it declares every field, but requires none, since the view may leave some out. Resource arguments in the URI are converted by their declared types, so ?limit=5 passes the number 5 to an Int argument.

A command called as a tool, unless it is @idempotent(false), gets an idempotency key made from the operation and its arguments: the same call sent again is answered with the first one's result rather than run twice.

The .simulate variant appears only where the command declares @simulate. The runtime cannot make a resolver honour a dry run that never checks ctx.simulate, so it does not offer a dry run it cannot keep — an agent that wants to check before it acts can trust the tool being there.

What the schema resource serves ​

rayfold://schema serves the IR without policy expressions by default. A policy expression is a description of what your authorization depends on, which is a map of what to probe; the rules still run, the agent simply does not get to read them.

ts
createMcpHandler(server, { viewer, schema: "redacted" }); // the default
createMcpHandler(server, { viewer, schema: "full" });     // everything, for a trusted deployment
createMcpHandler(server, { viewer, schema: "off" });      // no schema resource at all

resources/read is a read verb, and it refuses to run a command however the URI is written — a read cannot be made to write.

Giving an agent less than a user ​

An agent usually needs a fraction of what the user it acts for can do. A capability token is the narrow credential for that: mint one scoped to the operations the agent needs, hand it over, and let it expire.

Next ​

Released under the Apache-2.0 license.