@stacksjs/ai 0.75.189 → 0.75.191

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.
@@ -1 +1 @@
1
- import{readJsonBody,RequestBodyError}from"@stacksjs/router/body";import{validateSchema}from"./schema";export const MCP_PROTOCOL_VERSIONS=["2025-11-25","2025-06-18","2025-03-26"];class ProtocolError extends Error{code;constructor(code,message){super(message);this.code=code}}const invalid=(message)=>{throw new ProtocolError(-32602,message)};function record(value){return value!==null&&typeof value==="object"&&!Array.isArray(value)}function json(body,status=200,headers){const response=Response.json(body,{status,headers});response.headers.set("Cache-Control","no-store");response.headers.set("X-Content-Type-Options","nosniff");return response}function failure(id,code,message,status=200){return json({jsonrpc:"2.0",id,error:{code,message}},status)}function accepts(header,type){return(header??"").split(",").some((part)=>{const[media,...params]=part.trim().toLowerCase().split(";"),quality=params.find((param)=>param.trim().startsWith("q="))?.trim().slice(2);return media?.trim()===type&&(quality===void 0||Number(quality)>0&&Number(quality)<=1)})}function checkSchema(schema){const supported=new Set(["$schema","type","description","title","default","examples","enum","const","properties","required","additionalProperties","items","minimum","maximum","minLength","maxLength","minItems","maxItems","pattern"]);for(const key of Object.keys(schema))if(!supported.has(key))throw TypeError(`Unsupported MCP schema keyword: ${key}; use an execute validator for domain rules`);if(schema.additionalProperties!==void 0&&typeof schema.additionalProperties!=="boolean")throw TypeError("MCP additionalProperties must be a boolean");if(schema.pattern!==void 0)new RegExp(String(schema.pattern));if(record(schema.properties))for(const child of Object.values(schema.properties)){if(!record(child))throw TypeError("MCP property schemas must be objects");checkSchema(child)}if(schema.items!==void 0){if(!record(schema.items))throw TypeError("MCP items must be an object schema");checkSchema(schema.items)}}export function createMCPServer(options){const tools=[...options.tools??[]],resources=[...options.resources??[]],prompts=[...options.prompts??[]],pageSize=options.pageSize??50,maxBodyBytes=options.maxBodyBytes??1048576;if(!Number.isSafeInteger(pageSize)||pageSize<1||pageSize>1000)throw RangeError("MCP pageSize must be between 1 and 1000");if(!Number.isSafeInteger(maxBodyBytes)||maxBodyBytes<1)throw RangeError("MCP maxBodyBytes must be positive");for(const list of[tools,prompts]){const names=new Set;for(const item of list){if(!/^[\w.-]{1,128}$/.test(item.name)||names.has(item.name))throw TypeError(`Invalid or duplicate MCP name: ${item.name}`);names.add(item.name)}}if(new Set(resources.map((item)=>item.uri)).size!==resources.length)throw TypeError("Duplicate MCP resource URI");for(const tool of tools){if(tool.inputSchema.type!=="object")throw TypeError("MCP tool inputSchema must be an object schema");checkSchema(tool.inputSchema);if(tool.outputSchema){if(tool.outputSchema.type!=="object")throw TypeError("MCP tool outputSchema must be an object schema");checkSchema(tool.outputSchema)}}function page(items,params,kind){let offset=0;if(params.cursor!==void 0){if(typeof params.cursor!=="string")invalid("Invalid pagination cursor");const match=Buffer.from(params.cursor,"base64url").toString().match(new RegExp(`^${kind}:(\\d+)$`));if(!match)invalid("Invalid pagination cursor");offset=Number(match[1]);if(!Number.isSafeInteger(offset)||offset<1||offset>=items.length||offset%pageSize!==0)invalid("Invalid pagination cursor")}const next=offset+pageSize;return{[kind]:items.slice(offset,next),...next<items.length?{nextCursor:Buffer.from(`${kind}:${next}`).toString("base64url")}:{}}}async function dispatch(method,params,context){const visible=(items)=>items.filter((item)=>!item.available||item.available(context.principal));if(method==="initialize"){if(typeof params.protocolVersion!=="string"||!record(params.capabilities)||!record(params.clientInfo)||typeof params.clientInfo.name!=="string"||typeof params.clientInfo.version!=="string")invalid("Invalid initialization parameters");return{protocolVersion:MCP_PROTOCOL_VERSIONS.includes(params.protocolVersion)?params.protocolVersion:MCP_PROTOCOL_VERSIONS[0],capabilities:{tools:{listChanged:!1},resources:{subscribe:!1,listChanged:!1},prompts:{listChanged:!1}},serverInfo:{name:options.name,version:options.version},...options.instructions?{instructions:options.instructions}:{}}}if(method==="ping")return{};if(method==="tools/list")return page(visible(tools).map(({execute,available,...tool})=>tool),params,"tools");if(method==="tools/call"){if(typeof params.name!=="string"||params.arguments!==void 0&&!record(params.arguments))invalid("Send a tool name and object arguments");const tool=visible(tools).find((item)=>item.name===params.name);if(!tool)invalid("Unknown or unavailable tool");const args=params.arguments??{},errors=validateSchema(args,tool.inputSchema);if(errors.length)invalid(errors.slice(0,5).join("; "));try{context.signal.throwIfAborted();const result=await tool.execute(args,context);if(tool.outputSchema&&!result.isError){if(!result.structuredContent||validateSchema(result.structuredContent,tool.outputSchema).length)throw Error(`MCP output failed validation: ${tool.name}`)}return result}catch(error){options.onError?.(error,context);return{content:[{type:"text",text:"The operation failed. Try again or check the application."}],isError:!0}}}if(method==="resources/list")return page(visible(resources).map(({read,available,...resource})=>resource),params,"resources");if(method==="resources/templates/list")return{resourceTemplates:[]};if(method==="resources/read"){if(typeof params.uri!=="string")invalid("Send a resource URI");const resource=visible(resources).find((item)=>item.uri===params.uri);if(!resource)throw new ProtocolError(-32002,"Unknown or unavailable resource");return{contents:[{uri:resource.uri,mimeType:resource.mimeType??"text/plain",text:await resource.read(context)}]}}if(method==="prompts/list")return page(visible(prompts).map(({get,available,...prompt})=>prompt),params,"prompts");if(method==="prompts/get"){if(typeof params.name!=="string"||params.arguments!==void 0&&!record(params.arguments))invalid("Send a prompt name and string arguments");const prompt=visible(prompts).find((item)=>item.name===params.name);if(!prompt)invalid("Unknown or unavailable prompt");const args=params.arguments??{},declared=prompt.arguments??[];if(Object.entries(args).some(([key,value])=>typeof value!=="string"||value.length>16000||!declared.some((arg)=>arg.name===key)))invalid("Invalid prompt arguments");if(declared.some((arg)=>arg.required&&!args[arg.name]))invalid("Missing required prompt argument");return prompt.get(args,context)}throw new ProtocolError(-32601,"Method not found")}return{async handle(request){const origin=request.headers.get("origin");if(origin&&origin!==new URL(request.url).origin&&!options.allowedOrigins?.includes(origin))return failure(null,-32600,"Origin is not allowed",403);let principal;try{principal=await options.authenticate(request)}catch{return failure(null,-32603,"Authentication service failed",500)}if(principal instanceof Response)return principal;const finish=(response)=>{if(options.responseHeaders)new Headers(options.responseHeaders(principal)).forEach((value,key)=>response.headers.set(key,value));return response};if(request.method!=="POST")return finish(json({error:"Use POST for this stateless MCP endpoint"},405,{Allow:"POST"}));const version=request.headers.get("mcp-protocol-version");if(version&&!MCP_PROTOCOL_VERSIONS.includes(version))return finish(failure(null,-32600,"Unsupported MCP protocol version",400));if(!accepts(request.headers.get("accept"),"application/json")||!accepts(request.headers.get("accept"),"text/event-stream"))return finish(failure(null,-32600,"Accept must include application/json and text/event-stream",406));let message;try{message=await readJsonBody(request,maxBodyBytes)}catch(error){if(error instanceof RequestBodyError)return finish(failure(null,-32700,error.message,error.status));return finish(failure(null,-32603,"Could not read request body",500))}if(!record(message)||message.jsonrpc!=="2.0"||typeof message.method!=="string"||Object.hasOwn(message,"result")||Object.hasOwn(message,"error")||message.params!==void 0&&!record(message.params))return finish(failure(null,-32600,"Invalid JSON-RPC request",400));if(!Object.hasOwn(message,"id")){if(!message.method.startsWith("notifications/"))return finish(failure(null,-32600,"Requests require an id",400));return finish(new Response(null,{status:202,headers:{"Cache-Control":"no-store"}}))}const id=message.id;if(typeof id!=="string"&&(typeof id!=="number"||!Number.isSafeInteger(id)))return finish(failure(null,-32600,"Request id must be a string or safe integer",400));const context={principal,request,signal:request.signal};try{const result=await dispatch(message.method,message.params??{},context);return finish(json({jsonrpc:"2.0",id,result}))}catch(error){if(error instanceof ProtocolError)return finish(failure(id,error.code,error.message));options.onError?.(error,context);return finish(failure(id,-32603,"Internal error"))}}}}export function mcpTextResult(data,isError=!1){return{content:[{type:"text",text:JSON.stringify(data)}],structuredContent:data,...isError?{isError:!0}:{}}}
1
+ import{readJsonBody,RequestBodyError}from"@stacksjs/router/body";import{validateSchema}from"./schema";export const MCP_PROTOCOL_VERSIONS=["2025-11-25","2025-06-18","2025-03-26"];class ProtocolError extends Error{code;constructor(code,message){super(message);this.code=code}}const invalid=(message)=>{throw new ProtocolError(-32602,message)};function record(value){return value!==null&&typeof value==="object"&&!Array.isArray(value)}function json(body,status=200,headers){const response=Response.json(body,{status,headers});response.headers.set("Cache-Control","no-store");response.headers.set("X-Content-Type-Options","nosniff");return response}function failure(id,code,message,status=200){return json({jsonrpc:"2.0",id,error:{code,message}},status)}function accepts(header,type){return(header??"").split(",").some((part)=>{const[media,...params]=part.trim().toLowerCase().split(";"),quality=params.find((param)=>param.trim().startsWith("q="))?.trim().slice(2);return media?.trim()===type&&(quality===void 0||Number(quality)>0&&Number(quality)<=1)})}function checkSchema(schema){const supported=new Set(["$schema","type","description","title","default","examples","enum","const","properties","required","additionalProperties","items","minimum","maximum","minLength","maxLength","minItems","maxItems","pattern"]);for(const key of Object.keys(schema))if(!supported.has(key))throw TypeError(`Unsupported MCP schema keyword: ${key}; use an execute validator for domain rules`);if(schema.additionalProperties!==void 0&&typeof schema.additionalProperties!=="boolean")throw TypeError("MCP additionalProperties must be a boolean");if(schema.pattern!==void 0)new RegExp(String(schema.pattern));if(record(schema.properties))for(const child of Object.values(schema.properties)){if(!record(child))throw TypeError("MCP property schemas must be objects");checkSchema(child)}if(schema.items!==void 0){if(!record(schema.items))throw TypeError("MCP items must be an object schema");checkSchema(schema.items)}}export function createMCPServer(options){const tools=[...options.tools??[]],resources=[...options.resources??[]],prompts=[...options.prompts??[]],pageSize=options.pageSize??50,maxBodyBytes=options.maxBodyBytes??1048576;if(!Number.isSafeInteger(pageSize)||pageSize<1||pageSize>1000)throw RangeError("MCP pageSize must be between 1 and 1000");if(!Number.isSafeInteger(maxBodyBytes)||maxBodyBytes<1)throw RangeError("MCP maxBodyBytes must be positive");for(const list of[tools,prompts]){const names=new Set;for(const item of list){if(!/^[\w.-]{1,128}$/.test(item.name)||names.has(item.name))throw TypeError(`Invalid or duplicate MCP name: ${item.name}`);names.add(item.name)}}for(const origin of options.allowedOrigins??[])if(origin==="null"||new URL(origin).origin!==origin||!/^https?:/.test(origin))throw TypeError("MCP allowedOrigins must contain exact HTTP(S) origins");if(new Set(resources.map((item)=>item.uri)).size!==resources.length)throw TypeError("Duplicate MCP resource URI");for(const tool of tools){if(tool.inputSchema.type!=="object")throw TypeError("MCP tool inputSchema must be an object schema");checkSchema(tool.inputSchema);if(tool.outputSchema){if(tool.outputSchema.type!=="object")throw TypeError("MCP tool outputSchema must be an object schema");checkSchema(tool.outputSchema)}}function page(items,params,kind){let offset=0;if(params.cursor!==void 0){if(typeof params.cursor!=="string")invalid("Invalid pagination cursor");const match=Buffer.from(params.cursor,"base64url").toString().match(new RegExp(`^${kind}:(\\d+)$`));if(!match)invalid("Invalid pagination cursor");offset=Number(match[1]);if(!Number.isSafeInteger(offset)||offset<1||offset>=items.length||offset%pageSize!==0)invalid("Invalid pagination cursor")}const next=offset+pageSize;return{[kind]:items.slice(offset,next),...next<items.length?{nextCursor:Buffer.from(`${kind}:${next}`).toString("base64url")}:{}}}async function dispatch(method,params,context){const visible=(items)=>items.filter((item)=>!item.available||item.available(context.principal));if(method==="initialize"){if(typeof params.protocolVersion!=="string"||!record(params.capabilities)||!record(params.clientInfo)||typeof params.clientInfo.name!=="string"||typeof params.clientInfo.version!=="string")invalid("Invalid initialization parameters");return{protocolVersion:MCP_PROTOCOL_VERSIONS.includes(params.protocolVersion)?params.protocolVersion:MCP_PROTOCOL_VERSIONS[0],capabilities:{tools:{listChanged:!1},resources:{subscribe:!1,listChanged:!1},prompts:{listChanged:!1}},serverInfo:{name:options.name,version:options.version},...options.instructions?{instructions:options.instructions}:{}}}if(method==="ping")return{};if(method==="tools/list")return page(visible(tools).map(({execute,available,...tool})=>tool),params,"tools");if(method==="tools/call"){if(typeof params.name!=="string"||params.arguments!==void 0&&!record(params.arguments))invalid("Send a tool name and object arguments");const tool=visible(tools).find((item)=>item.name===params.name);if(!tool)invalid("Unknown or unavailable tool");const args=params.arguments??{},errors=validateSchema(args,tool.inputSchema);if(errors.length)invalid(errors.slice(0,5).join("; "));try{context.signal.throwIfAborted();const result=await tool.execute(args,context);if(tool.outputSchema&&!result.isError){if(!result.structuredContent||validateSchema(result.structuredContent,tool.outputSchema).length)throw Error(`MCP output failed validation: ${tool.name}`)}return result}catch(error){options.onError?.(error,context);return{content:[{type:"text",text:"The operation failed. Try again or check the application."}],isError:!0}}}if(method==="resources/list")return page(visible(resources).map(({read,available,...resource})=>resource),params,"resources");if(method==="resources/templates/list")return{resourceTemplates:[]};if(method==="resources/read"){if(typeof params.uri!=="string")invalid("Send a resource URI");const resource=visible(resources).find((item)=>item.uri===params.uri);if(!resource)throw new ProtocolError(-32002,"Unknown or unavailable resource");return{contents:[{uri:resource.uri,mimeType:resource.mimeType??"text/plain",text:await resource.read(context)}]}}if(method==="prompts/list")return page(visible(prompts).map(({get,available,...prompt})=>prompt),params,"prompts");if(method==="prompts/get"){if(typeof params.name!=="string"||params.arguments!==void 0&&!record(params.arguments))invalid("Send a prompt name and string arguments");const prompt=visible(prompts).find((item)=>item.name===params.name);if(!prompt)invalid("Unknown or unavailable prompt");const args=params.arguments??{},declared=prompt.arguments??[];if(Object.entries(args).some(([key,value])=>typeof value!=="string"||value.length>16000||!declared.some((arg)=>arg.name===key)))invalid("Invalid prompt arguments");if(declared.some((arg)=>arg.required&&!args[arg.name]))invalid("Missing required prompt argument");return prompt.get(args,context)}throw new ProtocolError(-32601,"Method not found")}return{async handle(request){const origin=request.headers.get("origin");if(origin&&!options.allowedOrigins?.includes(origin))return failure(null,-32600,"Origin is not allowed",403);let principal;try{principal=await options.authenticate(request)}catch{return failure(null,-32603,"Authentication service failed",500)}if(principal instanceof Response)return principal;const finish=(response)=>{if(options.responseHeaders)new Headers(options.responseHeaders(principal)).forEach((value,key)=>response.headers.set(key,value));return response};if(request.method!=="POST")return finish(json({error:"Use POST for this stateless MCP endpoint"},405,{Allow:"POST"}));const version=request.headers.get("mcp-protocol-version");if(version&&!MCP_PROTOCOL_VERSIONS.includes(version))return finish(failure(null,-32600,"Unsupported MCP protocol version",400));if(!accepts(request.headers.get("accept"),"application/json")||!accepts(request.headers.get("accept"),"text/event-stream"))return finish(failure(null,-32600,"Accept must include application/json and text/event-stream",406));let message;try{message=await readJsonBody(request,maxBodyBytes)}catch(error){if(error instanceof RequestBodyError)return finish(failure(null,-32700,error.message,error.status));return finish(failure(null,-32603,"Could not read request body",500))}if(!record(message)||message.jsonrpc!=="2.0"||typeof message.method!=="string"||Object.hasOwn(message,"result")||Object.hasOwn(message,"error")||message.params!==void 0&&!record(message.params))return finish(failure(null,-32600,"Invalid JSON-RPC request",400));if(!Object.hasOwn(message,"id")){if(!message.method.startsWith("notifications/"))return finish(failure(null,-32600,"Requests require an id",400));return finish(new Response(null,{status:202,headers:{"Cache-Control":"no-store"}}))}const id=message.id;if(typeof id!=="string"&&(typeof id!=="number"||!Number.isSafeInteger(id)))return finish(failure(null,-32600,"Request id must be a string or safe integer",400));const context={principal,request,signal:request.signal};try{const result=await dispatch(message.method,message.params??{},context);return finish(json({jsonrpc:"2.0",id,result}))}catch(error){if(error instanceof ProtocolError)return finish(failure(id,error.code,error.message));options.onError?.(error,context);return finish(failure(id,-32603,"Internal error"))}}}}export function mcpTextResult(data,isError=!1){return{content:[{type:"text",text:JSON.stringify(data)}],structuredContent:data,...isError?{isError:!0}:{}}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/ai",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.189",
5
+ "version": "0.75.191",
6
6
  "description": "Stacks Artificial Intelligence.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -65,10 +65,10 @@
65
65
  "prepublishOnly": "bun run build"
66
66
  },
67
67
  "dependencies": {
68
- "@stacksjs/cli": "0.75.189",
69
- "@stacksjs/config": "0.75.189",
70
- "@stacksjs/router": "0.75.189",
71
- "@stacksjs/storage": "0.75.189",
68
+ "@stacksjs/cli": "0.75.191",
69
+ "@stacksjs/config": "0.75.191",
70
+ "@stacksjs/router": "0.75.191",
71
+ "@stacksjs/storage": "0.75.191",
72
72
  "@stacksjs/ts-cloud": "^0.16.56"
73
73
  },
74
74
  "peerDependencies": {