PackagesMcp

@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/mcp
npm install @lunora/mcp
yarn add @lunora/mcp
bun add @lunora/mcp

The 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 to 1/true/yes/on to expose the mutation/action run tools. Default: read-only (writes disabled).
  • LUNORA_MCP_ALLOW_AGENTS (optional) — set to 1/true/yes/on to expose the agent tools. Default: agents disabled.
  • LUNORA_MCP_AGENTS (optional) — a ;-separated list of name:description pairs 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.

ToolInputWhat it does
lunora_list_functionsnoneLists the deployment's public functions (queries, mutations, actions) with their kinds.
lunora_list_tablesnoneLists the deployment's .global() tables with their row counts.
lunora_get_function_schemafunctionPathReturns one function's argument descriptors and kind, so a caller can build valid args.
lunora_run_queryfunctionPath, args?, shardKey?Runs a query and returns its result. Read-only.
lunora_run_mutationfunctionPath, args?, shardKey?Runs a mutation and returns its result. Writes data. Requires writes enabled.
lunora_run_actionfunctionPath, 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_statusthreadKeyPolls 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.

An agent discovers the surface before it calls anything:

  1. lunora_list_functions — discover the available paths and their kinds.
  2. lunora_get_function_schema — fetch the argument descriptors for one path.
  3. lunora_run_query / lunora_run_mutation / lunora_run_action — call the function with a well-formed args object.

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_AGENTS1/true/yes/on to expose the agent tools.
  • LUNORA_MCP_AGENTS — a ;-separated list of name:description pairs selecting which agents to expose, e.g. "support:Support questions;billing:Billing help".
  • LUNORA_MCP_AGENT_TIMEOUT_MS (optional) — wall-clock budget a single agent_<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 the threadKey it 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 shardKey the run-tools accept