@lunora/mcp 1.0.0-alpha.25 → 1.0.0-alpha.251

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.
Files changed (47) hide show
  1. package/LICENSE.md +120 -0
  2. package/README.md +202 -14
  3. package/dist/bin.mjs +4 -47
  4. package/dist/docs/index.d.mts +259 -0
  5. package/dist/docs/index.d.ts +259 -0
  6. package/dist/docs/index.mjs +1 -0
  7. package/dist/index.d.mts +83 -82
  8. package/dist/index.d.ts +83 -82
  9. package/dist/index.mjs +1 -5
  10. package/dist/node/index.d.mts +90 -0
  11. package/dist/node/index.d.ts +90 -0
  12. package/dist/node/index.mjs +1 -0
  13. package/dist/packem_shared/AGENT_RUN_INPUT_SCHEMA-BBVvXopY.mjs +1 -0
  14. package/dist/packem_shared/DEFAULT_DOCS_BASE_URL-CiHy_Dbz.mjs +4 -0
  15. package/dist/packem_shared/DEFAULT_MAX_REQUEST_BYTES-CbbpkHRK.mjs +1 -0
  16. package/dist/packem_shared/DEFAULT_SEARCH_LIMIT-DpGKT-Xt.mjs +3 -0
  17. package/dist/packem_shared/DOCS_SERVER_NAME-B4fL5sGF.mjs +1 -0
  18. package/dist/packem_shared/DOCS_URI_SCHEME-B_C2tA0y.mjs +3 -0
  19. package/dist/packem_shared/LOCAL_SERVER_NAME-3ydSaBjA.mjs +1 -0
  20. package/dist/packem_shared/OBSERVABILITY_TOOL_DEFINITIONS-DrY5Gp38.mjs +1 -0
  21. package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-BEaoA4xz.mjs +1 -0
  22. package/dist/packem_shared/compose.d-CNiBViBW.d.mts +153 -0
  23. package/dist/packem_shared/compose.d-CNiBViBW.d.ts +153 -0
  24. package/dist/packem_shared/connectStdio-D_skPLhV.mjs +1 -0
  25. package/dist/packem_shared/createAuthedMcpFetchHandler-3-m_Hrp0.mjs +1 -0
  26. package/dist/packem_shared/createLunoraMcpServer-BYOigZMZ.mjs +1 -0
  27. package/dist/packem_shared/createMcpFetchHandler-Bw2OFJ7f.mjs +1 -0
  28. package/dist/packem_shared/createPaidMcpServer-DQmcP8ls.mjs +1 -0
  29. package/dist/packem_shared/createToolServer-Bf_IxX3S.mjs +1 -0
  30. package/dist/packem_shared/promise-memo-OqVPWoUX.mjs +1 -0
  31. package/dist/packem_shared/serve-stateless.d-DD5arqFx.d.mts +45 -0
  32. package/dist/packem_shared/serve-stateless.d-DD5arqFx.d.ts +45 -0
  33. package/dist/packem_shared/server.d-C7ciln6z.d.mts +152 -0
  34. package/dist/packem_shared/server.d-CXO7cw_h.d.ts +152 -0
  35. package/dist/packem_shared/toDocsSearchHits-CBLmtWXt.mjs +1 -0
  36. package/dist/packem_shared/tool-call-Bs-QeKp6.mjs +1 -0
  37. package/dist/packem_shared/tool-result--Yq36pE-.mjs +1 -0
  38. package/dist/packem_shared/tools-BrvavFhl.mjs +2 -0
  39. package/dist/worker/index.d.mts +257 -0
  40. package/dist/worker/index.d.ts +257 -0
  41. package/dist/worker/index.mjs +1 -0
  42. package/package.json +27 -5
  43. package/dist/packem_shared/AGENT_RUN_INPUT_SCHEMA-DWobae3P.mjs +0 -168
  44. package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-CWb_wFd5.mjs +0 -204
  45. package/dist/packem_shared/connectStdio-bkJcaXYE.mjs +0 -76
  46. package/dist/packem_shared/createMcpFetchHandler-DfzBSLrm.mjs +0 -14
  47. package/dist/packem_shared/createPaidMcpServer-b-qp_aEF.mjs +0 -93
@@ -0,0 +1,257 @@
1
+ import { L as LunoraMcpServerOptions } from "../packem_shared/server.d-CXO7cw_h.js";
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,
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, p as parseAgentsEnv } from "../packem_shared/server.d-CXO7cw_h.js";
7
+ import { M as McpFetchHandler } from "../packem_shared/serve-stateless.d-DD5arqFx.js";
8
+ export { D as DEFAULT_MAX_REQUEST_BYTES, type S as ServeStatelessOptions, s as serveStateless } from "../packem_shared/serve-stateless.d-DD5arqFx.js";
9
+ import { T as ToolDefinition, d as ToolResult, h as ToolAnnotations, b as ToolInputSchema } from "../packem_shared/compose.d-CNiBViBW.js";
10
+ export { type M as McpServerInfo, type a as McpTool, e as createToolServer } from "../packem_shared/compose.d-CNiBViBW.js";
11
+ import { LunoraClient } from '@lunora/client';
12
+ import '@modelcontextprotocol/sdk/server/index.js';
13
+ import '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
14
+ /**
15
+ * The observability tools. Descriptions say WHEN to call the tool, not just
16
+ * what it returns — an agent picking between five similar reads needs the
17
+ * trigger, and it pays for these strings on every turn.
18
+ */
19
+ declare const OBSERVABILITY_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
20
+ /** The read-only tool surface: introspection + query. Always exposed. */
21
+ declare const READ_ONLY_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
22
+ /** The write tool surface (mutations + actions). Exposed ONLY when writes are enabled. */
23
+ declare const WRITE_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
24
+ /**
25
+ * The tools this server advertises: every family whose gate is open, in table
26
+ * order. A gated family is OMITTED rather than refused — an AI agent can't
27
+ * invoke what it can't see — and {@link callTool} re-checks the same gate at
28
+ * dispatch, so the guarantee does not depend on a client honouring this list.
29
+ */
30
+ declare const toolDefinitions: (allowWrites: boolean, allowObservability?: boolean, allowDataReads?: boolean) => ReadonlyArray<ToolDefinition>;
31
+ /**
32
+ * Dispatch a tool call against `client`: find the family that owns `name`, check
33
+ * its gate, hand off. Unknown tools and thrown errors are returned as `isError`
34
+ * results (rather than rejections) so the calling model sees the failure as tool
35
+ * output, per the MCP convention.
36
+ *
37
+ * Each gate is enforced HERE as well as in {@link toolDefinitions}, so a call to
38
+ * a gated tool is refused even if the client somehow names one it was never
39
+ * shown.
40
+ */
41
+ declare const callTool: (client: LunoraClient, name: string, input: Record<string, unknown>, allowWrites?: boolean, allowObservability?: boolean, allowDataReads?: boolean) => Promise<ToolResult>;
42
+ /**
43
+ * The verified access-token payload better-auth hands a protected handler.
44
+ *
45
+ * A JWT payload is an open bag of claims, so this is deliberately an index
46
+ * signature with the two entries this module reads named. It is structurally
47
+ * satisfied by `jose`'s `JWTPayload`, which is what better-auth passes.
48
+ */
49
+ interface McpAccessTokenClaims {
50
+ readonly [claim: string]: unknown;
51
+ /** Space-delimited granted scopes (RFC 6749 §3.3). */
52
+ readonly scope?: unknown;
53
+ /** Subject — the user the token was issued for. */
54
+ readonly sub?: string;
55
+ }
56
+ /**
57
+ * The MCP auth gate: wraps a claims-aware handler into a plain fetch handler.
58
+ *
59
+ * Declared structurally here rather than imported from `@better-auth/mcp`,
60
+ * following the same rule `./paid` follows for `@lunora/x402`: a type import
61
+ * from a package this one does not depend on puts that package's `.d.ts` into
62
+ * the build graph, and a consumer that never installs it never builds it
63
+ * either — so the dts bundler looks for a `dist/` that does not exist and fails
64
+ * the build. Structural typing costs nothing here because this module never
65
+ * inspects the gate; it only applies it.
66
+ *
67
+ * Both better-auth entry points partially apply to this shape:
68
+ * `(handler) => requireMcpAuth(auth, handler, opts)` and
69
+ * `(handler) => createMcpProtectedRequestHandler(options, handler)`.
70
+ */
71
+ type McpAuthProtect = (handler: (request: Request, claims: McpAccessTokenClaims) => Promise<Response>) => McpFetchHandler;
72
+ /** Server options, or a function deriving them from the request's verified claims. */
73
+ type AuthedMcpServerOptions = ((claims: McpAccessTokenClaims) => LunoraMcpServerOptions | Promise<LunoraMcpServerOptions>) | LunoraMcpServerOptions;
74
+ interface AuthedMcpFetchHandlerOptions {
75
+ /**
76
+ * Largest accepted request body, in bytes — enforced while the body streams
77
+ * in, not after it is buffered. Defaults to `DEFAULT_MAX_REQUEST_BYTES`
78
+ * (128 KiB), which a value that is not a non-negative safe integer also
79
+ * falls back to.
80
+ */
81
+ maxRequestBytes?: number;
82
+ /**
83
+ * The OAuth gate to mount the MCP server behind. Pass
84
+ * `(handler) => requireMcpAuth(auth, handler, opts)` from
85
+ * `@lunora/auth/plugins`.
86
+ */
87
+ protect: McpAuthProtect;
88
+ /**
89
+ * The Lunora MCP server to serve once a request is authorized — either a
90
+ * fixed options object, or a function of the verified token claims so tool
91
+ * exposure can follow the scopes the token actually carries.
92
+ */
93
+ server: AuthedMcpServerOptions;
94
+ /**
95
+ * Step-up for the side-effecting tools: the write tools
96
+ * (`lunora_run_mutation` / `lunora_run_action`) and the agent tools (each
97
+ * `agent_<name>` and `lunora_agent_status`, since starting a durable run is a
98
+ * side effect). A token without `scope` still sees them in `tools/list`, and
99
+ * a `tools/call` to one throws `challenge([scope])` instead of running, so the
100
+ * client can re-authorize for the scope and retry. Unset, tools follow
101
+ * `allowWrites` / `allowAgents` alone. The raw-row and observability families
102
+ * need their own token scopes with or without `stepUp`; without it a call
103
+ * without the scope is refused as a tool error, not challenged.
104
+ */
105
+ stepUp?: {
106
+ /**
107
+ * Build the error that makes the gate answer 403 `insufficient_scope`
108
+ * naming `scopes`. Pass `createInsufficientScopeError` from
109
+ * `@lunora/auth/plugins`: better-auth only turns an error that factory
110
+ * created into the challenge.
111
+ */
112
+ challenge: (scopes: string[]) => unknown;
113
+ /** The scope a call to a side-effecting tool needs, e.g. `"lunora:write"`. */
114
+ scope: string;
115
+ };
116
+ }
117
+ /**
118
+ * Parse an access token's `scope` claim into a set.
119
+ *
120
+ * RFC 6749 §3.3 makes `scope` a space-delimited string, and better-auth issues
121
+ * it that way; anything else (absent, or a non-string an extension wrote)
122
+ * yields an empty set rather than throwing, so a scope check on a malformed
123
+ * token denies instead of crashing the tool call.
124
+ */
125
+ declare const mcpTokenScopes: (claims: McpAccessTokenClaims) => ReadonlySet<string>;
126
+ declare const createAuthedMcpFetchHandler: (options: AuthedMcpFetchHandlerOptions) => McpFetchHandler;
127
+ /** {@link createMcpFetchHandler} options: the server's, plus this transport's body limit. */
128
+ interface McpFetchHandlerOptions extends LunoraMcpServerOptions {
129
+ /**
130
+ * Largest accepted request body, in bytes — enforced while the body streams
131
+ * in, not after it is buffered. Defaults to `DEFAULT_MAX_REQUEST_BYTES`
132
+ * (128 KiB), which a value that is not a non-negative safe integer also
133
+ * falls back to.
134
+ */
135
+ maxRequestBytes?: number;
136
+ }
137
+ /**
138
+ * Build a stateless Streamable-HTTP fetch handler for a Lunora MCP server. Each
139
+ * invocation constructs a fresh proxy server and serves the request through
140
+ * {@link serveStateless}.
141
+ *
142
+ * The `LunoraClient` is resolved ONCE, here, and shared by every request: it is
143
+ * the same deployment on each one, and the public-function registry memo in
144
+ * `./tools` is keyed by client identity, so a per-request client would re-fetch
145
+ * that registry on every tool call. A misconfiguration (`url` without `token`)
146
+ * therefore throws when the handler is built rather than on first request,
147
+ * which is where `createLunoraMcpServer` already documents reporting it.
148
+ */
149
+ declare const createMcpFetchHandler: (options: McpFetchHandlerOptions) => McpFetchHandler;
150
+ /** A tool handler: receives the call's `arguments` bag, returns an MCP tool result. */
151
+ /**
152
+ * The x402 vocabulary this module needs, declared here rather than imported.
153
+ *
154
+ * The x402 package is an optional peer, and a type import from its `charge`
155
+ * entry puts its `.d.ts` back into this package's build graph:
156
+ * a consumer that never installs x402 never builds it either, so the dts bundler
157
+ * looks for a `dist/` that does not exist and fails. That is not hypothetical —
158
+ * it broke the docs site build, which runs a filtered build over the docs app and its dependency closure
159
+ * and therefore never builds x402.
160
+ *
161
+ * Declaring them locally is safe because this module never *inspects* a charge
162
+ * config; it forwards it whole to `createChargeMiddleware`. The index signature
163
+ * keeps a real charge config assignable as x402 grows fields.
164
+ */
165
+ /** Mirrors x402's `X402Price` — a decimal string like `"$0.05"`, or a number. */
166
+ type X402Price = number | string;
167
+ /** Mirrors x402's charge config with the per-tool price omitted. */
168
+ interface X402ChargeSettings {
169
+ /** Network this resource settles on. */
170
+ readonly network: string;
171
+ /** Everything else x402 accepts, forwarded untouched. */
172
+ readonly [key: string]: unknown;
173
+ /** Payout wallet(s), per network family. */
174
+ readonly recipient: {
175
+ readonly evm?: string;
176
+ readonly svm?: string;
177
+ };
178
+ }
179
+ type ToolHandler = (arguments_: Record<string, unknown>) => Promise<ToolResult> | ToolResult;
180
+ /** Registration shape for a free tool. */
181
+ interface RegisterToolOptions {
182
+ /** MCP tool annotations. Required: a paid tool declares its access the same as every other tool. */
183
+ annotations: ToolAnnotations;
184
+ /** Human/model-facing description of what the tool does. */
185
+ description: string;
186
+ /** JSON-Schema object describing the tool's arguments. */
187
+ inputSchema: ToolInputSchema;
188
+ /** Unique tool name (the MCP `tools/call` `name`). */
189
+ name: string;
190
+ }
191
+ /** Registration shape for a paid tool: a {@link RegisterToolOptions} plus its USD price. */
192
+ interface RegisterPaidToolOptions extends RegisterToolOptions {
193
+ /** USD price per call (e.g. `"$0.05"`), charged via x402 before dispatch. */
194
+ price: X402Price;
195
+ }
196
+ /** x402 settlement vocabulary shared by every paid tool (network, recipient, facilitator); price is per-tool. */
197
+ type PaidMcpChargeConfig = X402ChargeSettings;
198
+ /** Config for `createPaidMcpServer`. */
199
+ interface PaidMcpServerConfig {
200
+ /** The worker-level x402 charge config; each paid tool supplies only its own `price`. */
201
+ charge: PaidMcpChargeConfig;
202
+ /**
203
+ * Largest accepted request body, in bytes — enforced while the body streams
204
+ * in, not after it is buffered. Defaults to `DEFAULT_MAX_REQUEST_BYTES`
205
+ * (128 KiB), which a value that is not a non-negative safe integer also
206
+ * falls back to.
207
+ */
208
+ maxRequestBytes?: number;
209
+ /** Name/version advertised in the MCP `initialize` handshake. Defaults to `lunora-paid-mcp`. */
210
+ serverInfo?: {
211
+ name: string;
212
+ version: string;
213
+ };
214
+ }
215
+ /**
216
+ * The Worker execution context, as this module reads it: only `waitUntil`, and
217
+ * structurally, so the package takes no `@cloudflare/workers-types` dependency.
218
+ */
219
+ interface PaidMcpExecutionContext {
220
+ waitUntil?: (promise: Promise<unknown>) => void;
221
+ }
222
+ /**
223
+ * The paid server's fetch handler.
224
+ *
225
+ * Unlike the free `McpFetchHandler` it takes the Worker's full
226
+ * `(request, env, ctx)` triple — the shape `export default { fetch }` is called
227
+ * with — because the x402 receipt sink NEEDS `ctx.waitUntil`: work that is
228
+ * neither awaited into the response nor registered with it is cancelled when the
229
+ * request ends, so an async `onReceipt` (inserting the settled payment into a
230
+ * durable table) frequently never runs while the money has already moved
231
+ * on-chain. `env` is accepted and ignored so the handler drops straight into the
232
+ * default export; both are optional, so a non-Workers caller may still invoke it
233
+ * with a bare `Request` (the middleware then simply sees no `waitUntil`).
234
+ */
235
+ type PaidMcpFetchHandler = (request: Request, env?: unknown, context?: PaidMcpExecutionContext) => Promise<Response>;
236
+ /** A paid MCP server: register free/paid tools, then serve over Streamable HTTP. */
237
+ interface PaidMcpServer {
238
+ /** The Streamable-HTTP fetch handler; gates each paid `tools/call` behind x402. */
239
+ readonly fetchHandler: PaidMcpFetchHandler;
240
+ /** Register a **paid** tool: its dispatch runs the x402 charge middleware first. */
241
+ paidTool: (options: RegisterPaidToolOptions, handler: ToolHandler) => void;
242
+ /** Register a **free** tool (coexists with paid tools on the same server). */
243
+ tool: (options: RegisterToolOptions, handler: ToolHandler) => void;
244
+ }
245
+ /**
246
+ * Create a paid MCP server. Register free tools with `tool()` and priced tools
247
+ * with `paidTool()` (they coexist), then serve `fetchHandler` over HTTP.
248
+ *
249
+ * The server is **stateless**: `fetchHandler` builds a fresh `Server` per
250
+ * request (reading the live tool registry), so tools registered before the
251
+ * first request are all visible. Each priced tool memoises one initialised
252
+ * `ChargeMiddleware` (keyed by tool name, baking that tool's price and naming
253
+ * the tool as the challenge `resource`); a failed init is not cached, so a
254
+ * transient facilitator outage retries on the next call.
255
+ */
256
+ declare const createPaidMcpServer: (config: PaidMcpServerConfig) => PaidMcpServer;
257
+ 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 };
@@ -0,0 +1 @@
1
+ import{AGENT_RUN_INPUT_SCHEMA as r,AGENT_STATUS_TOOL_NAME as t,agentToolDefinitions as T,callAgentTool as a,parseAgentsEnv as _}from"../packem_shared/AGENT_RUN_INPUT_SCHEMA-BBVvXopY.mjs";import{createAuthedMcpFetchHandler as c,mcpTokenScopes as p}from"../packem_shared/createAuthedMcpFetchHandler-3-m_Hrp0.mjs";import{createToolServer as O}from"../packem_shared/createToolServer-Bf_IxX3S.mjs";import{createMcpFetchHandler as l}from"../packem_shared/createMcpFetchHandler-Bw2OFJ7f.mjs";import{createPaidMcpServer as A}from"../packem_shared/createPaidMcpServer-DQmcP8ls.mjs";import{R as n,W as f,c as m,t as x}from"../packem_shared/tools-BrvavFhl.mjs";import{DEFAULT_MAX_REQUEST_BYTES as D,serveStateless as L}from"../packem_shared/DEFAULT_MAX_REQUEST_BYTES-CbbpkHRK.mjs";import{OBSERVABILITY_TOOL_DEFINITIONS as M}from"../packem_shared/OBSERVABILITY_TOOL_DEFINITIONS-DrY5Gp38.mjs";export{r as AGENT_RUN_INPUT_SCHEMA,t as AGENT_STATUS_TOOL_NAME,D as DEFAULT_MAX_REQUEST_BYTES,M as OBSERVABILITY_TOOL_DEFINITIONS,n as READ_ONLY_TOOL_DEFINITIONS,f as WRITE_TOOL_DEFINITIONS,T as agentToolDefinitions,a as callAgentTool,m as callTool,c as createAuthedMcpFetchHandler,l as createMcpFetchHandler,A as createPaidMcpServer,O as createToolServer,p as mcpTokenScopes,_ as parseAgentsEnv,L as serveStateless,x as toolDefinitions};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/mcp",
3
- "version": "1.0.0-alpha.25",
3
+ "version": "1.0.0-alpha.251",
4
4
  "description": "Model Context Protocol server exposing a Lunora deployment to AI agents",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -43,16 +43,38 @@
43
43
  "types": "./dist/index.d.ts",
44
44
  "import": "./dist/index.mjs"
45
45
  },
46
+ "./docs": {
47
+ "types": "./dist/docs/index.d.ts",
48
+ "import": "./dist/docs/index.mjs"
49
+ },
50
+ "./node": {
51
+ "types": "./dist/node/index.d.ts",
52
+ "import": "./dist/node/index.mjs"
53
+ },
54
+ "./worker": {
55
+ "types": "./dist/worker/index.d.ts",
56
+ "import": "./dist/worker/index.mjs"
57
+ },
46
58
  "./package.json": "./package.json"
47
59
  },
48
60
  "publishConfig": {
49
61
  "access": "public"
50
62
  },
51
63
  "dependencies": {
52
- "@lunora/client": "1.0.0-alpha.23",
53
- "@lunora/errors": "1.0.0-alpha.5",
54
- "@lunora/x402": "1.0.0-alpha.3",
55
- "@modelcontextprotocol/sdk": "^1.29.0"
64
+ "@cfworker/json-schema": "^4.1.1",
65
+ "@lunora/client": "1.0.0-alpha.236",
66
+ "@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
67
+ "@lunora/shard-engine": "1.0.0-alpha.149",
68
+ "@lunora/wire": "1.0.0-alpha.2",
69
+ "@modelcontextprotocol/sdk": "^1.32.0"
70
+ },
71
+ "peerDependencies": {
72
+ "@lunora/x402": ">=1.0.0-alpha.21 <2.0.0-0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "@lunora/x402": {
76
+ "optional": true
77
+ }
56
78
  },
57
79
  "engines": {
58
80
  "node": "^22.15.0 || >=24.11.0"
@@ -1,168 +0,0 @@
1
- const AGENT_RUN_PATH = "agents:agentRun";
2
- const AGENT_THREAD_PATH = "agents:agentThread";
3
- const AGENT_MESSAGES_PATH = "agents:agentMessages";
4
- const AGENT_STATUS_TOOL_NAME = "lunora_agent_status";
5
- const TERMINAL_STATUSES = /* @__PURE__ */ new Set(["cancelled", "error", "idle"]);
6
- const DEFAULT_MAX_WAIT_MS = 6e4;
7
- const DEFAULT_POLL_INTERVAL_MS = 600;
8
- const AGENT_RUN_INPUT_SCHEMA = {
9
- properties: {
10
- prompt: { description: "The task or message for the agent.", type: "string" },
11
- threadKey: { description: "Reuse to continue a conversation; omit to start a new thread.", type: "string" },
12
- title: { description: "Optional thread title (first run only).", type: "string" }
13
- },
14
- required: ["prompt"],
15
- type: "object"
16
- };
17
- const AGENT_STATUS_INPUT_SCHEMA = {
18
- properties: {
19
- threadKey: { description: "The thread key returned by an agent tool call.", type: "string" }
20
- },
21
- required: ["threadKey"],
22
- type: "object"
23
- };
24
- const agentToolName = (exposure) => exposure.toolName ?? `agent_${exposure.name}`;
25
- const parseAgentsEnv = (raw) => {
26
- if (raw === void 0) {
27
- return [];
28
- }
29
- const exposures = [];
30
- for (const entry of raw.split(";")) {
31
- const trimmed = entry.trim();
32
- if (trimmed.length === 0) {
33
- continue;
34
- }
35
- const separator = trimmed.indexOf(":");
36
- if (separator <= 0) {
37
- continue;
38
- }
39
- const name = trimmed.slice(0, separator).trim();
40
- const description = trimmed.slice(separator + 1).trim();
41
- if (name.length === 0 || description.length === 0) {
42
- continue;
43
- }
44
- exposures.push({ description, name });
45
- }
46
- return exposures;
47
- };
48
- const agentToolDefinitions = (exposures, allowAgents) => {
49
- if (allowAgents !== true || exposures.length === 0) {
50
- return [];
51
- }
52
- const perAgent = exposures.map((exposure) => {
53
- return {
54
- description: `${exposure.description} Starts a durable agent run and returns its final answer.`,
55
- inputSchema: AGENT_RUN_INPUT_SCHEMA,
56
- name: agentToolName(exposure)
57
- };
58
- });
59
- return [
60
- ...perAgent,
61
- {
62
- description: "Check the status of a durable agent run (and its answer if finished) by its threadKey.",
63
- inputSchema: AGENT_STATUS_INPUT_SCHEMA,
64
- name: AGENT_STATUS_TOOL_NAME
65
- }
66
- ];
67
- };
68
- const isAgentToolName = (name, exposures) => name === AGENT_STATUS_TOOL_NAME || exposures.some((exposure) => agentToolName(exposure) === name);
69
- const freshThreadKey = () => `mcp-${crypto.randomUUID()}`;
70
- const reference = (path) => {
71
- return { __lunoraRef: path };
72
- };
73
- const defaultWait = async (ms) => {
74
- await new Promise((resolve) => {
75
- setTimeout(resolve, ms);
76
- });
77
- };
78
- const ok = (value) => {
79
- return { content: [{ text: JSON.stringify(value, void 0, 2), type: "text" }] };
80
- };
81
- const fail = (text) => {
82
- return { content: [{ text, type: "text" }], isError: true };
83
- };
84
- const readString = (input, key) => {
85
- const value = input[key];
86
- return typeof value === "string" && value.length > 0 ? value : void 0;
87
- };
88
- const finalAnswer = (messages) => {
89
- for (let index = messages.length - 1; index >= 0; index -= 1) {
90
- const row = messages[index];
91
- const toolCalls = row?.["toolCalls"];
92
- const pending = Array.isArray(toolCalls) && toolCalls.length > 0;
93
- if (row?.["role"] === "assistant" && !pending) {
94
- return typeof row["content"] === "string" ? row["content"] : "";
95
- }
96
- }
97
- return "";
98
- };
99
- const readThreadStatus = (thread) => {
100
- if (thread !== null && typeof thread === "object" && typeof thread.status === "string") {
101
- return thread.status;
102
- }
103
- return "unknown";
104
- };
105
- const readTerminal = async (client, threadKey, status, thread) => {
106
- const messages = await client.query(reference(AGENT_MESSAGES_PATH), { key: threadKey });
107
- if (status === "error") {
108
- const error = thread !== null && typeof thread === "object" ? thread.error : void 0;
109
- return ok({ error: typeof error === "string" ? error : "the agent run failed", status, threadKey });
110
- }
111
- return ok({ status, text: finalAnswer(messages), threadKey });
112
- };
113
- const callAgentStatus = async (client, input) => {
114
- const threadKey = readString(input, "threadKey");
115
- if (threadKey === void 0) {
116
- return fail('"threadKey" is required and must be a non-empty string');
117
- }
118
- const thread = await client.query(reference(AGENT_THREAD_PATH), { key: threadKey });
119
- const status = readThreadStatus(thread);
120
- if (TERMINAL_STATUSES.has(status)) {
121
- return readTerminal(client, threadKey, status, thread);
122
- }
123
- return ok({ status: status === "unknown" ? "running" : status, threadKey });
124
- };
125
- const callAgentTool = async (client, name, input, options) => {
126
- try {
127
- if (options.allowAgents !== true) {
128
- return fail(`tool "${name}" is disabled: agent tools are off. Enable them with the LUNORA_MCP_ALLOW_AGENTS env var.`);
129
- }
130
- if (name === AGENT_STATUS_TOOL_NAME) {
131
- return await callAgentStatus(client, input);
132
- }
133
- const exposure = options.exposures.find((candidate) => agentToolName(candidate) === name);
134
- if (exposure === void 0) {
135
- return fail(`agent tool "${name}" is not exposed by this MCP server`);
136
- }
137
- const prompt = readString(input, "prompt");
138
- if (prompt === void 0) {
139
- return fail('"prompt" is required and must be a non-empty string');
140
- }
141
- const threadKey = readString(input, "threadKey") ?? freshThreadKey();
142
- const title = readString(input, "title");
143
- const { id } = await client.mutation(reference(AGENT_RUN_PATH), {
144
- agent: exposure.name,
145
- input: prompt,
146
- threadKey,
147
- ...title === void 0 ? {} : { title }
148
- });
149
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
150
- const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
151
- const wait = options.wait ?? defaultWait;
152
- const maxPolls = Math.max(1, Math.ceil(maxWaitMs / pollIntervalMs));
153
- for (let attempt = 0; attempt < maxPolls; attempt += 1) {
154
- const thread = await client.query(reference(AGENT_THREAD_PATH), { key: threadKey });
155
- const status = readThreadStatus(thread);
156
- if (TERMINAL_STATUSES.has(status)) {
157
- return await readTerminal(client, threadKey, status, thread);
158
- }
159
- await wait(pollIntervalMs);
160
- }
161
- return ok({ hint: "call lunora_agent_status with this threadKey to poll for the answer", runId: id, status: "running", threadKey });
162
- } catch (error) {
163
- const message = error instanceof Error ? error.message : String(error);
164
- return fail(message);
165
- }
166
- };
167
-
168
- export { AGENT_RUN_INPUT_SCHEMA, AGENT_STATUS_TOOL_NAME, agentToolDefinitions, callAgentTool, finalAnswer, isAgentToolName, parseAgentsEnv };