@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
JavaScript
"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