@lunora/mcp 1.0.0-alpha.14 → 1.0.0-alpha.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +126 -0
- package/README.md +229 -10
- package/dist/bin.mjs +4 -37
- package/dist/docs/index.d.mts +258 -0
- package/dist/docs/index.d.ts +258 -0
- package/dist/docs/index.mjs +1 -0
- package/dist/index.d.mts +516 -16
- package/dist/index.d.ts +516 -16
- package/dist/index.mjs +1 -2
- package/dist/packem_shared/AGENT_RUN_INPUT_SCHEMA-hKbpa3Dg.mjs +1 -0
- package/dist/packem_shared/DEFAULT_DOCS_BASE_URL-CZ3fVsSc.mjs +4 -0
- package/dist/packem_shared/DEFAULT_MAX_REQUEST_BYTES-CbbpkHRK.mjs +1 -0
- package/dist/packem_shared/DEFAULT_SEARCH_LIMIT-BqSYN5vr.mjs +3 -0
- package/dist/packem_shared/DOCS_SERVER_NAME-BMeAWi5h.mjs +1 -0
- package/dist/packem_shared/DOCS_URI_SCHEME-Buo752CV.mjs +3 -0
- package/dist/packem_shared/LOCAL_SERVER_NAME-BbopFImI.mjs +1 -0
- package/dist/packem_shared/OBSERVABILITY_TOOL_DEFINITIONS-mO4JuHgV.mjs +1 -0
- package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-DKcVAOe2.mjs +1 -0
- package/dist/packem_shared/connectStdio-CQI9JGJC.mjs +1 -0
- package/dist/packem_shared/createAuthedMcpFetchHandler-Mfw8jlOV.mjs +1 -0
- package/dist/packem_shared/createMcpFetchHandler-C5doZK5c.mjs +1 -0
- package/dist/packem_shared/createPaidMcpServer-Ba1gMM4Y.mjs +1 -0
- package/dist/packem_shared/createToolServer-BtGuPyMU.mjs +1 -0
- package/dist/packem_shared/observability-tools-D4Z8dGS-.mjs +1 -0
- package/dist/packem_shared/promise-memo-OqVPWoUX.mjs +1 -0
- package/dist/packem_shared/serve-stateless.d-CKsbI0fP.d.mts +154 -0
- package/dist/packem_shared/serve-stateless.d-CKsbI0fP.d.ts +154 -0
- package/dist/packem_shared/toDocsSearchHits-CBLmtWXt.mjs +1 -0
- package/dist/packem_shared/tools-D-JWERWj.mjs +2 -0
- package/package.json +17 -3
- package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-DV2VkuuU.mjs +0 -143
- package/dist/packem_shared/connectStdio-nl-Tt_T1.mjs +0 -65
package/LICENSE.md
CHANGED
|
@@ -103,3 +103,129 @@ Unless required by applicable law or agreed to in writing, software distributed
|
|
|
103
103
|
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
104
|
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
105
|
specific language governing permissions and limitations under the License.
|
|
106
|
+
|
|
107
|
+
<!-- DEPENDENCIES -->
|
|
108
|
+
<!-- /DEPENDENCIES -->
|
|
109
|
+
|
|
110
|
+
<!-- TYPE_DEPENDENCIES -->
|
|
111
|
+
|
|
112
|
+
# Licenses of bundled types
|
|
113
|
+
The published @lunora/mcp artifact additionally contains code with the following licenses:
|
|
114
|
+
FSL-1.1-Apache-2.0
|
|
115
|
+
|
|
116
|
+
# Bundled types:
|
|
117
|
+
## @lunora/x402
|
|
118
|
+
License: FSL-1.1-Apache-2.0
|
|
119
|
+
By: Daniel Bannert
|
|
120
|
+
Repository: git+https://github.com/anolilab/lunora.git
|
|
121
|
+
|
|
122
|
+
> # Functional Source License, Version 1.1, Apache 2.0 Future License
|
|
123
|
+
>
|
|
124
|
+
> ## Abbreviation
|
|
125
|
+
>
|
|
126
|
+
> FSL-1.1-Apache-2.0
|
|
127
|
+
>
|
|
128
|
+
> ## Notice
|
|
129
|
+
>
|
|
130
|
+
> Copyright 2026 anolilab and contributors
|
|
131
|
+
>
|
|
132
|
+
> ## Terms and Conditions
|
|
133
|
+
>
|
|
134
|
+
> ### Licensor ("We")
|
|
135
|
+
>
|
|
136
|
+
> The party offering the Software under these Terms and Conditions.
|
|
137
|
+
>
|
|
138
|
+
> ### The Software
|
|
139
|
+
>
|
|
140
|
+
> The "Software" is each version of the software that we make available under
|
|
141
|
+
> these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
142
|
+
> Conditions with the Software.
|
|
143
|
+
>
|
|
144
|
+
> ### License Grant
|
|
145
|
+
>
|
|
146
|
+
> Subject to your compliance with this License Grant and the Patents,
|
|
147
|
+
> Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
148
|
+
> use, copy, modify, create derivative works, publicly perform, publicly display
|
|
149
|
+
> and redistribute the Software for any Permitted Purpose identified below.
|
|
150
|
+
>
|
|
151
|
+
> ### Permitted Purpose
|
|
152
|
+
>
|
|
153
|
+
> A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
154
|
+
> means making the Software available to others in a commercial product or service
|
|
155
|
+
> that:
|
|
156
|
+
>
|
|
157
|
+
> 1. substitutes for the Software;
|
|
158
|
+
>
|
|
159
|
+
> 2. substitutes for any other product or service we offer using the Software that
|
|
160
|
+
> exists as of the date we make the Software available; or
|
|
161
|
+
>
|
|
162
|
+
> 3. offers the same or substantially similar functionality as the Software.
|
|
163
|
+
>
|
|
164
|
+
> Permitted Purposes specifically include using the Software:
|
|
165
|
+
>
|
|
166
|
+
> 1. for your internal use and access;
|
|
167
|
+
>
|
|
168
|
+
> 2. for non-commercial education;
|
|
169
|
+
>
|
|
170
|
+
> 3. for non-commercial research; and
|
|
171
|
+
>
|
|
172
|
+
> 4. in connection with professional services that you provide to a licensee using
|
|
173
|
+
> the Software in accordance with these Terms and Conditions.
|
|
174
|
+
>
|
|
175
|
+
> ### Patents
|
|
176
|
+
>
|
|
177
|
+
> To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
178
|
+
> patents, the license grant above includes a license under our patents. If you
|
|
179
|
+
> make a claim against any party that the Software infringes or contributes to the
|
|
180
|
+
> infringement of any patent, then your patent license to the Software ends
|
|
181
|
+
> immediately.
|
|
182
|
+
>
|
|
183
|
+
> ### Redistribution
|
|
184
|
+
>
|
|
185
|
+
> The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
186
|
+
> the Software.
|
|
187
|
+
>
|
|
188
|
+
> If you redistribute any copies, modifications or derivatives of the Software,
|
|
189
|
+
> you must include a copy of or a link to these Terms and Conditions and not
|
|
190
|
+
> remove any copyright notices provided in or with the Software.
|
|
191
|
+
>
|
|
192
|
+
> ### Disclaimer
|
|
193
|
+
>
|
|
194
|
+
> THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
195
|
+
> IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
196
|
+
> PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
197
|
+
>
|
|
198
|
+
> IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
199
|
+
> SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, EVEN
|
|
200
|
+
> IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
201
|
+
>
|
|
202
|
+
> ### Trademarks
|
|
203
|
+
>
|
|
204
|
+
> Except for displaying the License Details and identifying us as the origin of
|
|
205
|
+
> the Software, you have no right under these Terms and Conditions to use our
|
|
206
|
+
> trademarks, trade names, service marks or product names.
|
|
207
|
+
>
|
|
208
|
+
> ## Grant of Future License
|
|
209
|
+
>
|
|
210
|
+
> We hereby irrevocably grant you an additional license to use the Software under
|
|
211
|
+
> the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
212
|
+
> the date we make the Software available. On or after that date, you may use the
|
|
213
|
+
> Software under the Apache License, Version 2.0, in which case the following will
|
|
214
|
+
> apply:
|
|
215
|
+
>
|
|
216
|
+
> Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
217
|
+
> this file except in compliance with the License.
|
|
218
|
+
>
|
|
219
|
+
> You may obtain a copy of the License at
|
|
220
|
+
>
|
|
221
|
+
> http://www.apache.org/licenses/LICENSE-2.0
|
|
222
|
+
>
|
|
223
|
+
> Unless required by applicable law or agreed to in writing, software distributed
|
|
224
|
+
> under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
225
|
+
> CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
226
|
+
> specific language governing permissions and limitations under the License.
|
|
227
|
+
>
|
|
228
|
+
> <!-- DEPENDENCIES -->
|
|
229
|
+
> <!-- /DEPENDENCIES -->
|
|
230
|
+
|
|
231
|
+
<!-- /TYPE_DEPENDENCIES -->
|
package/README.md
CHANGED
|
@@ -34,20 +34,32 @@
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
[Model Context Protocol](https://modelcontextprotocol.io) servers for Lunora, in two flavours:
|
|
38
|
+
|
|
39
|
+
- **Deployment** (the main entry) — exposes a deployed Lunora app to AI agents: introspection tools (`lunora_list_functions`, `lunora_list_tables`, `lunora_get_function_schema`) and invocation tools (`lunora_run_query`, `lunora_run_mutation`, `lunora_run_action`), each backed by `@lunora/client` over HTTP RPC. Needs an admin token.
|
|
40
|
+
- **Documentation** ([`@lunora/mcp/docs`](#documentation-server)) — exposes the framework's _docs_ so an agent writing Lunora code can look up the real API instead of guessing. Credential-free, and safe to host publicly; Lunora runs it at `https://lunora.sh/mcp`.
|
|
41
|
+
|
|
42
|
+
Most users never install this package directly — `lunora mcp install` wires both servers into their editor. See [`@lunora/cli`](https://www.npmjs.com/package/@lunora/cli).
|
|
38
43
|
|
|
39
44
|
Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-safe, real-time backend on Cloudflare Workers + Durable Objects with a Vite-first DX.
|
|
40
45
|
|
|
41
46
|
## Tools
|
|
42
47
|
|
|
43
|
-
| Tool
|
|
44
|
-
|
|
|
45
|
-
| `lunora_list_functions`
|
|
46
|
-
| `lunora_list_tables`
|
|
47
|
-
| `lunora_get_function_schema`
|
|
48
|
-
| `lunora_run_query`
|
|
49
|
-
| `lunora_run_mutation`
|
|
50
|
-
| `lunora_run_action`
|
|
48
|
+
| Tool | Description |
|
|
49
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `lunora_list_functions` | List the deployment's public functions (queries, mutations, actions) with their kinds. |
|
|
51
|
+
| `lunora_list_tables` | List the deployment's `.global()` tables with their row counts. |
|
|
52
|
+
| `lunora_get_function_schema` | Return a function's argument descriptors and kind by path, so a caller can construct a valid arguments object. |
|
|
53
|
+
| `lunora_run_query` | Run a query and return its result. Read-only. |
|
|
54
|
+
| `lunora_run_mutation` | Run a mutation. Writes data. Two-step: propose, then confirm with the returned `actionDigest`. |
|
|
55
|
+
| `lunora_run_action` | Run an action. May call external services. Two-step: propose, then confirm with the returned `actionDigest`. |
|
|
56
|
+
| `lunora_get_logs` | Read the deployment's recent log entries (newest first). Requires an admin token. |
|
|
57
|
+
| `lunora_get_issues` | List errors grouped into Issues by fingerprint, with counts and triage status. Requires an admin token. |
|
|
58
|
+
| `lunora_get_advisories` | List the deployment's schema/query advisories. Requires an admin token. |
|
|
59
|
+
| `lunora_get_query_insights` | Per-statement execution counts and latency over a recent window. Requires an admin token. |
|
|
60
|
+
| `lunora_get_migration_status` | Which migrations are applied and which are pending. Requires an admin token. |
|
|
61
|
+
| `agent_<name>` | Start a durable [`@lunora/agent`](https://www.npmjs.com/package/@lunora/agent) run and await its answer. One tool per exposed agent. Requires agents enabled. |
|
|
62
|
+
| `lunora_agent_status` | Poll a running agent by `threadKey` and return its answer once finished. Requires agents enabled. |
|
|
51
63
|
|
|
52
64
|
### Recommended agent flow
|
|
53
65
|
|
|
@@ -56,8 +68,101 @@ Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-sa
|
|
|
56
68
|
2. lunora_get_function_schema → retrieve the argument descriptors for a specific path
|
|
57
69
|
3. lunora_run_query / lunora_run_mutation / lunora_run_action
|
|
58
70
|
→ call the function with a well-formed arguments object
|
|
71
|
+
(the two write tools take a second, confirming call)
|
|
59
72
|
```
|
|
60
73
|
|
|
74
|
+
### Write confirmation (the two-step handshake)
|
|
75
|
+
|
|
76
|
+
`LUNORA_MCP_ALLOW_WRITES` decides whether this server may write at **all**. It
|
|
77
|
+
never said anything about whether a _particular_ write was reviewed, so past that
|
|
78
|
+
gate `lunora_run_mutation` and `lunora_run_action` each take two calls.
|
|
79
|
+
|
|
80
|
+
The first call **executes nothing**. It returns the proposed action and a digest:
|
|
81
|
+
|
|
82
|
+
```jsonc
|
|
83
|
+
{
|
|
84
|
+
"status": "action_required",
|
|
85
|
+
"actionDigest": "1789129912052.0ZR2…",
|
|
86
|
+
"expiresAt": "2026-09-11T14:41:52.052Z",
|
|
87
|
+
"proposedAction": {
|
|
88
|
+
"tool": "lunora_run_mutation",
|
|
89
|
+
"kind": "mutation",
|
|
90
|
+
"functionPath": "messages:send",
|
|
91
|
+
"args": { "roomId": "r1", "text": "hi" },
|
|
92
|
+
},
|
|
93
|
+
"nextStep": "Show proposedAction to a human. To execute, call …",
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Render `proposedAction` for a human, then call the same tool again — before
|
|
98
|
+
`expiresAt` — with the identical `functionPath` / `args` / `shardKey` /
|
|
99
|
+
`idempotencyKey`, plus `confirmed: true` and that `actionDigest`. Only then does
|
|
100
|
+
the write happen.
|
|
101
|
+
|
|
102
|
+
The digest is `<expiresAt>.<signature>`, where the signature is an HMAC over a
|
|
103
|
+
canonical (sorted-key) encoding of the tool name, function path, arguments, shard
|
|
104
|
+
key, idempotency key **and that deadline**, keyed by the deployment's own
|
|
105
|
+
identity. So:
|
|
106
|
+
|
|
107
|
+
- **Argument key order is irrelevant** — re-serializing `args` does not invalidate a confirmation.
|
|
108
|
+
- **Any real edit invalidates it.** A different target, argument, or shard key produces a different digest, and the confirmation is refused with nothing written. That is the guarantee: what executes is exactly what was reviewed.
|
|
109
|
+
- **It expires after 10 minutes.** The deadline travels in the clear (the verifying instance has to read it) but is signed alongside the proposal, so moving it breaks the signature. An expired digest is refused, not silently re-proposed — call again without `confirmed` for a fresh one.
|
|
110
|
+
- **No server state is involved.** The HTTP handler serves statelessly (a fresh server per request), so a confirmation is revalidated by recomputation on whichever instance receives it, not looked up in a store.
|
|
111
|
+
|
|
112
|
+
`idempotencyKey` is optional and is folded into the digest. A client that timed
|
|
113
|
+
out can resubmit the confirmation it already holds, for as long as that digest is
|
|
114
|
+
inside its window, without asking for a second review; and a deliberately-repeated
|
|
115
|
+
identical write under a **new** key gets its own review instead of riding the
|
|
116
|
+
first one. It does **not** deduplicate the write: this server keeps no state
|
|
117
|
+
between requests and never forwards the key to your function, so a resubmitted
|
|
118
|
+
confirmed call executes again. Make the function itself idempotent if the write
|
|
119
|
+
must happen at most once.
|
|
120
|
+
|
|
121
|
+
#### What the handshake does not do
|
|
122
|
+
|
|
123
|
+
It binds **intent, not human presence**, and the difference matters when you
|
|
124
|
+
decide whether to enable writes at all.
|
|
125
|
+
|
|
126
|
+
A verified digest proves the call about to run is exactly the call that was
|
|
127
|
+
proposed, on this deployment, inside its window. It does **not** prove a human
|
|
128
|
+
saw it, and no server-side check can: an MCP server has no channel to a person —
|
|
129
|
+
no session, no end-user identity, no UI — and MCP deliberately puts the
|
|
130
|
+
human-in-the-loop at the **host**. The client is what renders a tool call for
|
|
131
|
+
approval. A client that asks nobody can take the digest it was just handed, send
|
|
132
|
+
it straight back with `confirmed: true`, and the write runs.
|
|
133
|
+
|
|
134
|
+
That is why writes are off by default and refused at dispatch as well as omitted
|
|
135
|
+
from `ListTools`: enabling `LUNORA_MCP_ALLOW_WRITES` is **your** statement that
|
|
136
|
+
the client on the other end does the asking. Treat the handshake as a client-UI
|
|
137
|
+
affordance and an audit record of what was proposed, not as a gate against the
|
|
138
|
+
model.
|
|
139
|
+
|
|
140
|
+
Two more scope limits, stated rather than implied:
|
|
141
|
+
|
|
142
|
+
- **The digest is deployment-wide, not principal-bound.** Its key is the domain separator, the deployment URL and the admin bearer — nothing identifying a user. On an OAuth-fronted server (`createAuthedMcpFetchHandler`) every principal shares that bearer, so within the 10-minute window any principal holding write scope can confirm another's identical proposal. Binding it to a person would mean folding the verified `sub` claim into the signing key, which this package does not do today.
|
|
143
|
+
- **It survives an admin-bearer rotation only as long as the bearer does.** Rotating `LUNORA_ADMIN_TOKEN`, or moving the deployment URL, invalidates every outstanding digest — which is the intended behaviour, not a bug to work around.
|
|
144
|
+
|
|
145
|
+
### Observability tools (privileged)
|
|
146
|
+
|
|
147
|
+
The five `lunora_get_*` observability tools are read-only, but they surface the
|
|
148
|
+
deployment's **operational data** — log lines, request metadata, and grouped
|
|
149
|
+
error messages, all of which may contain user data, and all of which land in the
|
|
150
|
+
model's context (and therefore at its provider). They are therefore **off by
|
|
151
|
+
default**: set `LUNORA_MCP_ALLOW_OBSERVABILITY=1` (or pass
|
|
152
|
+
`allowObservability: true`) to expose them. Without it they are omitted from
|
|
153
|
+
`ListTools` entirely and refused at dispatch, the same omit-don't-refuse rule the
|
|
154
|
+
write tools use. They are independent of `--allow-writes`, which is about
|
|
155
|
+
changing data, not reading operational data — and independent of the admin
|
|
156
|
+
bearer, which every tool already needs, so holding it is not the opt-in.
|
|
157
|
+
|
|
158
|
+
They return `structuredContent` alongside the usual text block, described by each
|
|
159
|
+
tool's `outputSchema` (MCP revision `2025-06-18` and later; older clients keep
|
|
160
|
+
reading the text block). All but `lunora_get_migration_status` take a `limit`
|
|
161
|
+
clamped server-side; migration status takes only `shardKey` and returns every
|
|
162
|
+
migration, because truncating that list would hide the pending one. Each also
|
|
163
|
+
takes an optional `shardKey` — on a `.shardBy()`-partitioned deployment these
|
|
164
|
+
reads are **per-shard**, not deployment-wide.
|
|
165
|
+
|
|
61
166
|
`lunora_get_function_schema` returns a JSON object with three fields:
|
|
62
167
|
|
|
63
168
|
- `path` — the function path (e.g. `"messages:send"`)
|
|
@@ -70,6 +175,8 @@ Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-sa
|
|
|
70
175
|
npm install @lunora/mcp
|
|
71
176
|
```
|
|
72
177
|
|
|
178
|
+
Paid MCP tools (`createPaidMcpServer`) additionally need the optional peer [`@lunora/x402`](https://www.npmjs.com/package/@lunora/x402); it is loaded lazily, so installs that never charge for a tool don't pay for its dependency tree.
|
|
179
|
+
|
|
73
180
|
```sh
|
|
74
181
|
yarn add @lunora/mcp
|
|
75
182
|
```
|
|
@@ -80,7 +187,7 @@ pnpm add @lunora/mcp
|
|
|
80
187
|
|
|
81
188
|
## Usage
|
|
82
189
|
|
|
83
|
-
MCP clients spawn the `lunora-mcp` binary over stdio. Configuration comes from `LUNORA_URL`
|
|
190
|
+
MCP clients spawn the `lunora-mcp` binary over stdio. Configuration comes from `LUNORA_URL` and `LUNORA_ADMIN_TOKEN` — both required (see [Tokens](#tokens)):
|
|
84
191
|
|
|
85
192
|
```jsonc
|
|
86
193
|
{
|
|
@@ -105,6 +212,117 @@ const server = createLunoraMcpServer({ url: "https://app.example.workers.dev", t
|
|
|
105
212
|
await server.connect(myTransport);
|
|
106
213
|
```
|
|
107
214
|
|
|
215
|
+
## Expose an agent
|
|
216
|
+
|
|
217
|
+
A deployment's durable [`@lunora/agent`](https://www.npmjs.com/package/@lunora/agent) agents can be fronted as MCP tools. This is **opt-in and fail-closed**, mirroring `allowWrites`: starting an agent run is a side effect, so the agent tools are omitted from the advertised list _and_ refused at dispatch unless you explicitly enable them. `@lunora/mcp` takes no dependency on `@lunora/agent` — it reaches the agent's public `agents:agentRun` mutation over RPC like any other function.
|
|
218
|
+
|
|
219
|
+
Two opt-ins are needed, on **both** sides:
|
|
220
|
+
|
|
221
|
+
1. **On the agent**, `defineAgent({ publicRun: true })`. `agents:agentRun` refuses
|
|
222
|
+
any agent without it (`FORBIDDEN`), because starting a durable run from
|
|
223
|
+
outside the deployment is a side effect the agent's author has to allow. The
|
|
224
|
+
env vars below do not grant it — an agent exposed here but not marked
|
|
225
|
+
`publicRun` is advertised and fails on its first call.
|
|
226
|
+
2. **On this server**, the env vars (or the matching `createLunoraMcpServer` options):
|
|
227
|
+
|
|
228
|
+
- `LUNORA_MCP_ALLOW_AGENTS` — set to `1`/`true`/`yes`/`on` to expose the agent tools. Default: agents disabled.
|
|
229
|
+
- `LUNORA_MCP_AGENTS` — a `;`-separated list of `name:description` pairs selecting which agents to expose, e.g. `"support:Support questions;billing:Billing help"`.
|
|
230
|
+
- `LUNORA_MCP_AGENT_TIMEOUT_MS` (optional) — wall-clock budget a single `agent_<name>` call awaits before returning a pending result to poll.
|
|
231
|
+
|
|
232
|
+
```jsonc
|
|
233
|
+
{
|
|
234
|
+
"mcpServers": {
|
|
235
|
+
"lunora": {
|
|
236
|
+
"command": "lunora-mcp",
|
|
237
|
+
"env": {
|
|
238
|
+
"LUNORA_URL": "https://app.example.workers.dev",
|
|
239
|
+
"LUNORA_ADMIN_TOKEN": "...",
|
|
240
|
+
"LUNORA_MCP_ALLOW_AGENTS": "1",
|
|
241
|
+
"LUNORA_MCP_AGENTS": "support:Support questions;billing:Billing help",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Each exposed agent gets an `agent_<name>` tool taking `prompt` (required), an optional `threadKey` (reuse to continue a conversation; omit to start a new thread), and an optional `title`. The tool starts a durable run and awaits it up to the timeout budget; if the run outlasts the budget it returns a pending result whose `threadKey` you feed to the generic `lunora_agent_status` tool to poll for the final answer.
|
|
249
|
+
|
|
250
|
+
Runs are **owner-scoped** to the identity the configured token resolves to — which is the deployment's **admin** identity, because `LUNORA_ADMIN_TOKEN` is what every tool needs (see [Tokens](#tokens)). Every agent thread this server starts therefore belongs to that one identity; run a separate MCP server per deployment if you need them kept apart.
|
|
251
|
+
|
|
252
|
+
### Tokens
|
|
253
|
+
|
|
254
|
+
`LUNORA_ADMIN_TOKEN` must be the deployment's **admin bearer**. It cannot be scoped down: `lunora_list_functions`, `lunora_list_tables`, and the allowlist precheck that runs before _every_ `lunora_run_*` call all hit admin-gated `/_lunora/admin/*` routes, so a least-privilege token returns `ADMIN_FORBIDDEN` on the first tool call. Constructing a server without one fails fast rather than advertising tools that cannot work.
|
|
255
|
+
|
|
256
|
+
The read-only guarantee therefore does **not** come from the token's scope — it comes from `LUNORA_MCP_ALLOW_WRITES` defaulting off, which omits the write tools from `tools/list` _and_ refuses them at dispatch. Treat the MCP server itself as the trust boundary: give it the admin token, and gate who can reach it (the OAuth-protected `createAuthedMcpFetchHandler` is the supported way to expose it beyond a local stdio process).
|
|
257
|
+
|
|
258
|
+
## Resources and annotations
|
|
259
|
+
|
|
260
|
+
Both servers implement MCP **tools**; the documentation server additionally exposes every page as an MCP **resource** (`lunora-docs:/docs/…`, `text/markdown`), so a client can enumerate and attach a page directly instead of the model having to guess a search query first.
|
|
261
|
+
|
|
262
|
+
Every tool carries **annotations** — `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` and a human-facing `title` — so a client can badge the read-only surface and confirm before a write. They are hints; the actual guarantee is still made at dispatch, where a write tool is refused unless writes are enabled.
|
|
263
|
+
|
|
264
|
+
## Documentation server
|
|
265
|
+
|
|
266
|
+
`@lunora/mcp/docs` is a second, independent surface: it serves **published documentation**, not a deployment. No credentials, no writes, no `@lunora/client` — and no Node built-ins, so it runs unchanged on Workers, Netlify/Vercel functions, Deno, and Bun.
|
|
267
|
+
|
|
268
|
+
| Tool | Description |
|
|
269
|
+
| -------------------- | --------------------------------------------------------------------- |
|
|
270
|
+
| `lunora_search_docs` | Search the docs; returns matching pages and sections with their URLs. |
|
|
271
|
+
| `lunora_get_doc` | Return one page in full, as Markdown. |
|
|
272
|
+
| `lunora_list_docs` | List every page with its title and description. |
|
|
273
|
+
|
|
274
|
+
The tools read a `DocsIndex`, which has two implementations. A docs site wires up its own in-process index and mounts the server as an HTTP route:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { createDocsMcpFetchHandler } from "@lunora/mcp/docs";
|
|
278
|
+
|
|
279
|
+
const handle = createDocsMcpFetchHandler({ index: myDocsIndex });
|
|
280
|
+
|
|
281
|
+
// e.g. in a Worker: export default { fetch: handle }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Anything else reads a published site over HTTP:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { createDocsMcpServer, createRemoteDocsIndex } from "@lunora/mcp/docs";
|
|
288
|
+
|
|
289
|
+
const server = createDocsMcpServer({ index: createRemoteDocsIndex({ baseUrl: "https://lunora.sh" }) });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Point a client at the hosted endpoint with no install at all:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
claude mcp add --transport http lunora-docs https://lunora.sh/mcp
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Hosting it safely
|
|
299
|
+
|
|
300
|
+
`createDocsMcpFetchHandler` screens each request before the transport sees it, because this surface is meant to be public and unauthenticated:
|
|
301
|
+
|
|
302
|
+
- **Bodies are capped** (128 KiB by default; override with `maxRequestBytes`).
|
|
303
|
+
- **JSON-RPC batches are refused.** The stateless transport buffers a whole batch's replies into one response, so a single small request carrying thousands of `tools/call` messages would amplify into hundreds of megabytes out, with no session to rate-limit against. A docs client gains nothing from batching.
|
|
304
|
+
- `lunora_search_docs` bounds its `query`, and `lunora_list_docs` caps how many pages it serialises.
|
|
305
|
+
|
|
306
|
+
## Local development server
|
|
307
|
+
|
|
308
|
+
`createLocalMcpServer` / `connectLocalStdio` compose the docs tools, the deployment tools, and any extra tools a host supplies into one stdio server — this is what `lunora mcp serve` runs.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { connectLocalStdio } from "@lunora/mcp";
|
|
312
|
+
|
|
313
|
+
await connectLocalStdio({
|
|
314
|
+
// Consulted per tool call, so a dev server started later is picked up
|
|
315
|
+
// without reconnecting.
|
|
316
|
+
deployment: () => readMyDevServer(),
|
|
317
|
+
docs: { baseUrl: "https://lunora.sh" },
|
|
318
|
+
extraTools: myLocalTools,
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The deployment tools are advertised even when the resolver currently returns nothing — MCP clients cache the tool list, so a surface that appeared only when the dev server happened to be up would stay invisible for the rest of the session. Calling one with nothing running returns an actionable error instead.
|
|
323
|
+
|
|
324
|
+
The observability tools are the exception: their gate is snapshotted when the tool list is built, so a session started before `lunora dev` never advertises them (and the cached list keeps them absent afterwards). Restart the MCP server once the dev server is up.
|
|
325
|
+
|
|
108
326
|
> This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs)**.
|
|
109
327
|
|
|
110
328
|
## Related
|
|
@@ -112,6 +330,7 @@ await server.connect(myTransport);
|
|
|
112
330
|
- [`@lunora/client`](https://www.npmjs.com/package/@lunora/client) — the HTTP RPC client backing every tool.
|
|
113
331
|
- [`@lunora/cli`](https://www.npmjs.com/package/@lunora/cli) — deploy the app the server introspects and invokes.
|
|
114
332
|
- [`@lunora/server`](https://www.npmjs.com/package/@lunora/server) — defines the queries, mutations, and actions the tools call.
|
|
333
|
+
- [`@lunora/agent`](https://www.npmjs.com/package/@lunora/agent) — the durable agents the `agent_<name>` tools front over RPC.
|
|
115
334
|
|
|
116
335
|
## Supported Node.js Versions
|
|
117
336
|
|
package/dist/bin.mjs
CHANGED
|
@@ -1,38 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class BinError extends Error {
|
|
7
|
-
code;
|
|
8
|
-
constructor(message, code) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = "BinError";
|
|
11
|
-
this.code = code;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
const runBin = async (environment, dependencies = {}) => {
|
|
15
|
-
const connect = dependencies.connect ?? connectStdio;
|
|
16
|
-
const writeError = dependencies.writeError ?? ((message) => {
|
|
17
|
-
process.stderr.write(message);
|
|
18
|
-
});
|
|
19
|
-
const url = environment.LUNORA_URL;
|
|
20
|
-
if (url === void 0 || url.length === 0) {
|
|
21
|
-
writeError("lunora-mcp: LUNORA_URL environment variable is required\n");
|
|
22
|
-
throw new BinError("LUNORA_URL environment variable is required", 1);
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
await connect({ allowWrites: isEnvEnabled(environment.LUNORA_MCP_ALLOW_WRITES), token: environment.LUNORA_ADMIN_TOKEN, url });
|
|
26
|
-
} catch (error) {
|
|
27
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
-
writeError(`lunora-mcp: failed to start — ${message}
|
|
29
|
-
`);
|
|
30
|
-
throw new BinError(`failed to start — ${message}`, 1);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
await runBin(process.env);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
process.exit(error instanceof BinError ? error.code : 1);
|
|
38
|
-
}
|
|
2
|
+
import{parseAgentsEnv as N}from"./packem_shared/AGENT_RUN_INPUT_SCHEMA-hKbpa3Dg.mjs";import{connectStdio as l}from"./packem_shared/connectStdio-CQI9JGJC.mjs";const E=new Set(["1","on","true","yes"]),o=r=>r!==void 0&&E.has(r.trim().toLowerCase());class s extends Error{code;constructor(e,a){super(e),this.name="BinError",this.code=a}}const O=async(r,e={})=>{const a=e.connect??l,i=e.writeError??(t=>{process.stderr.write(t)}),n=r.LUNORA_URL;if(n===void 0||n.length===0)throw i(`lunora-mcp: LUNORA_URL environment variable is required
|
|
3
|
+
`),new s("LUNORA_URL environment variable is required",1);const c=r.LUNORA_ADMIN_TOKEN;if(c===void 0||c.length===0)throw i(`lunora-mcp: LUNORA_ADMIN_TOKEN environment variable is required (every tool reads admin-gated routes)
|
|
4
|
+
`),new s("LUNORA_ADMIN_TOKEN environment variable is required",1);const A=Number(r.LUNORA_MCP_AGENT_TIMEOUT_MS),_=Number.isFinite(A)&&A>0?A:void 0;try{await a({agents:N(r.LUNORA_MCP_AGENTS),allowAgents:o(r.LUNORA_MCP_ALLOW_AGENTS),allowDataReads:o(r.LUNORA_MCP_ALLOW_DATA_READS),allowObservability:o(r.LUNORA_MCP_ALLOW_OBSERVABILITY),allowWrites:o(r.LUNORA_MCP_ALLOW_WRITES),token:c,url:n,..._===void 0?{}:{agentMaxWaitMs:_}})}catch(t){const L=t instanceof Error?t.message:String(t);throw i(`lunora-mcp: failed to start — ${L}
|
|
5
|
+
`),new s(`failed to start — ${L}`,1)}};try{await O(process.env)}catch(r){process.exit(r instanceof s?r.code:1)}
|