@stacksjs/ai 0.75.187 → 0.75.189
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/dist/client.d.ts +1 -1
- package/dist/client.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/mcp-server.d.ts +67 -0
- package/dist/mcp-server.js +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +3 -3
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +1 -0
- package/package.json +5 -4
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateSchema } from './schema';
|
|
1
2
|
import type { AIProvider, AIProviderConfiguration, ConfiguredAIClient, ConfiguredAIOptions } from './types';
|
|
2
3
|
export declare function resolveAIProvider(config: ConfiguredAIOptions, override?: AIProvider): AIProvider;
|
|
3
4
|
/**
|
|
@@ -15,7 +16,6 @@ export declare function isBedrockModelId(id: string): boolean;
|
|
|
15
16
|
*/
|
|
16
17
|
export declare function getAIProviderConfiguration(config: ConfiguredAIOptions, override?: AIProvider, environment?: Record<string, string | undefined>): AIProviderConfiguration;
|
|
17
18
|
declare function parseObject(content: string): unknown;
|
|
18
|
-
declare function validateSchema(value: unknown, schema: Record<string, any>, path?: string): string[];
|
|
19
19
|
/**
|
|
20
20
|
* Create a provider-neutral AI client from an application's `config/ai.ts`.
|
|
21
21
|
* Secrets remain server-side and may be supplied by config or provider env vars.
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as anthropic from"./drivers/anthropic";import*as bedrock from"./drivers/bedrock";import*as ollama from"./drivers/ollama";import*as openai from"./drivers/openai";function textContent(content){if(typeof content==="string")return content;return content.filter((block)=>block.type==="text").map((block)=>block.text??"").join(`
|
|
2
|
-
`)}export function resolveAIProvider(config,override){if(override)return override;const configured=String(config.default||"").toLowerCase();if(configured==="bedrock"||isBedrockModelId(configured))return"bedrock";if(configured==="anthropic"||configured.startsWith("claude-"))return"anthropic";if(configured==="openai"||configured.startsWith("gpt-")||configured.startsWith("o1")||configured.startsWith("o3"))return"openai";if(configured==="ollama")return"ollama";throw Error(`Unsupported configured AI driver: ${config.default||"(empty)"}. Expected anthropic, openai, ollama, bedrock, or a model id.`)}export function isBedrockModelId(id){return/^arn:aws[\w-]*:bedrock:/.test(id)||/^(?:(?:global|us|us-gov|eu|apac|jp|au|ca)\.)?(?:amazon|anthropic|meta|mistral|cohere|ai21|deepseek|openai|qwen|writer|stability)\.[a-z0-9]/.test(id)}function configuredModel(config,provider){const driverModel=config.drivers?.[provider]?.model?.trim();if(driverModel)return driverModel;const configuredDefault=String(config.default||"").trim();if(!configuredDefault||configuredDefault.toLowerCase()===provider)return;return configuredDefault}export function getAIProviderConfiguration(config,override,environment=process.env){const provider=resolveAIProvider(config,override),model=configuredModel(config,provider);if(provider==="ollama")return{provider,model,configured:!0,source:"local"};if(provider==="bedrock")return{provider,model:model??(environment.BEDROCK_MODEL_ID||bedrock.DEFAULT_BEDROCK_MODEL),configured:!0,source:"aws-credential-chain"};const driver=config.drivers?.[provider]??{};if(driver.apiKey?.trim())return{provider,model,configured:!0,source:"config"};if(environment[provider==="openai"?"OPENAI_API_KEY":"ANTHROPIC_API_KEY"]?.trim())return{provider,model,configured:!0,source:"environment"};if(driver.baseUrl?.trim())return{provider,model,configured:!0,source:"base-url"};return{provider,model,configured:!1,source:"none"}}function parseObject(content){const unfenced=content.trim().replace(/^```(?:json)?\s*/i,"").replace(/\s*```$/,"");try{return JSON.parse(unfenced)}catch{const start=unfenced.indexOf("{"),end=unfenced.lastIndexOf("}");if(start>=0&&end>start)return JSON.parse(unfenced.slice(start,end+1));throw Error("AI response did not contain a valid JSON object")}}function
|
|
1
|
+
import{validateSchema}from"./schema";import*as anthropic from"./drivers/anthropic";import*as bedrock from"./drivers/bedrock";import*as ollama from"./drivers/ollama";import*as openai from"./drivers/openai";function textContent(content){if(typeof content==="string")return content;return content.filter((block)=>block.type==="text").map((block)=>block.text??"").join(`
|
|
2
|
+
`)}export function resolveAIProvider(config,override){if(override)return override;const configured=String(config.default||"").toLowerCase();if(configured==="bedrock"||isBedrockModelId(configured))return"bedrock";if(configured==="anthropic"||configured.startsWith("claude-"))return"anthropic";if(configured==="openai"||configured.startsWith("gpt-")||configured.startsWith("o1")||configured.startsWith("o3"))return"openai";if(configured==="ollama")return"ollama";throw Error(`Unsupported configured AI driver: ${config.default||"(empty)"}. Expected anthropic, openai, ollama, bedrock, or a model id.`)}export function isBedrockModelId(id){return/^arn:aws[\w-]*:bedrock:/.test(id)||/^(?:(?:global|us|us-gov|eu|apac|jp|au|ca)\.)?(?:amazon|anthropic|meta|mistral|cohere|ai21|deepseek|openai|qwen|writer|stability)\.[a-z0-9]/.test(id)}function configuredModel(config,provider){const driverModel=config.drivers?.[provider]?.model?.trim();if(driverModel)return driverModel;const configuredDefault=String(config.default||"").trim();if(!configuredDefault||configuredDefault.toLowerCase()===provider)return;return configuredDefault}export function getAIProviderConfiguration(config,override,environment=process.env){const provider=resolveAIProvider(config,override),model=configuredModel(config,provider);if(provider==="ollama")return{provider,model,configured:!0,source:"local"};if(provider==="bedrock")return{provider,model:model??(environment.BEDROCK_MODEL_ID||bedrock.DEFAULT_BEDROCK_MODEL),configured:!0,source:"aws-credential-chain"};const driver=config.drivers?.[provider]??{};if(driver.apiKey?.trim())return{provider,model,configured:!0,source:"config"};if(environment[provider==="openai"?"OPENAI_API_KEY":"ANTHROPIC_API_KEY"]?.trim())return{provider,model,configured:!0,source:"environment"};if(driver.baseUrl?.trim())return{provider,model,configured:!0,source:"base-url"};return{provider,model,configured:!1,source:"none"}}function parseObject(content){const unfenced=content.trim().replace(/^```(?:json)?\s*/i,"").replace(/\s*```$/,"");try{return JSON.parse(unfenced)}catch{const start=unfenced.indexOf("{"),end=unfenced.lastIndexOf("}");if(start>=0&&end>start)return JSON.parse(unfenced.slice(start,end+1));throw Error("AI response did not contain a valid JSON object")}}function createGenerate(config,provider){if(provider==="anthropic"){const driver=config.drivers?.anthropic??{};anthropic.configure({apiKey:driver.apiKey||process.env.ANTHROPIC_API_KEY||"",model:driver.model,maxTokens:driver.maxTokens,anthropicVersion:driver.anthropicVersion});return async(messages,options={})=>{const systemMessages=messages.filter((message)=>message.role==="system").map((message)=>textContent(message.content)),nonSystem=messages.filter((message)=>message.role!=="system");return anthropic.chat(nonSystem,{...options,system:[options.system,...systemMessages].filter(Boolean).join(`
|
|
3
3
|
|
|
4
4
|
`)||void 0})}}if(provider==="bedrock"){const driver=config.drivers?.bedrock??{};bedrock.configure({model:configuredModel(config,"bedrock"),region:driver.region,maxTokens:driver.maxTokens});return(messages,options={})=>bedrock.chat(messages,options)}if(provider==="openai"){const driver=config.drivers?.openai??{};openai.configure({apiKey:driver.apiKey||process.env.OPENAI_API_KEY||"",model:driver.model,maxTokens:driver.maxTokens,baseUrl:driver.baseUrl,embeddingModel:driver.embeddingModel});return(messages,options={})=>{const finalMessages=options.system?[{role:"system",content:options.system},...messages]:messages;return openai.chat(finalMessages,options)}}const driver=config.drivers?.ollama??{};ollama.configure({host:driver.host||driver.baseUrl,model:driver.model,embeddingModel:driver.embeddingModel});return(messages,options={})=>{const finalMessages=options.system?[{role:"system",content:options.system},...messages]:messages;return ollama.chat(finalMessages,options)}}export function createAIClient(config,override){const provider=resolveAIProvider(config,override),configuration=getAIProviderConfiguration(config,provider),generate=createGenerate(config,provider);return{provider,configuration,generate,async generateObject(messages,schema,options={}){const{attempts=2,system,...completionOptions}=options,history=[...messages];let lastError;for(let attempt=1;attempt<=Math.max(1,attempts);attempt++){const result=await generate(history,{...completionOptions,system,responseFormat:{type:"json_schema",json_schema:{name:"structured_output",schema,strict:!0}}});try{const data=parseObject(result.content),errors=validateSchema(data,schema);if(errors.length)throw Error(`AI response failed schema validation: ${errors.slice(0,8).join("; ")}`);return{data,result}}catch(error){lastError=error instanceof Error?error:Error(String(error));if(attempt<attempts){history.push({role:"assistant",content:result.content});history.push({role:"user",content:`Return only a corrected JSON object. Validation error: ${lastError.message}`})}}}throw lastError??Error("AI object generation failed")}}}export{parseObject as parseAIObject,validateSchema as validateAISchema};
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from './search';
|
|
|
25
25
|
export * from './personalization';
|
|
26
26
|
// Model Context Protocol (MCP) client
|
|
27
27
|
export * from './mcp';
|
|
28
|
+
export * from './mcp-server';
|
|
28
29
|
export * from './models';
|
|
29
30
|
// AWS Bedrock utilities
|
|
30
31
|
export * from './utils/client-bedrock';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./types";export*from"./drivers";export*from"./client";export*from"./agents";export*from"./buddy";export*from"./context";export*from"./text";export*from"./image";export*from"./speech";export*from"./search";export*from"./personalization";export*from"./mcp";export*from"./models";export*from"./utils/client-bedrock";export*from"./utils/client-bedrock-runtime";export{buildMessageWithImages,normalizeMessagesForProvider}from"./utils/vision";export{assistantTurn,toolResultsTurn}from"./utils/tools";export{fetchWithRetry}from"./utils/retry";export{clearUsageReporters,listUsageReporters,onUsage,recordUsage}from"./utils/usage";export{estimateMessageTokens,estimateTokens,sanitizePrompt}from"./utils/tokens";export*from"./utils/model-access";
|
|
1
|
+
export*from"./types";export*from"./drivers";export*from"./client";export*from"./agents";export*from"./buddy";export*from"./context";export*from"./text";export*from"./image";export*from"./speech";export*from"./search";export*from"./personalization";export*from"./mcp";export*from"./mcp-server";export*from"./models";export*from"./utils/client-bedrock";export*from"./utils/client-bedrock-runtime";export{buildMessageWithImages,normalizeMessagesForProvider}from"./utils/vision";export{assistantTurn,toolResultsTurn}from"./utils/tools";export{fetchWithRetry}from"./utils/retry";export{clearUsageReporters,listUsageReporters,onUsage,recordUsage}from"./utils/usage";export{estimateMessageTokens,estimateTokens,sanitizePrompt}from"./utils/tokens";export*from"./utils/model-access";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless Streamable HTTP MCP endpoint. JSON replies; GET/DELETE return 405.
|
|
3
|
+
* No session ids, SSE subscriptions, server requests or cross-request state.
|
|
4
|
+
* Mount with Stacks route.post(path, server.handle).skipCsrf() and mount GET/DELETE
|
|
5
|
+
* to the same handler. Bearer authentication is required on every invocation.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createMCPServer<Principal>(options: MCPServerOptions<Principal>): { handle: (request: Request) => Promise<Response> };
|
|
8
|
+
export declare function mcpTextResult(data: JsonObject, isError?: boolean): MCPServerToolResult;
|
|
9
|
+
export declare const MCP_PROTOCOL_VERSIONS: readonly ['2025-11-25', '2025-06-18', '2025-03-26'];
|
|
10
|
+
export declare interface MCPServerContext<Principal> {
|
|
11
|
+
principal: Principal
|
|
12
|
+
request: Request
|
|
13
|
+
signal: AbortSignal
|
|
14
|
+
}
|
|
15
|
+
export declare interface MCPServerTool<Principal> {
|
|
16
|
+
name: string
|
|
17
|
+
title?: string
|
|
18
|
+
description: string
|
|
19
|
+
inputSchema: JsonObject
|
|
20
|
+
outputSchema?: JsonObject
|
|
21
|
+
annotations?: { readOnlyHint?: boolean, destructiveHint?: boolean, idempotentHint?: boolean, openWorldHint?: boolean }
|
|
22
|
+
available?: (principal: Principal) => boolean
|
|
23
|
+
execute: (args: JsonObject, context: MCPServerContext<Principal>) => MaybePromise<MCPServerToolResult>
|
|
24
|
+
}
|
|
25
|
+
export declare interface MCPServerToolResult {
|
|
26
|
+
content: Array<{ type: 'text', text: string }>
|
|
27
|
+
structuredContent?: JsonObject
|
|
28
|
+
isError?: boolean
|
|
29
|
+
}
|
|
30
|
+
export declare interface MCPServerResource<Principal> {
|
|
31
|
+
uri: string
|
|
32
|
+
name: string
|
|
33
|
+
description?: string
|
|
34
|
+
mimeType?: string
|
|
35
|
+
available?: (principal: Principal) => boolean
|
|
36
|
+
read: (context: MCPServerContext<Principal>) => MaybePromise<string>
|
|
37
|
+
}
|
|
38
|
+
export declare interface MCPServerPrompt<Principal> {
|
|
39
|
+
name: string
|
|
40
|
+
description?: string
|
|
41
|
+
arguments?: Array<{ name: string, description?: string, required?: boolean }>
|
|
42
|
+
available?: (principal: Principal) => boolean
|
|
43
|
+
get: (args: Record<string, string>, context: MCPServerContext<Principal>) => MaybePromise<{
|
|
44
|
+
description?: string
|
|
45
|
+
messages: Array<{ role: 'user' | 'assistant', content: { type: 'text', text: string } }>
|
|
46
|
+
}>
|
|
47
|
+
}
|
|
48
|
+
export declare interface MCPServerOptions<Principal> {
|
|
49
|
+
name: string
|
|
50
|
+
version: string
|
|
51
|
+
instructions?: string
|
|
52
|
+
authenticate: (request: Request) => MaybePromise<Principal | Response>
|
|
53
|
+
responseHeaders?: (principal: Principal) => HeadersInit
|
|
54
|
+
allowedOrigins?: readonly string[]
|
|
55
|
+
maxBodyBytes?: number
|
|
56
|
+
pageSize?: number
|
|
57
|
+
tools?: readonly MCPServerTool<Principal>[]
|
|
58
|
+
resources?: readonly MCPServerResource<Principal>[]
|
|
59
|
+
prompts?: readonly MCPServerPrompt<Principal>[]
|
|
60
|
+
onError?: (error: unknown, context: MCPServerContext<Principal>) => void
|
|
61
|
+
}
|
|
62
|
+
declare type JsonObject = Record<string, unknown>;
|
|
63
|
+
declare type MaybePromise<T> = T | Promise<T>;
|
|
64
|
+
declare class ProtocolError extends Error {
|
|
65
|
+
readonly code: number;
|
|
66
|
+
constructor(code: number, message: string);
|
|
67
|
+
}
|
|
@@ -0,0 +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}:{}}}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export declare interface MCPTool {
|
|
|
41
41
|
name: string
|
|
42
42
|
description: string
|
|
43
43
|
inputSchema: Record<string, unknown>
|
|
44
|
+
outputSchema?: Record<string, unknown>
|
|
45
|
+
annotations?: { readOnlyHint?: boolean, destructiveHint?: boolean, idempotentHint?: boolean, openWorldHint?: boolean }
|
|
44
46
|
}
|
|
45
47
|
export declare interface MCPResource {
|
|
46
48
|
uri: string
|
|
@@ -58,6 +60,7 @@ export declare interface MCPPrompt {
|
|
|
58
60
|
}>
|
|
59
61
|
}
|
|
60
62
|
export declare interface MCPToolCallResult {
|
|
63
|
+
structuredContent?: Record<string, unknown>
|
|
61
64
|
content: Array<{
|
|
62
65
|
type: 'text' | 'image' | 'resource'
|
|
63
66
|
text?: string
|
package/dist/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export class MCPClient{config;process=null;requestId=0;pendingRequests=new Map;tools=[];resources=[];prompts=[];initialized=!1;buffer="";constructor(config){this.config=config}async connect(){if(this.config.transport.type==="stdio")await this.connectStdio();else if(this.config.transport.type==="sse"||this.config.transport.type==="streamable-http")await this.connectHTTP();else throw Error(`Unsupported transport type: ${this.config.transport.type}`);this.initialized=!0}async connectStdio(){const transport=this.config.transport,{spawn}=await import("bun");this.process=spawn([transport.command,...transport.args||[]],{stdin:"pipe",stdout:"pipe",stderr:"pipe",env:{...process.env,...transport.env}});this.readStdout();await this.sendRequest("initialize",{protocolVersion:
|
|
1
|
+
export class MCPClient{config;process=null;requestId=0;pendingRequests=new Map;tools=[];resources=[];prompts=[];initialized=!1;buffer="";protocolVersion="2025-11-25";sessionId=null;serverCapabilities=null;constructor(config){this.config=config}async connect(){if(this.config.transport.type==="stdio")await this.connectStdio();else if(this.config.transport.type==="sse"||this.config.transport.type==="streamable-http")await this.connectHTTP();else throw Error(`Unsupported transport type: ${this.config.transport.type}`);this.initialized=!0}async connectStdio(){const transport=this.config.transport,{spawn}=await import("bun");this.process=spawn([transport.command,...transport.args||[]],{stdin:"pipe",stdout:"pipe",stderr:"pipe",env:{...process.env,...transport.env}});this.readStdout();const initialized=await this.sendRequest("initialize",{protocolVersion:this.protocolVersion,capabilities:{},clientInfo:{name:"stacks-ai",version:"1.0.0"}});this.serverCapabilities=initialized?.capabilities??{};if(typeof initialized?.protocolVersion!=="string")throw Error("Invalid MCP initialization response");this.protocolVersion=initialized.protocolVersion;await this.sendNotification("notifications/initialized",{});await this.discoverCapabilities()}async connectHTTP(){const transport=this.config.transport,response=await fetch(transport.url,{signal:AbortSignal.timeout(30000),method:"POST",headers:{...transport.headers,"Content-Type":"application/json",Accept:"application/json, text/event-stream"},body:JSON.stringify({jsonrpc:"2.0",id:this.nextId(),method:"initialize",params:{protocolVersion:this.protocolVersion,capabilities:{},clientInfo:{name:"stacks-ai",version:"1.0.0"}}})});if(!response.ok){const error=await response.text();throw Error(`MCP HTTP connection error: ${error}`)}const initialized=await response.json();if(initialized.error||typeof initialized.result?.protocolVersion!=="string")throw Error(initialized.error?.message||"Invalid MCP initialization response");this.serverCapabilities=initialized.result.capabilities??{};this.protocolVersion=initialized.result.protocolVersion;this.sessionId=response.headers.get("mcp-session-id");await this.sendNotification("notifications/initialized");await this.discoverCapabilities()}async readStdout(){if(!this.process)return;const reader=this.process.stdout?.getReader();if(!reader)return;const decoder=new TextDecoder;try{while(!0){const{done,value}=await reader.read();if(done)break;this.buffer+=decoder.decode(value,{stream:!0});this.processBuffer()}}catch{}}processBuffer(){const lines=this.buffer.split(`
|
|
2
2
|
`);this.buffer=lines.pop()||"";for(const line of lines){if(!line.trim())continue;try{const message=JSON.parse(line);if(message.id!==void 0&&this.pendingRequests.has(message.id)){const pending=this.pendingRequests.get(message.id);this.pendingRequests.delete(message.id);if(message.error)pending.reject(Error(message.error.message||"MCP error"));else pending.resolve(message.result)}}catch{}}}nextId(){return++this.requestId}async sendRequest(method,params={}){const id=this.nextId(),message=JSON.stringify({jsonrpc:"2.0",id,method,params})+`
|
|
3
|
-
`;if(this.config.transport.type==="stdio"&&this.process){const writer=this.process.stdin?.getWriter();if(writer){await writer.write(new TextEncoder().encode(message));writer.releaseLock()}return new Promise((resolve,reject)=>{const timeout=setTimeout(()=>{this.pendingRequests.delete(id);reject(Error(`MCP request timeout: ${method}`))},30000);this.pendingRequests.set(id,{resolve:(value)=>{clearTimeout(timeout);resolve(value)},reject:(error)=>{clearTimeout(timeout);reject(error)}})})}if(this.config.transport.type==="sse"||this.config.transport.type==="streamable-http"){const transport=this.config.transport,response=await fetch(transport.url,{method:"POST",headers:{"Content-Type":"application/json",...
|
|
4
|
-
`;if(this.config.transport.type==="stdio"&&this.process){const writer=this.process.stdin?.getWriter();if(writer){await writer.write(new TextEncoder().encode(message));writer.releaseLock()}}}async discoverCapabilities(){const capabilities=this.config.capabilities||{tools:!0,resources:!0,prompts:!0}
|
|
3
|
+
`;if(this.config.transport.type==="stdio"&&this.process){const writer=this.process.stdin?.getWriter();if(writer){await writer.write(new TextEncoder().encode(message));writer.releaseLock()}return new Promise((resolve,reject)=>{const timeout=setTimeout(()=>{this.pendingRequests.delete(id);reject(Error(`MCP request timeout: ${method}`))},30000);this.pendingRequests.set(id,{resolve:(value)=>{clearTimeout(timeout);resolve(value)},reject:(error)=>{clearTimeout(timeout);reject(error)}})})}if(this.config.transport.type==="sse"||this.config.transport.type==="streamable-http"){const transport=this.config.transport,response=await fetch(transport.url,{signal:AbortSignal.timeout(30000),method:"POST",headers:{...transport.headers,"Content-Type":"application/json",Accept:"application/json, text/event-stream","MCP-Protocol-Version":this.protocolVersion,...this.sessionId?{"Mcp-Session-Id":this.sessionId}:{}},body:message});if(!response.ok){const error=await response.text();throw Error(`MCP request error: ${error}`)}const result=await response.json();if(result.error)throw Error(result.error.message||"MCP error");return result.result}throw Error("No transport available")}async sendNotification(method,params={}){const message=JSON.stringify({jsonrpc:"2.0",method,params})+`
|
|
4
|
+
`;if(this.config.transport.type==="stdio"&&this.process){const writer=this.process.stdin?.getWriter();if(writer){await writer.write(new TextEncoder().encode(message));writer.releaseLock()}}else if(this.config.transport.type==="sse"||this.config.transport.type==="streamable-http"){const transport=this.config.transport,response=await fetch(transport.url,{method:"POST",signal:AbortSignal.timeout(30000),headers:{...transport.headers,"Content-Type":"application/json",Accept:"application/json, text/event-stream","MCP-Protocol-Version":this.protocolVersion,...this.sessionId?{"Mcp-Session-Id":this.sessionId}:{}},body:message});if(!response.ok)throw Error(`MCP notification failed: ${response.status}`);await response.body?.cancel()}}async discoverCapabilities(){const capabilities=this.config.capabilities||{tools:!0,resources:!0,prompts:!0},collect=async(method,key)=>{const items=[],seen=new Set;let cursor;do{const result=await this.sendRequest(method,cursor?{cursor}:{});if(!Array.isArray(result?.[key]))throw Error(`Invalid MCP ${key} response`);items.push(...result[key]);cursor=result.nextCursor;if(cursor!==void 0){if(typeof cursor!=="string"||seen.has(cursor)||seen.size>=1000)throw Error("Invalid or repeated MCP discovery cursor");seen.add(cursor)}}while(cursor!==void 0);return items};if(capabilities.tools!==!1&&(!this.serverCapabilities||this.serverCapabilities.tools))this.tools=await collect("tools/list","tools");if(capabilities.resources!==!1&&(!this.serverCapabilities||this.serverCapabilities.resources))this.resources=await collect("resources/list","resources");if(capabilities.prompts!==!1&&(!this.serverCapabilities||this.serverCapabilities.prompts))this.prompts=await collect("prompts/list","prompts")}listTools(){return this.tools}listResources(){return this.resources}listPrompts(){return this.prompts}async callTool(name,args={}){if(!this.initialized)throw Error("MCP client not connected. Call connect() first.");return await this.sendRequest("tools/call",{name,arguments:args})}async readResource(uri){if(!this.initialized)throw Error("MCP client not connected. Call connect() first.");const contents=(await this.sendRequest("resources/read",{uri}))?.contents?.[0];return{uri:contents?.uri||uri,mimeType:contents?.mimeType,text:contents?.text,blob:contents?.blob}}async getPrompt(name,args={}){if(!this.initialized)throw Error("MCP client not connected. Call connect() first.");return this.sendRequest("prompts/get",{name,arguments:args})}toAnthropicTools(){return this.tools.map((tool)=>({name:tool.name,description:tool.description,input_schema:tool.inputSchema}))}toOpenAITools(){return this.tools.map((tool)=>({type:"function",function:{name:tool.name,description:tool.description,parameters:tool.inputSchema}}))}async disconnect(){if(this.process){this.process.kill();this.process=null}for(const pending of this.pendingRequests.values())try{pending.reject(Error("MCP client disconnected"))}catch{}this.pendingRequests.clear();this.initialized=!1;this.tools=[];this.resources=[];this.prompts=[]}get isConnected(){return this.initialized}get serverName(){return this.config.name}}export class MCPManager{clients=new Map;async addServer(config){if(this.clients.has(config.name))throw Error(`MCP server already registered: ${config.name}`);const client=new MCPClient(config);await client.connect();this.clients.set(config.name,client);return client}async removeServer(name){const client=this.clients.get(name);if(client){await client.disconnect();this.clients.delete(name)}}getServer(name){return this.clients.get(name)}listServers(){return Array.from(this.clients.keys())}getAllTools(){const allTools=[];for(const[name,client]of this.clients)for(const tool of client.listTools())allTools.push({...tool,serverName:name});return allTools}async callTool(toolPath,args={}){if(toolPath.includes("/")){const[serverName,toolName]=toolPath.split("/",2);if(!serverName||!toolName)throw Error(`Invalid MCP tool path: ${toolPath}`);const client=this.clients.get(serverName);if(!client)throw Error(`MCP server not found: ${serverName}`);return client.callTool(toolName,args)}for(const[,client]of this.clients)if(client.listTools().some((t)=>t.name===toolPath))return client.callTool(toolPath,args);throw Error(`MCP tool not found: ${toolPath}`)}toAnthropicTools(){const tools=[];for(const[serverName,client]of this.clients)for(const tool of client.listTools())tools.push({name:`${serverName}__${tool.name}`,description:`[${serverName}] ${tool.description}`,input_schema:tool.inputSchema});return tools}toOpenAITools(){const tools=[];for(const[serverName,client]of this.clients)for(const tool of client.listTools())tools.push({type:"function",function:{name:`${serverName}__${tool.name}`,description:`[${serverName}] ${tool.description}`,parameters:tool.inputSchema}});return tools}async disconnectAll(){const disconnects=Array.from(this.clients.values()).map((c)=>c.disconnect());await Promise.all(disconnects);this.clients.clear()}}export async function connectStdio(name,command,args,env){const client=new MCPClient({name,transport:{type:"stdio",command,args,env}});await client.connect();return client}export async function connectHTTP(name,url,headers){const client=new MCPClient({name,transport:{type:"streamable-http",url,headers}});await client.connect();return client}export const mcp={MCPClient,MCPManager,connectStdio,connectHTTP};
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function validateSchema(value: unknown, schema: Record<string, any>, path?: string): string[];
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function typeMatches(value,type){if(type==="array")return Array.isArray(value);if(type==="integer")return typeof value==="number"&&Number.isInteger(value);if(type==="object")return typeof value==="object"&&value!==null&&!Array.isArray(value);if(type==="null")return value===null;return typeof value===type&&(type!=="number"||Number.isFinite(value))}export function validateSchema(value,schema,path="$"){const errors=[];if(schema.type){const allowed=Array.isArray(schema.type)?schema.type:[schema.type];if(!allowed.some((type)=>typeMatches(value,type)))return[`${path} must be ${allowed.join(" or ")}`]}if(Object.hasOwn(schema,"const")&&!Object.is(schema.const,value))errors.push(`${path} must equal ${String(schema.const)}`);if(schema.enum&&!schema.enum.some((candidate)=>Object.is(candidate,value)))errors.push(`${path} must be one of ${schema.enum.join(", ")}`);if(typeof value==="string"){if(schema.pattern&&!new RegExp(schema.pattern).test(value))errors.push(`${path} does not match the required pattern`);if(schema.minLength!==void 0&&value.length<schema.minLength)errors.push(`${path} must contain at least ${schema.minLength} characters`);if(schema.maxLength!==void 0&&value.length>schema.maxLength)errors.push(`${path} must contain at most ${schema.maxLength} characters`)}if(typeof value==="number"){if(schema.minimum!==void 0&&value<schema.minimum)errors.push(`${path} must be at least ${schema.minimum}`);if(schema.maximum!==void 0&&value>schema.maximum)errors.push(`${path} must be at most ${schema.maximum}`)}if(Array.isArray(value)){if(schema.minItems!==void 0&&value.length<schema.minItems)errors.push(`${path} must contain at least ${schema.minItems} items`);if(schema.maxItems!==void 0&&value.length>schema.maxItems)errors.push(`${path} must contain at most ${schema.maxItems} items`);if(schema.items)value.forEach((item,index)=>errors.push(...validateSchema(item,schema.items,`${path}[${index}]`)))}if(typeof value==="object"&&value!==null&&!Array.isArray(value)){const record=value;for(const key of schema.required??[])if(!Object.hasOwn(record,key))errors.push(`${path}.${key} is required`);for(const[key,child]of Object.entries(schema.properties??{}))if(Object.hasOwn(record,key))errors.push(...validateSchema(record[key],child,`${path}.${key}`));if(schema.additionalProperties===!1){const known=new Set(Object.keys(schema.properties??{}));for(const key of Object.keys(record))if(!known.has(key))errors.push(`${path}.${key} is not allowed`)}}return errors}
|
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.
|
|
5
|
+
"version": "0.75.189",
|
|
6
6
|
"description": "Stacks Artificial Intelligence.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -65,9 +65,10 @@
|
|
|
65
65
|
"prepublishOnly": "bun run build"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@stacksjs/cli": "0.75.
|
|
69
|
-
"@stacksjs/config": "0.75.
|
|
70
|
-
"@stacksjs/
|
|
68
|
+
"@stacksjs/cli": "0.75.189",
|
|
69
|
+
"@stacksjs/config": "0.75.189",
|
|
70
|
+
"@stacksjs/router": "0.75.189",
|
|
71
|
+
"@stacksjs/storage": "0.75.189",
|
|
71
72
|
"@stacksjs/ts-cloud": "^0.16.56"
|
|
72
73
|
},
|
|
73
74
|
"peerDependencies": {
|