Skip to main content
Glama
ToolGenerator.ts7.55 kB
import { z } from 'zod'; import { ApiOperation, McpToolMetadata, ApiParameter } from '../types'; import { SchemaConverter } from './SchemaConverter'; import { logger } from '../utils/logger'; export class ToolGenerator { private schemaConverter: SchemaConverter; private toolPrefix: string; constructor(definitions?: Record<string, any>, toolPrefix: string = '') { this.schemaConverter = new SchemaConverter(definitions); this.toolPrefix = toolPrefix; } // Generate MCP tool from OpenAPI operation generateTool(operation: ApiOperation): McpToolMetadata { const toolName = this.generateToolName(operation); const title = this.generateToolTitle(operation); const description = this.generateToolDescription(operation); const inputSchema = this.generateInputSchema(operation); logger.debug(`Generated tool: ${toolName}`); return { name: toolName, title, description, inputSchema, operation, }; } private generateToolName(operation: ApiOperation): string { // Use operationId if available and well-formed if (operation.operationId && operation.operationId.includes('-')) { let name = operation.operationId.toLowerCase(); // Add prefix if provided if (this.toolPrefix) { name = `${this.toolPrefix}-${name}`; } // Ensure name doesn't exceed 64 characters return this.truncateToolName(name); } // Generate name in format: method-meaningful-path-parts (excluding parameters) const method = operation.method.toLowerCase(); // Parse path components, skip common prefixes and parameters const pathParts = operation.path .split('/') .filter(part => part.length > 0) .filter(part => !['api', 'v1', 'v2', 'v3'].includes(part.toLowerCase())) // Skip common API prefixes .filter(part => !(part.startsWith('{') && part.endsWith('}'))) // Remove parameters like {id}, {BeginDate}, etc. .map(part => part.toLowerCase()); // Build the tool name: method + meaningful path parts (excluding parameters) // Join all meaningful parts to create a descriptive name const meaningfulParts = pathParts.length > 0 ? pathParts : ['unknown']; const nameParts = [method, ...meaningfulParts]; // Join with hyphens and clean up let name = nameParts .join('-') .replace(/[^a-zA-Z0-9\-]/g, '') // Remove invalid characters except hyphens .replace(/-+/g, '-') // Collapse multiple hyphens .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens // Add prefix if provided if (this.toolPrefix) { name = `${this.toolPrefix}-${name}`; } // Ensure name doesn't exceed 64 characters return this.truncateToolName(name); } private truncateToolName(name: string): string { const maxLength = 64; if (name.length <= maxLength) { return name; } // Try to truncate intelligently by removing less important parts // Priority: keep method bracket, keep last part (endpoint), truncate middle parts const parts = name.split('-'); if (parts.length <= 2) { // Simple truncation if we can't break it down further return name.substring(0, maxLength); } // Keep first part (method) and last part (endpoint), truncate middle const firstPart = parts[0]; const lastPart = parts[parts.length - 1]; const middleParts = parts.slice(1, -1); let truncatedName = `${firstPart}-${lastPart}`; // Add back middle parts if there's room for (const part of middleParts) { const testName = `${firstPart}-${part}-${lastPart}`; if (testName.length <= maxLength) { truncatedName = testName; break; } } // Final safety check if (truncatedName.length > maxLength) { truncatedName = truncatedName.substring(0, maxLength); } logger.warn(`Tool name truncated: '${name}' → '${truncatedName}' (${truncatedName.length}/64 chars)`); return truncatedName; } private generateToolTitle(operation: ApiOperation): string { if (operation.summary) { return operation.summary; } // Generate title from method and path const method = operation.method; const path = operation.path .split('/') .filter(p => p.length > 0) .map(p => p.startsWith('{') ? p.slice(1, -1) : p) .map(p => p.charAt(0).toUpperCase() + p.slice(1)) .join(' '); return `${method} ${path}`; } private generateToolDescription(operation: ApiOperation): string { let description = operation.description || operation.summary || ''; if (!description) { description = `${operation.method} request to ${operation.path}`; } // Add parameter information if (operation.parameters && operation.parameters.length > 0) { const paramDescriptions = operation.parameters .map(p => `- ${p.name} (${p.in}${p.required ? ', required' : ''}): ${p.description || 'No description'}`) .join('\n'); description += `\n\nParameters:\n${paramDescriptions}`; } return description; } private generateInputSchema(operation: ApiOperation): z.ZodRawShape { const shape: z.ZodRawShape = {}; // Process parameters if (operation.parameters) { for (const param of operation.parameters) { const paramSchema = this.convertParameter(param); if (paramSchema) { shape[param.name] = paramSchema; } } } // Process request body if (operation.requestBody && operation.requestBody.content) { // Prefer application/json content const jsonContent = operation.requestBody.content['application/json']; if (jsonContent && jsonContent.schema) { const bodySchema = this.schemaConverter.convert(jsonContent.schema); // If request body is required, merge its properties into the main shape // Otherwise, make it optional if (operation.requestBody.required) { // If the body schema is an object, merge its shape if (bodySchema instanceof z.ZodObject) { const bodyShape = bodySchema.shape; Object.assign(shape, bodyShape); } else { // For non-object bodies, add as 'body' parameter shape['body'] = bodySchema; } } else { if (bodySchema instanceof z.ZodObject) { const bodyShape = bodySchema.shape; // Make all body properties optional for (const [key, schema] of Object.entries(bodyShape)) { shape[key] = (schema as z.ZodType<any>).optional(); } } else { shape['body'] = bodySchema.optional(); } } } } // Return the shape object (not wrapped in z.object()) return shape; } private convertParameter(param: ApiParameter): z.ZodType<any> | null { if (!param.schema) { // For OpenAPI 2.0 style parameters without schema logger.warn(`Parameter ${param.name} has no schema, using any type`); return param.required ? z.any() : z.any().optional(); } let zodSchema = this.schemaConverter.convert(param.schema); // Apply required/optional if (!param.required) { zodSchema = zodSchema.optional(); } // Add description as metadata (for documentation purposes) if (param.description) { zodSchema = zodSchema.describe(param.description); } return zodSchema; } }

Latest Blog Posts

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/NakiriYuuzu/SwaggerMcp'

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