@lunora/mcp 1.0.0-alpha.6 → 1.0.0-alpha.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +126 -0
- package/README.md +115 -9
- package/dist/bin.mjs +3 -35
- package/dist/docs/index.d.mts +150 -0
- package/dist/docs/index.d.ts +150 -0
- package/dist/docs/index.mjs +1 -0
- package/dist/index.d.mts +286 -14
- package/dist/index.d.ts +286 -14
- package/dist/index.mjs +1 -2
- package/dist/packem_shared/AGENT_RUN_INPUT_SCHEMA-ihZg6vIZ.mjs +1 -0
- package/dist/packem_shared/DEFAULT_DOCS_BASE_URL-DhaPz6PK.mjs +4 -0
- package/dist/packem_shared/DEFAULT_SEARCH_LIMIT-CKtOASrB.mjs +3 -0
- package/dist/packem_shared/DOCS_SERVER_NAME-BYSXEkgD.mjs +1 -0
- package/dist/packem_shared/DOCS_URI_SCHEME-DbyotZvf.mjs +3 -0
- package/dist/packem_shared/LOCAL_SERVER_NAME-BoQv1iu8.mjs +1 -0
- package/dist/packem_shared/READ_ONLY_TOOL_DEFINITIONS-48jMcpI-.mjs +1 -0
- package/dist/packem_shared/connectStdio-Dt-DqEgA.mjs +1 -0
- package/dist/packem_shared/createMcpFetchHandler-DjvXAX80.mjs +1 -0
- package/dist/packem_shared/createPaidMcpServer-CwdcSPpN.mjs +1 -0
- package/dist/packem_shared/createToolServer-CP5bNUcS.mjs +1 -0
- package/dist/packem_shared/serve-stateless.d-DX0di_lv.d.mts +119 -0
- package/dist/packem_shared/serve-stateless.d-DX0di_lv.d.ts +119 -0
- package/dist/packem_shared/serveStateless-BqoRk-xU.mjs +1 -0
- package/dist/packem_shared/toDocsSearchHits-C1xXPRPy.mjs +1 -0
- package/package.json +15 -2
- package/dist/packem_shared/TOOL_DEFINITIONS-Dpiu38ji.mjs +0 -112
- package/dist/packem_shared/connectStdio-C_mvQBs2.mjs +0 -64
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{toDocsSearchHits as h}from"./toDocsSearchHits-C1xXPRPy.mjs";const m="https://lunora.sh",f=1e4,d=t=>t.endsWith("/")?t.slice(0,-1):t,g="\\",p=(t,e,i)=>{for(let s=i;s<t.length;s+=1)if(t[s]===e&&t[s-1]!==g)return s;return-1},u=t=>t.replaceAll(String.raw`\[`,"[").replaceAll(String.raw`\]`,"]").replaceAll(String.raw`\(`,"(").replaceAll(String.raw`\)`,")"),S=t=>{const e=t.trim();if(!e.startsWith("- ["))return;const i=e.lastIndexOf("](");if(i===-1)return;const s=p(e,")",i+2);if(s===-1)return;const a=u(e.slice(3,i).trim()),c=u(e.slice(i+2,s).trim());if(a.length===0||c.length===0)return;const n=e.slice(s+1).trim(),r=n.startsWith(":")?n.slice(1).trim():"";return r.length>0?{description:r,title:a,url:c}:{title:a,url:c}},x=t=>{if(!t.endsWith(")"))return t;const e=t.lastIndexOf(" (");if(e===-1)return t;const i=t.slice(e+2,-1);return i.startsWith("/")||i.startsWith("http")?t.slice(0,e).trim():t},y=(t,e)=>{for(const i of t.split(`
|
|
2
|
+
`)){if(i.startsWith("# "))return x(i.slice(2).trim());if(i.trim().length>0)break}return e},w=t=>{if(!t.startsWith("# "))return t;const e=t.indexOf(`
|
|
3
|
+
`);return e===-1?"":t.slice(e+1).trimStart()},A=(t={})=>{const e=d(t.baseUrl??m),i=t.fetch??globalThis.fetch,s=t.timeoutMs??f,a=async n=>{try{const r=await i(`${e}${n}`,{headers:{accept:"text/plain, application/json"},signal:AbortSignal.timeout(s)});return r.ok?{body:await r.text()}:{missing:!0}}catch(r){return{unreachable:r instanceof Error?r.message:String(r)}}},c=(n,r)=>{if("unreachable"in n)throw new Error(`could not reach the documentation site at ${e}${r}: ${n.unreachable}`);return"missing"in n?void 0:n.body};return{getPage:async n=>{const r=c(await a(`/llms.mdx${n}`),`/llms.mdx${n}`);if(!(r===void 0||r.trim().length===0))return{content:w(r),title:y(r,n),url:n}},listPages:async()=>{const n=c(await a("/llms.txt"),"/llms.txt");if(n===void 0)return[];const r=[];for(const o of n.split(`
|
|
4
|
+
`)){const l=S(o);l!==void 0&&r.push(l)}return r},search:async n=>{const r=`/api/search?query=${encodeURIComponent(n)}`,o=c(await a(r),r);if(o===void 0)return[];let l;try{l=JSON.parse(o)}catch{return[]}return Array.isArray(l)?h(l):[]}}};export{m as DEFAULT_DOCS_BASE_URL,f as DEFAULT_REQUEST_TIMEOUT_MS,A as createRemoteDocsIndex,S as parseIndexLine};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
const w=10,S=50,h="docs",L=500,p={properties:{limit:{description:`Maximum hits to return (default ${String(10)}, max ${String(50)})`,type:"number"},query:{description:'Search terms, e.g. "shardBy" or "optimistic updates"',type:"string"}},required:["query"],type:"object"},g={properties:{url:{description:'Site-relative page URL from a search hit, e.g. "/docs/sharding"',type:"string"}},required:["url"],type:"object"},m={properties:{},type:"object"},i={destructiveHint:!1,idempotentHint:!0,openWorldHint:!0,readOnlyHint:!0},u={annotations:{...i,title:"Search the Lunora documentation"},description:"Search the Lunora documentation and return matching pages and sections. Use this before writing Lunora code (schema, queries, mutations, actions, sharding, .global(), client hooks) so the answer reflects the framework's current API rather than a guess. Follow a hit with lunora_get_doc to read the full page.",inputSchema:p,name:"lunora_search_docs"},l={annotations:{...i,title:"Read a Lunora documentation page"},description:"Return one Lunora documentation page in full, as Markdown. Takes the `url` of a lunora_search_docs hit or a lunora_list_docs entry.",inputSchema:g,name:"lunora_get_doc"},d={annotations:{...i,title:"List the Lunora documentation pages"},description:"List every Lunora documentation page with its title and description. Prefer lunora_search_docs when you know what you're looking for.",inputSchema:m,name:"lunora_list_docs"},$=[u,l,d],o=e=>({content:[{text:JSON.stringify(e,void 0,2),type:"text"}]}),f=e=>({content:[{text:e,type:"text"}],isError:!0}),a=512,c=(e,t)=>{const n=e[t];if(typeof n!="string"||n.trim().length===0)throw new TypeError(`"${t}" is required and must be a non-empty string`);if(n.length>a)throw new RangeError(`"${t}" must be at most ${String(a)} characters`);return n.trim()},_=e=>{const t=typeof e=="string"?Number(e):e;return typeof t!="number"||!Number.isFinite(t)?10:Math.min(50,Math.max(1,Math.floor(t)))},y=e=>{let t=e.trim();if(t.startsWith("http://")||t.startsWith("https://")){const n=t.slice(t.indexOf("//")+2),r=n.indexOf("/");t=r===-1?"/":n.slice(r)}for(const n of["?","#"]){const r=t.indexOf(n);r!==-1&&(t=t.slice(0,r))}for(;t.endsWith("/")&&t.length>1;)t=t.slice(0,-1);if(t.startsWith("/")||(t=t.startsWith("docs/")?`/${t}`:`/${h}/${t}`),t.split("/").includes(".."))throw new RangeError(`"url" must not contain ".." segments: ${e}`);return t},x=e=>[{definition:u,handle:async t=>{const n=c(t,"query"),r=_(t.limit),s=(await e.search(n)).slice(0,r);return s.length===0?o({hits:[],note:`no documentation matched "${n}" — try fewer or more general terms, or lunora_list_docs to browse`}):o({hits:s})}},{definition:l,handle:async t=>{const n=y(c(t,"url")),r=await e.getPage(n);return r===void 0?f(`documentation page not found: ${n}. Use lunora_search_docs or lunora_list_docs to find a valid url.`):{content:[{text:`# ${r.title} (${r.url})
|
|
2
|
+
|
|
3
|
+
${r.content}`,type:"text"}]}}},{definition:d,handle:async()=>{const t=await e.listPages();return t.length>500?o({note:`showing the first ${String(500)} of ${String(t.length)} pages — use lunora_search_docs to find the rest`,pages:t.slice(0,500)}):o(t)}}];export{w as DEFAULT_SEARCH_LIMIT,$ as DOCS_TOOL_DEFINITIONS,a as MAX_ARGUMENT_LENGTH,L as MAX_LISTED_PAGES,S as MAX_SEARCH_LIMIT,x as docsTools,y as normalizeDocUrl};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createToolServer as d}from"./createToolServer-CP5bNUcS.mjs";import{serveStateless as c}from"./serveStateless-BqoRk-xU.mjs";import{docsResources as i}from"./DOCS_URI_SCHEME-DbyotZvf.mjs";import{docsTools as p}from"./DEFAULT_SEARCH_LIMIT-CKtOASrB.mjs";const u="lunora-docs",m=128*1024,y=e=>({name:u,version:e??"0.0.0"}),S=e=>d(y(e.version),p(e.index),i(e.index)),l=e=>new TextEncoder().encode(e).length,n=(e,r,s)=>Response.json({error:{code:r,message:s},id:null,jsonrpc:"2.0"},{status:e}),v=async(e,r)=>{if(e.method!=="POST")return{parsedBody:void 0};const s={response:n(413,-32600,`request body exceeds ${String(r)} bytes`)},o=Number(e.headers.get("content-length"));if(Number.isFinite(o)&&o>r)return s;const a=await e.text();if(l(a)>r)return s;let t;try{t=JSON.parse(a)}catch{return{response:n(400,-32700,"parse error: body is not valid JSON")}}return Array.isArray(t)?{response:n(400,-32600,"batched requests are not supported: send one JSON-RPC message per request")}:{parsedBody:t}},B=e=>{const r=e.maxRequestBytes??m;return async s=>{const o=await v(s,r);return"response"in o?o.response:c(S(e),s,o.parsedBody===void 0?void 0:{parsedBody:o.parsedBody})}};export{m as DEFAULT_MAX_REQUEST_BYTES,u as DOCS_SERVER_NAME,B as createDocsMcpFetchHandler,S as createDocsMcpServer};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
const s="lunora-docs:",r=t=>`${s}${t}`,a=t=>t.startsWith(s)?t.slice(s.length):void 0,n=t=>({list:async()=>(await t.listPages()).map(i=>({...i.description===void 0?{}:{description:i.description},mimeType:"text/markdown",name:i.title,uri:r(i.url)})),read:async i=>{const e=a(i);if(e===void 0)return;const o=await t.getPage(e);return o===void 0?void 0:{mimeType:"text/markdown",text:`# ${o.title} (${o.url})
|
|
2
|
+
|
|
3
|
+
${o.content}`}}});export{s as DOCS_URI_SCHEME,n as docsResources,a as fromDocsUri,r as toDocsUri};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraClient as p}from"@lunora/client";import{StdioServerTransport as u}from"@modelcontextprotocol/sdk/server/stdio.js";import{createToolServer as f}from"./createToolServer-CP5bNUcS.mjs";import{createRemoteDocsIndex as m}from"./DEFAULT_DOCS_BASE_URL-DhaPz6PK.mjs";import{docsResources as v}from"./DOCS_URI_SCHEME-DbyotZvf.mjs";import{docsTools as h}from"./DEFAULT_SEARCH_LIMIT-CKtOASrB.mjs";import{toolDefinitions as y,callTool as O}from"./READ_ONLY_TOOL_DEFINITIONS-48jMcpI-.mjs";const R=(t,e)=>{if(t.size<e)return;const o=t.keys().next().value;o!==void 0&&t.delete(o)},c=t=>t.docs===!1?void 0:m({...t.docs?.baseUrl===void 0?{}:{baseUrl:t.docs.baseUrl},...t.fetch===void 0?{}:{fetch:t.fetch}}),E="lunora",P="no Lunora dev server is running for this project — start one with `lunora dev`, then call this tool again (call lunora_dev_status to check).",_=8,l=t=>{const e=new Map;return o=>{const n=JSON.stringify([o.url,o.token??""]),r=e.get(n);if(r!==void 0)return r;const i=new p({fetch:t,url:o.url});return o.token!==void 0&&o.token.length>0&&i.setAuthToken(o.token),R(e,_),e.set(n,i),i}},w=(t,e,o)=>{const n=typeof t=="function"?t:()=>t;return y(e).map(r=>({definition:r,handle:async i=>{const s=n();return s===void 0?{content:[{text:P,type:"text"}],isError:!0}:O(o(s),r.name,i,e)}}))},S="lunora-spec:openrpc",T="lunora-spec:openapi",a=[{description:"The deployment's generated OpenRPC 1.x document — every RPC function's path, kind, and argument schema in one read, instead of list_functions plus one get_function_schema call per function.",fetch:async t=>t.fetchOpenRpc(),name:"OpenRPC specification",uri:S},{description:"The deployment's generated OpenAPI 3.1 document.",fetch:async t=>t.fetchOpenApi(),name:"OpenAPI specification",uri:T}],g=(t,e)=>{const o=typeof t=="function"?t:()=>t,n=async r=>{const i=o();if(i!==void 0)try{return await r.fetch(e(i))}catch{return}};return{list:async()=>(await Promise.all(a.map(async r=>await n(r)===void 0?void 0:{description:r.description,mimeType:"application/json",name:r.name,uri:r.uri}))).filter(r=>r!==void 0),read:async r=>{const i=a.find(d=>d.uri===r);if(i===void 0)return;const s=await n(i);return s===void 0?void 0:{mimeType:"application/json",text:JSON.stringify(s,void 0,2)}}}},A=t=>({list:async()=>(await Promise.all(t.map(async e=>e.list()))).flat(),read:async e=>{for(const o of t){const n=await o.read(e);if(n!==void 0)return n}}}),x=(t,e)=>{const o=[],n=c(t);return n!==void 0&&o.push(...h(n)),o.push(...t.extraTools??[]),t.deployment!==void 0&&o.push(...w(t.deployment,t.allowWrites??!1,e??l(t.fetch))),o},C=(t={})=>{const e=c(t),o=[];e!==void 0&&o.push(v(e));const n=l(t.fetch);return t.deployment!==void 0&&o.push(g(t.deployment,n)),f({name:E,version:t.version??"0.0.0"},x(t,n),o.length===0?void 0:A(o))},$=async(t={})=>{const e=C(t);return await e.connect(new u),e};export{E as LOCAL_SERVER_NAME,P as NO_DEPLOYMENT_MESSAGE,T as OPENAPI_RESOURCE_URI,S as OPENRPC_RESOURCE_URI,$ as connectLocalStdio,C as createLocalMcpServer,x as localTools};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";const h={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"},m={properties:{},type:"object"},E={properties:{functionPath:{description:'Function reference, e.g. "messages:send"',type:"string"}},required:["functionPath"],type:"object"},c={destructiveHint:!1,idempotentHint:!0,openWorldHint:!0,readOnlyHint:!0},g=[{annotations:{...c,title:"List deployment functions"},description:"List the deployment's public functions (queries, mutations, actions) with their kinds.",inputSchema:m,name:"lunora_list_functions"},{annotations:{...c,title:"List global tables"},description:"List the deployment's .global() tables and their column shapes.",inputSchema:m,name:"lunora_list_tables"},{annotations:{...c,title:"Describe a function's arguments"},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.",inputSchema:E,name:"lunora_get_function_schema"},{annotations:{...c,title:"Run a query"},description:"Run a query and return its result. Read-only.",inputSchema:h,name:"lunora_run_query"}],S=[{annotations:{destructiveHint:!0,idempotentHint:!1,openWorldHint:!0,readOnlyHint:!1,title:"Run a mutation (writes data)"},description:"Run a mutation and return its result. Writes data — use with care.",inputSchema:h,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 and return its result. May call external services.",inputSchema:h,name:"lunora_run_action"}],A=new Set(S.map(t=>t.name)),T=t=>t===!0?[...g,...S]:g,O=t=>{const{functionPath:e}=t;if(typeof e!="string"||e.length===0)throw new u("INTERNAL",'"functionPath" is required and must be a non-empty string');return e},_=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),w=t=>Array.isArray(t)?"an array":`a ${typeof t}`,v=t=>{if(t==null)return{};if(typeof t=="string"){let e;try{e=JSON.parse(t)}catch{throw new u("BAD_REQUEST",'"args" must be a JSON object; received a string that is not valid JSON')}if(!_(e))throw new u("BAD_REQUEST",`"args" must be a JSON object; the provided string decoded to ${w(e)}`);return e}if(!_(t))throw new u("BAD_REQUEST",`"args" must be a JSON object, got ${w(t)}`);return t},d=t=>{const e=O(t),n=v(t.args),i=typeof t.shardKey=="string"&&t.shardKey.length>0?t.shardKey:void 0;return{args:n,functionPath:e,shardKey:i}},f=t=>({__lunoraRef:t}),b=t=>{let e="";for(let n=0;n<t.length;n+=32768)e+=String.fromCharCode(...t.subarray(n,n+32768));return btoa(e)},R=(t,e)=>{if(typeof e=="bigint")return e.toString();if(e instanceof ArrayBuffer)return b(new Uint8Array(e));if(ArrayBuffer.isView(e)){const n=e;return b(new Uint8Array(n.buffer,n.byteOffset,n.byteLength))}return e},s=t=>({content:[{text:t===void 0?"null":JSON.stringify(t,R,2),type:"text"}]}),N=3e4,l=new WeakMap,y=t=>{const e=Date.now(),n=l.get(t);if(n!==void 0&&n.expiresAt>e)return n.promise;const i=t.listFunctions().catch(r=>{throw l.get(t)?.promise===i&&l.delete(t),r});return l.set(t,{expiresAt:e+N,promise:i}),i},p=async(t,e,n)=>{const i=(await y(t)).find(r=>r.path===e);if(i===void 0)throw new u("NOT_FOUND",`function not found or not public: ${e}`);if(i.kind!==n)throw new u("BAD_REQUEST",`function ${e} is a ${i.kind}, not a ${n}`)},L=async(t,e,n,i=!1)=>{try{if(i!==!0&&A.has(e))return{content:[{text:`tool "${e}" is disabled: this MCP server is read-only. Enable writes with the LUNORA_MCP_ALLOW_WRITES env var.`,type:"text"}],isError:!0};switch(e){case"lunora_get_function_schema":{const r=O(n),a=(await y(t)).find(o=>o.path===r);return a===void 0?{content:[{text:`function not found: ${r}`,type:"text"}],isError:!0}:s({args:a.args??[],kind:a.kind,path:a.path})}case"lunora_list_functions":return s(await y(t));case"lunora_list_tables":return s(await t.listGlobalTables());case"lunora_run_action":{const{args:r,functionPath:a,shardKey:o}=d(n);return await p(t,a,"action"),s(await t.action(f(a),r,{shardKey:o}))}case"lunora_run_mutation":{const{args:r,functionPath:a,shardKey:o}=d(n);return await p(t,a,"mutation"),s(await t.mutation(f(a),r,{shardKey:o}))}case"lunora_run_query":{const{args:r,functionPath:a,shardKey:o}=d(n);return await p(t,a,"query"),s(await t.query(f(a),r,{shardKey:o}))}default:return{content:[{text:`unknown tool: ${e}`,type:"text"}],isError:!0}}}catch(r){return{content:[{text:r instanceof Error?r.message:String(r),type:"text"}],isError:!0}}};export{g as READ_ONLY_TOOL_DEFINITIONS,S as WRITE_TOOL_DEFINITIONS,L as callTool,T as toolDefinitions};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as u}from"node:fs";import{dirname as l,join as f}from"node:path";import{fileURLToPath as p}from"node:url";import{LunoraClient as v}from"@lunora/client";import{LunoraError as g}from"@lunora/errors";import{Server as d}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as h}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListToolsRequestSchema as w,CallToolRequestSchema as M}from"@modelcontextprotocol/sdk/types.js";import{agentToolDefinitions as S,isAgentToolName as T,callAgentTool as L}from"./AGENT_RUN_INPUT_SCHEMA-ihZg6vIZ.mjs";import{toolDefinitions as k,callTool as x}from"./READ_ONLY_TOOL_DEFINITIONS-48jMcpI-.mjs";const y=()=>{try{let t=l(p(import.meta.url));for(let r=0;r<8;r+=1){try{const a=u(f(t,"package.json"),"utf8"),o=JSON.parse(a);if(o.name==="@lunora/mcp"&&typeof o.version=="string"&&o.version.length>0)return o.version}catch{}const e=l(t);if(e===t)break;t=e}}catch{}return"0.0.0"},A={name:"lunora",version:y()},R=t=>{if(t.client!==void 0)return t.client;if(t.url===void 0)throw new g("INTERNAL","createLunoraMcpServer requires either a `client` or a `url`");const r=new v({fetch:t.fetch,url:t.url});return t.token!==void 0&&r.setAuthToken(t.token),r},q=t=>{const r=R(t),e=t.allowWrites??!1,a=t.allowAgents??!1,o=t.agents??[],n=new d(A,{capabilities:{tools:{}}});return n.setRequestHandler(w,()=>({tools:[...k(e),...S(o,a)]})),n.setRequestHandler(M,async c=>{const{arguments:m,name:s}=c.params,i=m??{};return T(s,o)?await L(r,s,i,{allowAgents:a,exposures:o,...t.agentMaxWaitMs===void 0?{}:{maxWaitMs:t.agentMaxWaitMs},...t.agentPollIntervalMs===void 0?{}:{pollIntervalMs:t.agentPollIntervalMs}}):await x(r,s,i,e)}),n},O=async t=>{const r=q(t);return await r.connect(new h),r};export{O as connectStdio,q as createLunoraMcpServer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{serveStateless as s}from"./serveStateless-BqoRk-xU.mjs";import{createLunoraMcpServer as t}from"./connectStdio-Dt-DqEgA.mjs";const o=e=>r=>s(t(e),r);export{o as createMcpFetchHandler,s as serveStateless};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as p}from"@lunora/errors";import{Server as f}from"@modelcontextprotocol/sdk/server/index.js";import{ListToolsRequestSchema as y,CallToolRequestSchema as g}from"@modelcontextprotocol/sdk/types.js";import{serveStateless as w}from"./serveStateless-BqoRk-xU.mjs";const S=async()=>{try{return(await import("@lunora/x402/charge")).createChargeMiddleware}catch(a){throw new p("INTERNAL",`paid MCP tools need the optional peer "@lunora/x402" — install it alongside @lunora/mcp to charge for tools (${a instanceof Error?a.message:String(a)})`)}},M={name:"lunora-paid-mcp",version:"0.0.0"},R="tools/call",d=a=>{if(typeof a!="object"||a===null)return;const{method:s,params:n}=a;if(s!==R||typeof n!="object"||n===null)return;const{name:i}=n;return typeof i=="string"?i:void 0},C=()=>Response.json({error:"A JSON-RPC batch may not reference a paid MCP tool; send paid tools/call requests individually."},{status:400}),P=()=>Response.json({error:"The request body could not be parsed as JSON; a priced MCP tool is registered, so the call cannot be gated."},{status:400}),j=a=>{const s=new Map,n=new Map,i=new Map,m=a.serverInfo??M,l=(e,t,o)=>{if(s.has(e.name))throw new p("BAD_REQUEST",`MCP tool "${e.name}" is already registered.`);const r={description:e.description,inputSchema:e.inputSchema,name:e.name};e.annotations!==void 0&&(r.annotations=e.annotations),s.set(e.name,{definition:r,handler:t}),o!==void 0&&n.set(e.name,o)},u=()=>{const e=new f(m,{capabilities:{tools:{}}});return e.setRequestHandler(y,()=>({tools:[...s.values()].map(t=>t.definition)})),e.setRequestHandler(g,async t=>{const o=s.get(t.params.name);return o===void 0?{content:[{text:`unknown tool: ${t.params.name}`,type:"text"}],isError:!0}:await o.handler(t.params.arguments??{})}),e},h=(e,t)=>{let o=i.get(e);if(o===void 0){const r=S().then(async c=>c({...a.charge,price:t},{resource:e})).catch(c=>{throw i.delete(e),c});o=r,i.set(e,r)}return o};return{fetchHandler:async e=>{let t;try{t=await e.clone().json()}catch{t=void 0}const o=()=>w(u(),e,t===void 0?void 0:{parsedBody:t});if(t===void 0&&n.size>0&&e.method==="POST")return P();if(Array.isArray(t))return t.some(v=>n.has(d(v)??""))?C():o();const r=d(t),c=r===void 0?void 0:n.get(r);return r===void 0||c===void 0?o():(await h(r,c)).handle(e,o)},paidTool:(e,t)=>{l(e,t,e.price)},tool:(e,t)=>{l(e,t)}}};export{j as createPaidMcpServer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Server as l}from"@modelcontextprotocol/sdk/server/index.js";import{ListToolsRequestSchema as d,CallToolRequestSchema as p,ListResourcesRequestSchema as R,ReadResourceRequestSchema as f}from"@modelcontextprotocol/sdk/types.js";const h=(c,u,s)=>{const n=new Map;for(const e of u)n.has(e.definition.name)||n.set(e.definition.name,e);const m=[...n.values()].map(e=>e.definition),r=new l(c,{capabilities:s===void 0?{tools:{}}:{resources:{},tools:{}}});return r.setRequestHandler(d,()=>({tools:m})),r.setRequestHandler(p,async e=>{const{arguments:o,name:t}=e.params,i=n.get(t);if(i===void 0)return{content:[{text:`unknown tool: ${t}`,type:"text"}],isError:!0};try{return await i.handle(o??{})}catch(a){return{content:[{text:a instanceof Error?a.message:String(a),type:"text"}],isError:!0}}}),s!==void 0&&(r.setRequestHandler(R,async()=>({resources:[...await s.list()]})),r.setRequestHandler(f,async e=>{const{uri:o}=e.params,t=await s.read(o);if(t===void 0)throw new Error(`unknown resource: ${o}`);return{contents:[{mimeType:t.mimeType??"text/plain",text:t.text,uri:o}]}})),r};export{h as createToolServer};
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
/** The MCP `CallToolResult` shape this package's tools return. */
|
|
47
|
+
interface ToolResult {
|
|
48
|
+
content: {
|
|
49
|
+
text: string;
|
|
50
|
+
type: "text";
|
|
51
|
+
}[];
|
|
52
|
+
isError?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** A tool: what to advertise in `ListTools`, and what to run on `CallTool`. */
|
|
55
|
+
interface McpTool {
|
|
56
|
+
definition: ToolDefinition;
|
|
57
|
+
handle: (input: Record<string, unknown>) => Promise<ToolResult>;
|
|
58
|
+
}
|
|
59
|
+
/** Name/version pair reported in the MCP `initialize` handshake. */
|
|
60
|
+
interface McpServerInfo {
|
|
61
|
+
name: string;
|
|
62
|
+
version: string;
|
|
63
|
+
}
|
|
64
|
+
/** One addressable document a client can list and read directly. */
|
|
65
|
+
interface McpResourceSummary {
|
|
66
|
+
description?: string;
|
|
67
|
+
mimeType?: string;
|
|
68
|
+
name: string;
|
|
69
|
+
uri: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A set of resources the server exposes alongside its tools.
|
|
73
|
+
*
|
|
74
|
+
* Tools are calls a model chooses to make; resources are documents a *client*
|
|
75
|
+
* can enumerate and attach on its own — the user picking a page to put in
|
|
76
|
+
* context, without the model having to guess a search query first. The same
|
|
77
|
+
* corpus is worth offering both ways.
|
|
78
|
+
*/
|
|
79
|
+
interface McpResourceProvider {
|
|
80
|
+
/** Every resource, for `resources/list`. */
|
|
81
|
+
list: () => Promise<ReadonlyArray<McpResourceSummary>>;
|
|
82
|
+
/** One resource's body, or `undefined` when the uri is unknown. */
|
|
83
|
+
read: (uri: string) => Promise<{
|
|
84
|
+
mimeType?: string;
|
|
85
|
+
text: string;
|
|
86
|
+
} | undefined>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build an MCP `Server` that advertises and dispatches `tools`.
|
|
90
|
+
*
|
|
91
|
+
* On duplicate names the FIRST wins, so a caller can layer surfaces in
|
|
92
|
+
* precedence order without the later list silently shadowing the earlier one.
|
|
93
|
+
* Thrown errors become `isError` results rather than rejections, per the MCP
|
|
94
|
+
* convention that a tool failure is output the model can read and react to,
|
|
95
|
+
* not a protocol-level fault.
|
|
96
|
+
*/
|
|
97
|
+
declare const createToolServer: (info: McpServerInfo, tools: ReadonlyArray<McpTool>, resources?: McpResourceProvider) => Server;
|
|
98
|
+
/** A Web-Standard fetch handler: takes a `Request`, returns the MCP `Response`. */
|
|
99
|
+
type McpFetchHandler = (request: Request) => Promise<Response>;
|
|
100
|
+
/**
|
|
101
|
+
* Drive one request through a fresh **stateless** Streamable-HTTP transport bound
|
|
102
|
+
* to `server`, then tear both down.
|
|
103
|
+
*
|
|
104
|
+
* `sessionIdGenerator: undefined` → stateless (no cross-request session store);
|
|
105
|
+
* `enableJsonResponse: true` → the whole JSON-RPC response is buffered into the
|
|
106
|
+
* returned `Response`, so closing the transport/server afterwards can't truncate
|
|
107
|
+
* an in-flight body. Cleanup is fire-and-forget with swallowed rejections (a
|
|
108
|
+
* `.catch()`-terminated chain) so it never delays or masks the resolved response.
|
|
109
|
+
*
|
|
110
|
+
* `options.parsedBody` lets a caller hand over a body it already read (e.g. the
|
|
111
|
+
* paid-tool gate, which peeks the JSON-RPC message to price the call) so the
|
|
112
|
+
* transport doesn't re-read a consumed stream.
|
|
113
|
+
*
|
|
114
|
+
* Teardown runs in a `finally`: a rejection from `connect` or `handleRequest`
|
|
115
|
+
* would otherwise skip it and leak a server + transport per failed request,
|
|
116
|
+
* which on a public endpoint is exactly the request an attacker can repeat.
|
|
117
|
+
*/
|
|
118
|
+
declare const serveStateless: (server: Server, request: Request, options?: HandleRequestOptions) => Promise<Response>;
|
|
119
|
+
export { McpResourceProvider as M, 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,119 @@
|
|
|
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
|
+
/** The MCP `CallToolResult` shape this package's tools return. */
|
|
47
|
+
interface ToolResult {
|
|
48
|
+
content: {
|
|
49
|
+
text: string;
|
|
50
|
+
type: "text";
|
|
51
|
+
}[];
|
|
52
|
+
isError?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** A tool: what to advertise in `ListTools`, and what to run on `CallTool`. */
|
|
55
|
+
interface McpTool {
|
|
56
|
+
definition: ToolDefinition;
|
|
57
|
+
handle: (input: Record<string, unknown>) => Promise<ToolResult>;
|
|
58
|
+
}
|
|
59
|
+
/** Name/version pair reported in the MCP `initialize` handshake. */
|
|
60
|
+
interface McpServerInfo {
|
|
61
|
+
name: string;
|
|
62
|
+
version: string;
|
|
63
|
+
}
|
|
64
|
+
/** One addressable document a client can list and read directly. */
|
|
65
|
+
interface McpResourceSummary {
|
|
66
|
+
description?: string;
|
|
67
|
+
mimeType?: string;
|
|
68
|
+
name: string;
|
|
69
|
+
uri: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A set of resources the server exposes alongside its tools.
|
|
73
|
+
*
|
|
74
|
+
* Tools are calls a model chooses to make; resources are documents a *client*
|
|
75
|
+
* can enumerate and attach on its own — the user picking a page to put in
|
|
76
|
+
* context, without the model having to guess a search query first. The same
|
|
77
|
+
* corpus is worth offering both ways.
|
|
78
|
+
*/
|
|
79
|
+
interface McpResourceProvider {
|
|
80
|
+
/** Every resource, for `resources/list`. */
|
|
81
|
+
list: () => Promise<ReadonlyArray<McpResourceSummary>>;
|
|
82
|
+
/** One resource's body, or `undefined` when the uri is unknown. */
|
|
83
|
+
read: (uri: string) => Promise<{
|
|
84
|
+
mimeType?: string;
|
|
85
|
+
text: string;
|
|
86
|
+
} | undefined>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build an MCP `Server` that advertises and dispatches `tools`.
|
|
90
|
+
*
|
|
91
|
+
* On duplicate names the FIRST wins, so a caller can layer surfaces in
|
|
92
|
+
* precedence order without the later list silently shadowing the earlier one.
|
|
93
|
+
* Thrown errors become `isError` results rather than rejections, per the MCP
|
|
94
|
+
* convention that a tool failure is output the model can read and react to,
|
|
95
|
+
* not a protocol-level fault.
|
|
96
|
+
*/
|
|
97
|
+
declare const createToolServer: (info: McpServerInfo, tools: ReadonlyArray<McpTool>, resources?: McpResourceProvider) => Server;
|
|
98
|
+
/** A Web-Standard fetch handler: takes a `Request`, returns the MCP `Response`. */
|
|
99
|
+
type McpFetchHandler = (request: Request) => Promise<Response>;
|
|
100
|
+
/**
|
|
101
|
+
* Drive one request through a fresh **stateless** Streamable-HTTP transport bound
|
|
102
|
+
* to `server`, then tear both down.
|
|
103
|
+
*
|
|
104
|
+
* `sessionIdGenerator: undefined` → stateless (no cross-request session store);
|
|
105
|
+
* `enableJsonResponse: true` → the whole JSON-RPC response is buffered into the
|
|
106
|
+
* returned `Response`, so closing the transport/server afterwards can't truncate
|
|
107
|
+
* an in-flight body. Cleanup is fire-and-forget with swallowed rejections (a
|
|
108
|
+
* `.catch()`-terminated chain) so it never delays or masks the resolved response.
|
|
109
|
+
*
|
|
110
|
+
* `options.parsedBody` lets a caller hand over a body it already read (e.g. the
|
|
111
|
+
* paid-tool gate, which peeks the JSON-RPC message to price the call) so the
|
|
112
|
+
* transport doesn't re-read a consumed stream.
|
|
113
|
+
*
|
|
114
|
+
* Teardown runs in a `finally`: a rejection from `connect` or `handleRequest`
|
|
115
|
+
* would otherwise skip it and leak a server + transport per failed request,
|
|
116
|
+
* which on a public endpoint is exactly the request an attacker can repeat.
|
|
117
|
+
*/
|
|
118
|
+
declare const serveStateless: (server: Server, request: Request, options?: HandleRequestOptions) => Promise<Response>;
|
|
119
|
+
export { McpResourceProvider as M, 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
|
+
import{WebStandardStreamableHTTPServerTransport as n}from"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";const o=async(a,s,t)=>{const e=new n({enableJsonResponse:!0,sessionIdGenerator:void 0});try{return await a.connect(e),await e.handleRequest(s,t)}finally{e.close().catch(()=>{}),a.close().catch(()=>{})}};export{o as serveStateless};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const i=r=>typeof r=="object"&&r!==null,l=r=>r.split("<mark>").join("").split("</mark>").join(""),c=r=>Array.isArray(r)?r.filter(t=>typeof t=="string"):[],u=r=>{if(!i(r)||typeof r.url!="string"||r.url.length===0)return;const t=r,n=typeof t.content=="string"?l(t.content):"",e=c(t.breadcrumbs),o=t.type==="page",s=o?n:e.at(-1)??n;return{...o||n.length===0?{}:{excerpt:n},...e.length>0?{section:e.join(" › ")}:{},title:s.length>0?s:r.url,url:r.url}},p=r=>{const t=[];for(const n of r){const e=u(n);e!==void 0&&t.push(e)}return t};export{p as toDocsSearchHits};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/mcp",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.60",
|
|
4
4
|
"description": "Model Context Protocol server exposing a Lunora deployment to AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -43,15 +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.
|
|
56
|
+
"@lunora/client": "1.0.0-alpha.43",
|
|
57
|
+
"@lunora/errors": "1.0.0-alpha.16",
|
|
53
58
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
54
59
|
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@lunora/x402": ">=1.0.0-alpha.21 <2.0.0-0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@lunora/x402": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
55
68
|
"engines": {
|
|
56
69
|
"node": "^22.15.0 || >=24.11.0"
|
|
57
70
|
}
|
|
@@ -1,112 +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 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
|
-
description: "Run a mutation and return its result. Writes data — use with care.",
|
|
41
|
-
inputSchema: RUN_INPUT_SCHEMA,
|
|
42
|
-
name: "lunora_run_mutation"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
description: "Run an action and return its result. May call external services.",
|
|
46
|
-
inputSchema: RUN_INPUT_SCHEMA,
|
|
47
|
-
name: "lunora_run_action"
|
|
48
|
-
}
|
|
49
|
-
];
|
|
50
|
-
const readFunctionPath = (input) => {
|
|
51
|
-
const { functionPath } = input;
|
|
52
|
-
if (typeof functionPath !== "string" || functionPath.length === 0) {
|
|
53
|
-
throw new Error('"functionPath" is required and must be a non-empty string');
|
|
54
|
-
}
|
|
55
|
-
return functionPath;
|
|
56
|
-
};
|
|
57
|
-
const readRunArguments = (input) => {
|
|
58
|
-
const functionPath = readFunctionPath(input);
|
|
59
|
-
const rawArguments = input.args;
|
|
60
|
-
const isPlainObject = typeof rawArguments === "object" && rawArguments !== null && !Array.isArray(rawArguments);
|
|
61
|
-
const args = isPlainObject ? rawArguments : {};
|
|
62
|
-
const shardKey = typeof input.shardKey === "string" && input.shardKey.length > 0 ? input.shardKey : void 0;
|
|
63
|
-
return { args, functionPath, shardKey };
|
|
64
|
-
};
|
|
65
|
-
const reference = (functionPath) => {
|
|
66
|
-
return { __lunoraRef: functionPath };
|
|
67
|
-
};
|
|
68
|
-
const ok = (value) => {
|
|
69
|
-
const text = value === void 0 ? "null" : JSON.stringify(value, void 0, 2);
|
|
70
|
-
return { content: [{ text, type: "text" }] };
|
|
71
|
-
};
|
|
72
|
-
const callTool = async (client, name, input) => {
|
|
73
|
-
try {
|
|
74
|
-
switch (name) {
|
|
75
|
-
case "lunora_get_function_schema": {
|
|
76
|
-
const functionPath = readFunctionPath(input);
|
|
77
|
-
const functions = await client.listFunctions();
|
|
78
|
-
const descriptor = functions.find((function_) => function_.path === functionPath);
|
|
79
|
-
if (descriptor === void 0) {
|
|
80
|
-
return { content: [{ text: `function not found: ${functionPath}`, type: "text" }], isError: true };
|
|
81
|
-
}
|
|
82
|
-
return ok({ args: descriptor.args ?? [], kind: descriptor.kind, path: descriptor.path });
|
|
83
|
-
}
|
|
84
|
-
case "lunora_list_functions": {
|
|
85
|
-
return ok(await client.listFunctions());
|
|
86
|
-
}
|
|
87
|
-
case "lunora_list_tables": {
|
|
88
|
-
return ok(await client.listGlobalTables());
|
|
89
|
-
}
|
|
90
|
-
case "lunora_run_action": {
|
|
91
|
-
const { args, functionPath, shardKey } = readRunArguments(input);
|
|
92
|
-
return ok(await client.action(reference(functionPath), args, { shardKey }));
|
|
93
|
-
}
|
|
94
|
-
case "lunora_run_mutation": {
|
|
95
|
-
const { args, functionPath, shardKey } = readRunArguments(input);
|
|
96
|
-
return ok(await client.mutation(reference(functionPath), args, { shardKey }));
|
|
97
|
-
}
|
|
98
|
-
case "lunora_run_query": {
|
|
99
|
-
const { args, functionPath, shardKey } = readRunArguments(input);
|
|
100
|
-
return ok(await client.query(reference(functionPath), args, { shardKey }));
|
|
101
|
-
}
|
|
102
|
-
default: {
|
|
103
|
-
return { content: [{ text: `unknown tool: ${name}`, type: "text" }], isError: true };
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
} catch (error) {
|
|
107
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
-
return { content: [{ text: message, type: "text" }], isError: true };
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
export { TOOL_DEFINITIONS, callTool };
|
|
@@ -1,64 +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 { TOOL_DEFINITIONS, callTool } from './TOOL_DEFINITIONS-Dpiu38ji.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 server = new Server(SERVER_INFO, { capabilities: { tools: {} } });
|
|
49
|
-
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
50
|
-
return { tools: [...TOOL_DEFINITIONS] };
|
|
51
|
-
});
|
|
52
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
53
|
-
const result = await callTool(client, request.params.name, request.params.arguments ?? {});
|
|
54
|
-
return result;
|
|
55
|
-
});
|
|
56
|
-
return server;
|
|
57
|
-
};
|
|
58
|
-
const connectStdio = async (options) => {
|
|
59
|
-
const server = createLunoraMcpServer(options);
|
|
60
|
-
await server.connect(new StdioServerTransport());
|
|
61
|
-
return server;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export { connectStdio, createLunoraMcpServer };
|