Skip to content

MCP for AI agents ​

An agent that uses your API needs to know what it can call, what arguments each operation takes, and what comes back. A Rayfold schema already says that, so the MCP endpoint translates the schema into MCP. Mount it and an agent sees the operations you already wrote.

Authorization does not change. 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.

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.

The endpoint accepts only POST and only application/json, and it checks the Origin of the request. Without that check, 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.

Since 0.2.2

maxBody on the Node handler came with 0.2.2; in 0.2.1 it reads a body whole, however large.

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 (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 and does not run twice.

The .simulate variant appears only where the command declares @simulate. A resolver that never checks ctx.simulate would ignore a dry run, and the runtime cannot force it to, so the runtime offers no dry run it cannot keep. When the tool is there, an agent can rely on it to check before it acts.

What the schema resource serves ​

rayfold://schema serves the IR without policy expressions by default. A policy expression shows what your authorization depends on, which tells a caller what to probe. The rules still run; the agent cannot 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. It refuses to run a command however the URI is written, so 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 covers 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.