@lunora/mcp 1.0.0-alpha.28 → 1.0.0-alpha.281
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 +120 -0
- package/README.md +250 -14
- package/dist/bin.mjs +4 -47
- package/dist/docs/index.d.mts +259 -0
- package/dist/docs/index.d.ts +259 -0
- package/dist/docs/index.mjs +1 -0
- package/dist/index.d.mts +77 -226
- package/dist/index.d.ts +77 -226
- package/dist/index.mjs +1 -5
- package/dist/node/index.d.mts +94 -0
- package/dist/node/index.d.ts +94 -0
- package/dist/node/index.mjs +1 -0
- package/dist/packem_shared/AGENT_RUN_INPUT_SCHEMA-BGbKIek4.mjs +1 -0
- package/dist/packem_shared/DEFAULT_DOCS_BASE_URL-CiHy_Dbz.mjs +4 -0
- package/dist/packem_shared/DEFAULT_MAX_REQUEST_BYTES-CbbpkHRK.mjs +1 -0
- package/dist/packem_shared/DEFAULT_SEARCH_LIMIT-BMQ6HQ-x.mjs +3 -0
- package/dist/packem_shared/DOCS_SERVER_NAME-ClS4cCGm.mjs +1 -0
- package/dist/packem_shared/DOCS_URI_SCHEME-DBlxtzS6.mjs +3 -0
- package/dist/packem_shared/LOCAL_SERVER_NAME-xQYvyGAB.mjs +1 -0
- package/dist/packem_shared/OBSERVABILITY_TOOL_DEFINITIONS-DQ7AUah0.mjs +1 -0
- package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-DI9mSjr0.mjs +1 -0
- package/dist/packem_shared/compose.d-DiPnHeQv.d.mts +154 -0
- package/dist/packem_shared/compose.d-DiPnHeQv.d.ts +154 -0
- package/dist/packem_shared/connectFunctionStdio-DghsMY25.mjs +1 -0
- package/dist/packem_shared/connectStdio-B2-baZGh.mjs +1 -0
- package/dist/packem_shared/createAuthedMcpFetchHandler-BT9V7dkd.mjs +1 -0
- package/dist/packem_shared/createFunctionMcpServer-CmoQM4PR.mjs +1 -0
- package/dist/packem_shared/createLunoraMcpServer-BUaOKwlx.mjs +1 -0
- package/dist/packem_shared/createMcpFetchHandler-CkenPLyK.mjs +1 -0
- package/dist/packem_shared/createPaidMcpServer-CdDLw3G4.mjs +1 -0
- package/dist/packem_shared/createToolServer-CHAjfuNx.mjs +1 -0
- package/dist/packem_shared/function-server.d-BoX-fd2B.d.mts +197 -0
- package/dist/packem_shared/function-server.d-CnHt2Sb1.d.ts +197 -0
- package/dist/packem_shared/package-Dhh3Sr4Z.mjs +1 -0
- package/dist/packem_shared/promise-memo-OqVPWoUX.mjs +1 -0
- package/dist/packem_shared/serve-stateless.d-DD5arqFx.d.mts +45 -0
- package/dist/packem_shared/serve-stateless.d-DD5arqFx.d.ts +45 -0
- package/dist/packem_shared/toDocsSearchHits-CBLmtWXt.mjs +1 -0
- package/dist/packem_shared/tool-call-Bs-QeKp6.mjs +1 -0
- package/dist/packem_shared/tool-result-CPDsNNuJ.mjs +1 -0
- package/dist/packem_shared/tools-BMou4kPK.mjs +2 -0
- package/dist/worker/index.d.mts +258 -0
- package/dist/worker/index.d.ts +258 -0
- package/dist/worker/index.mjs +1 -0
- package/package.json +27 -5
- package/dist/packem_shared/AGENT_RUN_INPUT_SCHEMA-DWobae3P.mjs +0 -168
- package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-CWb_wFd5.mjs +0 -204
- package/dist/packem_shared/connectStdio-bkJcaXYE.mjs +0 -76
- package/dist/packem_shared/createMcpFetchHandler-DfzBSLrm.mjs +0 -14
- package/dist/packem_shared/createPaidMcpServer-b-qp_aEF.mjs +0 -93
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { LunoraAdminClient } from '@lunora/client/admin';
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { LunoraClient } from '@lunora/client';
|
|
4
|
+
import { b as ToolInputSchema, T as ToolDefinition, d as ToolResult } from "./compose.d-DiPnHeQv.js";
|
|
5
|
+
/**
|
|
6
|
+
* Agent exposure for the MCP server: a durable `@lunora/agent` run fronted as an
|
|
7
|
+
* MCP tool an external agent can call. The capability boundary is the MCP-server
|
|
8
|
+
* process + its token, so WHICH agents are exposed is config on the server (like
|
|
9
|
+
* `allowWrites`), not on `defineAgent` — keeping `@lunora/agent` codegen
|
|
10
|
+
* byte-identical.
|
|
11
|
+
*/
|
|
12
|
+
interface McpAgentExposure {
|
|
13
|
+
/** What the agent does — shown to the calling model, which decides from it. */
|
|
14
|
+
description: string;
|
|
15
|
+
/** The agent's export name (its `ctx.agents.<name>` / `AGENT_<NAME>` binding). */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Override the model-facing tool name (default `agent_<name>`). */
|
|
18
|
+
toolName?: string;
|
|
19
|
+
}
|
|
20
|
+
/** The generic status/poll tool advertised alongside the per-agent tools. */
|
|
21
|
+
declare const AGENT_STATUS_TOOL_NAME = "lunora_agent_status";
|
|
22
|
+
/**
|
|
23
|
+
* The uniform input schema every agent tool advertises. Agents share ONE run
|
|
24
|
+
* input (`@lunora/agent` has no per-agent validator), so there is nothing to
|
|
25
|
+
* derive per agent — a single static schema is reused for every agent tool.
|
|
26
|
+
*/
|
|
27
|
+
declare const AGENT_RUN_INPUT_SCHEMA: ToolInputSchema;
|
|
28
|
+
/**
|
|
29
|
+
* Parse `LUNORA_MCP_AGENTS` — a `;`-separated list of `name:description` pairs,
|
|
30
|
+
* e.g. `"support:Handles support questions;billing:Billing help"`. The
|
|
31
|
+
* description may itself contain colons (only the FIRST colon splits). Blank
|
|
32
|
+
* entries and entries with an empty name/description are skipped.
|
|
33
|
+
*/
|
|
34
|
+
declare const parseAgentsEnv: (raw: string | undefined) => McpAgentExposure[];
|
|
35
|
+
/**
|
|
36
|
+
* The tools this module advertises. Fail-closed: only the boolean `true` opts
|
|
37
|
+
* in (an env-plumbed caller could pass a truthy string), and the tools appear
|
|
38
|
+
* ONLY when at least one agent is exposed — so an agent-free or non-opted-in
|
|
39
|
+
* server never lists them.
|
|
40
|
+
*/
|
|
41
|
+
declare const agentToolDefinitions: (exposures: ReadonlyArray<McpAgentExposure>, allowAgents: boolean) => ReadonlyArray<ToolDefinition>;
|
|
42
|
+
/** Options threaded into a single agent tool dispatch. */
|
|
43
|
+
interface CallAgentToolOptions {
|
|
44
|
+
/** Opt-in gate — must be exactly `true` or the call is refused fail-closed. */
|
|
45
|
+
allowAgents: boolean;
|
|
46
|
+
/** The exposures advertised by this server. */
|
|
47
|
+
exposures: ReadonlyArray<McpAgentExposure>;
|
|
48
|
+
/** Wall-clock budget a single call awaits before returning a pending result. */
|
|
49
|
+
maxWaitMs?: number;
|
|
50
|
+
/** Delay between thread-status polls. */
|
|
51
|
+
pollIntervalMs?: number;
|
|
52
|
+
/** Test seam replacing the between-poll wait; production uses a real timer. */
|
|
53
|
+
wait?: (ms: number) => Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Dispatch an agent tool call: start a durable run via `agents:agentRun`, then
|
|
57
|
+
* await-with-timeout — poll `agents:agentThread` until terminal (returning the
|
|
58
|
+
* final answer from `agents:agentMessages`) or, on budget exhaustion, return a
|
|
59
|
+
* NON-error pending payload the caller resumes with `lunora_agent_status`.
|
|
60
|
+
*
|
|
61
|
+
* Fail-closed: refused at dispatch unless `allowAgents === true`, mirroring the
|
|
62
|
+
* `allowWrites` guard — starting a run is a side effect and must not ride the
|
|
63
|
+
* read-only default.
|
|
64
|
+
*/
|
|
65
|
+
declare const callAgentTool: (client: LunoraClient, name: string, input: Record<string, unknown>, options: CallAgentToolOptions) => Promise<ToolResult>;
|
|
66
|
+
interface LunoraMcpServerOptions {
|
|
67
|
+
/** Wall-clock budget a single agent tool call awaits before returning a pending result. */
|
|
68
|
+
agentMaxWaitMs?: number;
|
|
69
|
+
/** Delay between agent thread-status polls. */
|
|
70
|
+
agentPollIntervalMs?: number;
|
|
71
|
+
/** The agents this server fronts as MCP tools (see `allowAgents`). */
|
|
72
|
+
agents?: ReadonlyArray<McpAgentExposure>;
|
|
73
|
+
/**
|
|
74
|
+
* Expose the per-agent tools (`agent_<name>` + the generic
|
|
75
|
+
* `lunora_agent_status`). Defaults to `false`, mirroring `allowWrites`:
|
|
76
|
+
* starting a durable agent run is a side effect, so the agent tools are
|
|
77
|
+
* omitted from the advertised list AND refused at dispatch unless explicitly
|
|
78
|
+
* opted in. Only takes effect together with a non-empty `agents` list.
|
|
79
|
+
*/
|
|
80
|
+
allowAgents?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Expose `lunora_find_related`, which returns raw table rows read through the
|
|
83
|
+
* admin writer, so RLS policies and column masks do not apply. Defaults to
|
|
84
|
+
* `false`, for the same reason as `allowObservability`. Only takes effect
|
|
85
|
+
* when a `token` resolved. On the OAuth-protected path the verified token must
|
|
86
|
+
* also carry the data-read scope (see `DATA_READ_SCOPE` in `./tools`).
|
|
87
|
+
*/
|
|
88
|
+
allowDataReads?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Expose the observability tools (`lunora_get_logs`, `lunora_get_issues`,
|
|
91
|
+
* `lunora_get_advisories`, `lunora_get_query_insights`,
|
|
92
|
+
* `lunora_get_migration_status`). Defaults to `false`, mirroring
|
|
93
|
+
* `allowWrites`: they are read-only, but every row they return — log lines,
|
|
94
|
+
* request metadata, grouped error messages — is production user data that
|
|
95
|
+
* lands in the model's context and therefore at its provider. Holding the
|
|
96
|
+
* admin bearer is not consent to ship that, so it is a separate opt-in;
|
|
97
|
+
* without it the tools are omitted from the advertised list AND refused at
|
|
98
|
+
* dispatch. Only takes effect when a `token` resolved. On the OAuth-protected
|
|
99
|
+
* path the verified token must also carry the observability scope (see
|
|
100
|
+
* `OBSERVABILITY_SCOPE` in `./tools`); this option stays the outer gate.
|
|
101
|
+
*/
|
|
102
|
+
allowObservability?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Expose the write tools (`lunora_run_mutation` / `lunora_run_action`).
|
|
105
|
+
* Defaults to `false`: the server is READ-ONLY unless explicitly opted in,
|
|
106
|
+
* so a prompt-injected or misaligned agent can't mutate the deployment with
|
|
107
|
+
* the configured token. When false the write tools are omitted from the
|
|
108
|
+
* advertised tool list AND refused at dispatch.
|
|
109
|
+
*/
|
|
110
|
+
allowWrites?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Pre-built client (test injection). When omitted a `LunoraAdminClient` is
|
|
113
|
+
* created from `url`/`token`/`fetch`.
|
|
114
|
+
*/
|
|
115
|
+
client?: LunoraAdminClient;
|
|
116
|
+
/** `fetch` implementation; defaults to the ambient global. */
|
|
117
|
+
fetch?: typeof fetch;
|
|
118
|
+
/**
|
|
119
|
+
* Bearer token sent on every RPC. **Required** alongside `url`, and it must
|
|
120
|
+
* be the deployment's **admin bearer**: the introspection/allowlist path
|
|
121
|
+
* every tool depends on (`lunora_list_functions`, `lunora_list_tables`, and
|
|
122
|
+
* the `assertRunnable` precheck that runs before every `run` tool) hits
|
|
123
|
+
* admin-gated `/_lunora/admin/*` routes, so no scoped/app token works
|
|
124
|
+
* today — it would 403 (`ADMIN_FORBIDDEN`) on the first tool call. The
|
|
125
|
+
* read-only guarantee is therefore NOT enforced by the token's scope; it is
|
|
126
|
+
* enforced in-process via `allowWrites: false` (the default), which omits
|
|
127
|
+
* the write tools from the advertised list and refuses them at dispatch.
|
|
128
|
+
*
|
|
129
|
+
* Because EVERY tool needs it, omitting it is a misconfiguration rather than
|
|
130
|
+
* a reduced-capability mode, and `createLunoraMcpServer` says so at
|
|
131
|
+
* construction instead of advertising a surface that 403s on first use. The
|
|
132
|
+
* one exception is the `client` injection seam (tests / a pre-authenticated
|
|
133
|
+
* client), where this server cannot know what the client can reach and so
|
|
134
|
+
* reads "unknown" fail-closed: the privileged observability tools stay
|
|
135
|
+
* unadvertised and are refused at dispatch.
|
|
136
|
+
*/
|
|
137
|
+
token?: string;
|
|
138
|
+
/** Base URL of the deployed Lunora Worker. Required unless `client` is given. */
|
|
139
|
+
url?: string;
|
|
140
|
+
/** Version reported in the `initialize` handshake. Defaults to the version bundled at build time. */
|
|
141
|
+
version?: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Build an MCP `Server` whose tools talk to a Lunora deployment. The server is
|
|
145
|
+
* transport-agnostic — call `.connect(transport)` yourself, or use
|
|
146
|
+
* `connectStdio` for the common stdio case.
|
|
147
|
+
*
|
|
148
|
+
* Tool calls are dispatched through `callTool`, which the deployment reaches
|
|
149
|
+
* over HTTP RPC. No WebSocket is opened (the tools never subscribe), so this is
|
|
150
|
+
* safe to run as a short-lived stdio process.
|
|
151
|
+
*/
|
|
152
|
+
declare const createLunoraMcpServer: (options: LunoraMcpServerOptions) => Server;
|
|
153
|
+
interface FunctionMcpServerOptions {
|
|
154
|
+
/**
|
|
155
|
+
* Advertise and allow mutation and action tools. Off by default. A closed write
|
|
156
|
+
* is not listed and is refused at dispatch.
|
|
157
|
+
*/
|
|
158
|
+
allowWrites?: boolean;
|
|
159
|
+
fetch?: typeof fetch;
|
|
160
|
+
/** The MCP server name clients show. Defaults to `lunora`. */
|
|
161
|
+
name?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Told when a rewritten document cannot be read or built. The server keeps the tools it
|
|
164
|
+
* last built, so the error is reported here rather than failing the request.
|
|
165
|
+
*/
|
|
166
|
+
onReloadError?: (error: unknown) => void;
|
|
167
|
+
/**
|
|
168
|
+
* The `openRpcSpec` export of `_generated/openrpc`, or a function returning it. A
|
|
169
|
+
* function is called on every `tools/list` and `tools/call`, so the tools follow a
|
|
170
|
+
* document that codegen rewrites while the server runs. Return the same object
|
|
171
|
+
* while the document is unchanged: a different object rebuilds the tools.
|
|
172
|
+
*/
|
|
173
|
+
openRpcSpec: unknown;
|
|
174
|
+
/** The CALLER's bearer. Writes need one: the confirmation digest is keyed by it. */
|
|
175
|
+
token?: string;
|
|
176
|
+
/**
|
|
177
|
+
* The Lunora deployment URL the app talks to. A function is resolved on every
|
|
178
|
+
* call, so a server started before `lunora dev` picks the URL up once it exists.
|
|
179
|
+
* `undefined` means no deployment is running yet, and the call says so.
|
|
180
|
+
*/
|
|
181
|
+
url: string | (() => string | undefined);
|
|
182
|
+
version?: string;
|
|
183
|
+
/**
|
|
184
|
+
* The WebSocket class streams open with. Defaults to the global `WebSocket`, which a
|
|
185
|
+
* runtime without one (older Node) lacks, so such a host passes its own here.
|
|
186
|
+
*/
|
|
187
|
+
WebSocket?: typeof WebSocket;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Build an MCP `Server` with one tool per function in `openRpcSpec`. Transport
|
|
191
|
+
* agnostic: call `.connect(transport)` yourself.
|
|
192
|
+
*
|
|
193
|
+
* Throws at construction when the document is malformed or two functions share a
|
|
194
|
+
* tool name, so a bad integration fails at startup rather than on the first call.
|
|
195
|
+
*/
|
|
196
|
+
declare const createFunctionMcpServer: (options: FunctionMcpServerOptions) => Server;
|
|
197
|
+
export { AGENT_RUN_INPUT_SCHEMA as A, CallAgentToolOptions as C, FunctionMcpServerOptions as F, LunoraMcpServerOptions as L, McpAgentExposure as M, AGENT_STATUS_TOOL_NAME as a, agentToolDefinitions as b, callAgentTool as c, createFunctionMcpServer as d, createLunoraMcpServer as e, parseAgentsEnv as p };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var a="1.0.0-alpha.280";const o={version:a};export{o as p};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const d=(e,t)=>{if(e.size<t)return;const o=e.keys().next().value;o!==void 0&&e.delete(o)},r=(e,t,o,s)=>{const n=e.get(t);if(n!==void 0)return n;s!==void 0&&d(e,s);const i=o().catch(c=>{throw e.get(t)===i&&e.delete(t),c});return e.set(t,i),i};export{d as e,r as m};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { HandleRequestOptions } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
3
|
+
/** A Web-Standard fetch handler: takes a `Request`, returns the MCP `Response`. */
|
|
4
|
+
type McpFetchHandler = (request: Request) => Promise<Response>;
|
|
5
|
+
/**
|
|
6
|
+
* Largest request body served. Every MCP call is a short JSON-RPC message — a
|
|
7
|
+
* few hundred bytes — so this is orders of magnitude of headroom while still
|
|
8
|
+
* bounding what a caller can push through the parser.
|
|
9
|
+
*/
|
|
10
|
+
declare const DEFAULT_MAX_REQUEST_BYTES: number;
|
|
11
|
+
/** What {@link serveStateless} accepts on top of the transport's own options. */
|
|
12
|
+
interface ServeStatelessOptions extends HandleRequestOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Largest accepted request body, in bytes — enforced while the body streams
|
|
15
|
+
* in, not after it is buffered. Defaults to
|
|
16
|
+
* {@link DEFAULT_MAX_REQUEST_BYTES}, which a value that is not a
|
|
17
|
+
* non-negative safe integer also falls back to (see `resolveLimit`).
|
|
18
|
+
*/
|
|
19
|
+
maxRequestBytes?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Drive one request through a fresh **stateless** Streamable-HTTP transport bound
|
|
23
|
+
* to `server`, then tear both down.
|
|
24
|
+
*
|
|
25
|
+
* `sessionIdGenerator: undefined` → stateless (no cross-request session store);
|
|
26
|
+
* `enableJsonResponse: true` → the whole JSON-RPC response is buffered into the
|
|
27
|
+
* returned `Response`, so closing the transport/server afterwards can't truncate
|
|
28
|
+
* an in-flight body. Cleanup is fire-and-forget with swallowed rejections (a
|
|
29
|
+
* `.catch()`-terminated chain) so it never delays or masks the resolved response.
|
|
30
|
+
*
|
|
31
|
+
* The request is screened first ({@link screenRequest}); an oversized body or a
|
|
32
|
+
* JSON-RPC batch is refused here and no server is ever constructed for it.
|
|
33
|
+
*
|
|
34
|
+
* `options.parsedBody` lets a caller hand over a body it already read (e.g. the
|
|
35
|
+
* paid-tool gate, which peeks the JSON-RPC message to price the call) so the
|
|
36
|
+
* transport doesn't re-read a consumed stream. It is trusted as already
|
|
37
|
+
* screened, so read it with {@link readScreenedBody} — anything else hands this
|
|
38
|
+
* surface an unbounded body.
|
|
39
|
+
*
|
|
40
|
+
* Teardown runs in a `finally`: a rejection from `connect` or `handleRequest`
|
|
41
|
+
* would otherwise skip it and leak a server + transport per failed request,
|
|
42
|
+
* which on a public endpoint is exactly the request an attacker can repeat.
|
|
43
|
+
*/
|
|
44
|
+
declare const serveStateless: (server: Server, request: Request, options?: ServeStatelessOptions) => Promise<Response>;
|
|
45
|
+
export { DEFAULT_MAX_REQUEST_BYTES as D, McpFetchHandler as M, ServeStatelessOptions as S, serveStateless as s };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { HandleRequestOptions } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
3
|
+
/** A Web-Standard fetch handler: takes a `Request`, returns the MCP `Response`. */
|
|
4
|
+
type McpFetchHandler = (request: Request) => Promise<Response>;
|
|
5
|
+
/**
|
|
6
|
+
* Largest request body served. Every MCP call is a short JSON-RPC message — a
|
|
7
|
+
* few hundred bytes — so this is orders of magnitude of headroom while still
|
|
8
|
+
* bounding what a caller can push through the parser.
|
|
9
|
+
*/
|
|
10
|
+
declare const DEFAULT_MAX_REQUEST_BYTES: number;
|
|
11
|
+
/** What {@link serveStateless} accepts on top of the transport's own options. */
|
|
12
|
+
interface ServeStatelessOptions extends HandleRequestOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Largest accepted request body, in bytes — enforced while the body streams
|
|
15
|
+
* in, not after it is buffered. Defaults to
|
|
16
|
+
* {@link DEFAULT_MAX_REQUEST_BYTES}, which a value that is not a
|
|
17
|
+
* non-negative safe integer also falls back to (see `resolveLimit`).
|
|
18
|
+
*/
|
|
19
|
+
maxRequestBytes?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Drive one request through a fresh **stateless** Streamable-HTTP transport bound
|
|
23
|
+
* to `server`, then tear both down.
|
|
24
|
+
*
|
|
25
|
+
* `sessionIdGenerator: undefined` → stateless (no cross-request session store);
|
|
26
|
+
* `enableJsonResponse: true` → the whole JSON-RPC response is buffered into the
|
|
27
|
+
* returned `Response`, so closing the transport/server afterwards can't truncate
|
|
28
|
+
* an in-flight body. Cleanup is fire-and-forget with swallowed rejections (a
|
|
29
|
+
* `.catch()`-terminated chain) so it never delays or masks the resolved response.
|
|
30
|
+
*
|
|
31
|
+
* The request is screened first ({@link screenRequest}); an oversized body or a
|
|
32
|
+
* JSON-RPC batch is refused here and no server is ever constructed for it.
|
|
33
|
+
*
|
|
34
|
+
* `options.parsedBody` lets a caller hand over a body it already read (e.g. the
|
|
35
|
+
* paid-tool gate, which peeks the JSON-RPC message to price the call) so the
|
|
36
|
+
* transport doesn't re-read a consumed stream. It is trusted as already
|
|
37
|
+
* screened, so read it with {@link readScreenedBody} — anything else hands this
|
|
38
|
+
* surface an unbounded body.
|
|
39
|
+
*
|
|
40
|
+
* Teardown runs in a `finally`: a rejection from `connect` or `handleRequest`
|
|
41
|
+
* would otherwise skip it and leak a server + transport per failed request,
|
|
42
|
+
* which on a public endpoint is exactly the request an attacker can repeat.
|
|
43
|
+
*/
|
|
44
|
+
declare const serveStateless: (server: Server, request: Request, options?: ServeStatelessOptions) => Promise<Response>;
|
|
45
|
+
export { DEFAULT_MAX_REQUEST_BYTES as D, McpFetchHandler as M, ServeStatelessOptions as S, serveStateless as s };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=t=>typeof t=="object"&&t!==null,i=t=>t.split("<mark>").join("").split("</mark>").join(""),l=t=>Array.isArray(t)?t.filter(o=>typeof o=="string"):[],p=t=>{if(!e(t)||typeof t.url!="string"||t.url.length===0)return;const o=t,r=typeof o.content=="string"?i(o.content):"",s=l(o.breadcrumbs),n=o.type==="page",c=n?r:s.at(-1)??r;return{...n||r.length===0?{}:{excerpt:r},...s.length>0?{section:s.join(" › ")}:{},title:c.length>0?c:t.url,url:t.url}},u=t=>{const o=[];for(const r of t){const s=p(r);s!==void 0&&o.push(s)}return o};export{u as toDocsSearchHits};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const c="tools/call",r=o=>{if(typeof o!="object"||o===null)return;const{method:l,params:t}=o;if(l!==c||typeof t!="object"||t===null)return;const{name:n}=t;return typeof n=="string"?n:void 0};export{r as c};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as E,isInternalCode as d,getCatalogEntry as c}from"@lunora/errors";const l=e=>{let t="";for(let n=0;n<e.length;n+=32768)t+=String.fromCharCode(...e.subarray(n,n+32768));return btoa(t)},p=e=>{const t=atob(e),r=new Uint8Array(t.length);for(let n=0;n<t.length;n+=1)r[n]=t.codePointAt(n)??0;return r},s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",O=e=>{let t="",r=0;const n=e.length-2;for(;r<n;r+=3){const o=e[r]<<16|e[r+1]<<8|e[r+2];t+=s.charAt(o>>18&63)+s.charAt(o>>12&63)+s.charAt(o>>6&63)+s.charAt(o&63)}const f=e.length-r;if(f===1){const o=e[r]<<16;t+=s.charAt(o>>18&63)+s.charAt(o>>12&63)}else if(f===2){const o=e[r]<<16|e[r+1]<<8;t+=s.charAt(o>>18&63)+s.charAt(o>>12&63)+s.charAt(o>>6&63)}return t},_=e=>{const t=e.replaceAll("-","+").replaceAll("_","/"),r=t+"=".repeat((4-t.length%4)%4);return p(r)},u=(e,t)=>{if(typeof t=="bigint")return t.toString();if(t instanceof Map)return[...t.keys()].every(r=>typeof r=="string")?Object.fromEntries(t):[...t];if(t instanceof Set)return[...t];if(t instanceof ArrayBuffer)return l(new Uint8Array(t));if(ArrayBuffer.isView(t)){const r=t;return l(new Uint8Array(r.buffer,r.byteOffset,r.byteLength))}return t},A=e=>JSON.parse(JSON.stringify(e,u)),R=e=>({content:[{text:e===void 0?"null":JSON.stringify(e,u,2),type:"text"}]}),x=e=>({...R(e),structuredContent:A(e)}),i=(e,t)=>({content:[{text:`${e}: ${t}`,type:"text"}],isError:!0,structuredContent:{code:e,message:t}}),a="the tool call failed unexpectedly",g=Symbol.for("lunora.serverVerdict"),w=e=>{if(e instanceof E){const{code:t}=e;return i(t,d(t)?c(t)?.title??a:e.message)}if(e instanceof Error){const{code:t}=e;if(typeof t=="string"&&c(t)!==void 0&&e[g]===!0)return i(t,d(t)?c(t)?.title??a:e.message)}return i("INTERNAL",a)},y={description:"Set on an error result: the machine-readable error code.",type:"string"},S={description:"Set on an error result: a message that is safe to show.",type:"string"},h={properties:{code:y,message:S},required:["code","message"],type:"object"},C=e=>{if(e.anyOf?.includes(h)===!0)return e;const{$defs:t,definitions:r,...n}=e;return{...t===void 0?{}:{$defs:t},...r===void 0?{}:{definitions:r},anyOf:[n,h],type:"object"}};export{w as a,R as b,_ as c,i as e,p as f,u as j,x as o,O as t,C as w};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{getCatalogEntry as q,resolveHint as Q,findIssueSolution as Y,isInternalCode as S,LunoraError as s}from"@lunora/errors";import{w as G,e as u,o as J,b as l,t as V,c as X,a as z}from"./tool-result-CPDsNNuJ.mjs";import{OBSERVABILITY_TOOL_NAMES as Z,OBSERVABILITY_TOOL_DEFINITIONS as ee,callObservabilityTool as te}from"./OBSERVABILITY_TOOL_DEFINITIONS-DQ7AUah0.mjs";import{ADMIN_FUNCTIONS as ne}from"@lunora/shard-engine";import{stableStringify as oe}from"@lunora/wire";import{m as ie}from"./promise-memo-OqVPWoUX.mjs";const re={destructiveHint:!1,idempotentHint:!0,openWorldHint:!1,readOnlyHint:!0},_="https://lunora.sh/docs/errors",se={properties:{code:{description:'The error code to explain, e.g. "CONFLICT" or "RLS_REQUIRED". Case-sensitive; codes are SCREAMING_SNAKE_CASE.',type:"string"},message:{description:"The raw error message, when there is no code (codegen and Cloudflare platform errors arrive as plain text). Matched against the solution tables.",type:"string"}},type:"object"},ae={properties:{code:{description:"The code that was looked up, when one was given.",type:"string"},docsUrl:{description:"Link to this code's section of the published error reference.",type:"string"},found:{description:"False when neither the code nor the message matched anything — the other fields are then absent.",type:"boolean"},hint:{description:"Actionable Markdown remediation, when the catalog or a message rule has one.",type:"string"},internal:{description:"True when the code is redacted on the wire: its real message is logged server-side and never reaches a client, so it is absent from the published reference.",type:"boolean"},solution:{description:"A message-matched solution: { id, header, body }, from Lunora's rules or the Cloudflare platform table.",type:"object"},status:{description:"HTTP/RPC status this code maps to on the wire.",type:"number"},title:{description:"Short human-readable summary of the code.",type:"string"}},required:["found"],type:"object"},I=[{annotations:{...re,title:"Explain a Lunora error"},description:"Explain a Lunora error before guessing at a fix: pass its code (CONFLICT, RLS_REQUIRED, …) or the raw error message, and get the catalog's transport status, title and actionable hint, plus any matched codegen/Cloudflare solution and a link to the reference. Static catalog data — no deployment and no credentials needed.",inputSchema:se,name:"lunora_explain_error",outputSchema:G(ae)}],ce=new Set(I.map(e=>e.name)),E=e=>typeof e=="string"&&e.length>0?e:void 0,de=e=>typeof e=="string"?e:e.join(`
|
|
2
|
+
`),le=(e,t)=>e===void 0||t===void 0||S(e)?_:`${_}#${e.toLowerCase()}`,ue=(e,t)=>{const n=e===void 0?void 0:q(e),o=e===void 0?void 0:Q({code:e}),i=t===void 0?void 0:Y(t),r={docsUrl:le(e,n),found:n!==void 0||i!==void 0};return e!==void 0&&(r.code=e,S(e)&&(r.internal=!0)),n!==void 0&&(r.status=n.status,r.title=n.title),o!==void 0&&(r.hint=de(o)),i!==void 0&&(r.solution={body:i.body,header:i.header,id:i.id}),r},he=(e,t)=>{if(e!=="lunora_explain_error")return u("NOT_FOUND",`unknown error tool: ${e}`);const n=E(t.code),o=E(t.message);return n===void 0&&o===void 0?u("BAD_REQUEST",'lunora_explain_error needs a "code" or a "message" (or both); received neither.'):J(ue(n,o))},pe={destructiveHint:!1,idempotentHint:!0,openWorldHint:!0,readOnlyHint:!0},fe={properties:{cursor:{description:"Opaque cursor from a previous call's continueCursor, to fetch the next page.",type:"string"},depth:{description:"How many hops to follow. 1 (default) is the direct neighbourhood; maximum 4.",type:"number"},direction:{description:'Which way to follow foreign keys: "out" follows the ids this row holds (ticket → customer), "in" the rows that point at it (customer → tickets), "both" (default) does both.',type:"string"},edges:{description:'Restrict the walk to these edge names, each "<table>.<column>" (e.g. "tickets.customerId"). Omit to follow every declared foreign key.',items:{type:"string"},type:"array"},id:{description:"Document id of the row to start from.",type:"string"},limit:{description:"Maximum related rows to return (default 50, maximum 200).",type:"number"},shardKey:{description:"Shard to read from on a .shardBy()-partitioned deployment. Omit for the default (unsharded) shard.",type:"string"},table:{description:'Table the start row lives in, e.g. "customers".',type:"string"}},required:["table","id"],type:"object"},D=[{annotations:{...pe,title:"Find rows related to a row"},description:"Follow the schema's foreign keys out of one row and return the rows it is connected to, each with its hop distance, the edge names walked to reach it, and a depth-decaying score. Use this to answer 'what is connected to this' — a customer's tickets, those tickets' messages — which keyword and semantic search cannot, because the connection lives in a foreign key rather than in the text. Read-only, but it reads through the deployment's ADMIN writer: RLS policies and column masks do not apply to what it returns.",inputSchema:fe,name:"lunora_find_related"}],me=new Set(D.map(e=>e.name)),ge=e=>{const{cursor:t,depth:n,direction:o,edges:i,limit:r}=e;if(t!=null&&typeof t!="string")throw new s("BAD_REQUEST","findRelated: `cursor` must be a string");if(n!==void 0&&typeof n!="number")throw new s("BAD_REQUEST","findRelated: `depth` must be a number");if(o!==void 0&&o!=="both"&&o!=="in"&&o!=="out")throw new s("BAD_REQUEST",'findRelated: `direction` must be one of "in", "out" or "both"');if(r!==void 0&&typeof r!="number")throw new s("BAD_REQUEST","findRelated: `limit` must be a number");if(i!==void 0&&(!Array.isArray(i)||!i.every(a=>typeof a=="string")))throw new s("BAD_REQUEST","findRelated: `edges` must be an array of edge-name strings")},ye=e=>{const{cursor:t,depth:n,direction:o,edges:i,id:r,limit:a,shardKey:d,table:c}=e;if(typeof c!="string"||c.trim()==="")throw new s("BAD_REQUEST","findRelated: `table` is required");if(typeof r!="string"||r.trim()==="")throw new s("BAD_REQUEST","findRelated: `id` is required");if(ge(e),d!==void 0&&(typeof d!="string"||d.trim()===""))throw new s("BAD_REQUEST","findRelated: `shardKey` must be a non-empty string when given");return{args:{id:r,table:c,...typeof t=="string"?{cursor:t}:{},...n===void 0?{}:{depth:n},...o===void 0?{}:{direction:o},...i===void 0?{}:{edges:i},...a===void 0?{}:{limit:a}},shardKey:d}},we=async(e,t,n)=>{if(t!=="lunora_find_related")throw new s("INTERNAL",`unknown row-read tool: ${t}`);const{args:o,shardKey:i}=ye(n),r={__lunoraRef:ne.findRelated};return l(await e.query(r,o,{...i===void 0?{}:{shardKey:i}}))},_e=new TextEncoder,Ee=64,Ae=new Map,C=async e=>ie(Ae,e,async()=>crypto.subtle.importKey("raw",_e.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]),Ee),L=new TextEncoder,Oe=Array.from({length:32},(e,t)=>t);new RegExp(`[${Oe.map(e=>String.fromCodePoint(e)).join("")}]`,"u");const Te=async(e,t)=>{const n=await C(e),o=await crypto.subtle.sign("HMAC",n,L.encode(t));return V(new Uint8Array(o))},be=async(e,t,n)=>{const o=await C(e);return crypto.subtle.verify("HMAC",o,n,L.encode(t))},Re="lunora-mcp-write-confirmation-v1",k=600*1e3,U=".",H=e=>{const t=e.getAuthToken();if(t===null||t==="")throw new s("UNAUTHORIZED","write confirmation needs a bearer token for this deployment: it is the secret half of the digest key, and without it a digest would be forgeable by anyone.");return`${Re}\0${e.url}\0${t}`},P=(e,t)=>`${oe(e)}\0${String(t)}`,ve=async(e,t)=>{const n=Date.now()+k,o=await Te(H(e),P(t,n));return{actionDigest:`${String(n)}${U}${o}`,expiresAt:n}},Ne=async(e,t,n)=>{const o=n.indexOf(U);if(o<=0)return"mismatch";const i=Number(n.slice(0,o));if(!Number.isSafeInteger(i)||i<=0)return"mismatch";if(Date.now()>=i)return"expired";let r;try{r=X(n.slice(o+1))}catch{return"mismatch"}return await be(H(e),P(t,i),r)?"valid":"mismatch"},Se="Optional caller-chosen token folded into the digest. GUARANTEE: a retry after a client timeout can resubmit the confirmation you already hold, for as long as that digest is inside its window, instead of asking for a second review — and a deliberately-repeated identical write sent under a NEW key gets its own digest, so it cannot ride the first review. NOT GUARANTEED: this does not deduplicate the write. The server keeps no state between requests and never forwards the key to the function, so a resubmitted confirmed call executes again. Make the function itself idempotent if the write must happen at most once.",Ie={actionDigest:{description:"The digest returned by the preceding action_required result. Required together with confirmed, and only valid until the expiresAt that result reported — past that, propose again.",type:"string"},confirmed:{description:"Set to true ONLY on the second call, after a human has reviewed the proposed action, and only together with the actionDigest that proposal returned. Omit it on the first call. The server cannot tell whether a human actually reviewed it — that is this client's responsibility, and the operator enabled writes on the understanding that this client asks.",type:"boolean"},idempotencyKey:{description:Se,type:"string"}},De=e=>{const t=typeof e.actionDigest=="string"&&e.actionDigest.length>0?e.actionDigest:void 0,n=typeof e.idempotencyKey=="string"&&e.idempotencyKey.length>0?e.idempotencyKey:void 0;return{actionDigest:t,confirmed:e.confirmed===!0,idempotencyKey:n}},M="«sensitive»",Ce=(e,t)=>{if(t.length===0)return e;const n=Object.fromEntries(Object.entries(e.args).map(([o,i])=>[o,t.includes(o)?M:i]));return{...e,args:n}},Le=["functionPath","args","shardKey","idempotencyKey"],ke=e=>e.length>1?`${e.slice(0,-1).join(", ")} and ${e.at(-1)??""}`:e.join(""),Ue=(e,t,n,o)=>l({actionDigest:t.actionDigest,expiresAt:new Date(t.expiresAt).toISOString(),nextStep:`Show proposedAction to a human. To execute, call ${e.tool} again with the IDENTICAL ${ke(o)}, plus confirmed: true and this actionDigest, before expiresAt. Resend the REAL values of any argument shown as "${M}": that placeholder is display only and will not verify. Nothing has been written or called yet.`,proposedAction:Ce(e,n),status:"action_required"}),He=async(e,t,n,o=[],i=Le)=>{if(!n.confirmed||n.actionDigest===void 0)return Ue(t,await ve(e,t),o,i);const r=await Ne(e,t,n.actionDigest);if(r!=="valid")return r==="expired"?u("CONFIRMATION_EXPIRED",`confirmation rejected: this actionDigest has expired. A confirmation is good for ${String(k/6e4)} minutes from the proposal that issued it, so an approval cannot be replayed in a later session. Nothing was written. Call ${t.tool} again WITHOUT confirmed to get a fresh actionDigest, have it reviewed, then resubmit those same arguments with it.`):u("CONFIRMATION_MISMATCH",`confirmation rejected: the actionDigest does not match this call. A digest is bound to the exact tool, function path, arguments, shard key and idempotency key it was issued for, so any edit to the proposal invalidates it — and a digest from another deployment never matches. Nothing was written. Call ${t.tool} again WITHOUT confirmed to get a fresh actionDigest for the current arguments, have it reviewed, then resubmit those same arguments with it.`)},x={properties:{args:{description:"Arguments object passed to the function",type:"object"},functionPath:{description:'Function reference, e.g. "messages:send"',type:"string"},shardKey:{description:"Optional shard key when the function is .shardBy()-partitioned",type:"string"}},required:["functionPath"],type:"object"},A={properties:{},type:"object"},Pe={properties:{functionPath:{description:'Function reference, e.g. "messages:send"',type:"string"}},required:["functionPath"],type:"object"},p={destructiveHint:!1,idempotentHint:!0,openWorldHint:!0,readOnlyHint:!0},$=[{annotations:{...p,title:"List deployment functions"},description:"List the deployment's public functions (queries, mutations, actions) with their kinds.",inputSchema:A,name:"lunora_list_functions"},{annotations:{...p,title:"List global tables"},description:"List the deployment's .global() tables with their row counts. Names and row counts only — no column shapes.",inputSchema:A,name:"lunora_list_tables"},{annotations:{...p,title:"Describe a function's arguments"},description:"Return a function's argument descriptors (name, validator kind, whether it is optional) and its kind, so a caller can construct a valid arguments object. Call lunora_list_functions first to discover available function paths.",inputSchema:Pe,name:"lunora_get_function_schema"},{annotations:{...p,title:"Run a query"},description:"Run a query and return its result. Read-only.",inputSchema:x,name:"lunora_run_query"}],Me=new Set($.map(e=>e.name)),O={properties:{...x.properties,...Ie},required:["functionPath"],type:"object"},T='TWO-STEP: the first call does NOT execute. It returns status "action_required" with the proposed action, an actionDigest and the expiresAt it is good until; show that to a human, then call again before expiresAt with the IDENTICAL functionPath/args/shardKey plus confirmed: true and that actionDigest. Any change to the target or the arguments produces a different digest and needs a fresh review, and an expired digest is refused rather than re-proposed.',F=[{annotations:{destructiveHint:!0,idempotentHint:!1,openWorldHint:!0,readOnlyHint:!1,title:"Run a mutation (writes data)"},description:`Run a mutation. Writes data. ${T}`,inputSchema:O,name:"lunora_run_mutation"},{annotations:{destructiveHint:!0,idempotentHint:!1,openWorldHint:!0,readOnlyHint:!1,title:"Run an action (may call external services)"},description:`Run an action. May call external services — send mail, charge a card, hit a third-party API — so the confirmation is the only thing between a proposal and a real-world side effect. ${T}`,inputSchema:O,name:"lunora_run_action"}],xe=new Set(F.map(e=>e.name)),B=e=>{const{functionPath:t}=e;if(typeof t!="string"||t.length===0)throw new s("BAD_REQUEST",'"functionPath" is required and must be a non-empty string');return t},b=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),R=e=>Array.isArray(e)?"an array":`a ${typeof e}`,$e=e=>{if(e==null)return{};if(typeof e=="string"){let t;try{t=JSON.parse(e)}catch{throw new s("BAD_REQUEST",'"args" must be a JSON object; received a string that is not valid JSON')}if(!b(t))throw new s("BAD_REQUEST",`"args" must be a JSON object; the provided string decoded to ${R(t)}`);return t}if(!b(e))throw new s("BAD_REQUEST",`"args" must be a JSON object, got ${R(e)}`);return e},K=e=>{const t=B(e),n=$e(e.args),o=typeof e.shardKey=="string"&&e.shardKey.length>0?e.shardKey:void 0;return{args:n,functionPath:t,shardKey:o}},W=e=>({__lunoraRef:e}),Fe=3e4,h=new WeakMap,m=e=>{const t=Date.now(),n=h.get(e);if(n!==void 0&&n.expiresAt>t)return n.promise;const o=e.listFunctions().catch(i=>{throw h.get(e)?.promise===o&&h.delete(e),i});return h.set(e,{expiresAt:t+Fe,promise:o}),o},j=async(e,t,n)=>{const i=(await m(e)).find(r=>r.path===t);if(i===void 0)throw new s("NOT_FOUND",`function not found or not public: ${t}`);if(i.kind!==n)throw new s("BAD_REQUEST",`function ${t} is a ${i.kind}, not a ${n}`);return i},Be=async(e,t,n,o,i,r)=>{const a=De(i);return He(e,{...o,idempotencyKey:a.idempotencyKey,kind:n,tool:t},a,r)},Ke=e=>(e.args??[]).filter(t=>t.sensitive===!0).map(t=>t.name),We=async(e,t,n)=>{switch(t){case"lunora_get_function_schema":{const o=B(n),r=(await m(e)).find(a=>a.path===o);return r===void 0?u("NOT_FOUND",`function not found: ${o}`):l({args:r.args??[],kind:r.kind,path:r.path})}case"lunora_list_functions":return l(await m(e));case"lunora_list_tables":return l(await e.listGlobalTables());case"lunora_run_query":{const{args:o,functionPath:i,shardKey:r}=K(n);return await j(e,i,"query"),l(await e.query(W(i),o,{shardKey:r}))}default:throw new s("INTERNAL",`unknown read-only tool: ${t}`)}},je=async(e,t,n)=>{if(t!=="lunora_run_action"&&t!=="lunora_run_mutation")throw new s("INTERNAL",`unknown write tool: ${t}`);const o=t==="lunora_run_action"?"action":"mutation",{args:i,functionPath:r,shardKey:a}=K(n);h.delete(e);const d=await j(e,r,o),c=Ke(d),y=await Be(e,t,o,{args:i,functionPath:r,shardKey:a},n,c);if(y!==void 0)return y;const w=c.length===0?W(r):{__lunoraRef:r,sensitive:!0};return l(o==="action"?await e.action(w,i,{shardKey:a}):await e.mutation(w,i,{onlineOnly:!0,shardKey:a}))},v="lunora:data-read",N="lunora:observability",qe=e=>e===!0,f=[{call:We,definitions:$,names:Me},{call:(e,t,n)=>he(t,n),definitions:I,names:ce},{call:we,definitions:D,gate:{flag:"allowDataReads",refuse:e=>`tool "${e}" is disabled: it returns raw table rows read through the deployment's ADMIN writer, so RLS policies and column masks do not apply. Enable it with the LUNORA_MCP_ALLOW_DATA_READS env var or the allowDataReads option; on the authed path the token also needs the ${v} scope.`,scope:v},names:me},{call:te,definitions:ee,gate:{flag:"allowObservability",refuse:e=>`tool "${e}" is disabled: it reads the deployment's logs, request metadata and grouped errors — user data that would land at the model provider. Enable it with the LUNORA_MCP_ALLOW_OBSERVABILITY env var or the allowObservability option; on the authed path the token also needs the ${N} scope.`,scope:N},names:Z},{call:je,definitions:F,gate:{flag:"allowWrites",refuse:e=>`tool "${e}" is disabled: this MCP server is read-only. Enable writes with the LUNORA_MCP_ALLOW_WRITES env var.`},names:xe}],g=(e,t)=>e.gate!==void 0&&!qe(t[e.gate.flag])?e.gate:void 0,ze=(e,t=!1,n=!1)=>{const o={allowDataReads:n,allowObservability:t,allowWrites:e};return f.filter(i=>g(i,o)===void 0).flatMap(i=>i.definitions)},Ze=async(e,t,n,o=!1,i=!1,r=!1)=>{const a=f.find(c=>c.names.has(t));if(a===void 0)return u("NOT_FOUND",`unknown tool: ${t}`);const d=g(a,{allowDataReads:r,allowObservability:i,allowWrites:o});if(d!==void 0)return u("FORBIDDEN",d.refuse(t));try{return await a.call(e,t,n)}catch(c){return z(c)}},et=(e,t)=>{const n=f.find(o=>o.names.has(e));if(!(n?.gate?.scope===void 0||g(n,t)!==void 0))return n.gate.scope},tt=f.flatMap(e=>e.gate?.scope===void 0?[]:[{flag:e.gate.flag,scope:e.gate.scope}]);export{v as D,I as E,N as O,$ as R,tt as S,F as W,he as a,ce as b,Ze as c,xe as d,Ie as e,De as f,He as g,D as h,$e as r,et as s,ze as t};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { L as LunoraMcpServerOptions } from "../packem_shared/function-server.d-BoX-fd2B.mjs";
|
|
2
|
+
export { A as AGENT_RUN_INPUT_SCHEMA, a as AGENT_STATUS_TOOL_NAME,
|
|
3
|
+
/** Worker-safe entry: fetch handlers and tool composition, with no stdio transport or `node:*` imports. */
|
|
4
|
+
type C as CallAgentToolOptions, type F as FunctionMcpServerOptions,
|
|
5
|
+
/** Worker-safe entry: fetch handlers and tool composition, with no stdio transport or `node:*` imports. */
|
|
6
|
+
type M as McpAgentExposure, b as agentToolDefinitions, c as callAgentTool, d as createFunctionMcpServer, p as parseAgentsEnv } from "../packem_shared/function-server.d-BoX-fd2B.mjs";
|
|
7
|
+
import { M as McpFetchHandler } from "../packem_shared/serve-stateless.d-DD5arqFx.mjs";
|
|
8
|
+
export { D as DEFAULT_MAX_REQUEST_BYTES, type S as ServeStatelessOptions, s as serveStateless } from "../packem_shared/serve-stateless.d-DD5arqFx.mjs";
|
|
9
|
+
import { T as ToolDefinition, d as ToolResult, h as ToolAnnotations, b as ToolInputSchema } from "../packem_shared/compose.d-DiPnHeQv.mjs";
|
|
10
|
+
export { type M as McpServerInfo, type a as McpTool, e as createToolServer } from "../packem_shared/compose.d-DiPnHeQv.mjs";
|
|
11
|
+
import { LunoraAdminClient } from '@lunora/client/admin';
|
|
12
|
+
import '@modelcontextprotocol/sdk/server/index.js';
|
|
13
|
+
import '@lunora/client';
|
|
14
|
+
import '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
15
|
+
/**
|
|
16
|
+
* The observability tools. Descriptions say WHEN to call the tool, not just
|
|
17
|
+
* what it returns — an agent picking between five similar reads needs the
|
|
18
|
+
* trigger, and it pays for these strings on every turn.
|
|
19
|
+
*/
|
|
20
|
+
declare const OBSERVABILITY_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
|
|
21
|
+
/** The read-only tool surface: introspection + query. Always exposed. */
|
|
22
|
+
declare const READ_ONLY_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
|
|
23
|
+
/** The write tool surface (mutations + actions). Exposed ONLY when writes are enabled. */
|
|
24
|
+
declare const WRITE_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
|
|
25
|
+
/**
|
|
26
|
+
* The tools this server advertises: every family whose gate is open, in table
|
|
27
|
+
* order. A gated family is OMITTED rather than refused — an AI agent can't
|
|
28
|
+
* invoke what it can't see — and {@link callTool} re-checks the same gate at
|
|
29
|
+
* dispatch, so the guarantee does not depend on a client honouring this list.
|
|
30
|
+
*/
|
|
31
|
+
declare const toolDefinitions: (allowWrites: boolean, allowObservability?: boolean, allowDataReads?: boolean) => ReadonlyArray<ToolDefinition>;
|
|
32
|
+
/**
|
|
33
|
+
* Dispatch a tool call against `client`: find the family that owns `name`, check
|
|
34
|
+
* its gate, hand off. Unknown tools and thrown errors are returned as `isError`
|
|
35
|
+
* results (rather than rejections) so the calling model sees the failure as tool
|
|
36
|
+
* output, per the MCP convention.
|
|
37
|
+
*
|
|
38
|
+
* Each gate is enforced HERE as well as in {@link toolDefinitions}, so a call to
|
|
39
|
+
* a gated tool is refused even if the client somehow names one it was never
|
|
40
|
+
* shown.
|
|
41
|
+
*/
|
|
42
|
+
declare const callTool: (client: LunoraAdminClient, name: string, input: Record<string, unknown>, allowWrites?: boolean, allowObservability?: boolean, allowDataReads?: boolean) => Promise<ToolResult>;
|
|
43
|
+
/**
|
|
44
|
+
* The verified access-token payload better-auth hands a protected handler.
|
|
45
|
+
*
|
|
46
|
+
* A JWT payload is an open bag of claims, so this is deliberately an index
|
|
47
|
+
* signature with the two entries this module reads named. It is structurally
|
|
48
|
+
* satisfied by `jose`'s `JWTPayload`, which is what better-auth passes.
|
|
49
|
+
*/
|
|
50
|
+
interface McpAccessTokenClaims {
|
|
51
|
+
readonly [claim: string]: unknown;
|
|
52
|
+
/** Space-delimited granted scopes (RFC 6749 §3.3). */
|
|
53
|
+
readonly scope?: unknown;
|
|
54
|
+
/** Subject — the user the token was issued for. */
|
|
55
|
+
readonly sub?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The MCP auth gate: wraps a claims-aware handler into a plain fetch handler.
|
|
59
|
+
*
|
|
60
|
+
* Declared structurally here rather than imported from `@better-auth/mcp`,
|
|
61
|
+
* following the same rule `./paid` follows for `@lunora/x402`: a type import
|
|
62
|
+
* from a package this one does not depend on puts that package's `.d.ts` into
|
|
63
|
+
* the build graph, and a consumer that never installs it never builds it
|
|
64
|
+
* either — so the dts bundler looks for a `dist/` that does not exist and fails
|
|
65
|
+
* the build. Structural typing costs nothing here because this module never
|
|
66
|
+
* inspects the gate; it only applies it.
|
|
67
|
+
*
|
|
68
|
+
* Both better-auth entry points partially apply to this shape:
|
|
69
|
+
* `(handler) => requireMcpAuth(auth, handler, opts)` and
|
|
70
|
+
* `(handler) => createMcpProtectedRequestHandler(options, handler)`.
|
|
71
|
+
*/
|
|
72
|
+
type McpAuthProtect = (handler: (request: Request, claims: McpAccessTokenClaims) => Promise<Response>) => McpFetchHandler;
|
|
73
|
+
/** Server options, or a function deriving them from the request's verified claims. */
|
|
74
|
+
type AuthedMcpServerOptions = ((claims: McpAccessTokenClaims) => LunoraMcpServerOptions | Promise<LunoraMcpServerOptions>) | LunoraMcpServerOptions;
|
|
75
|
+
interface AuthedMcpFetchHandlerOptions {
|
|
76
|
+
/**
|
|
77
|
+
* Largest accepted request body, in bytes — enforced while the body streams
|
|
78
|
+
* in, not after it is buffered. Defaults to `DEFAULT_MAX_REQUEST_BYTES`
|
|
79
|
+
* (128 KiB), which a value that is not a non-negative safe integer also
|
|
80
|
+
* falls back to.
|
|
81
|
+
*/
|
|
82
|
+
maxRequestBytes?: number;
|
|
83
|
+
/**
|
|
84
|
+
* The OAuth gate to mount the MCP server behind. Pass
|
|
85
|
+
* `(handler) => requireMcpAuth(auth, handler, opts)` from
|
|
86
|
+
* `@lunora/auth/plugins`.
|
|
87
|
+
*/
|
|
88
|
+
protect: McpAuthProtect;
|
|
89
|
+
/**
|
|
90
|
+
* The Lunora MCP server to serve once a request is authorized — either a
|
|
91
|
+
* fixed options object, or a function of the verified token claims so tool
|
|
92
|
+
* exposure can follow the scopes the token actually carries.
|
|
93
|
+
*/
|
|
94
|
+
server: AuthedMcpServerOptions;
|
|
95
|
+
/**
|
|
96
|
+
* Step-up for the side-effecting tools: the write tools
|
|
97
|
+
* (`lunora_run_mutation` / `lunora_run_action`) and the agent tools (each
|
|
98
|
+
* `agent_<name>` and `lunora_agent_status`, since starting a durable run is a
|
|
99
|
+
* side effect). A token without `scope` still sees them in `tools/list`, and
|
|
100
|
+
* a `tools/call` to one throws `challenge([scope])` instead of running, so the
|
|
101
|
+
* client can re-authorize for the scope and retry. Unset, tools follow
|
|
102
|
+
* `allowWrites` / `allowAgents` alone. The raw-row and observability families
|
|
103
|
+
* need their own token scopes with or without `stepUp`; without it a call
|
|
104
|
+
* without the scope is refused as a tool error, not challenged.
|
|
105
|
+
*/
|
|
106
|
+
stepUp?: {
|
|
107
|
+
/**
|
|
108
|
+
* Build the error that makes the gate answer 403 `insufficient_scope`
|
|
109
|
+
* naming `scopes`. Pass `createInsufficientScopeError` from
|
|
110
|
+
* `@lunora/auth/plugins`: better-auth only turns an error that factory
|
|
111
|
+
* created into the challenge.
|
|
112
|
+
*/
|
|
113
|
+
challenge: (scopes: string[]) => unknown;
|
|
114
|
+
/** The scope a call to a side-effecting tool needs, e.g. `"lunora:write"`. */
|
|
115
|
+
scope: string;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse an access token's `scope` claim into a set.
|
|
120
|
+
*
|
|
121
|
+
* RFC 6749 §3.3 makes `scope` a space-delimited string, and better-auth issues
|
|
122
|
+
* it that way; anything else (absent, or a non-string an extension wrote)
|
|
123
|
+
* yields an empty set rather than throwing, so a scope check on a malformed
|
|
124
|
+
* token denies instead of crashing the tool call.
|
|
125
|
+
*/
|
|
126
|
+
declare const mcpTokenScopes: (claims: McpAccessTokenClaims) => ReadonlySet<string>;
|
|
127
|
+
declare const createAuthedMcpFetchHandler: (options: AuthedMcpFetchHandlerOptions) => McpFetchHandler;
|
|
128
|
+
/** {@link createMcpFetchHandler} options: the server's, plus this transport's body limit. */
|
|
129
|
+
interface McpFetchHandlerOptions extends LunoraMcpServerOptions {
|
|
130
|
+
/**
|
|
131
|
+
* Largest accepted request body, in bytes — enforced while the body streams
|
|
132
|
+
* in, not after it is buffered. Defaults to `DEFAULT_MAX_REQUEST_BYTES`
|
|
133
|
+
* (128 KiB), which a value that is not a non-negative safe integer also
|
|
134
|
+
* falls back to.
|
|
135
|
+
*/
|
|
136
|
+
maxRequestBytes?: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build a stateless Streamable-HTTP fetch handler for a Lunora MCP server. Each
|
|
140
|
+
* invocation constructs a fresh proxy server and serves the request through
|
|
141
|
+
* {@link serveStateless}.
|
|
142
|
+
*
|
|
143
|
+
* The `LunoraClient` is resolved ONCE, here, and shared by every request: it is
|
|
144
|
+
* the same deployment on each one, and the public-function registry memo in
|
|
145
|
+
* `./tools` is keyed by client identity, so a per-request client would re-fetch
|
|
146
|
+
* that registry on every tool call. A misconfiguration (`url` without `token`)
|
|
147
|
+
* therefore throws when the handler is built rather than on first request,
|
|
148
|
+
* which is where `createLunoraMcpServer` already documents reporting it.
|
|
149
|
+
*/
|
|
150
|
+
declare const createMcpFetchHandler: (options: McpFetchHandlerOptions) => McpFetchHandler;
|
|
151
|
+
/** A tool handler: receives the call's `arguments` bag, returns an MCP tool result. */
|
|
152
|
+
/**
|
|
153
|
+
* The x402 vocabulary this module needs, declared here rather than imported.
|
|
154
|
+
*
|
|
155
|
+
* The x402 package is an optional peer, and a type import from its `charge`
|
|
156
|
+
* entry puts its `.d.ts` back into this package's build graph:
|
|
157
|
+
* a consumer that never installs x402 never builds it either, so the dts bundler
|
|
158
|
+
* looks for a `dist/` that does not exist and fails. That is not hypothetical —
|
|
159
|
+
* it broke the docs site build, which runs a filtered build over the docs app and its dependency closure
|
|
160
|
+
* and therefore never builds x402.
|
|
161
|
+
*
|
|
162
|
+
* Declaring them locally is safe because this module never *inspects* a charge
|
|
163
|
+
* config; it forwards it whole to `createChargeMiddleware`. The index signature
|
|
164
|
+
* keeps a real charge config assignable as x402 grows fields.
|
|
165
|
+
*/
|
|
166
|
+
/** Mirrors x402's `X402Price` — a decimal string like `"$0.05"`, or a number. */
|
|
167
|
+
type X402Price = number | string;
|
|
168
|
+
/** Mirrors x402's charge config with the per-tool price omitted. */
|
|
169
|
+
interface X402ChargeSettings {
|
|
170
|
+
/** Network this resource settles on. */
|
|
171
|
+
readonly network: string;
|
|
172
|
+
/** Everything else x402 accepts, forwarded untouched. */
|
|
173
|
+
readonly [key: string]: unknown;
|
|
174
|
+
/** Payout wallet(s), per network family. */
|
|
175
|
+
readonly recipient: {
|
|
176
|
+
readonly evm?: string;
|
|
177
|
+
readonly svm?: string;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
type ToolHandler = (arguments_: Record<string, unknown>) => Promise<ToolResult> | ToolResult;
|
|
181
|
+
/** Registration shape for a free tool. */
|
|
182
|
+
interface RegisterToolOptions {
|
|
183
|
+
/** MCP tool annotations. Required: a paid tool declares its access the same as every other tool. */
|
|
184
|
+
annotations: ToolAnnotations;
|
|
185
|
+
/** Human/model-facing description of what the tool does. */
|
|
186
|
+
description: string;
|
|
187
|
+
/** JSON-Schema object describing the tool's arguments. */
|
|
188
|
+
inputSchema: ToolInputSchema;
|
|
189
|
+
/** Unique tool name (the MCP `tools/call` `name`). */
|
|
190
|
+
name: string;
|
|
191
|
+
}
|
|
192
|
+
/** Registration shape for a paid tool: a {@link RegisterToolOptions} plus its USD price. */
|
|
193
|
+
interface RegisterPaidToolOptions extends RegisterToolOptions {
|
|
194
|
+
/** USD price per call (e.g. `"$0.05"`), charged via x402 before dispatch. */
|
|
195
|
+
price: X402Price;
|
|
196
|
+
}
|
|
197
|
+
/** x402 settlement vocabulary shared by every paid tool (network, recipient, facilitator); price is per-tool. */
|
|
198
|
+
type PaidMcpChargeConfig = X402ChargeSettings;
|
|
199
|
+
/** Config for `createPaidMcpServer`. */
|
|
200
|
+
interface PaidMcpServerConfig {
|
|
201
|
+
/** The worker-level x402 charge config; each paid tool supplies only its own `price`. */
|
|
202
|
+
charge: PaidMcpChargeConfig;
|
|
203
|
+
/**
|
|
204
|
+
* Largest accepted request body, in bytes — enforced while the body streams
|
|
205
|
+
* in, not after it is buffered. Defaults to `DEFAULT_MAX_REQUEST_BYTES`
|
|
206
|
+
* (128 KiB), which a value that is not a non-negative safe integer also
|
|
207
|
+
* falls back to.
|
|
208
|
+
*/
|
|
209
|
+
maxRequestBytes?: number;
|
|
210
|
+
/** Name/version advertised in the MCP `initialize` handshake. Defaults to `lunora-paid-mcp`. */
|
|
211
|
+
serverInfo?: {
|
|
212
|
+
name: string;
|
|
213
|
+
version: string;
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The Worker execution context, as this module reads it: only `waitUntil`, and
|
|
218
|
+
* structurally, so the package takes no `@cloudflare/workers-types` dependency.
|
|
219
|
+
*/
|
|
220
|
+
interface PaidMcpExecutionContext {
|
|
221
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The paid server's fetch handler.
|
|
225
|
+
*
|
|
226
|
+
* Unlike the free `McpFetchHandler` it takes the Worker's full
|
|
227
|
+
* `(request, env, ctx)` triple — the shape `export default { fetch }` is called
|
|
228
|
+
* with — because the x402 receipt sink NEEDS `ctx.waitUntil`: work that is
|
|
229
|
+
* neither awaited into the response nor registered with it is cancelled when the
|
|
230
|
+
* request ends, so an async `onReceipt` (inserting the settled payment into a
|
|
231
|
+
* durable table) frequently never runs while the money has already moved
|
|
232
|
+
* on-chain. `env` is accepted and ignored so the handler drops straight into the
|
|
233
|
+
* default export; both are optional, so a non-Workers caller may still invoke it
|
|
234
|
+
* with a bare `Request` (the middleware then simply sees no `waitUntil`).
|
|
235
|
+
*/
|
|
236
|
+
type PaidMcpFetchHandler = (request: Request, env?: unknown, context?: PaidMcpExecutionContext) => Promise<Response>;
|
|
237
|
+
/** A paid MCP server: register free/paid tools, then serve over Streamable HTTP. */
|
|
238
|
+
interface PaidMcpServer {
|
|
239
|
+
/** The Streamable-HTTP fetch handler; gates each paid `tools/call` behind x402. */
|
|
240
|
+
readonly fetchHandler: PaidMcpFetchHandler;
|
|
241
|
+
/** Register a **paid** tool: its dispatch runs the x402 charge middleware first. */
|
|
242
|
+
paidTool: (options: RegisterPaidToolOptions, handler: ToolHandler) => void;
|
|
243
|
+
/** Register a **free** tool (coexists with paid tools on the same server). */
|
|
244
|
+
tool: (options: RegisterToolOptions, handler: ToolHandler) => void;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Create a paid MCP server. Register free tools with `tool()` and priced tools
|
|
248
|
+
* with `paidTool()` (they coexist), then serve `fetchHandler` over HTTP.
|
|
249
|
+
*
|
|
250
|
+
* The server is **stateless**: `fetchHandler` builds a fresh `Server` per
|
|
251
|
+
* request (reading the live tool registry), so tools registered before the
|
|
252
|
+
* first request are all visible. Each priced tool memoises one initialised
|
|
253
|
+
* `ChargeMiddleware` (keyed by tool name, baking that tool's price and naming
|
|
254
|
+
* the tool as the challenge `resource`); a failed init is not cached, so a
|
|
255
|
+
* transient facilitator outage retries on the next call.
|
|
256
|
+
*/
|
|
257
|
+
declare const createPaidMcpServer: (config: PaidMcpServerConfig) => PaidMcpServer;
|
|
258
|
+
export { type AuthedMcpFetchHandlerOptions, type AuthedMcpServerOptions, type McpAccessTokenClaims, type McpAuthProtect, type McpFetchHandler, type McpFetchHandlerOptions, OBSERVABILITY_TOOL_DEFINITIONS, type PaidMcpChargeConfig, type PaidMcpExecutionContext, type PaidMcpFetchHandler, type PaidMcpServer, type PaidMcpServerConfig, READ_ONLY_TOOL_DEFINITIONS, type RegisterPaidToolOptions, type RegisterToolOptions, type ToolDefinition, type ToolHandler, type ToolInputSchema, type ToolResult, WRITE_TOOL_DEFINITIONS, callTool, createAuthedMcpFetchHandler, createMcpFetchHandler, createPaidMcpServer, mcpTokenScopes, toolDefinitions };
|