Skip to main content
Glama
generateEndpointToolCode.ts23.3 kB
/** * Generate Endpoint Tool Code Service * Generates TypeScript code for an MCP tool definition based on a Swagger endpoint * with full model schemas included in the inputSchema */ import fs from 'fs'; import yaml from 'js-yaml'; import logger from '../utils/logger.js'; import { validateMCPSchema, formatValidationErrors } from '../utils/validateMCPSchema.js'; // Interface for the function parameters export interface GenerateEndpointToolCodeParams { path: string; method: string; swaggerFilePath: string; // Required path to the Swagger file includeApiInName?: boolean; includeVersionInName?: boolean; singularizeResourceNames?: boolean; } /** * Generates TypeScript code for an MCP tool definition based on a Swagger endpoint * @param params Object containing the endpoint path, method, and swagger file path * @returns TypeScript code for the MCP tool definition */ async function generateEndpointToolCode(params: GenerateEndpointToolCodeParams): Promise<string> { try { const { path: endpointPath, method, swaggerFilePath, includeApiInName = false, includeVersionInName = false, singularizeResourceNames = true } = params; if (!swaggerFilePath) { throw new Error('Swagger file path is required'); } if (!fs.existsSync(swaggerFilePath)) { throw new Error(`Swagger file not found at ${swaggerFilePath}`); } // Read the Swagger definition file logger.info(`Reading Swagger definition from ${swaggerFilePath}`); const swaggerContent = fs.readFileSync(swaggerFilePath, 'utf8'); // Parse the Swagger definition based on file extension let swaggerDefinition; if (swaggerFilePath.endsWith('.yml') || swaggerFilePath.endsWith('.yaml')) { swaggerDefinition = yaml.load(swaggerContent); } else { swaggerDefinition = JSON.parse(swaggerContent); } // Find the endpoint in the Swagger definition const endpoint = findEndpoint(swaggerDefinition, endpointPath, method); if (!endpoint) { throw new Error(`Invalid or unsupported HTTP method '${method}' for endpoint path '${endpointPath}'`); } // Generate a tool name const toolName = generateToolName(method, endpointPath, endpoint.operationId, includeApiInName, includeVersionInName, singularizeResourceNames); // Generate the inputSchema const inputSchema = generateInputSchema(swaggerDefinition, endpoint); // Generate the tool definition const toolDefinition = generateToolDefinition(toolName, endpoint, inputSchema); // Generate the handler function const handlerFunction = generateHandlerFunction(toolName, method, endpointPath); // Combine the tool definition and handler function const generatedCode = `${toolDefinition}\n\n${handlerFunction}`; // Validate the generated code against the MCP schema logger.info('Validating generated code against MCP schema'); const validationResult = validateMCPSchema(generatedCode); if (!validationResult.isValid) { logger.error('Generated code failed MCP schema validation'); return formatValidationErrors(validationResult.errors); } logger.info('Generated code passed MCP schema validation'); return generatedCode; } catch (error: any) { logger.error(`Error generating endpoint tool code: ${error.message}`); throw error; } } /** * Find an endpoint in the Swagger definition * @param swaggerDefinition The Swagger definition object * @param endpointPath The path of the endpoint * @param method The HTTP method of the endpoint * @returns The endpoint object or undefined if not found */ function findEndpoint(swaggerDefinition: any, endpointPath: string, method: string): any { const paths = swaggerDefinition.paths || {}; const pathObj = paths[endpointPath]; if (!pathObj) { throw new Error(`Endpoint path '${endpointPath}' not found in Swagger definition`); } const endpoint = pathObj[method.toLowerCase()]; if (!endpoint) { throw new Error(`Invalid or unsupported HTTP method '${method}' for endpoint path '${endpointPath}'`); } return endpoint; } /** * Generate a tool name based on the method, path, and operationId * @param method The HTTP method of the endpoint * @param path The path of the endpoint * @param operationId The operationId from the Swagger definition * @param includeApiInName Whether to include 'api' segments in the name * @param includeVersionInName Whether to include version segments in the name * @param singularizeResourceNames Whether to singularize resource names * @returns The generated tool name */ function generateToolName( method: string, path: string, operationId?: string, includeApiInName = false, includeVersionInName = false, singularizeResourceNames = true ): string { const methodPrefix = { 'GET': 'get', 'POST': 'create', 'PUT': 'update', 'PATCH': 'update', 'DELETE': 'delete' }[method.toUpperCase()] || method.toLowerCase(); const methodPrefixShort = { 'GET': 'get', 'POST': 'crt', 'PUT': 'upd', 'PATCH': 'upd', 'DELETE': 'del' }[method.toUpperCase()] || method.toLowerCase(); if (operationId && !operationId.includes('_') && !operationId.includes('.')) { return operationId.substring(0, 64); } const abbreviationMap: Record<string, string> = { 'organization': 'org', 'organizations': 'orgs', 'generate': 'gen', 'information': 'info', 'application': 'app', 'applications': 'apps', 'identification': 'id', 'parameter': 'param', 'parameters': 'params', 'report': 'rpt', 'configuration': 'config', 'administrator': 'admin', 'authentication': 'auth', 'authorization': 'authz', 'notification': 'notice', 'notifications': 'notices', 'document': 'doc', 'documents': 'docs', 'category': 'cat', 'categories': 'cats', 'subscription': 'sub', 'subscriptions': 'subs', 'preference': 'pref', 'preferences': 'prefs', 'message': 'msg', 'messages': 'msgs', 'profile': 'prof', 'profiles': 'profs', 'setting': 'set', 'settings': 'sets' }; let cleanPath = path.replace(/\.[^/.]+$/, '').split('?')[0]; const segments = cleanPath.split('/').filter(Boolean); const processedSegments = segments.map((segment, index) => { if (segment.toLowerCase() === 'api' && !includeApiInName) { return ''; } if (segment.match(/^v\d+$/) && !includeVersionInName) { return ''; } if (segment.startsWith('{') && segment.endsWith('}')) { const param = segment.substring(1, segment.length - 1); return param.charAt(0).toUpperCase() + param.slice(1); } let processedSegment = segment; if (singularizeResourceNames && index === segments.length - 1 && segment.endsWith('s')) { processedSegment = segment.endsWith('ies') ? segment.slice(0, -3) + 'y' : segment.slice(0, -1); } const shortSegment = abbreviationMap[processedSegment.toLowerCase()] || processedSegment; return shortSegment.charAt(0).toUpperCase() + shortSegment.slice(1); }).filter(Boolean); // Combine the method prefix and segments let toolName = methodPrefix; for (const segment of processedSegments) { toolName += segment; } // If the name exceeds 64 characters, try to make it meaningful if (toolName.length > 64) { // Try to shorten by removing less important segments let reducedName = methodPrefixShort; for (const segment of processedSegments) { if ((reducedName + segment).length <= 64) { reducedName += segment; } else { break; } } return reducedName; } return toolName; } /** * Generate the inputSchema for the tool * @param swaggerDefinition The Swagger definition object * @param endpoint The endpoint object * @returns The inputSchema object */ function generateInputSchema(swaggerDefinition: any, endpoint: any): any { const inputSchema: any = { type: 'object', properties: {}, required: [] }; // Process parameters if (endpoint.parameters) { for (const param of endpoint.parameters) { // Skip parameters in header or formData if (param.in === 'header' || param.in === 'formData') { continue; } // Process path parameters if (param.in === 'path') { inputSchema.properties[param.name] = { type: mapSwaggerTypeToJsonSchema(param.type), description: param.description || `Path parameter: ${param.name}` }; if (param.required) { inputSchema.required.push(param.name); } } // Process query parameters if (param.in === 'query') { inputSchema.properties[param.name] = { type: mapSwaggerTypeToJsonSchema(param.type), description: param.description || `Query parameter: ${param.name}` }; if (param.enum) { inputSchema.properties[param.name].enum = param.enum; } if (param.required) { inputSchema.required.push(param.name); } } // Process body parameters if (param.in === 'body') { // Use a better parameter name (without dots) const paramName = param.name.replace(/\./g, ''); if (param.schema && param.schema.$ref) { // Extract the model name from the reference const modelName = param.schema.$ref.split('/').pop(); // Extract the full schema for the model const modelSchema = extractModelSchema(swaggerDefinition, modelName); // Add the model schema to the inputSchema inputSchema.properties[paramName] = { ...modelSchema, description: param.description || `Request body: ${paramName}` }; } else if (param.schema) { // Inline schema inputSchema.properties[paramName] = { ...processSchema(swaggerDefinition, param.schema), description: param.description || `Request body: ${paramName}` }; } if (param.required) { inputSchema.required.push(paramName); } } } } // Handle OpenAPI 3.0 requestBody if (endpoint.requestBody) { const contentTypes = endpoint.requestBody.content || {}; const jsonContent = contentTypes['application/json']; if (jsonContent && jsonContent.schema) { const schema = jsonContent.schema; const paramName = 'requestBody'; if (schema.$ref) { // Extract the model name from the reference const modelName = schema.$ref.split('/').pop(); // Extract the full schema for the model const modelSchema = extractModelSchema(swaggerDefinition, modelName); // Add the model schema to the inputSchema inputSchema.properties[paramName] = { ...modelSchema, description: endpoint.requestBody.description || `Request body` }; } else { // Inline schema inputSchema.properties[paramName] = { ...processSchema(swaggerDefinition, schema), description: endpoint.requestBody.description || `Request body` }; } if (endpoint.requestBody.required) { inputSchema.required.push(paramName); } } } return inputSchema; } /** * Extract the complete schema for a model * @param swaggerDefinition The Swagger definition object * @param modelName The name of the model * @returns The model schema */ function extractModelSchema(swaggerDefinition: any, modelName: string): any { const model = swaggerDefinition.definitions?.[modelName] || swaggerDefinition.components?.schemas?.[modelName]; if (!model) { return { type: 'object', description: `Model '${modelName}' not found` }; } return processSchema(swaggerDefinition, model); } /** * Process a schema recursively * @param swaggerDefinition The Swagger definition object * @param schema The schema to process * @returns The processed schema */ function processSchema(swaggerDefinition: any, schema: any): any { if (!schema) { return { type: 'object' }; } // Handle $ref if (schema.$ref) { const modelName = schema.$ref.split('/').pop(); // Check if this is a potential json.Unmarshaler interface if (isLikelyUnmarshaler(swaggerDefinition, modelName)) { return processUnmarshalerType(swaggerDefinition, modelName); } return extractModelSchema(swaggerDefinition, modelName); } // Handle array if (schema.type === 'array' && schema.items) { return { type: 'array', items: processSchema(swaggerDefinition, schema.items), description: schema.description }; } // Handle object if (schema.type === 'object' || schema.properties) { const result: any = { type: 'object', properties: {}, required: schema.required || [] }; if (schema.description) { result.description = schema.description; } // Process properties if (schema.properties) { for (const [propName, propSchema] of Object.entries<any>(schema.properties)) { result.properties[propName] = processSchema(swaggerDefinition, propSchema); } } return result; } // Handle primitive types const result: any = { type: mapSwaggerTypeToJsonSchema(schema.type) }; if (schema.description) { result.description = schema.description; } if (schema.enum) { result.enum = schema.enum; } if (schema.format) { result.format = schema.format; } return result; } /** * Check if a model is likely to be a json.Unmarshaler interface * @param swaggerDefinition The Swagger definition object * @param modelName The name of the model * @returns True if the model is likely to be a json.Unmarshaler interface */ function isLikelyUnmarshaler(swaggerDefinition: any, modelName: string): boolean { const model = swaggerDefinition.definitions?.[modelName] || swaggerDefinition.components?.schemas?.[modelName]; // If the model is not found, check if the name suggests a special type if (!model) { // Check if the model name suggests a date/time or special type const specialTypeNames = [ 'Date', 'DateTime', 'Time', 'Duration', 'Timestamp', 'Nullable', 'Optional', 'Slice', 'Array', 'List', 'Int64', 'Float64', 'Bool', 'String' ]; return specialTypeNames.some(name => modelName.includes(name)); } // Check if the model name suggests a date/time type const dateTimeNames = ['Date', 'DateTime', 'Time', 'Duration', 'Timestamp']; if (dateTimeNames.some(name => modelName.includes(name))) { return true; } // Check if the description mentions unmarshaler if (model.description && (model.description.toLowerCase().includes('unmarshaler') || model.description.toLowerCase().includes('unmarshal'))) { return true; } // Check if it's an object with a single 'value' property if (model.type === 'object' && model.properties && Object.keys(model.properties).length === 1 && model.properties.value) { return true; } return false; } /** * Process a json.Unmarshaler type * @param swaggerDefinition The Swagger definition object * @param modelName The name of the model * @returns The processed schema */ function processUnmarshalerType(swaggerDefinition: any, modelName: string): any { const model = swaggerDefinition.definitions?.[modelName] || swaggerDefinition.components?.schemas?.[modelName]; // Handle specific known types by name if (modelName.includes('NullableDate')) { return { type: 'string', format: 'date', description: model?.description || 'A nullable date value (format: YYYY-MM-DD)' }; } if (modelName.includes('NullableInt64Slice') || modelName.includes('NullableIntSlice')) { return { type: 'array', items: { type: 'integer' }, description: model?.description || 'A nullable array of integers' }; } if (modelName.includes('NullableTaskPriority')) { return { type: 'string', enum: ['low', 'normal', 'high'], description: model?.description || 'A nullable task priority value' }; } // If the model is not found, infer the type from the name if (!model) { // Infer the type based on the model name let inferredType = 'string'; let inferredFormat = ''; let description = `Model '${modelName}' not found`; // Check for date/time types if (modelName.includes('Date') || modelName.includes('Time')) { inferredType = 'string'; inferredFormat = modelName.includes('Time') ? 'date-time' : 'date'; description = `${description} - Inferred as a date${modelName.includes('Time') ? '-time' : ''} value`; } // Check for numeric types else if (modelName.includes('Int') || modelName.includes('Float') || modelName.includes('Number')) { inferredType = modelName.includes('Float') ? 'number' : 'integer'; description = `${description} - Inferred as a numeric value`; } // Check for boolean types else if (modelName.includes('Bool')) { inferredType = 'boolean'; description = `${description} - Inferred as a boolean value`; } // Check for array/slice types else if (modelName.includes('Slice') || modelName.includes('Array') || modelName.includes('List')) { inferredType = 'array'; description = `${description} - Inferred as an array`; // Try to infer the item type let itemType = 'string'; if (modelName.includes('Int64Slice') || modelName.includes('IntArray')) { itemType = 'integer'; } else if (modelName.includes('Float64Slice') || modelName.includes('FloatArray')) { itemType = 'number'; } else if (modelName.includes('BoolSlice') || modelName.includes('BoolArray')) { itemType = 'boolean'; } return { type: inferredType, items: { type: itemType }, description: description }; } return { type: inferredType, format: inferredFormat || undefined, description: description }; } // If it's an object with a single 'value' property, extract that property if (model.type === 'object' && model.properties && model.properties.value) { const valueProperty = model.properties.value; // Create a primitive type with the appropriate format const result: any = { type: mapSwaggerTypeToJsonSchema(valueProperty.type || 'string') }; // Add description from the model if (model.description) { result.description = model.description; } // Add format if available if (valueProperty.format) { result.format = valueProperty.format; } // Add description from the value property if available if (valueProperty.description) { result.description = result.description ? `${result.description} (${valueProperty.description})` : valueProperty.description; } return result; } // For other cases, return a simplified representation return { type: model.type || 'object', description: model.description || `Model '${modelName}'` }; } /** * Map Swagger types to JSON Schema types * @param swaggerType The Swagger type * @returns The JSON Schema type */ function mapSwaggerTypeToJsonSchema(swaggerType: string): string { const typeMap: Record<string, string> = { 'integer': 'integer', 'number': 'number', 'string': 'string', 'boolean': 'boolean', 'array': 'array', 'object': 'object', 'file': 'string', // Map file to string (base64) }; return typeMap[swaggerType] || 'string'; } /** * Generate the tool definition * @param toolName The name of the tool * @param endpoint The endpoint object * @param inputSchema The inputSchema object * @returns The tool definition as a string */ function generateToolDefinition(toolName: string, endpoint: any, inputSchema: any): string { // Combine summary and description let description = ''; // Add user-facing description if (endpoint.summary || endpoint.description) { description = [endpoint.summary, endpoint.description].filter(Boolean).join('. '); } // Generate basic AI instructions let aiDescription = ''; const method = endpoint.operationId ? endpoint.operationId : (endpoint.method || 'unknown'); aiDescription = `AI INSTRUCTIONS: This endpoint allows you to ${method.toLowerCase()} resources. `; // Add information about parameters if (endpoint.parameters && endpoint.parameters.length > 0) { const requiredParams = endpoint.parameters .filter((p: any) => p.required) .map((p: any) => p.name); if (requiredParams.length > 0) { aiDescription += `It requires the following parameters: ${requiredParams.join(', ')}. `; } } // Add information about response if (endpoint.responses && endpoint.responses['200']) { aiDescription += `On success, it returns a ${endpoint.responses['200'].description || '200 response'}.`; } // If there's already a description, add a separator if (description) { description += ' '; } description += aiDescription; // Format the inputSchema as a string with proper indentation const inputSchemaStr = JSON.stringify(inputSchema, null, 2) .replace(/"([^"]+)":/g, '$1:') // Remove quotes around property names .replace(/"/g, "'"); // Replace double quotes with single quotes // According to the MCP schema, we can only use name, description, and inputSchema return `/** * ${endpoint.summary || ''} * ${endpoint.description || ''} */ export const ${toolName} = { name: "${toolName}", description: "${escapeString(description)}", inputSchema: ${inputSchemaStr} };`; } /** * Generate the handler function * @param toolName The name of the tool * @param method The HTTP method of the endpoint * @param endpointPath The path of the endpoint * @returns The handler function as a string */ function generateHandlerFunction(toolName: string, method: string, endpointPath: string): string { return ` // Tool handler export async function handle${toolName}(input: any) { try { // TODO: Implement API call to ${method.toUpperCase()} ${endpointPath} // const response = await apiClient.${method.toLowerCase()}('${endpointPath}', input); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Not implemented yet" }, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: \`Error: \${error.message}\` }] }; } }`; } /** * Escape special characters in a string for use in a JavaScript string literal * @param str The string to escape * @returns The escaped string */ function escapeString(str: string): string { return str .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n'); } export default generateEndpointToolCode;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Vizioz/Swagger-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server