@lunora/mcp 1.0.0-alpha.21 → 1.0.0-alpha.23

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/README.md CHANGED
@@ -40,14 +40,16 @@ Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-sa
40
40
 
41
41
  ## Tools
42
42
 
43
- | Tool | Description |
44
- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- |
45
- | `lunora_list_functions` | List the deployment's public functions (queries, mutations, actions) with their kinds. |
46
- | `lunora_list_tables` | List the deployment's `.global()` tables with their row counts. |
47
- | `lunora_get_function_schema` | Return a function's argument descriptors and kind by path, so a caller can construct a valid arguments object. |
48
- | `lunora_run_query` | Run a query and return its result. Read-only. |
49
- | `lunora_run_mutation` | Run a mutation and return its result. Writes data — use with care. |
50
- | `lunora_run_action` | Run an action and return its result. May call external services. |
43
+ | Tool | Description |
44
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | `lunora_list_functions` | List the deployment's public functions (queries, mutations, actions) with their kinds. |
46
+ | `lunora_list_tables` | List the deployment's `.global()` tables with their row counts. |
47
+ | `lunora_get_function_schema` | Return a function's argument descriptors and kind by path, so a caller can construct a valid arguments object. |
48
+ | `lunora_run_query` | Run a query and return its result. Read-only. |
49
+ | `lunora_run_mutation` | Run a mutation and return its result. Writes data — use with care. |
50
+ | `lunora_run_action` | Run an action and return its result. May call external services. |
51
+ | `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. |
52
+ | `lunora_agent_status` | Poll a running agent by `threadKey` and return its answer once finished. Requires agents enabled. |
51
53
 
52
54
  ### Recommended agent flow
53
55
 
@@ -105,6 +107,36 @@ const server = createLunoraMcpServer({ url: "https://app.example.workers.dev", t
105
107
  await server.connect(myTransport);
106
108
  ```
107
109
 
110
+ ## Expose an agent
111
+
112
+ 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.
113
+
114
+ Enable it with two env vars (or the matching `createLunoraMcpServer` options):
115
+
116
+ - `LUNORA_MCP_ALLOW_AGENTS` — set to `1`/`true`/`yes`/`on` to expose the agent tools. Default: agents disabled.
117
+ - `LUNORA_MCP_AGENTS` — a `;`-separated list of `name:description` pairs selecting which agents to expose, e.g. `"support:Support questions;billing:Billing help"`.
118
+ - `LUNORA_MCP_AGENT_TIMEOUT_MS` (optional) — wall-clock budget a single `agent_<name>` call awaits before returning a pending result to poll.
119
+
120
+ ```jsonc
121
+ {
122
+ "mcpServers": {
123
+ "lunora": {
124
+ "command": "lunora-mcp",
125
+ "env": {
126
+ "LUNORA_URL": "https://app.example.workers.dev",
127
+ "LUNORA_ADMIN_TOKEN": "...",
128
+ "LUNORA_MCP_ALLOW_AGENTS": "1",
129
+ "LUNORA_MCP_AGENTS": "support:Support questions;billing:Billing help",
130
+ },
131
+ },
132
+ },
133
+ }
134
+ ```
135
+
136
+ 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.
137
+
138
+ Runs are **owner-scoped** to the identity the configured token resolves to. Grant a **least-privilege** token mapped to a bot identity so an agent's threads stay isolated per deployment — never the admin token.
139
+
108
140
  > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs)**.
109
141
 
110
142
  ## Related
@@ -112,6 +144,7 @@ await server.connect(myTransport);
112
144
  - [`@lunora/client`](https://www.npmjs.com/package/@lunora/client) — the HTTP RPC client backing every tool.
113
145
  - [`@lunora/cli`](https://www.npmjs.com/package/@lunora/cli) — deploy the app the server introspects and invokes.
114
146
  - [`@lunora/server`](https://www.npmjs.com/package/@lunora/server) — defines the queries, mutations, and actions the tools call.
147
+ - [`@lunora/agent`](https://www.npmjs.com/package/@lunora/agent) — the durable agents the `agent_<name>` tools front over RPC.
115
148
 
116
149
  ## Supported Node.js Versions
117
150
 
package/dist/bin.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { connectStdio } from './packem_shared/connectStdio-mHv2dT3u.mjs';
2
+ import { parseAgentsEnv } from './packem_shared/AGENT_RUN_INPUT_SCHEMA-DWobae3P.mjs';
3
+ import { connectStdio } from './packem_shared/connectStdio-bkJcaXYE.mjs';
3
4
 
4
5
  const ENABLED_ENV_VALUES = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
5
6
  const isEnvEnabled = (value) => value !== void 0 && ENABLED_ENV_VALUES.has(value.trim().toLowerCase());
@@ -21,8 +22,17 @@ const runBin = async (environment, dependencies = {}) => {
21
22
  writeError("lunora-mcp: LUNORA_URL environment variable is required\n");
22
23
  throw new BinError("LUNORA_URL environment variable is required", 1);
23
24
  }
25
+ const rawTimeout = Number(environment.LUNORA_MCP_AGENT_TIMEOUT_MS);
26
+ const agentMaxWaitMs = Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : void 0;
24
27
  try {
25
- await connect({ allowWrites: isEnvEnabled(environment.LUNORA_MCP_ALLOW_WRITES), token: environment.LUNORA_ADMIN_TOKEN, url });
28
+ await connect({
29
+ agents: parseAgentsEnv(environment.LUNORA_MCP_AGENTS),
30
+ allowAgents: isEnvEnabled(environment.LUNORA_MCP_ALLOW_AGENTS),
31
+ allowWrites: isEnvEnabled(environment.LUNORA_MCP_ALLOW_WRITES),
32
+ token: environment.LUNORA_ADMIN_TOKEN,
33
+ url,
34
+ ...agentMaxWaitMs === void 0 ? {} : { agentMaxWaitMs }
35
+ });
26
36
  } catch (error) {
27
37
  const message = error instanceof Error ? error.message : String(error);
28
38
  writeError(`lunora-mcp: failed to start — ${message}
package/dist/index.d.mts CHANGED
@@ -1,19 +1,8 @@
1
+ import { LunoraClient } from '@lunora/client';
1
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
3
  import '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
3
- import { LunoraClient } from '@lunora/client';
4
4
  import { X402ChargeConfig, X402Price } from '@lunora/x402/charge';
5
5
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
6
- interface LunoraMcpServerOptions {
7
- allowWrites?: boolean;
8
- client?: LunoraClient;
9
- fetch?: typeof fetch;
10
- token?: string;
11
- url?: string;
12
- }
13
- declare const createLunoraMcpServer: (options: LunoraMcpServerOptions) => Server;
14
- declare const connectStdio: (options: LunoraMcpServerOptions) => Promise<Server>;
15
- type McpFetchHandler = (request: Request) => Promise<Response>;
16
- declare const createMcpFetchHandler: (options: LunoraMcpServerOptions) => McpFetchHandler;
17
6
  interface ToolInputSchema {
18
7
  properties: Record<string, unknown>;
19
8
  required?: ReadonlyArray<string>;
@@ -35,6 +24,38 @@ declare const READ_ONLY_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
35
24
  declare const WRITE_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
36
25
  declare const toolDefinitions: (allowWrites: boolean) => ReadonlyArray<ToolDefinition>;
37
26
  declare const callTool: (client: LunoraClient, name: string, input: Record<string, unknown>, allowWrites?: boolean) => Promise<ToolResult>;
27
+ interface McpAgentExposure {
28
+ description: string;
29
+ name: string;
30
+ toolName?: string;
31
+ }
32
+ declare const AGENT_STATUS_TOOL_NAME = "lunora_agent_status";
33
+ declare const AGENT_RUN_INPUT_SCHEMA: ToolInputSchema;
34
+ declare const parseAgentsEnv: (raw: string | undefined) => McpAgentExposure[];
35
+ declare const agentToolDefinitions: (exposures: ReadonlyArray<McpAgentExposure>, allowAgents: boolean) => ReadonlyArray<ToolDefinition>;
36
+ interface CallAgentToolOptions {
37
+ allowAgents: boolean;
38
+ exposures: ReadonlyArray<McpAgentExposure>;
39
+ maxWaitMs?: number;
40
+ pollIntervalMs?: number;
41
+ wait?: (ms: number) => Promise<void>;
42
+ }
43
+ declare const callAgentTool: (client: LunoraClient, name: string, input: Record<string, unknown>, options: CallAgentToolOptions) => Promise<ToolResult>;
44
+ interface LunoraMcpServerOptions {
45
+ agentMaxWaitMs?: number;
46
+ agentPollIntervalMs?: number;
47
+ agents?: ReadonlyArray<McpAgentExposure>;
48
+ allowAgents?: boolean;
49
+ allowWrites?: boolean;
50
+ client?: LunoraClient;
51
+ fetch?: typeof fetch;
52
+ token?: string;
53
+ url?: string;
54
+ }
55
+ declare const createLunoraMcpServer: (options: LunoraMcpServerOptions) => Server;
56
+ declare const connectStdio: (options: LunoraMcpServerOptions) => Promise<Server>;
57
+ type McpFetchHandler = (request: Request) => Promise<Response>;
58
+ declare const createMcpFetchHandler: (options: LunoraMcpServerOptions) => McpFetchHandler;
38
59
  type ToolHandler = (arguments_: Record<string, unknown>) => Promise<ToolResult> | ToolResult;
39
60
  interface RegisterToolOptions {
40
61
  annotations?: Tool["annotations"];
@@ -59,4 +80,4 @@ interface PaidMcpServer {
59
80
  tool: (options: RegisterToolOptions, handler: ToolHandler) => void;
60
81
  }
61
82
  declare const createPaidMcpServer: (config: PaidMcpServerConfig) => PaidMcpServer;
62
- export { type LunoraMcpServerOptions, type McpFetchHandler, type PaidMcpChargeConfig, type PaidMcpServer, type PaidMcpServerConfig, READ_ONLY_TOOL_DEFINITIONS, type RegisterPaidToolOptions, type RegisterToolOptions, type ToolDefinition, type ToolHandler, type ToolInputSchema, type ToolResult, WRITE_TOOL_DEFINITIONS, callTool, connectStdio, createLunoraMcpServer, createMcpFetchHandler, createPaidMcpServer, toolDefinitions };
83
+ export { AGENT_RUN_INPUT_SCHEMA, AGENT_STATUS_TOOL_NAME, type CallAgentToolOptions, type LunoraMcpServerOptions, type McpAgentExposure, type McpFetchHandler, type PaidMcpChargeConfig, type PaidMcpServer, type PaidMcpServerConfig, READ_ONLY_TOOL_DEFINITIONS, type RegisterPaidToolOptions, type RegisterToolOptions, type ToolDefinition, type ToolHandler, type ToolInputSchema, type ToolResult, WRITE_TOOL_DEFINITIONS, agentToolDefinitions, callAgentTool, callTool, connectStdio, createLunoraMcpServer, createMcpFetchHandler, createPaidMcpServer, parseAgentsEnv, toolDefinitions };
package/dist/index.d.ts CHANGED
@@ -1,19 +1,8 @@
1
+ import { LunoraClient } from '@lunora/client';
1
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
3
  import '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
3
- import { LunoraClient } from '@lunora/client';
4
4
  import { X402ChargeConfig, X402Price } from '@lunora/x402/charge';
5
5
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
6
- interface LunoraMcpServerOptions {
7
- allowWrites?: boolean;
8
- client?: LunoraClient;
9
- fetch?: typeof fetch;
10
- token?: string;
11
- url?: string;
12
- }
13
- declare const createLunoraMcpServer: (options: LunoraMcpServerOptions) => Server;
14
- declare const connectStdio: (options: LunoraMcpServerOptions) => Promise<Server>;
15
- type McpFetchHandler = (request: Request) => Promise<Response>;
16
- declare const createMcpFetchHandler: (options: LunoraMcpServerOptions) => McpFetchHandler;
17
6
  interface ToolInputSchema {
18
7
  properties: Record<string, unknown>;
19
8
  required?: ReadonlyArray<string>;
@@ -35,6 +24,38 @@ declare const READ_ONLY_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
35
24
  declare const WRITE_TOOL_DEFINITIONS: ReadonlyArray<ToolDefinition>;
36
25
  declare const toolDefinitions: (allowWrites: boolean) => ReadonlyArray<ToolDefinition>;
37
26
  declare const callTool: (client: LunoraClient, name: string, input: Record<string, unknown>, allowWrites?: boolean) => Promise<ToolResult>;
27
+ interface McpAgentExposure {
28
+ description: string;
29
+ name: string;
30
+ toolName?: string;
31
+ }
32
+ declare const AGENT_STATUS_TOOL_NAME = "lunora_agent_status";
33
+ declare const AGENT_RUN_INPUT_SCHEMA: ToolInputSchema;
34
+ declare const parseAgentsEnv: (raw: string | undefined) => McpAgentExposure[];
35
+ declare const agentToolDefinitions: (exposures: ReadonlyArray<McpAgentExposure>, allowAgents: boolean) => ReadonlyArray<ToolDefinition>;
36
+ interface CallAgentToolOptions {
37
+ allowAgents: boolean;
38
+ exposures: ReadonlyArray<McpAgentExposure>;
39
+ maxWaitMs?: number;
40
+ pollIntervalMs?: number;
41
+ wait?: (ms: number) => Promise<void>;
42
+ }
43
+ declare const callAgentTool: (client: LunoraClient, name: string, input: Record<string, unknown>, options: CallAgentToolOptions) => Promise<ToolResult>;
44
+ interface LunoraMcpServerOptions {
45
+ agentMaxWaitMs?: number;
46
+ agentPollIntervalMs?: number;
47
+ agents?: ReadonlyArray<McpAgentExposure>;
48
+ allowAgents?: boolean;
49
+ allowWrites?: boolean;
50
+ client?: LunoraClient;
51
+ fetch?: typeof fetch;
52
+ token?: string;
53
+ url?: string;
54
+ }
55
+ declare const createLunoraMcpServer: (options: LunoraMcpServerOptions) => Server;
56
+ declare const connectStdio: (options: LunoraMcpServerOptions) => Promise<Server>;
57
+ type McpFetchHandler = (request: Request) => Promise<Response>;
58
+ declare const createMcpFetchHandler: (options: LunoraMcpServerOptions) => McpFetchHandler;
38
59
  type ToolHandler = (arguments_: Record<string, unknown>) => Promise<ToolResult> | ToolResult;
39
60
  interface RegisterToolOptions {
40
61
  annotations?: Tool["annotations"];
@@ -59,4 +80,4 @@ interface PaidMcpServer {
59
80
  tool: (options: RegisterToolOptions, handler: ToolHandler) => void;
60
81
  }
61
82
  declare const createPaidMcpServer: (config: PaidMcpServerConfig) => PaidMcpServer;
62
- export { type LunoraMcpServerOptions, type McpFetchHandler, type PaidMcpChargeConfig, type PaidMcpServer, type PaidMcpServerConfig, READ_ONLY_TOOL_DEFINITIONS, type RegisterPaidToolOptions, type RegisterToolOptions, type ToolDefinition, type ToolHandler, type ToolInputSchema, type ToolResult, WRITE_TOOL_DEFINITIONS, callTool, connectStdio, createLunoraMcpServer, createMcpFetchHandler, createPaidMcpServer, toolDefinitions };
83
+ export { AGENT_RUN_INPUT_SCHEMA, AGENT_STATUS_TOOL_NAME, type CallAgentToolOptions, type LunoraMcpServerOptions, type McpAgentExposure, type McpFetchHandler, type PaidMcpChargeConfig, type PaidMcpServer, type PaidMcpServerConfig, READ_ONLY_TOOL_DEFINITIONS, type RegisterPaidToolOptions, type RegisterToolOptions, type ToolDefinition, type ToolHandler, type ToolInputSchema, type ToolResult, WRITE_TOOL_DEFINITIONS, agentToolDefinitions, callAgentTool, callTool, connectStdio, createLunoraMcpServer, createMcpFetchHandler, createPaidMcpServer, parseAgentsEnv, toolDefinitions };
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- export { createMcpFetchHandler } from './packem_shared/createMcpFetchHandler-CGqlLHxj.mjs';
2
- export { createPaidMcpServer } from './packem_shared/createPaidMcpServer-S7RdldZo.mjs';
3
- export { connectStdio, createLunoraMcpServer } from './packem_shared/connectStdio-mHv2dT3u.mjs';
4
- export { READ_ONLY_TOOL_DEFINITIONS, WRITE_TOOL_DEFINITIONS, callTool, toolDefinitions } from './packem_shared/READ_ONLY_TOOL_DEFINITIONS-B5x61zt4.mjs';
1
+ export { AGENT_RUN_INPUT_SCHEMA, AGENT_STATUS_TOOL_NAME, agentToolDefinitions, callAgentTool, parseAgentsEnv } from './packem_shared/AGENT_RUN_INPUT_SCHEMA-DWobae3P.mjs';
2
+ export { createMcpFetchHandler } from './packem_shared/createMcpFetchHandler-DfzBSLrm.mjs';
3
+ export { createPaidMcpServer } from './packem_shared/createPaidMcpServer-b-qp_aEF.mjs';
4
+ export { connectStdio, createLunoraMcpServer } from './packem_shared/connectStdio-bkJcaXYE.mjs';
5
+ export { READ_ONLY_TOOL_DEFINITIONS, WRITE_TOOL_DEFINITIONS, callTool, toolDefinitions } from './packem_shared/READ_ONLY_TOOL_DEFINITIONS-CWb_wFd5.mjs';
@@ -0,0 +1,168 @@
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 };
@@ -66,23 +66,82 @@ const readFunctionPath = (input) => {
66
66
  }
67
67
  return functionPath;
68
68
  };
69
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
70
+ const describeArgs = (value) => Array.isArray(value) ? "an array" : `a ${typeof value}`;
71
+ const readArgumentsBag = (raw) => {
72
+ if (raw === void 0 || raw === null) {
73
+ return {};
74
+ }
75
+ if (typeof raw === "string") {
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(raw);
79
+ } catch {
80
+ throw new LunoraError("BAD_REQUEST", `"args" must be a JSON object; received a string that is not valid JSON`);
81
+ }
82
+ if (!isPlainObject(parsed)) {
83
+ throw new LunoraError("BAD_REQUEST", `"args" must be a JSON object; the provided string decoded to ${describeArgs(parsed)}`);
84
+ }
85
+ return parsed;
86
+ }
87
+ if (!isPlainObject(raw)) {
88
+ throw new LunoraError("BAD_REQUEST", `"args" must be a JSON object, got ${describeArgs(raw)}`);
89
+ }
90
+ return raw;
91
+ };
69
92
  const readRunArguments = (input) => {
70
93
  const functionPath = readFunctionPath(input);
71
- const rawArguments = input.args;
72
- const isPlainObject = typeof rawArguments === "object" && rawArguments !== null && !Array.isArray(rawArguments);
73
- const args = isPlainObject ? rawArguments : {};
94
+ const args = readArgumentsBag(input.args);
74
95
  const shardKey = typeof input.shardKey === "string" && input.shardKey.length > 0 ? input.shardKey : void 0;
75
96
  return { args, functionPath, shardKey };
76
97
  };
77
98
  const reference = (functionPath) => {
78
99
  return { __lunoraRef: functionPath };
79
100
  };
101
+ const bytesToBase64 = (bytes) => {
102
+ let binary = "";
103
+ const chunk = 32768;
104
+ for (let index = 0; index < bytes.length; index += chunk) {
105
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
106
+ }
107
+ return btoa(binary);
108
+ };
109
+ const jsonResultReplacer = (_key, value) => {
110
+ if (typeof value === "bigint") {
111
+ return value.toString();
112
+ }
113
+ if (value instanceof ArrayBuffer) {
114
+ return bytesToBase64(new Uint8Array(value));
115
+ }
116
+ if (ArrayBuffer.isView(value)) {
117
+ const view = value;
118
+ return bytesToBase64(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
119
+ }
120
+ return value;
121
+ };
80
122
  const ok = (value) => {
81
- const text = value === void 0 ? "null" : JSON.stringify(value, void 0, 2);
123
+ const text = value === void 0 ? "null" : JSON.stringify(value, jsonResultReplacer, 2);
82
124
  return { content: [{ text, type: "text" }] };
83
125
  };
126
+ const FUNCTIONS_CACHE_TTL_MS = 3e4;
127
+ const functionsCache = /* @__PURE__ */ new WeakMap();
128
+ const listFunctionsCached = (client) => {
129
+ const now = Date.now();
130
+ const cached = functionsCache.get(client);
131
+ if (cached !== void 0 && cached.expiresAt > now) {
132
+ return cached.promise;
133
+ }
134
+ const promise = client.listFunctions().catch((error) => {
135
+ if (functionsCache.get(client)?.promise === promise) {
136
+ functionsCache.delete(client);
137
+ }
138
+ throw error;
139
+ });
140
+ functionsCache.set(client, { expiresAt: now + FUNCTIONS_CACHE_TTL_MS, promise });
141
+ return promise;
142
+ };
84
143
  const assertRunnable = async (client, functionPath, expectedKind) => {
85
- const functions = await client.listFunctions();
144
+ const functions = await listFunctionsCached(client);
86
145
  const descriptor = functions.find((function_) => function_.path === functionPath);
87
146
  if (descriptor === void 0) {
88
147
  throw new LunoraError("NOT_FOUND", `function not found or not public: ${functionPath}`);
@@ -104,7 +163,7 @@ const callTool = async (client, name, input, allowWrites = false) => {
104
163
  switch (name) {
105
164
  case "lunora_get_function_schema": {
106
165
  const functionPath = readFunctionPath(input);
107
- const functions = await client.listFunctions();
166
+ const functions = await listFunctionsCached(client);
108
167
  const descriptor = functions.find((function_) => function_.path === functionPath);
109
168
  if (descriptor === void 0) {
110
169
  return { content: [{ text: `function not found: ${functionPath}`, type: "text" }], isError: true };
@@ -112,7 +171,7 @@ const callTool = async (client, name, input, allowWrites = false) => {
112
171
  return ok({ args: descriptor.args ?? [], kind: descriptor.kind, path: descriptor.path });
113
172
  }
114
173
  case "lunora_list_functions": {
115
- return ok(await client.listFunctions());
174
+ return ok(await listFunctionsCached(client));
116
175
  }
117
176
  case "lunora_list_tables": {
118
177
  return ok(await client.listGlobalTables());
@@ -6,7 +6,8 @@ import { LunoraError } from '@lunora/errors';
6
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
8
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
9
- import { toolDefinitions, callTool } from './READ_ONLY_TOOL_DEFINITIONS-B5x61zt4.mjs';
9
+ import { agentToolDefinitions, isAgentToolName, callAgentTool } from './AGENT_RUN_INPUT_SCHEMA-DWobae3P.mjs';
10
+ import { toolDefinitions, callTool } from './READ_ONLY_TOOL_DEFINITIONS-CWb_wFd5.mjs';
10
11
 
11
12
  const resolveVersion = () => {
12
13
  try {
@@ -47,12 +48,21 @@ const resolveClient = (options) => {
47
48
  const createLunoraMcpServer = (options) => {
48
49
  const client = resolveClient(options);
49
50
  const allowWrites = options.allowWrites ?? false;
51
+ const allowAgents = options.allowAgents ?? false;
52
+ const agents = options.agents ?? [];
50
53
  const server = new Server(SERVER_INFO, { capabilities: { tools: {} } });
51
54
  server.setRequestHandler(ListToolsRequestSchema, () => {
52
- return { tools: [...toolDefinitions(allowWrites)] };
55
+ return { tools: [...toolDefinitions(allowWrites), ...agentToolDefinitions(agents, allowAgents)] };
53
56
  });
54
57
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
55
- const result = await callTool(client, request.params.name, request.params.arguments ?? {}, allowWrites);
58
+ const { arguments: rawArguments, name } = request.params;
59
+ const input = rawArguments ?? {};
60
+ const result = isAgentToolName(name, agents) ? await callAgentTool(client, name, input, {
61
+ allowAgents,
62
+ exposures: agents,
63
+ ...options.agentMaxWaitMs === void 0 ? {} : { maxWaitMs: options.agentMaxWaitMs },
64
+ ...options.agentPollIntervalMs === void 0 ? {} : { pollIntervalMs: options.agentPollIntervalMs }
65
+ }) : await callTool(client, name, input, allowWrites);
56
66
  return result;
57
67
  });
58
68
  return server;
@@ -1,5 +1,5 @@
1
1
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
2
- import { createLunoraMcpServer } from './connectStdio-mHv2dT3u.mjs';
2
+ import { createLunoraMcpServer } from './connectStdio-bkJcaXYE.mjs';
3
3
 
4
4
  const serveStateless = async (server, request, options) => {
5
5
  const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true, sessionIdGenerator: void 0 });
@@ -2,7 +2,7 @@ import { LunoraError } from '@lunora/errors';
2
2
  import { createChargeMiddleware } from '@lunora/x402/charge';
3
3
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
4
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
- import { serveStateless } from './createMcpFetchHandler-CGqlLHxj.mjs';
5
+ import { serveStateless } from './createMcpFetchHandler-DfzBSLrm.mjs';
6
6
 
7
7
  const DEFAULT_SERVER_INFO = { name: "lunora-paid-mcp", version: "0.0.0" };
8
8
  const CALL_TOOL_METHOD = "tools/call";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/mcp",
3
- "version": "1.0.0-alpha.21",
3
+ "version": "1.0.0-alpha.23",
4
4
  "description": "Model Context Protocol server exposing a Lunora deployment to AI agents",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -49,9 +49,9 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@lunora/client": "1.0.0-alpha.20",
53
- "@lunora/errors": "1.0.0-alpha.3",
54
- "@lunora/x402": "1.0.0-alpha.1",
52
+ "@lunora/client": "1.0.0-alpha.21",
53
+ "@lunora/errors": "1.0.0-alpha.4",
54
+ "@lunora/x402": "1.0.0-alpha.2",
55
55
  "@modelcontextprotocol/sdk": "^1.29.0"
56
56
  },
57
57
  "engines": {