@lunora/mcp 1.0.0-alpha.14 → 1.0.0-alpha.140

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 (32) hide show
  1. package/LICENSE.md +126 -0
  2. package/README.md +229 -10
  3. package/dist/bin.mjs +4 -37
  4. package/dist/docs/index.d.mts +258 -0
  5. package/dist/docs/index.d.ts +258 -0
  6. package/dist/docs/index.mjs +1 -0
  7. package/dist/index.d.mts +516 -16
  8. package/dist/index.d.ts +516 -16
  9. package/dist/index.mjs +1 -2
  10. package/dist/packem_shared/AGENT_RUN_INPUT_SCHEMA-hKbpa3Dg.mjs +1 -0
  11. package/dist/packem_shared/DEFAULT_DOCS_BASE_URL-CZ3fVsSc.mjs +4 -0
  12. package/dist/packem_shared/DEFAULT_MAX_REQUEST_BYTES-CbbpkHRK.mjs +1 -0
  13. package/dist/packem_shared/DEFAULT_SEARCH_LIMIT-BqSYN5vr.mjs +3 -0
  14. package/dist/packem_shared/DOCS_SERVER_NAME-BMeAWi5h.mjs +1 -0
  15. package/dist/packem_shared/DOCS_URI_SCHEME-Buo752CV.mjs +3 -0
  16. package/dist/packem_shared/LOCAL_SERVER_NAME-BbopFImI.mjs +1 -0
  17. package/dist/packem_shared/OBSERVABILITY_TOOL_DEFINITIONS-mO4JuHgV.mjs +1 -0
  18. package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-DKcVAOe2.mjs +1 -0
  19. package/dist/packem_shared/connectStdio-CQI9JGJC.mjs +1 -0
  20. package/dist/packem_shared/createAuthedMcpFetchHandler-Mfw8jlOV.mjs +1 -0
  21. package/dist/packem_shared/createMcpFetchHandler-C5doZK5c.mjs +1 -0
  22. package/dist/packem_shared/createPaidMcpServer-Ba1gMM4Y.mjs +1 -0
  23. package/dist/packem_shared/createToolServer-BtGuPyMU.mjs +1 -0
  24. package/dist/packem_shared/observability-tools-D4Z8dGS-.mjs +1 -0
  25. package/dist/packem_shared/promise-memo-OqVPWoUX.mjs +1 -0
  26. package/dist/packem_shared/serve-stateless.d-CKsbI0fP.d.mts +154 -0
  27. package/dist/packem_shared/serve-stateless.d-CKsbI0fP.d.ts +154 -0
  28. package/dist/packem_shared/toDocsSearchHits-CBLmtWXt.mjs +1 -0
  29. package/dist/packem_shared/tools-D-JWERWj.mjs +2 -0
  30. package/package.json +17 -3
  31. package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-DV2VkuuU.mjs +0 -143
  32. package/dist/packem_shared/connectStdio-nl-Tt_T1.mjs +0 -65
@@ -0,0 +1 @@
1
+ import{serveStateless as c}from"./DEFAULT_MAX_REQUEST_BYTES-CbbpkHRK.mjs";import{DEFAULT_MAX_REQUEST_BYTES as u}from"./DEFAULT_MAX_REQUEST_BYTES-CbbpkHRK.mjs";import{resolveClient as s,createLunoraMcpServer as a}from"./connectStdio-CQI9JGJC.mjs";const l=e=>{const r=s(e);return t=>c(a({...e,client:r}),t,{maxRequestBytes:e.maxRequestBytes})};export{u as DEFAULT_MAX_REQUEST_BYTES,l as createMcpFetchHandler,c as serveStateless};
@@ -0,0 +1 @@
1
+ import{LunoraError as y}from"@lunora/errors";import{Server as E}from"@modelcontextprotocol/sdk/server/index.js";import{ListToolsRequestSchema as T,CallToolRequestSchema as g}from"@modelcontextprotocol/sdk/types.js";import{m as x}from"./promise-memo-OqVPWoUX.mjs";import{readScreenedBody as B,serveStateless as M}from"./DEFAULT_MAX_REQUEST_BYTES-CbbpkHRK.mjs";const C=async()=>{try{return(await import("@lunora/x402/charge")).createChargeMiddleware}catch(t){throw new y("INTERNAL",`paid MCP tools need the optional peer "@lunora/x402" — install it alongside @lunora/mcp to charge for tools (${t instanceof Error?t.message:String(t)})`)}},A={name:"lunora-paid-mcp",version:"0.0.0"},L="tools/call",f=t=>{if(typeof t!="object"||t===null)return;const{method:s,params:a}=t;if(s!==L||typeof a!="object"||a===null)return;const{name:c}=a;return typeof c=="string"?c:void 0},q=()=>Response.json({error:"A JSON-RPC batch may not reference a paid MCP tool; send paid tools/call requests individually."},{status:400}),j=t=>{const s=new Map,a=new Map,c=new Map,h=t.serverInfo??A,u=(e,r,n)=>{if(s.has(e.name))throw new y("BAD_REQUEST",`MCP tool "${e.name}" is already registered.`);const o={description:e.description,inputSchema:e.inputSchema,name:e.name};e.annotations!==void 0&&(o.annotations=e.annotations),s.set(e.name,{definition:o,handler:r}),n!==void 0&&a.set(e.name,n)},v=()=>{const e=new E(h,{capabilities:{tools:{}}});return e.setRequestHandler(T,()=>({tools:[...s.values()].map(r=>r.definition)})),e.setRequestHandler(g,async r=>{const n=s.get(r.params.name);if(n===void 0)return{content:[{text:`unknown tool: ${r.params.name}`,type:"text"}],isError:!0};try{return await n.handler(r.params.arguments??{})}catch(o){return{content:[{text:o instanceof Error?o.message:String(o),type:"text"}],isError:!0}}}),e},w=(e,r)=>x(c,e,async()=>(await C())({...t.charge,price:r},{resource:e}));return{fetchHandler:async(e,r,n)=>{const o=await B(e.clone(),t.maxRequestBytes);if("response"in o)return o.response;const{parsedBody:i}=o,d=()=>M(v(),e,i===void 0?{maxRequestBytes:t.maxRequestBytes}:{maxRequestBytes:t.maxRequestBytes,parsedBody:i});if(Array.isArray(i))return i.some(m=>a.has(f(m)??""))?q():d();const l=f(i),p=l===void 0?void 0:a.get(l);if(l===void 0||p===void 0)return d();const R=await w(l,p),S=typeof n?.waitUntil=="function"?{waitUntil:m=>{n.waitUntil?.(m)}}:void 0;return R.handle(e,d,S)},paidTool:(e,r)=>{u(e,r,e.price)},tool:(e,r)=>{u(e,r)}}};export{j as createPaidMcpServer};
@@ -0,0 +1 @@
1
+ import{Server as l}from"@modelcontextprotocol/sdk/server/index.js";import{ListToolsRequestSchema as d,CallToolRequestSchema as f,ListResourcesRequestSchema as p,ReadResourceRequestSchema as R}from"@modelcontextprotocol/sdk/types.js";const v=(c,u,o)=>{const s=new Map;for(const e of u)s.has(e.definition.name)||s.set(e.definition.name,e);const m=[...s.values()].map(e=>e.definition),r=new l(c,{capabilities:o===void 0?{tools:{}}:{resources:{},tools:{}}});return r.setRequestHandler(d,()=>({tools:m})),r.setRequestHandler(f,async e=>{const{arguments:n,name:t}=e.params,i=s.get(t);if(i===void 0)return{content:[{text:`unknown tool: ${t}`,type:"text"}],isError:!0};try{return await i.handle(n??{})}catch(a){return{content:[{text:a instanceof Error?a.message:String(a),type:"text"}],isError:!0}}}),o!==void 0&&(r.setRequestHandler(p,async()=>({resources:[...await o.list()]})),r.setRequestHandler(R,async e=>{const{uri:n}=e.params,t=await o.read(n);if(t===void 0)throw new Error(`unknown resource: ${n}`);return{contents:[{mimeType:t.mimeType??"text/plain",text:t.text,uri:n}]}})),r};export{v as createToolServer};
@@ -0,0 +1 @@
1
+ import{LunoraError as N}from"@lunora/errors";import{ADMIN_FUNCTIONS as l}from"@lunora/shard-engine";const A=e=>{let t="";for(let s=0;s<e.length;s+=32768)t+=String.fromCharCode(...e.subarray(s,s+32768));return btoa(t)},R=e=>{const t=atob(e),r=new Uint8Array(t.length);for(let s=0;s<t.length;s+=1)r[s]=t.codePointAt(s)??0;return r},a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",F=e=>{let t="",r=0;const s=e.length-2;for(;r<s;r+=3){const n=e[r]<<16|e[r+1]<<8|e[r+2];t+=a.charAt(n>>18&63)+a.charAt(n>>12&63)+a.charAt(n>>6&63)+a.charAt(n&63)}const o=e.length-r;if(o===1){const n=e[r]<<16;t+=a.charAt(n>>18&63)+a.charAt(n>>12&63)}else if(o===2){const n=e[r]<<16|e[r+1]<<8;t+=a.charAt(n>>18&63)+a.charAt(n>>12&63)+a.charAt(n>>6&63)}return t},$=e=>{const t=e.replaceAll("-","+").replaceAll("_","/"),r=t+"=".repeat((4-t.length%4)%4);return R(r)},b=(e,t)=>{if(typeof t=="bigint")return t.toString();if(t instanceof ArrayBuffer)return A(new Uint8Array(t));if(ArrayBuffer.isView(t)){const r=t;return A(new Uint8Array(r.buffer,r.byteOffset,r.byteLength))}return t},U=e=>JSON.parse(JSON.stringify(e,b)),P=e=>({content:[{text:e===void 0?"null":JSON.stringify(e,b,2),type:"text"}]}),p=e=>({...P(e),structuredContent:U(e)}),J=e=>({content:[{text:e,type:"text"}],isError:!0}),u={destructiveHint:!1,idempotentHint:!0,openWorldHint:!0,readOnlyHint:!0},I=50,w=500,E=["1m","5m","15m","1h"],O=["open","resolved","ignored"],v=["trace","debug","log","info","warn","error","fatal"],g={description:"Shard to read from on a .shardBy()-partitioned deployment. Omit for the default (unsharded) shard — these reads are PER-SHARD, not deployment-wide.",type:"string"},y={description:`Maximum rows to return (default ${I.toString()}, clamped to ${w.toString()}).`,type:"number"},L=e=>{const t=typeof e=="number"?e:Number.NaN;return Number.isFinite(t)?Math.max(1,Math.min(Math.floor(t),w)):I},S=(e,t)=>t.includes(e)?e:void 0,T=e=>typeof e=="string"&&e.length>0?e:void 0,M={properties:{level:{description:`Keep only entries at this severity. One of: ${v.join(", ")}.`,type:"string"},limit:y,shardKey:g},type:"object"},k={properties:{functionPathPrefix:{description:'Keep only Issues whose function path starts with this, e.g. "messages:".',type:"string"},limit:y,shardKey:g,status:{description:`Triage status to keep. One of: ${O.join(", ")}. Default: all.`,type:"string"}},type:"object"},H={properties:{limit:y,shardKey:g},type:"object"},x={properties:{limit:y,range:{description:`Time window to report over. One of: ${E.join(", ")}. Default: 15m.`,type:"string"},shardKey:g},type:"object"},C={properties:{shardKey:g},type:"object"},j={properties:{dropped:{description:"Entries the shard's in-memory ring EVICTED before this read — they are gone and cannot be fetched. Non-zero means `entries` + `total` describe only the newest slice of what the deployment logged.",type:"number"},entries:{description:"Recent log entries, NEWEST FIRST: { level, message, timestamp, functionPath?, fields? }.",type:"array"},total:{description:"Entries still in the ring matching `level`, before `limit` narrowed them. NOT the number of lines logged — see `dropped`.",type:"number"}},required:["dropped","entries","total"],type:"object"},q={properties:{issues:{description:"Grouped error Issues, newest first: { hash, title, count, status, functionPath, lastSeen, … }.",type:"array"}},required:["issues"],type:"object"},B={properties:{advisories:{description:"Schema/query advisories: { id, level, title, detail, … }.",type:"array"},total:{description:"Advisories available before `limit` narrowed them.",type:"number"}},required:["advisories","total"],type:"object"},D={properties:{buckets:{description:"Combined throughput/latency series across the range.",type:"array"},capped:{description:"True when the deployment's tracked-statement cap was reached, so coverage is partial.",type:"boolean"},entries:{description:"Per-statement activity in the range, hottest first.",type:"array"},total:{description:"Statements available before `limit` narrowed them.",type:"number"},trackedStatements:{description:"Distinct statements the deployment is tracking.",type:"number"}},required:["entries","buckets"],type:"object"},G={properties:{migrations:{description:"Every declared migration with its applied/pending state.",type:"array"}},required:["migrations"],type:"object"},K=[{annotations:{...u,title:"Read recent logs"},description:"Read the deployment's recent log entries (newest first) after running a function, to see what it printed and where it failed. In-memory and per-shard: resets when the shard hibernates.",inputSchema:M,name:"lunora_get_logs",outputSchema:j},{annotations:{...u,title:"List grouped error Issues"},description:"List errors grouped into Issues by fingerprint, with occurrence counts and triage status — the first call when asking what is currently broken, rather than reading raw logs.",inputSchema:k,name:"lunora_get_issues",outputSchema:q},{annotations:{...u,title:"List schema and query advisories"},description:"List the deployment's schema/query advisories (missing indexes, unsafe policies, and similar lints) before or after changing the schema.",inputSchema:H,name:"lunora_get_advisories",outputSchema:B},{annotations:{...u,title:"Read query insights"},description:"Read per-statement execution counts and latency over a recent time window, to find which query is slow or hot before optimizing one.",inputSchema:x,name:"lunora_get_query_insights",outputSchema:D},{annotations:{...u,title:"Read migration status"},description:"Read which migrations have been applied and which are pending, to check whether a schema change has actually landed on the deployment.",inputSchema:C,name:"lunora_get_migration_status",outputSchema:G}],z=new Set(K.map(e=>e.name)),h=async(e,t,r,s)=>{const o={__lunoraRef:t};return e.query(o,r,{...s===void 0?{}:{shardKey:s}})},m=(e,t)=>{const r=e?.[t];return Array.isArray(r)?r:[]},W=async(e,t,r)=>{const s=T(r.shardKey),o=L(r.limit);switch(t){case"lunora_get_advisories":{const n=await h(e,l.getAdvisories,{},s),i=m(n,"advisories");return p({advisories:i.slice(0,o),total:i.length})}case"lunora_get_issues":{const n=S(r.status,O),i=T(r.functionPathPrefix),c=await h(e,l.getIssues,{limit:o,...n===void 0?{}:{status:n},...i===void 0?{}:{functionPathPrefix:i}},s);return p({issues:m(c,"issues")})}case"lunora_get_logs":{const n=S(r.level,v),i=await h(e,l.getLogs,{},s),c=m(i,"entries").filter(f=>n===void 0||f.level===n),{dropped:d}=i??{};return p({dropped:typeof d=="number"?d:0,entries:c.slice(0,o),total:c.length})}case"lunora_get_migration_status":{const n=await h(e,l.migrationStatus,{},s);return p({migrations:m(n,"migrations")})}case"lunora_get_query_insights":{const n=S(r.range,E),i=await h(e,l.getQueryInsights,{...n===void 0?{}:{range:n}},s),c=m(i,"entries"),{buckets:d,capped:f,trackedStatements:_}=i??{};return p({buckets:Array.isArray(d)?d:[],capped:f===!0,entries:c.slice(0,o),total:c.length,trackedStatements:typeof _=="number"?_:c.length})}default:throw new N("INTERNAL",`unknown observability tool: ${t}`)}};export{I as D,w as M,K as O,P as a,z as b,W as c,J as e,$ as f,p as o,F as t};
@@ -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,154 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { HandleRequestOptions } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
3
+ /**
4
+ * The MCP wire shapes every tool surface in this package speaks — the
5
+ * deployment tools (`./tools`), the agent tools (`./agent-tools`), and the
6
+ * documentation tools (`./docs`).
7
+ *
8
+ * They live in their own module (rather than in `./tools`) so a consumer that
9
+ * only wants the docs surface can import the types without dragging
10
+ * `@lunora/client` into its bundle. `./tools` re-exports them, so the package's
11
+ * public API is unchanged.
12
+ */
13
+ /** A JSON-Schema object describing a tool's arguments, per the MCP spec. */
14
+ interface ToolInputSchema {
15
+ properties: Record<string, unknown>;
16
+ required?: ReadonlyArray<string>;
17
+ type: "object";
18
+ }
19
+ /**
20
+ * MCP tool annotations — hints a client may use to decide how to present a tool
21
+ * (badge it read-only, confirm before a destructive call).
22
+ *
23
+ * They are hints, not enforcement: this package's actual guarantees are made at
24
+ * dispatch, where a write tool is refused unless `allowWrites` is set. These
25
+ * exist so a client can *show* the user what the server already enforces,
26
+ * instead of every tool looking equally dangerous.
27
+ */
28
+ interface ToolAnnotations {
29
+ /** The tool may perform irreversible changes. Only meaningful when `readOnlyHint` is false. */
30
+ destructiveHint?: boolean;
31
+ /** Repeating the call with the same arguments has no additional effect. */
32
+ idempotentHint?: boolean;
33
+ /** The tool reaches systems beyond this server (the network, external services). */
34
+ openWorldHint?: boolean;
35
+ /** The tool does not modify anything. */
36
+ readOnlyHint?: boolean;
37
+ /** Human-facing title, shown instead of the raw tool name. */
38
+ title?: string;
39
+ }
40
+ interface ToolDefinition {
41
+ annotations?: ToolAnnotations;
42
+ description: string;
43
+ inputSchema: ToolInputSchema;
44
+ name: string;
45
+ /**
46
+ * JSON Schema for the tool's `structuredContent`. Optional: a tool that
47
+ * emits only a text block declares none, and a client that negotiated an
48
+ * MCP revision older than `2025-06-18` ignores it either way.
49
+ */
50
+ outputSchema?: ToolInputSchema;
51
+ }
52
+ /** The MCP `CallToolResult` shape this package's tools return. */
53
+ interface ToolResult {
54
+ content: {
55
+ text: string;
56
+ type: "text";
57
+ }[];
58
+ isError?: boolean;
59
+ /**
60
+ * The machine-readable result, mirroring the text block. Present only for
61
+ * tools that declare an `outputSchema`; per the MCP spec it must be a JSON
62
+ * OBJECT (never a bare array) and must survive `JSON.stringify` — the
63
+ * transport serializes it, so an un-mapped `bigint` here fails the whole
64
+ * response rather than one field.
65
+ */
66
+ structuredContent?: Record<string, unknown>;
67
+ }
68
+ /** A tool: what to advertise in `ListTools`, and what to run on `CallTool`. */
69
+ interface McpTool {
70
+ definition: ToolDefinition;
71
+ handle: (input: Record<string, unknown>) => Promise<ToolResult>;
72
+ }
73
+ /** Name/version pair reported in the MCP `initialize` handshake. */
74
+ interface McpServerInfo {
75
+ name: string;
76
+ version: string;
77
+ }
78
+ /** One addressable document a client can list and read directly. */
79
+ interface McpResourceSummary {
80
+ description?: string;
81
+ mimeType?: string;
82
+ name: string;
83
+ uri: string;
84
+ }
85
+ /**
86
+ * A set of resources the server exposes alongside its tools.
87
+ *
88
+ * Tools are calls a model chooses to make; resources are documents a *client*
89
+ * can enumerate and attach on its own — the user picking a page to put in
90
+ * context, without the model having to guess a search query first. The same
91
+ * corpus is worth offering both ways.
92
+ */
93
+ interface McpResourceProvider {
94
+ /** Every resource, for `resources/list`. */
95
+ list: () => Promise<ReadonlyArray<McpResourceSummary>>;
96
+ /** One resource's body, or `undefined` when the uri is unknown. */
97
+ read: (uri: string) => Promise<{
98
+ mimeType?: string;
99
+ text: string;
100
+ } | undefined>;
101
+ }
102
+ /**
103
+ * Build an MCP `Server` that advertises and dispatches `tools`.
104
+ *
105
+ * On duplicate names the FIRST wins, so a caller can layer surfaces in
106
+ * precedence order without the later list silently shadowing the earlier one.
107
+ * Thrown errors become `isError` results rather than rejections, per the MCP
108
+ * convention that a tool failure is output the model can read and react to,
109
+ * not a protocol-level fault.
110
+ */
111
+ declare const createToolServer: (info: McpServerInfo, tools: ReadonlyArray<McpTool>, resources?: McpResourceProvider) => Server;
112
+ /** A Web-Standard fetch handler: takes a `Request`, returns the MCP `Response`. */
113
+ type McpFetchHandler = (request: Request) => Promise<Response>;
114
+ /**
115
+ * Largest request body served. Every MCP call is a short JSON-RPC message — a
116
+ * few hundred bytes — so this is orders of magnitude of headroom while still
117
+ * bounding what a caller can push through the parser.
118
+ */
119
+ declare const DEFAULT_MAX_REQUEST_BYTES: number;
120
+ /** What {@link serveStateless} accepts on top of the transport's own options. */
121
+ interface ServeStatelessOptions extends HandleRequestOptions {
122
+ /**
123
+ * Largest accepted request body, in bytes — enforced while the body streams
124
+ * in, not after it is buffered. Defaults to
125
+ * {@link DEFAULT_MAX_REQUEST_BYTES}, which a value that is not a
126
+ * non-negative safe integer also falls back to (see `resolveLimit`).
127
+ */
128
+ maxRequestBytes?: number;
129
+ }
130
+ /**
131
+ * Drive one request through a fresh **stateless** Streamable-HTTP transport bound
132
+ * to `server`, then tear both down.
133
+ *
134
+ * `sessionIdGenerator: undefined` → stateless (no cross-request session store);
135
+ * `enableJsonResponse: true` → the whole JSON-RPC response is buffered into the
136
+ * returned `Response`, so closing the transport/server afterwards can't truncate
137
+ * an in-flight body. Cleanup is fire-and-forget with swallowed rejections (a
138
+ * `.catch()`-terminated chain) so it never delays or masks the resolved response.
139
+ *
140
+ * The request is screened first ({@link screenRequest}); an oversized body or a
141
+ * JSON-RPC batch is refused here and no server is ever constructed for it.
142
+ *
143
+ * `options.parsedBody` lets a caller hand over a body it already read (e.g. the
144
+ * paid-tool gate, which peeks the JSON-RPC message to price the call) so the
145
+ * transport doesn't re-read a consumed stream. It is trusted as already
146
+ * screened, so read it with {@link readScreenedBody} — anything else hands this
147
+ * surface an unbounded body.
148
+ *
149
+ * Teardown runs in a `finally`: a rejection from `connect` or `handleRequest`
150
+ * would otherwise skip it and leak a server + transport per failed request,
151
+ * which on a public endpoint is exactly the request an attacker can repeat.
152
+ */
153
+ declare const serveStateless: (server: Server, request: Request, options?: ServeStatelessOptions) => Promise<Response>;
154
+ export { DEFAULT_MAX_REQUEST_BYTES as D, McpResourceProvider as M, ServeStatelessOptions as S, ToolDefinition as T, McpFetchHandler as a, McpTool as b, McpResourceSummary as c, McpServerInfo as d, ToolInputSchema as e, ToolResult as f, createToolServer as g, serveStateless as s };
@@ -0,0 +1,154 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { HandleRequestOptions } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
3
+ /**
4
+ * The MCP wire shapes every tool surface in this package speaks — the
5
+ * deployment tools (`./tools`), the agent tools (`./agent-tools`), and the
6
+ * documentation tools (`./docs`).
7
+ *
8
+ * They live in their own module (rather than in `./tools`) so a consumer that
9
+ * only wants the docs surface can import the types without dragging
10
+ * `@lunora/client` into its bundle. `./tools` re-exports them, so the package's
11
+ * public API is unchanged.
12
+ */
13
+ /** A JSON-Schema object describing a tool's arguments, per the MCP spec. */
14
+ interface ToolInputSchema {
15
+ properties: Record<string, unknown>;
16
+ required?: ReadonlyArray<string>;
17
+ type: "object";
18
+ }
19
+ /**
20
+ * MCP tool annotations — hints a client may use to decide how to present a tool
21
+ * (badge it read-only, confirm before a destructive call).
22
+ *
23
+ * They are hints, not enforcement: this package's actual guarantees are made at
24
+ * dispatch, where a write tool is refused unless `allowWrites` is set. These
25
+ * exist so a client can *show* the user what the server already enforces,
26
+ * instead of every tool looking equally dangerous.
27
+ */
28
+ interface ToolAnnotations {
29
+ /** The tool may perform irreversible changes. Only meaningful when `readOnlyHint` is false. */
30
+ destructiveHint?: boolean;
31
+ /** Repeating the call with the same arguments has no additional effect. */
32
+ idempotentHint?: boolean;
33
+ /** The tool reaches systems beyond this server (the network, external services). */
34
+ openWorldHint?: boolean;
35
+ /** The tool does not modify anything. */
36
+ readOnlyHint?: boolean;
37
+ /** Human-facing title, shown instead of the raw tool name. */
38
+ title?: string;
39
+ }
40
+ interface ToolDefinition {
41
+ annotations?: ToolAnnotations;
42
+ description: string;
43
+ inputSchema: ToolInputSchema;
44
+ name: string;
45
+ /**
46
+ * JSON Schema for the tool's `structuredContent`. Optional: a tool that
47
+ * emits only a text block declares none, and a client that negotiated an
48
+ * MCP revision older than `2025-06-18` ignores it either way.
49
+ */
50
+ outputSchema?: ToolInputSchema;
51
+ }
52
+ /** The MCP `CallToolResult` shape this package's tools return. */
53
+ interface ToolResult {
54
+ content: {
55
+ text: string;
56
+ type: "text";
57
+ }[];
58
+ isError?: boolean;
59
+ /**
60
+ * The machine-readable result, mirroring the text block. Present only for
61
+ * tools that declare an `outputSchema`; per the MCP spec it must be a JSON
62
+ * OBJECT (never a bare array) and must survive `JSON.stringify` — the
63
+ * transport serializes it, so an un-mapped `bigint` here fails the whole
64
+ * response rather than one field.
65
+ */
66
+ structuredContent?: Record<string, unknown>;
67
+ }
68
+ /** A tool: what to advertise in `ListTools`, and what to run on `CallTool`. */
69
+ interface McpTool {
70
+ definition: ToolDefinition;
71
+ handle: (input: Record<string, unknown>) => Promise<ToolResult>;
72
+ }
73
+ /** Name/version pair reported in the MCP `initialize` handshake. */
74
+ interface McpServerInfo {
75
+ name: string;
76
+ version: string;
77
+ }
78
+ /** One addressable document a client can list and read directly. */
79
+ interface McpResourceSummary {
80
+ description?: string;
81
+ mimeType?: string;
82
+ name: string;
83
+ uri: string;
84
+ }
85
+ /**
86
+ * A set of resources the server exposes alongside its tools.
87
+ *
88
+ * Tools are calls a model chooses to make; resources are documents a *client*
89
+ * can enumerate and attach on its own — the user picking a page to put in
90
+ * context, without the model having to guess a search query first. The same
91
+ * corpus is worth offering both ways.
92
+ */
93
+ interface McpResourceProvider {
94
+ /** Every resource, for `resources/list`. */
95
+ list: () => Promise<ReadonlyArray<McpResourceSummary>>;
96
+ /** One resource's body, or `undefined` when the uri is unknown. */
97
+ read: (uri: string) => Promise<{
98
+ mimeType?: string;
99
+ text: string;
100
+ } | undefined>;
101
+ }
102
+ /**
103
+ * Build an MCP `Server` that advertises and dispatches `tools`.
104
+ *
105
+ * On duplicate names the FIRST wins, so a caller can layer surfaces in
106
+ * precedence order without the later list silently shadowing the earlier one.
107
+ * Thrown errors become `isError` results rather than rejections, per the MCP
108
+ * convention that a tool failure is output the model can read and react to,
109
+ * not a protocol-level fault.
110
+ */
111
+ declare const createToolServer: (info: McpServerInfo, tools: ReadonlyArray<McpTool>, resources?: McpResourceProvider) => Server;
112
+ /** A Web-Standard fetch handler: takes a `Request`, returns the MCP `Response`. */
113
+ type McpFetchHandler = (request: Request) => Promise<Response>;
114
+ /**
115
+ * Largest request body served. Every MCP call is a short JSON-RPC message — a
116
+ * few hundred bytes — so this is orders of magnitude of headroom while still
117
+ * bounding what a caller can push through the parser.
118
+ */
119
+ declare const DEFAULT_MAX_REQUEST_BYTES: number;
120
+ /** What {@link serveStateless} accepts on top of the transport's own options. */
121
+ interface ServeStatelessOptions extends HandleRequestOptions {
122
+ /**
123
+ * Largest accepted request body, in bytes — enforced while the body streams
124
+ * in, not after it is buffered. Defaults to
125
+ * {@link DEFAULT_MAX_REQUEST_BYTES}, which a value that is not a
126
+ * non-negative safe integer also falls back to (see `resolveLimit`).
127
+ */
128
+ maxRequestBytes?: number;
129
+ }
130
+ /**
131
+ * Drive one request through a fresh **stateless** Streamable-HTTP transport bound
132
+ * to `server`, then tear both down.
133
+ *
134
+ * `sessionIdGenerator: undefined` → stateless (no cross-request session store);
135
+ * `enableJsonResponse: true` → the whole JSON-RPC response is buffered into the
136
+ * returned `Response`, so closing the transport/server afterwards can't truncate
137
+ * an in-flight body. Cleanup is fire-and-forget with swallowed rejections (a
138
+ * `.catch()`-terminated chain) so it never delays or masks the resolved response.
139
+ *
140
+ * The request is screened first ({@link screenRequest}); an oversized body or a
141
+ * JSON-RPC batch is refused here and no server is ever constructed for it.
142
+ *
143
+ * `options.parsedBody` lets a caller hand over a body it already read (e.g. the
144
+ * paid-tool gate, which peeks the JSON-RPC message to price the call) so the
145
+ * transport doesn't re-read a consumed stream. It is trusted as already
146
+ * screened, so read it with {@link readScreenedBody} — anything else hands this
147
+ * surface an unbounded body.
148
+ *
149
+ * Teardown runs in a `finally`: a rejection from `connect` or `handleRequest`
150
+ * would otherwise skip it and leak a server + transport per failed request,
151
+ * which on a public endpoint is exactly the request an attacker can repeat.
152
+ */
153
+ declare const serveStateless: (server: Server, request: Request, options?: ServeStatelessOptions) => Promise<Response>;
154
+ export { DEFAULT_MAX_REQUEST_BYTES as D, McpResourceProvider as M, ServeStatelessOptions as S, ToolDefinition as T, McpFetchHandler as a, McpTool as b, McpResourceSummary as c, McpServerInfo as d, ToolInputSchema as e, ToolResult as f, createToolServer as g, 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,2 @@
1
+ import{getCatalogEntry as j,resolveHint as q,findIssueSolution as B,isInternalCode as N,LunoraError as c}from"@lunora/errors";import{e as l,o as Q,a as u,t as Y,f as J,b as G,O as V,c as X}from"./observability-tools-D4Z8dGS-.mjs";import{ADMIN_FUNCTIONS as z}from"@lunora/shard-engine";import{m as Z}from"./promise-memo-OqVPWoUX.mjs";const ee={destructiveHint:!1,idempotentHint:!0,openWorldHint:!1,readOnlyHint:!0},w="https://lunora.sh/docs/errors",te={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"},ne={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"},S=[{annotations:{...ee,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:te,name:"lunora_explain_error",outputSchema:ne}],oe=new Set(S.map(e=>e.name)),_=e=>typeof e=="string"&&e.length>0?e:void 0,ie=e=>typeof e=="string"?e:e.join(`
2
+ `),re=(e,t)=>t?.docsUrl!==void 0?t.docsUrl:e===void 0||t===void 0||N(e)?w:`${w}#${e.toLowerCase()}`,se=(e,t)=>{const n=e===void 0?void 0:j(e),o=e===void 0?void 0:q({code:e}),i=t===void 0?void 0:B(t),r={docsUrl:re(e,n),found:n!==void 0||i!==void 0};return e!==void 0&&(r.code=e,N(e)&&(r.internal=!0)),n!==void 0&&(r.status=n.status,r.title=n.title),o!==void 0&&(r.hint=ie(o)),i!==void 0&&(r.solution={body:i.body,header:i.header,id:i.id}),r},ae=(e,t)=>{if(e!=="lunora_explain_error")return l(`unknown error tool: ${e}`);const n=_(t.code),o=_(t.message);return n===void 0&&o===void 0?l('lunora_explain_error needs a "code" or a "message" (or both); received neither.'):Q(se(n,o))},ce={destructiveHint:!1,idempotentHint:!0,openWorldHint:!0,readOnlyHint:!0},de={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"},v=[{annotations:{...ce,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:de,name:"lunora_find_related"}],le=new Set(v.map(e=>e.name)),ue=e=>{const{cursor:t,depth:n,direction:o,edges:i,limit:r}=e;if(t!=null&&typeof t!="string")throw new c("BAD_REQUEST","findRelated: `cursor` must be a string");if(n!==void 0&&typeof n!="number")throw new c("BAD_REQUEST","findRelated: `depth` must be a number");if(o!==void 0&&o!=="both"&&o!=="in"&&o!=="out")throw new c("BAD_REQUEST",'findRelated: `direction` must be one of "in", "out" or "both"');if(r!==void 0&&typeof r!="number")throw new c("BAD_REQUEST","findRelated: `limit` must be a number");if(i!==void 0&&(!Array.isArray(i)||!i.every(s=>typeof s=="string")))throw new c("BAD_REQUEST","findRelated: `edges` must be an array of edge-name strings")},he=e=>{const{cursor:t,depth:n,direction:o,edges:i,id:r,limit:s,shardKey:a,table:d}=e;if(typeof d!="string"||d.trim()==="")throw new c("BAD_REQUEST","findRelated: `table` is required");if(typeof r!="string"||r.trim()==="")throw new c("BAD_REQUEST","findRelated: `id` is required");if(ue(e),a!==void 0&&(typeof a!="string"||a.trim()===""))throw new c("BAD_REQUEST","findRelated: `shardKey` must be a non-empty string when given");return{args:{id:r,table:d,...typeof t=="string"?{cursor:t}:{},...n===void 0?{}:{depth:n},...o===void 0?{}:{direction:o},...i===void 0?{}:{edges:i},...s===void 0?{}:{limit:s}},shardKey:a}},fe=async(e,t,n)=>{if(t!=="lunora_find_related")throw new c("INTERNAL",`unknown row-read tool: ${t}`);const{args:o,shardKey:i}=he(n),r={__lunoraRef:z.findRelated};return u(await e.query(r,o,{...i===void 0?{}:{shardKey:i}}))},g=new TextEncoder,pe=Array.from({length:32},(e,t)=>t);new RegExp(`[${pe.map(e=>String.fromCodePoint(e)).join("")}]`,"u");const me=64,ye=new Map,I=async e=>Z(ye,e,async()=>crypto.subtle.importKey("raw",g.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]),me),ge=async(e,t)=>{const n=await I(e),o=await crypto.subtle.sign("HMAC",n,g.encode(t));return Y(new Uint8Array(o))},we=async(e,t,n)=>{const o=await I(e);return crypto.subtle.verify("HMAC",o,n,g.encode(t))},_e=/["\\\u0000-\u001F\uD800-\uDFFF]/,b=e=>_e.test(e)?JSON.stringify(e):`"${e}"`,p=e=>{if(e===void 0)return"null";if(typeof e=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof e=="number"){if(Number.isNaN(e))return"nan";if(e===1/0)return"inf";if(e===-1/0)return"-inf";if(Object.is(e,-0))return"-0"}if(typeof e=="string")return b(e);if(e===null||typeof e!="object")return JSON.stringify(e);if(Array.isArray(e)){let s="[";for(let a=0;a<e.length;a++)a>0&&(s+=","),s+=p(e[a]);return s+"]"}const t=Object.getPrototypeOf(e);if(t!==null&&t!==Object.prototype){const s=e.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${s} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const n=e,o=Object.keys(n).sort();let i="{",r=!0;for(const s of o){const a=n[s];a!==void 0&&(r?r=!1:i+=",",i+=b(s),i+=":",i+=p(a))}return i+"}"},be="lunora-mcp-write-confirmation-v1",D=600*1e3,C=".",L=e=>{const t=e.getAuthToken();if(t===null||t==="")throw new c("UNAUTHORIZED","write confirmation needs the deployment's admin token: it is the secret half of the digest key, and without it a digest would be forgeable by anyone.");return`${be}\0${e.url}\0${t}`},k=(e,t)=>`${p(e)}\0${String(t)}`,Ee=async(e,t)=>{const n=Date.now()+D,o=await ge(L(e),k(t,n));return{actionDigest:`${String(n)}${C}${o}`,expiresAt:n}},Ae=async(e,t,n)=>{const o=n.indexOf(C);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=J(n.slice(o+1))}catch{return"mismatch"}return await we(L(e),k(t,i),r)?"valid":"mismatch"},Oe="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.",Te={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:Oe,type:"string"}},Re=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}},Ne=(e,t)=>u({actionDigest:t.actionDigest,expiresAt:new Date(t.expiresAt).toISOString(),nextStep:`Show proposedAction to a human. To execute, call ${e.tool} again with the IDENTICAL functionPath, args, shardKey and idempotencyKey, plus confirmed: true and this actionDigest, before expiresAt. Nothing has been written or called yet.`,proposedAction:e,status:"action_required"}),Se=async(e,t,n)=>{if(!n.confirmed||n.actionDigest===void 0)return Ne(t,await Ee(e,t));const o=await Ae(e,t,n.actionDigest);if(o!=="valid")return o==="expired"?l(`confirmation rejected: this actionDigest has expired. A confirmation is good for ${String(D/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.`):l(`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.`)},U={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"},E={properties:{},type:"object"},ve={properties:{functionPath:{description:'Function reference, e.g. "messages:send"',type:"string"}},required:["functionPath"],type:"object"},h={destructiveHint:!1,idempotentHint:!0,openWorldHint:!0,readOnlyHint:!0},H=[{annotations:{...h,title:"List deployment functions"},description:"List the deployment's public functions (queries, mutations, actions) with their kinds.",inputSchema:E,name:"lunora_list_functions"},{annotations:{...h,title:"List global tables"},description:"List the deployment's .global() tables with their row counts. Names and row counts only — no column shapes.",inputSchema:E,name:"lunora_list_tables"},{annotations:{...h,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:ve,name:"lunora_get_function_schema"},{annotations:{...h,title:"Run a query"},description:"Run a query and return its result. Read-only.",inputSchema:U,name:"lunora_run_query"}],Ie=new Set(H.map(e=>e.name)),A={properties:{...U.properties,...Te},required:["functionPath"],type:"object"},O='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.',P=[{annotations:{destructiveHint:!0,idempotentHint:!1,openWorldHint:!0,readOnlyHint:!1,title:"Run a mutation (writes data)"},description:`Run a mutation. Writes data. ${O}`,inputSchema:A,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. ${O}`,inputSchema:A,name:"lunora_run_action"}],De=new Set(P.map(e=>e.name)),x=e=>{const{functionPath:t}=e;if(typeof t!="string"||t.length===0)throw new c("BAD_REQUEST",'"functionPath" is required and must be a non-empty string');return t},T=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),R=e=>Array.isArray(e)?"an array":`a ${typeof e}`,Ce=e=>{if(e==null)return{};if(typeof e=="string"){let t;try{t=JSON.parse(e)}catch{throw new c("BAD_REQUEST",'"args" must be a JSON object; received a string that is not valid JSON')}if(!T(t))throw new c("BAD_REQUEST",`"args" must be a JSON object; the provided string decoded to ${R(t)}`);return t}if(!T(e))throw new c("BAD_REQUEST",`"args" must be a JSON object, got ${R(e)}`);return e},M=e=>{const t=x(e),n=Ce(e.args),o=typeof e.shardKey=="string"&&e.shardKey.length>0?e.shardKey:void 0;return{args:n,functionPath:t,shardKey:o}},m=e=>({__lunoraRef:e}),Le=3e4,f=new WeakMap,y=e=>{const t=Date.now(),n=f.get(e);if(n!==void 0&&n.expiresAt>t)return n.promise;const o=e.listFunctions().catch(i=>{throw f.get(e)?.promise===o&&f.delete(e),i});return f.set(e,{expiresAt:t+Le,promise:o}),o},$=async(e,t,n)=>{const i=(await y(e)).find(r=>r.path===t);if(i===void 0)throw new c("NOT_FOUND",`function not found or not public: ${t}`);if(i.kind!==n)throw new c("BAD_REQUEST",`function ${t} is a ${i.kind}, not a ${n}`)},ke=async(e,t,n,o,i)=>{const r=Re(i);return Se(e,{...o,idempotencyKey:r.idempotencyKey,kind:n,tool:t},r)},Ue=async(e,t,n)=>{switch(t){case"lunora_get_function_schema":{const o=x(n),r=(await y(e)).find(s=>s.path===o);return r===void 0?l(`function not found: ${o}`):u({args:r.args??[],kind:r.kind,path:r.path})}case"lunora_list_functions":return u(await y(e));case"lunora_list_tables":return u(await e.listGlobalTables());case"lunora_run_query":{const{args:o,functionPath:i,shardKey:r}=M(n);return await $(e,i,"query"),u(await e.query(m(i),o,{shardKey:r}))}default:throw new c("INTERNAL",`unknown read-only tool: ${t}`)}},He=async(e,t,n)=>{if(t!=="lunora_run_action"&&t!=="lunora_run_mutation")throw new c("INTERNAL",`unknown write tool: ${t}`);const o=t==="lunora_run_action"?"action":"mutation",{args:i,functionPath:r,shardKey:s}=M(n);await $(e,r,o);const a=await ke(e,t,o,{args:i,functionPath:r,shardKey:s},n);return a!==void 0?a:u(o==="action"?await e.action(m(r),i,{shardKey:s}):await e.mutation(m(r),i,{shardKey:s}))},Pe=e=>e===!0,K=[{call:Ue,definitions:H,names:Ie},{call:(e,t,n)=>ae(t,n),definitions:S,names:oe},{call:fe,definitions:v,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.`},names:le},{call:X,definitions:V,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.`},names:G},{call:He,definitions:P,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:De}],F=(e,t)=>e.gate!==void 0&&!Pe(t[e.gate.flag])?e.gate:void 0,Fe=(e,t=!1,n=!1)=>{const o={allowDataReads:n,allowObservability:t,allowWrites:e};return K.filter(i=>F(i,o)===void 0).flatMap(i=>i.definitions)},We=async(e,t,n,o=!1,i=!1,r=!1)=>{const s=K.find(d=>d.names.has(t));if(s===void 0)return l(`unknown tool: ${t}`);const a=F(s,{allowDataReads:r,allowObservability:i,allowWrites:o});if(a!==void 0)return l(a.refuse(t));try{return await s.call(e,t,n)}catch(d){const W=d instanceof Error?d.message:String(d);return l(W)}};export{S as E,H as R,P as W,ae as a,oe as b,We as c,v as d,Fe as t};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/mcp",
3
- "version": "1.0.0-alpha.14",
3
+ "version": "1.0.0-alpha.140",
4
4
  "description": "Model Context Protocol server exposing a Lunora deployment to AI agents",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -43,14 +43,28 @@
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
+ },
46
50
  "./package.json": "./package.json"
47
51
  },
48
52
  "publishConfig": {
49
53
  "access": "public"
50
54
  },
51
55
  "dependencies": {
52
- "@lunora/client": "1.0.0-alpha.14",
53
- "@modelcontextprotocol/sdk": "^1.29.0"
56
+ "@lunora/client": "1.0.0-alpha.105",
57
+ "@lunora/errors": "1.0.0-alpha.36",
58
+ "@lunora/shard-engine": "1.0.0-alpha.68",
59
+ "@modelcontextprotocol/sdk": "^1.30.0"
60
+ },
61
+ "peerDependencies": {
62
+ "@lunora/x402": ">=1.0.0-alpha.21 <2.0.0-0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@lunora/x402": {
66
+ "optional": true
67
+ }
54
68
  },
55
69
  "engines": {
56
70
  "node": "^22.15.0 || >=24.11.0"
@@ -1,143 +0,0 @@
1
- const RUN_INPUT_SCHEMA = {
2
- properties: {
3
- args: { description: "Arguments object passed to the function", type: "object" },
4
- functionPath: { description: 'Function reference, e.g. "messages:send"', type: "string" },
5
- shardKey: { description: "Optional shard key when the function is .shardBy()-partitioned", type: "string" }
6
- },
7
- required: ["functionPath"],
8
- type: "object"
9
- };
10
- const NO_INPUT_SCHEMA = { properties: {}, type: "object" };
11
- const FUNCTION_PATH_INPUT_SCHEMA = {
12
- properties: {
13
- functionPath: { description: 'Function reference, e.g. "messages:send"', type: "string" }
14
- },
15
- required: ["functionPath"],
16
- type: "object"
17
- };
18
- const READ_ONLY_TOOL_DEFINITIONS = [
19
- {
20
- description: "List the deployment's public functions (queries, mutations, actions) with their kinds.",
21
- inputSchema: NO_INPUT_SCHEMA,
22
- name: "lunora_list_functions"
23
- },
24
- {
25
- description: "List the deployment's .global() tables and their column shapes.",
26
- inputSchema: NO_INPUT_SCHEMA,
27
- name: "lunora_list_tables"
28
- },
29
- {
30
- description: "Return a function's argument JSON Schema and kind, so a caller can construct a valid arguments object. Call lunora_list_functions first to discover available function paths.",
31
- inputSchema: FUNCTION_PATH_INPUT_SCHEMA,
32
- name: "lunora_get_function_schema"
33
- },
34
- {
35
- description: "Run a query and return its result. Read-only.",
36
- inputSchema: RUN_INPUT_SCHEMA,
37
- name: "lunora_run_query"
38
- }
39
- ];
40
- const WRITE_TOOL_DEFINITIONS = [
41
- {
42
- description: "Run a mutation and return its result. Writes data — use with care.",
43
- inputSchema: RUN_INPUT_SCHEMA,
44
- name: "lunora_run_mutation"
45
- },
46
- {
47
- description: "Run an action and return its result. May call external services.",
48
- inputSchema: RUN_INPUT_SCHEMA,
49
- name: "lunora_run_action"
50
- }
51
- ];
52
- const WRITE_TOOL_NAMES = new Set(WRITE_TOOL_DEFINITIONS.map((tool) => tool.name));
53
- const toolDefinitions = (allowWrites) => (
54
- // Fail closed: only the boolean `true` opts in. These are exported helpers, so
55
- // an env-plumbed/JS caller could pass a truthy string like `"false"`/`"0"` —
56
- // the explicit `=== true` guards that despite the declared `boolean` type.
57
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare -- intentional runtime guard at an exported API boundary against non-boolean callers
58
- allowWrites === true ? [...READ_ONLY_TOOL_DEFINITIONS, ...WRITE_TOOL_DEFINITIONS] : READ_ONLY_TOOL_DEFINITIONS
59
- );
60
- const readFunctionPath = (input) => {
61
- const { functionPath } = input;
62
- if (typeof functionPath !== "string" || functionPath.length === 0) {
63
- throw new Error('"functionPath" is required and must be a non-empty string');
64
- }
65
- return functionPath;
66
- };
67
- const readRunArguments = (input) => {
68
- const functionPath = readFunctionPath(input);
69
- const rawArguments = input.args;
70
- const isPlainObject = typeof rawArguments === "object" && rawArguments !== null && !Array.isArray(rawArguments);
71
- const args = isPlainObject ? rawArguments : {};
72
- const shardKey = typeof input.shardKey === "string" && input.shardKey.length > 0 ? input.shardKey : void 0;
73
- return { args, functionPath, shardKey };
74
- };
75
- const reference = (functionPath) => {
76
- return { __lunoraRef: functionPath };
77
- };
78
- const ok = (value) => {
79
- const text = value === void 0 ? "null" : JSON.stringify(value, void 0, 2);
80
- return { content: [{ text, type: "text" }] };
81
- };
82
- const assertRunnable = async (client, functionPath, expectedKind) => {
83
- const functions = await client.listFunctions();
84
- const descriptor = functions.find((function_) => function_.path === functionPath);
85
- if (descriptor === void 0) {
86
- throw new Error(`function not found or not public: ${functionPath}`);
87
- }
88
- if (descriptor.kind !== expectedKind) {
89
- throw new Error(`function ${functionPath} is a ${descriptor.kind}, not a ${expectedKind}`);
90
- }
91
- };
92
- const callTool = async (client, name, input, allowWrites = false) => {
93
- try {
94
- if (allowWrites !== true && WRITE_TOOL_NAMES.has(name)) {
95
- return {
96
- content: [
97
- { text: `tool "${name}" is disabled: this MCP server is read-only. Enable writes with the LUNORA_MCP_ALLOW_WRITES env var.`, type: "text" }
98
- ],
99
- isError: true
100
- };
101
- }
102
- switch (name) {
103
- case "lunora_get_function_schema": {
104
- const functionPath = readFunctionPath(input);
105
- const functions = await client.listFunctions();
106
- const descriptor = functions.find((function_) => function_.path === functionPath);
107
- if (descriptor === void 0) {
108
- return { content: [{ text: `function not found: ${functionPath}`, type: "text" }], isError: true };
109
- }
110
- return ok({ args: descriptor.args ?? [], kind: descriptor.kind, path: descriptor.path });
111
- }
112
- case "lunora_list_functions": {
113
- return ok(await client.listFunctions());
114
- }
115
- case "lunora_list_tables": {
116
- return ok(await client.listGlobalTables());
117
- }
118
- case "lunora_run_action": {
119
- const { args, functionPath, shardKey } = readRunArguments(input);
120
- await assertRunnable(client, functionPath, "action");
121
- return ok(await client.action(reference(functionPath), args, { shardKey }));
122
- }
123
- case "lunora_run_mutation": {
124
- const { args, functionPath, shardKey } = readRunArguments(input);
125
- await assertRunnable(client, functionPath, "mutation");
126
- return ok(await client.mutation(reference(functionPath), args, { shardKey }));
127
- }
128
- case "lunora_run_query": {
129
- const { args, functionPath, shardKey } = readRunArguments(input);
130
- await assertRunnable(client, functionPath, "query");
131
- return ok(await client.query(reference(functionPath), args, { shardKey }));
132
- }
133
- default: {
134
- return { content: [{ text: `unknown tool: ${name}`, type: "text" }], isError: true };
135
- }
136
- }
137
- } catch (error) {
138
- const message = error instanceof Error ? error.message : String(error);
139
- return { content: [{ text: message, type: "text" }], isError: true };
140
- }
141
- };
142
-
143
- export { READ_ONLY_TOOL_DEFINITIONS, WRITE_TOOL_DEFINITIONS, callTool, toolDefinitions };
@@ -1,65 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { LunoraClient } from '@lunora/client';
5
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8
- import { toolDefinitions, callTool } from './READ_ONLY_TOOL_DEFINITIONS-DV2VkuuU.mjs';
9
-
10
- const resolveVersion = () => {
11
- try {
12
- let directory = dirname(fileURLToPath(import.meta.url));
13
- for (let depth = 0; depth < 8; depth += 1) {
14
- try {
15
- const raw = readFileSync(join(directory, "package.json"), "utf8");
16
- const pkg = JSON.parse(raw);
17
- if (pkg.name === "@lunora/mcp" && typeof pkg.version === "string" && pkg.version.length > 0) {
18
- return pkg.version;
19
- }
20
- } catch {
21
- }
22
- const parent = dirname(directory);
23
- if (parent === directory) {
24
- break;
25
- }
26
- directory = parent;
27
- }
28
- } catch {
29
- }
30
- return "0.0.0";
31
- };
32
- const SERVER_INFO = { name: "lunora", version: resolveVersion() };
33
- const resolveClient = (options) => {
34
- if (options.client !== void 0) {
35
- return options.client;
36
- }
37
- if (options.url === void 0) {
38
- throw new Error("createLunoraMcpServer requires either a `client` or a `url`");
39
- }
40
- const client = new LunoraClient({ fetch: options.fetch, url: options.url });
41
- if (options.token !== void 0) {
42
- client.setAuthToken(options.token);
43
- }
44
- return client;
45
- };
46
- const createLunoraMcpServer = (options) => {
47
- const client = resolveClient(options);
48
- const allowWrites = options.allowWrites ?? false;
49
- const server = new Server(SERVER_INFO, { capabilities: { tools: {} } });
50
- server.setRequestHandler(ListToolsRequestSchema, () => {
51
- return { tools: [...toolDefinitions(allowWrites)] };
52
- });
53
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
54
- const result = await callTool(client, request.params.name, request.params.arguments ?? {}, allowWrites);
55
- return result;
56
- });
57
- return server;
58
- };
59
- const connectStdio = async (options) => {
60
- const server = createLunoraMcpServer(options);
61
- await server.connect(new StdioServerTransport());
62
- return server;
63
- };
64
-
65
- export { connectStdio, createLunoraMcpServer };