UNPKG

@sigyl-dev/cli

Version:

Official Sigyl CLI for installing and managing MCP packages. Zero-config installation for public packages, secure API-based authentication.

671 lines (638 loc) 25.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.MCPGenerator = void 0; const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const yaml = __importStar(require("yaml")); const logger_1 = require("../logger"); class MCPGenerator { outDir; language; constructor(outDir, language) { this.outDir = outDir; this.language = language; } async generateFromEndpoints(endpoints, options = {}) { // Ensure output directory exists if (!(0, node_fs_1.existsSync)(this.outDir)) { (0, node_fs_1.mkdirSync)(this.outDir, { recursive: true }); } // Generate MCP configuration await this.generateMCPConfig(endpoints, options); // Generate server code if (this.language === "typescript") { await this.generateTypeScriptServer(endpoints, options); } else if (this.language === "javascript") { await this.generateJavaScriptServer(endpoints, options); } else { await this.generatePythonServer(endpoints, options); } } async generateMCPConfig(endpoints, options) { // Generate minimal SigylConfig format (no env, build, or entryPoint) const config = { runtime: "node", language: this.language, startCommand: { type: "http" } }; const yamlHeader = `# MCP-compatible server configuration\n# This template demonstrates all major JSON Schema features for configSchema.\n# - apiKey: Secret string field\n# - serviceName: Arbitrary string field\n# - logLevel: Enum string field\n# - timeout: Number field with min/max\n# - enableMetrics: Boolean field\n# - allowedClients: Array of strings\n# - customSettings: Object field\n# - environment: Enum for environment\n# Add/remove fields as needed for your server.\n`; const yamlContent = yamlHeader + yaml.stringify(config, { indent: 2 }); (0, node_fs_1.writeFileSync)((0, node_path_1.join)(this.outDir, "sigyl.yaml"), yamlContent); (0, logger_1.verboseLog)("Generated sigyl.yaml configuration"); } async generateTypeScriptServer(endpoints, options) { const serverCode = `/** * Auto-generated MCP Server from Express endpoints * * This server provides tools that map to your Express API endpoints. * Each tool makes HTTP requests to your Express application and returns the responses. * * To add a new tool manually, follow the template at the bottom of this file. */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { z } from "zod" // ============================================================================ // SERVER CONFIGURATION // ============================================================================ export default function createStatelessServer({ config, }: { config: any; }) { const server = new McpServer({ name: "generated-mcp-server", version: "1.0.0", }); // ============================================================================ // AUTO-GENERATED TOOLS FROM EXPRESS ENDPOINTS // ============================================================================ // These tools were automatically generated from your Express application. // Each tool corresponds to an endpoint in your Express app. ${endpoints.map(endpoint => { const toolName = this.generateToolName(endpoint); const shape = this.generateZodShapeObject(endpoint); const description = endpoint.description || `${endpoint.method} ${endpoint.path}`; const methodLiteral = endpoint.method.toUpperCase(); const pathLiteral = endpoint.path; return ` // ===== ${endpoint.method.toUpperCase()} ${endpoint.path} ===== server.tool( "${toolName}", "${description}", ${shape}, async (args) => { // ===== REQUEST CONFIGURATION ===== /** * IMPORTANT: This MCP tool calls your Express API at the address below. * To change the API base URL (host/port), set the APP_BASE_URL environment variable when starting this server, * or edit the code below. Default is http://localhost:3000 * Example: APP_BASE_URL=http://myhost:4000 node server.js */ const baseUrl = process.env.APP_BASE_URL || \`http://localhost:${'${config.appPort || 3000}'}\`; const url = \`${'${baseUrl}'}${'${endpoint.path}'}\`; const method = "${'${endpoint.method.toUpperCase()}'}"; // Build request options const requestOptions: any = { method, headers: { "Content-Type": "application/json", }, }; // ===== PARAMETER HANDLING ===== const queryParams = new URLSearchParams(); const bodyParams: any = {}; ${this.generateParameterHandling(endpoint)} // ===== URL CONSTRUCTION ===== if (queryParams.toString()) { requestOptions.url = url + (url.includes('?') ? '&' : '?') + queryParams.toString(); } else { requestOptions.url = url; } // Add body for POST/PUT/PATCH requests if (["POST", "PUT", "PATCH"].includes(method) && Object.keys(bodyParams).length > 0) { requestOptions.body = JSON.stringify(bodyParams); } // ===== HTTP REQUEST & RESPONSE ===== try { const response = await fetch(requestOptions.url, requestOptions); const data = await response.json(); return data; } catch (error) { return { content: [ { type: "text", text: \`Error calling ${endpoint.method.toUpperCase()} ${endpoint.path}: \${error instanceof Error ? error.message : String(error)}\` } ] }; } } ); `; }).join('\n\n')} // ============================================================================ // MANUAL TOOL TEMPLATE // ============================================================================ // To add a new tool manually, use the following simple template: /* server.tool( "reverseString", "Reverse a string value", { value: z.string().describe("String to reverse"), }, async ({ value }) => { return { content: [ { type: "text", text: value.split("").reverse().join("") } ] }; } ); */ return server.server; }`; (0, node_fs_1.writeFileSync)((0, node_path_1.join)(this.outDir, "server.ts"), serverCode); (0, logger_1.verboseLog)("Generated TypeScript server"); // Generate package.json for the server const packageJson = { name: "generated-mcp-server", version: "1.0.0", type: "module", main: "server.js", description: "Auto-generated MCP server from Express endpoints", scripts: { build: "tsc", start: "node server.js" }, dependencies: { "@modelcontextprotocol/sdk": "^1.10.1", "zod": "^3.22.0" }, devDependencies: { "typescript": "^5.0.0", "@types/node": "^20.0.0" } }; (0, node_fs_1.writeFileSync)((0, node_path_1.join)(this.outDir, "package.json"), JSON.stringify(packageJson, null, 2)); (0, logger_1.verboseLog)("Generated package.json"); // Always generate a valid tsconfig.json for TypeScript/ESM compatibility const tsConfig = { compilerOptions: { target: "ES2020", module: "ESNext", moduleResolution: "node", outDir: "./", rootDir: "./", strict: true, esModuleInterop: true, skipLibCheck: true }, include: ["*.ts"], exclude: ["node_modules", "*.js"] }; (0, node_fs_1.writeFileSync)((0, node_path_1.join)(this.outDir, "tsconfig.json"), JSON.stringify(tsConfig, null, 2)); (0, logger_1.verboseLog)("Generated tsconfig.json"); } async generateJavaScriptServer(endpoints, options) { const serverCode = `/** * Auto-generated MCP Server from Express endpoints (JavaScript) * * This server provides tools that map to your Express API endpoints. * Each tool makes HTTP requests to your Express application and returns the responses. * * To add a new tool manually, follow the template at the bottom of this file. */ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js"); const { z } = require("zod"); // ============================================================================ // SERVER CONFIGURATION // ============================================================================ function createStatelessServer({ config }) { const server = new McpServer({ name: "generated-mcp-server", version: "1.0.0", }); // ============================================================================ // AUTO-GENERATED TOOLS FROM EXPRESS ENDPOINTS // ============================================================================ // These tools were automatically generated from your Express application. // Each tool corresponds to an endpoint in your Express app. ${endpoints.map(endpoint => { const toolName = this.generateToolName(endpoint); const description = endpoint.description || `${endpoint.method} ${endpoint.path}`; return ` // ===== ${endpoint.method.toUpperCase()} ${endpoint.path} ===== server.tool( "${toolName}", "${description}", {}, async (args) => { return { content: [ { type: "text", text: "Dummy response from ${toolName}" } ] }; } ); `; }).join('\n\n')} // ============================================================================ // MANUAL TOOL TEMPLATE // ============================================================================ // To add a new tool manually, use the following simple template: /* server.tool( "reverseString", "Reverse a string value", { value: z.string().describe("String to reverse"), }, async ({ value }) => { return { content: [ { type: "text", text: value.split("").reverse().join("") } ] }; } ); */ return server.server; }`; (0, node_fs_1.writeFileSync)((0, node_path_1.join)(this.outDir, "server.js"), serverCode); (0, logger_1.verboseLog)("Generated JavaScript server"); // Generate package.json for the server const packageJson = { name: "generated-mcp-server", version: "1.0.0", type: "module", main: "server.js", description: "Auto-generated MCP server from Express endpoints", scripts: { start: "node server.js" }, dependencies: { "@modelcontextprotocol/sdk": "^1.10.1", "zod": "^3.22.0" } }; (0, node_fs_1.writeFileSync)((0, node_path_1.join)(this.outDir, "package.json"), JSON.stringify(packageJson, null, 2)); (0, logger_1.verboseLog)("Generated package.json"); // --- PATCH: Generate tool handler files --- const toolsDir = (0, node_path_1.join)(this.outDir, "tools"); if (!(0, node_fs_1.existsSync)(toolsDir)) { (0, node_fs_1.mkdirSync)(toolsDir, { recursive: true }); } for (const endpoint of endpoints) { const toolName = this.generateToolName(endpoint); const handlerCode = `export async function ${toolName}(args) { // TODO: Implement actual logic for ${endpoint.method} ${endpoint.path} return { content: [ { type: "text", text: "Dummy response from ${toolName}" } ] }; } `; (0, node_fs_1.writeFileSync)((0, node_path_1.join)(toolsDir, `${toolName}.js`), handlerCode); } } async generatePythonServer(endpoints, options) { // TODO: Implement Python server generation (0, logger_1.verboseLog)("Python server generation not yet implemented"); } generateToolName(endpoint) { // Convert path and method to camelCase tool name // e.g., GET /api/users/:id -> getApiUsersById // e.g., GET /api/users/advanced-search -> getApiUsersAdvancedSearch const method = endpoint.method.toLowerCase(); const pathParts = endpoint.path .split('/') .filter(part => part && part !== '') .map(part => { // Remove colons from path parameters if (part.startsWith(':')) { return 'By' + part.slice(1).charAt(0).toUpperCase() + part.slice(2); } // Handle hyphens and other special characters return part .replace(/[-_]/g, ' ') // Replace hyphens and underscores with spaces .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); }); return method + pathParts.join(''); } generateToolSchema(endpoint) { const schema = {}; // Add path and query parameters if (endpoint.parameters) { for (const param of endpoint.parameters) { schema[param.name] = { type: this.mapTypeToJSONSchema(param.type), description: param.description || `${param.location} parameter`, ...(param.location === "path" && { required: true }) }; } } // Add request body for POST/PUT/PATCH requests if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) { if (endpoint.requestBody) { if (endpoint.requestBody.properties) { // If we have detailed properties, create a proper object schema schema.body = { type: "object", description: "Request body data", properties: this.mapPropertiesToJSONSchema(endpoint.requestBody.properties), ...(endpoint.requestBody.required && endpoint.requestBody.required.length > 0 && { required: endpoint.requestBody.required }) }; } else { // Fallback to generic object schema.body = { type: this.mapTypeToJSONSchema(endpoint.requestBody.type), description: "Request body data" }; } } else { schema.body = { type: "object", description: "Request body data" }; } } return schema; } mapTypeToJSONSchema(type) { // Map TypeScript types to JSON Schema types switch (type.toLowerCase()) { case "string": return "string"; case "number": return "number"; case "boolean": return "boolean"; case "array": return "array"; case "object": return "object"; case "date": return "string"; case "any": return "object"; case "unknown": return "object"; default: return "object"; // Default for custom types } } mapPropertiesToJSONSchema(properties) { const mapped = {}; for (const [key, value] of Object.entries(properties)) { const propertySchema = { type: this.mapTypeToJSONSchema(value.type), description: value.description || `Property: ${key}` }; // Add additional schema properties based on type if (value.type === "string") { // Could add format, pattern, minLength, maxLength, etc. } else if (value.type === "number") { // Could add minimum, maximum, etc. } else if (value.type === "array") { // Could add items schema } mapped[key] = propertySchema; } return mapped; } generateToolHandler(endpoint, options) { const toolName = this.generateToolName(endpoint); const appPort = options.appPort || "3000"; if (this.language === "typescript") { // Generate TypeScript interface for the tool arguments const argInterface = this.generateToolArgInterface(endpoint); // Check if we have required parameters (path params are always required) const hasRequiredParams = endpoint.parameters?.some(p => p.required || p.location === "path") || false; const defaultValue = hasRequiredParams ? "" : " = {}"; return `// Tool handler for ${endpoint.method} ${endpoint.path} ${argInterface} export async function ${toolName}(args: ${toolName}Args${defaultValue}): Promise<{ content: Array<{ type: string; text: string }> }> { try { // Construct URL for the Express endpoint const baseUrl = "http://localhost:${appPort}" let url = "${endpoint.path}" // Replace path parameters ${endpoint.parameters?.filter(p => p.location === "path").map(param => `url = url.replace(":${param.name}", String(args.${param.name}) || "");`).join('\n\t\t') || ''} // Add query parameters const queryParams = new URLSearchParams() ${endpoint.parameters?.filter(p => p.location === "query").map(param => `if (args.${param.name} !== undefined) queryParams.append("${param.name}", String(args.${param.name}));`).join('\n\t\t') || ''} if (queryParams.toString()) { url += "?" + queryParams.toString() } const fullUrl = baseUrl + url // Make HTTP request to Express app const options: RequestInit = { method: "${endpoint.method}", headers: { "Content-Type": "application/json", }, } // Add body for POST/PUT/PATCH requests ${['POST', 'PUT', 'PATCH'].includes(endpoint.method) ? `if (args.body) {\n\t\t\toptions.body = JSON.stringify(args.body)\n\t\t}` : '// No body for GET requests'} const response = await fetch(fullUrl, options) const result = await response.text() return { content: [ { type: "text", text: "Request: " + options.method + " " + fullUrl + "\\nResponse: " + result } ] } } catch (error) { return { content: [ { type: "text", text: \`Error calling ${endpoint.method.toUpperCase()} ${endpoint.path}: \${error instanceof Error ? error.message : String(error)}\` } ] } } } `; } else if (this.language === "javascript") { // Generate plain JS handler (no types) return `// Tool handler for ${endpoint.method} ${endpoint.path} export async function ${toolName}(args = {}) { try { // Construct URL for the Express endpoint const baseUrl = "http://localhost:${appPort}" let url = "${endpoint.path}" // Replace path parameters ${endpoint.parameters?.filter(p => p.location === "path").map(param => `url = url.replace(":${param.name}", String(args.${param.name}) || "");`).join('\n\t\t') || ''} // Add query parameters const queryParams = new URLSearchParams() ${endpoint.parameters?.filter(p => p.location === "query").map(param => `if (args.${param.name} !== undefined) queryParams.append("${param.name}", String(args.${param.name}));`).join('\n\t\t') || ''} if (queryParams.toString()) { url += "?" + queryParams.toString() } const fullUrl = baseUrl + url // Make HTTP request to Express app const options = { method: "${endpoint.method}", headers: { "Content-Type": "application/json", }, } // Add body for POST/PUT/PATCH requests ${['POST', 'PUT', 'PATCH'].includes(endpoint.method) ? `if (args.body) {\n\t\t\toptions.body = JSON.stringify(args.body)\n\t\t}` : '// No body for GET requests'} const response = await fetch(fullUrl, options) const result = await response.text() return { content: [ { type: "text", text: "Request: " + options.method + " " + fullUrl + "\\nResponse: " + result } ] } } catch (error) { return { content: [ { type: "text", text: \`Error calling ${endpoint.method.toUpperCase()} ${endpoint.path}: \${error instanceof Error ? error.message : String(error)}\` } ] } } } `; } return ""; } generateToolArgInterface(endpoint) { const toolName = this.generateToolName(endpoint); const interfaceName = `${toolName}Args`; let properties = []; // Add path and query parameters if (endpoint.parameters) { for (const param of endpoint.parameters) { const optional = param.required ? "" : "?"; properties.push(`\t${param.name}${optional}: ${this.mapTypeToTypeScript(param.type)}`); } } // Add body parameter for POST/PUT/PATCH requests if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) { properties.push(`\tbody?: any`); } if (properties.length === 0) { return `interface ${interfaceName} {}`; } return `interface ${interfaceName} { ${properties.join('\n')} }`; } mapTypeToTypeScript(type) { switch (type.toLowerCase()) { case "string": return "string"; case "number": return "number"; case "boolean": return "boolean"; case "array": return "any[]"; case "object": return "any"; case "date": return "string"; default: return "any"; } } generateZodShapeObject(endpoint) { const properties = []; // Add path and query parameters if (endpoint.parameters) { for (const param of endpoint.parameters) { const zodType = this.mapTypeToZod(param.type); const description = param.description || `${param.location} parameter: ${param.name}`; const optional = param.required ? "" : ".optional()"; properties.push(`\t\t${param.name}: z.${zodType}()${optional}.describe("${description}")`); } } // Add body parameter for POST/PUT/PATCH requests if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) { properties.push(`\t\tbody: z.any().optional().describe("Request body data")`); } if (properties.length === 0) { return "{}"; } return `{ ${properties.join(',\n')} \t}`; } mapTypeToZod(type) { switch (type.toLowerCase()) { case "string": return "string"; case "number": return "number"; case "boolean": return "boolean"; case "array": return "array"; case "object": return "object"; default: return "any"; } } generateParameterHandling(endpoint) { let code = ""; if (endpoint.parameters) { for (const param of endpoint.parameters) { if (param.location === "path") { code += `\t\t\trequestOptions.url = requestOptions.url.replace(":${param.name}", String(args.${param.name}) || "");\n`; } else if (param.location === "query") { code += `\t\t\tif (args.${param.name} !== undefined) {\n`; code += `\t\t\t\tqueryParams.set("${param.name}", String(args.${param.name}));\n`; code += `\t\t\t}\n`; } else if (param.location === "body") { code += `\t\t\tif (args.${param.name} !== undefined) {\n`; code += `\t\t\t\tbodyParams.${param.name} = args.${param.name};\n`; code += `\t\t\t}\n`; } } } // Handle body parameters for POST/PUT/PATCH if (['POST', 'PUT', 'PATCH'].includes(endpoint.method)) { code += `\t\t\tif (args.body !== undefined) {\n`; code += `\t\t\t\tObject.assign(bodyParams, args.body);\n`; code += `\t\t\t}\n`; } return code; } } exports.MCPGenerator = MCPGenerator; //# sourceMappingURL=mcp-generator.js.map