UNPKG

@odel/module-sdk

Version:

SDK for building Odel modules - MCP protocol over HTTP for Cloudflare Workers

202 lines 8.33 kB
/** * Module builder for creating MCP-compliant Odel modules */ import { zodToJsonSchema } from 'zod-to-json-schema'; // Cloudflare Workers types /// <reference types="@cloudflare/workers-types" /> /** * Module builder class for creating MCP-compliant modules */ export class ModuleBuilder { constructor() { this.tools = []; } /** * Add a tool to the module */ tool(tool) { this.tools.push(tool); return this; } /** * Convert module tools to MCP format */ toMCPTools(extended = false) { return this.tools.map(tool => { const mcpTool = { name: tool.name, description: tool.description, inputSchema: zodToJsonSchema(tool.inputSchema, { $refStrategy: 'none', target: 'openApi3' }) }; // Include outputSchema only when extended=true (non-standard MCP extension) if (extended) { mcpTool.outputSchema = zodToJsonSchema(tool.outputSchema, { $refStrategy: 'none', target: 'openApi3' }); } return mcpTool; }); } /** * Build the Cloudflare Worker handler */ build() { const tools = this.tools; const toMCPTools = (extended = false) => this.toMCPTools(extended); return { async fetch(request, env, _ctx) { try { // Handle MCP tools/list request if (request.method === 'POST') { const body = await request.json(); // MCP initialize (required by spec) if (body.method === 'initialize') { return Response.json({ jsonrpc: '2.0', id: body.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'odel-module', version: '1.0.0' } } }); } // MCP initialized notification (client confirms ready) // Handles both 'initialized' and 'notifications/initialized' if (body.method === 'initialized' || body.method === 'notifications/initialized') { // No response needed for notifications return new Response(null, { status: 204 }); } // MCP tools/list (with optional extended parameter) if (body.method === 'tools/list') { const extended = body.params?.extended === true; const mcpTools = toMCPTools(extended); return Response.json({ jsonrpc: '2.0', id: body.id, result: { tools: mcpTools } }); } // MCP tools/call if (body.method === 'tools/call') { const { name, arguments: args } = body.params; const tool = tools.find(t => t.name === name); if (!tool) { return Response.json({ jsonrpc: '2.0', id: body.id, error: { code: -32601, message: `Tool ${name} not found` } }, { status: 404 }); } // Validate input with Zod const validationResult = tool.inputSchema.safeParse(args); if (!validationResult.success) { return Response.json({ jsonrpc: '2.0', id: body.id, error: { code: -32602, message: 'Invalid parameters', data: validationResult.error.errors } }, { status: 400 }); } // Execute tool const moduleContext = body.context || { userId: 'anonymous', displayName: 'Anonymous User', timestamp: Date.now(), requestId: crypto.randomUUID(), secrets: {} }; const context = { ...moduleContext, env }; const result = await tool.handler(validationResult.data, context); // Validate output const outputValidation = tool.outputSchema.safeParse(result); if (!outputValidation.success) { return Response.json({ jsonrpc: '2.0', id: body.id, error: { code: -32603, message: 'Tool returned invalid output', data: outputValidation.error.errors } }, { status: 500 }); } return Response.json({ jsonrpc: '2.0', id: body.id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } }); } // Unknown method return Response.json({ jsonrpc: '2.0', id: body.id, error: { code: -32601, message: `Unknown method: ${body.method}` } }, { status: 404 }); } return Response.json({ error: 'Method not allowed' }, { status: 405 }); } catch (error) { return Response.json({ jsonrpc: '2.0', id: null, error: { code: -32603, message: 'Internal error', data: error.message } }, { status: 500 }); } } }; } } /** * Create a new module builder with optional environment typing * * @example * ```typescript * interface Env { * RESEND_API_KEY: string; * ANALYTICS: AnalyticsEngine; * } * * export default createModule<Env>() * .tool({ ... }) * .build(); * ``` */ export function createModule() { return new ModuleBuilder(); } //# sourceMappingURL=module-builder.js.map