Skip to main content
Glama
extract-tools.ts8.79 kB
/** * Functions for extracting tools from an OpenAPI specification */ import { OpenAPIV3 } from 'openapi-types'; import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema'; import { generateOperationId } from '../utils/operation-id.js'; import type { McpToolDefinition } from '../types/index.js'; import type { OpenAPIV3DocumentX } from '../types/open-api-document-x.js'; /** * Extracts tool definitions from an OpenAPI document * * @param api OpenAPI document * @param prefix Tool prefix * @returns Array of MCP tool definitions */ export function extractToolsFromApi( api: OpenAPIV3DocumentX, prefix?: string ): McpToolDefinition[] { const tools: McpToolDefinition[] = []; const usedNames = new Set<string>(); const globalSecurity = api.security || []; if (!api.paths) return tools; for (const [path, pathItem] of Object.entries(api.paths)) { if (!pathItem) continue; for (const method of Object.values(OpenAPIV3.HttpMethods)) { const operation = pathItem[method]; if (!operation) continue; // Generate a unique name for the tool let baseName = operation.operationId || generateOperationId(method, path); if (!baseName) continue; // Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -) baseName = baseName .replace(/\./g, '_') .replace(/[^a-z0-9_-]/gi, '_') .toLowerCase(); let finalToolName = baseName; let counter = 1; while (usedNames.has(finalToolName)) { finalToolName = `${baseName}_${counter++}`; } usedNames.add(finalToolName); // Get or create a description const description = operation.description || operation.summary || `Executes ${method.toUpperCase()} ${path}`; const prompt = operation['x-mcp-prompt']; // Generate input schema and extract parameters const { inputSchema, parameters, requestBodyContentType } = generateInputSchemaAndDetails(operation); // Extract parameter details for execution const executionParameters = parameters.map((p) => ({ name: p.name, in: p.in, })); // Determine security requirements const securityRequirements = operation.security === null ? globalSecurity : operation.security || globalSecurity; // Create the tool definition tools.push({ name: prefix + finalToolName, description, inputSchema, method, pathTemplate: path, parameters, executionParameters, requestBodyContentType, securityRequirements, operationId: baseName, prompt, }); } } return tools; } /** * Generates input schema and extracts parameter details from an operation * * @param operation OpenAPI operation object * @returns Input schema, parameters, and request body content type */ export function generateInputSchemaAndDetails( operation: OpenAPIV3.OperationObject, ): { inputSchema: JSONSchema7 | boolean; parameters: OpenAPIV3.ParameterObject[]; requestBodyContentType?: string; } { const properties: { [key: string]: JSONSchema7 | boolean } = {}; const required: string[] = []; // Process parameters const allParameters: OpenAPIV3.ParameterObject[] = Array.isArray( operation.parameters, ) ? operation.parameters.map((p) => p as OpenAPIV3.ParameterObject) : []; allParameters.forEach((param) => { if (!param.name || !param.schema) return; const paramSchema = mapOpenApiSchemaToJsonSchema( param.schema as OpenAPIV3.SchemaObject, ); if (typeof paramSchema === 'object') { paramSchema.description = param.description || paramSchema.description; } properties[param.name] = paramSchema; if (param.required) required.push(param.name); }); // Process request body (if present) let requestBodyContentType: string | undefined = undefined; if (operation.requestBody) { const opRequestBody = operation.requestBody as OpenAPIV3.RequestBodyObject; const jsonContent = opRequestBody.content?.['application/json']; const firstContent = opRequestBody.content ? Object.entries(opRequestBody.content)[0] : undefined; if (jsonContent?.schema) { requestBodyContentType = 'application/json'; const bodySchema = mapOpenApiSchemaToJsonSchema( jsonContent.schema as OpenAPIV3.SchemaObject, ); if (typeof bodySchema === 'object') { bodySchema.description = opRequestBody.description || bodySchema.description || 'The JSON request body.'; } properties['requestBody'] = bodySchema; if (opRequestBody.required) required.push('requestBody'); } else if (firstContent) { const [contentType] = firstContent; requestBodyContentType = contentType; properties['requestBody'] = { type: 'string', description: opRequestBody.description || `Request body (content type: ${contentType})`, }; if (opRequestBody.required) required.push('requestBody'); } } // Combine everything into a JSON Schema const inputSchema: JSONSchema7 = { type: 'object', properties, ...(required.length > 0 && { required }), }; return { inputSchema, parameters: allParameters, requestBodyContentType }; } /** * Maps an OpenAPI schema to a JSON Schema * * @param schema OpenAPI schema object or reference * @returns JSON Schema representation */ export function mapOpenApiSchemaToJsonSchema( schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, ): JSONSchema7 | boolean { // Handle reference objects if ('$ref' in schema) { console.warn(`Unresolved $ref '${schema.$ref}'.`); return { type: 'object' }; } // Handle boolean schemas if (typeof schema === 'boolean') return schema; // Create a copy of the schema to modify let jsonSchema: JSONSchema7 = { ...schema } as any; if (schema.oneOf || schema.anyOf || schema.allOf) { const oneSchema = structuredClone( schema.oneOf || schema.anyOf || schema.allOf, ); if (oneSchema) { const combinedSchema = mapOpenApiSchemaToJsonSchema(oneSchema[0]); for (let i = 1; i < oneSchema.length; i++) { const mappedSubSchema = mapOpenApiSchemaToJsonSchema(oneSchema[i]); if ( typeof mappedSubSchema === 'object' && typeof combinedSchema === 'object' ) { // Handle enum values if (mappedSubSchema.enum) { if (!combinedSchema.enum) { combinedSchema.enum = []; } // Combine enum values from both schemas const uniqueEnums = new Set([ ...combinedSchema.enum, ...(mappedSubSchema.enum || []), ]); combinedSchema.enum = Array.from(uniqueEnums); } } } jsonSchema = combinedSchema as JSONSchema7; } } // Convert integer type to number (JSON Schema compatible) if (schema.type === 'integer') jsonSchema.type = 'number'; // Remove OpenAPI-specific properties that aren't in JSON Schema delete (jsonSchema as any).nullable; delete (jsonSchema as any).example; delete (jsonSchema as any).xml; delete (jsonSchema as any).externalDocs; delete (jsonSchema as any).deprecated; delete (jsonSchema as any).readOnly; delete (jsonSchema as any).writeOnly; // Handle nullable properties by adding null to the type if (schema.nullable) { if (Array.isArray(jsonSchema.type)) { if (!jsonSchema.type.includes('null')) jsonSchema.type.push('null'); } else if (typeof jsonSchema.type === 'string') { jsonSchema.type = [jsonSchema.type as JSONSchema7TypeName, 'null']; } else if (!jsonSchema.type) { jsonSchema.type = 'null'; } } // Recursively process object properties if (jsonSchema.type === 'object' && jsonSchema.properties) { const mappedProps: { [key: string]: JSONSchema7 | boolean } = {}; for (const [key, propSchema] of Object.entries(jsonSchema.properties)) { if (typeof propSchema === 'object' && propSchema !== null) { mappedProps[key] = mapOpenApiSchemaToJsonSchema( propSchema as OpenAPIV3.SchemaObject, ); } else if (typeof propSchema === 'boolean') { mappedProps[key] = propSchema; } } jsonSchema.properties = mappedProps; } // Recursively process array items if ( jsonSchema.type === 'array' && typeof jsonSchema.items === 'object' && jsonSchema.items !== null ) { jsonSchema.items = mapOpenApiSchemaToJsonSchema( jsonSchema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, ); } return jsonSchema; }

Implementation Reference

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/MoralisWeb3/moralis-mcp-server'

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