@lunora/mcp
A Model Context Protocol server that exposes a Lunora deployment to AI agents.
@lunora/mcp is a Model Context Protocol
server that exposes a deployed Lunora app to AI agents. It registers tools for
introspecting a deployment (lunora_list_functions, lunora_list_tables,
lunora_get_function_schema) and invoking its functions (lunora_run_query,
lunora_run_mutation, lunora_run_action), each backed by
@lunora/client over HTTP RPC.
pnpm add @lunora/mcpnpm install @lunora/mcpyarn add @lunora/mcpbun add @lunora/mcpThe server is transport-agnostic. The shipped lunora-mcp binary speaks
JSON-RPC over stdio — the transport MCP clients use when they spawn a
process — or you can build a server with createLunoraMcpServer and connect
any transport yourself.
The lunora-mcp binary
MCP clients spawn the lunora-mcp binary and talk to it over stdio.
Configuration comes from the environment, so the spawn config stays a plain
{ command, env }:
LUNORA_URL(required) — base URL of the deployed Worker.LUNORA_ADMIN_TOKEN(optional) — bearer token sent on every RPC.LUNORA_MCP_ALLOW_WRITES(optional) — set to1/true/yes/onto expose the mutation/action run tools. Default: read-only (writes disabled).LUNORA_MCP_ALLOW_AGENTS(optional) — set to1/true/yes/onto expose the agent tools. Default: agents disabled.LUNORA_MCP_AGENTS(optional) — a;-separated list ofname:descriptionpairs selecting which agents to expose (see Expose an agent).
{
"mcpServers": {
"lunora": {
"command": "lunora-mcp",
"env": {
"LUNORA_URL": "https://app.example.workers.dev",
"LUNORA_ADMIN_TOKEN": "...",
},
},
},
}The binary exits non-zero if LUNORA_URL is missing or the transport fails to connect, so the spawning client surfaces a startup failure immediately.
Exposed tools
Each tool maps onto a method LunoraClient already provides. TOOL_DEFINITIONS
is the registered tool surface; callTool dispatches a call against a client.
| Tool | Input | What it does |
|---|---|---|
lunora_list_functions | none | Lists the deployment's public functions (queries, mutations, actions) with their kinds. |
lunora_list_tables | none | Lists the deployment's .global() tables with their row counts. |
lunora_get_function_schema | functionPath | Returns one function's argument descriptors and kind, so a caller can build valid args. |
lunora_run_query | functionPath, args?, shardKey? | Runs a query and returns its result. Read-only. |
lunora_run_mutation | functionPath, args?, shardKey? | Runs a mutation and returns its result. Writes data. Requires writes enabled. |
lunora_run_action | functionPath, args?, shardKey? | Runs an action and returns its result. May call external services. Requires writes enabled. |
agent_<name> | prompt, threadKey?, title? | Starts a durable @lunora/agent run and awaits its answer. One tool per exposed agent. Requires agents enabled. |
lunora_agent_status | threadKey | Polls a running agent by threadKey and returns its answer once finished. Requires agents enabled. |
The server is read-only by default: only the introspection tools and lunora_run_query are exposed. lunora_run_mutation and lunora_run_action are
hidden from the advertised tool list and refused at dispatch unless you set LUNORA_MCP_ALLOW_WRITES (or pass allowWrites: true when building a server
programmatically). Grant a least-privilege token scoped to what the agent actually needs — never the deployment's admin token.
The three run-tools share one input schema:
functionPath(required, string) — a function reference, e.g."messages:send".args(object) — the arguments object passed to the function. Anything that isn't a plain object (including arrays) is coerced to an empty bag.shardKey(string) — an optional shard key when the function is.shardBy()-partitioned.
Results are returned as MCP content text — the function's return value JSON,
or the JSON null literal for a void-returning mutation/action. Unknown tools
and thrown errors come back as isError tool results (not rejections), so the
calling model sees the failure as tool output.
Recommended agent flow
An agent discovers the surface before it calls anything:
lunora_list_functions— discover the available paths and their kinds.lunora_get_function_schema— fetch the argument descriptors for one path.lunora_run_query/lunora_run_mutation/lunora_run_action— call the function with a well-formedargsobject.
lunora_get_function_schema returns { path, kind, args }, where kind is
"query", "mutation", or "action" and args is an array of argument
descriptors (name, kind, optional, and optionally element or table).
It returns an isError result if the path doesn't exist.
Building a server programmatically
createLunoraMcpServer returns a transport-agnostic MCP Server. Pass a url
(and optional token), and the tools dispatch against a LunoraClient built
from them:
import { createLunoraMcpServer } from "@lunora/mcp";
const server = createLunoraMcpServer({ url: "https://app.example.workers.dev", token: "..." });
await server.connect(myTransport);For the common stdio case, connectStdio builds the server and connects it over
a StdioServerTransport in one step:
import { connectStdio } from "@lunora/mcp";
await connectStdio({ url: process.env.LUNORA_URL, token: process.env.LUNORA_ADMIN_TOKEN });LunoraMcpServerOptions accepts either a url (with optional token and
fetch) or a pre-built client — the latter is the injection point for tests.
You must supply one or the other; passing neither throws.
Auth and admin gating
The token (sourced from LUNORA_ADMIN_TOKEN for the binary) is set as the
client's auth token and sent as a bearer token on every RPC the tools make.
Gating is enforced by your deployment: the tools call ordinary queries,
mutations, and actions, so whatever auth those functions require applies
unchanged. Run-tools open no WebSocket — every call is plain HTTP RPC — so the
server is safe to run as a short-lived stdio process.
Connecting an MCP client
Any MCP client that can spawn a stdio server works. With the mcpServers config
above, an agent like Claude can call lunora_list_functions to discover the
deployment's surface, then lunora_run_query / lunora_run_mutation /
lunora_run_action to invoke it — passing functionPath (e.g.
"messages:list"), an args object, and an optional shardKey.
Expose an agent
A deployment's durable @lunora/agent agents can be
fronted as MCP tools. Like writes, this is opt-in and fail-closed: starting
an agent run is a side effect, so the agent tools are hidden from the advertised
list and refused at dispatch unless you enable them. @lunora/mcp takes no
dependency on @lunora/agent — it calls the agent's public agents:agentRun
mutation over HTTP RPC like any other function.
Enable it with two env vars (or the matching createLunoraMcpServer options):
LUNORA_MCP_ALLOW_AGENTS—1/true/yes/onto expose the agent tools.LUNORA_MCP_AGENTS— a;-separated list ofname:descriptionpairs selecting which agents to expose, e.g."support:Support questions;billing:Billing help".LUNORA_MCP_AGENT_TIMEOUT_MS(optional) — wall-clock budget a singleagent_<name>call awaits before returning a pending result to poll.
{
"mcpServers": {
"lunora": {
"command": "lunora-mcp",
"env": {
"LUNORA_URL": "https://app.example.workers.dev",
"LUNORA_ADMIN_TOKEN": "...",
"LUNORA_MCP_ALLOW_AGENTS": "1",
"LUNORA_MCP_AGENTS": "support:Support questions;billing:Billing help",
},
},
},
}Each exposed agent gets an agent_<name> tool taking:
prompt(required, string) — the task or message for the agent.threadKey(string) — reuse to continue a conversation; omit to start a new thread. The tool returns thethreadKeyit used either way.title(string) — an optional thread title, applied on the first run only.
The tool starts a durable run and awaits it up to the timeout budget. If the run
outlasts the budget, the tool returns a pending result — { status: "running", threadKey, runId, hint } — instead of hanging. Feed that threadKey
to the generic lunora_agent_status tool to poll for the final answer once the
run finishes.
Agent runs are owner-scoped to the identity the configured token resolves to. Grant a least-privilege token mapped to a bot identity so each
deployment's threads stay isolated — never the deployment's admin token. If the token resolves to no identity, threads are not owner-isolated.
See also
- @lunora/client — the HTTP RPC client backing every tool
- @lunora/server — defines the queries, mutations, and actions the tools call
- @lunora/agent — the durable agents the
agent_<name>tools front - @lunora/cli — deploy the app the server introspects
- Sharding — the
shardKeythe run-tools accept