Skip to main content
Glama
simplified-openapi.mjs23.1 kB
import fs from 'fs'; import yaml from 'js-yaml'; export function createAndSaveSimplifiedOpenAPI(endpointsFile, openapiFile, openapiTrimmedFile) { const allEndpoints = JSON.parse(fs.readFileSync(endpointsFile, 'utf8')); const endpoints = allEndpoints.filter((endpoint) => !endpoint.disabled); const spec = fs.readFileSync(openapiFile, 'utf8'); const openApiSpec = yaml.load(spec); for (const endpoint of endpoints) { if (!openApiSpec.paths[endpoint.pathPattern]) { throw new Error(`Path "${endpoint.pathPattern}" not found in OpenAPI spec.`); } } for (const [key, value] of Object.entries(openApiSpec.paths)) { const e = endpoints.filter((ep) => ep.pathPattern === key); if (e.length === 0) { delete openApiSpec.paths[key]; } else { for (const [method, operation] of Object.entries(value)) { const eo = e.find((ep) => ep.method.toLowerCase() === method); if (eo) { operation.operationId = eo.toolName; if (!operation.description && operation.summary) { operation.description = operation.summary; } if (operation.parameters) { operation.parameters = operation.parameters.map((param) => { if (param.$ref && param.$ref.startsWith('#/components/parameters/')) { const paramName = param.$ref.replace('#/components/parameters/', ''); const resolvedParam = openApiSpec.components?.parameters?.[paramName]; if (resolvedParam) { return { ...resolvedParam }; } } return param; }); } } else { delete value[method]; } } } } if (openApiSpec.components && openApiSpec.components.schemas) { removeODataTypeRecursively(openApiSpec.components.schemas); flattenComplexSchemasRecursively(openApiSpec.components.schemas); } if (openApiSpec.paths) { removeODataTypeRecursively(openApiSpec.paths); simplifyAnyOfInPaths(openApiSpec.paths); } console.log('🧹 Pruning unused schemas...'); const usedSchemas = findUsedSchemas(openApiSpec); pruneUnusedSchemas(openApiSpec, usedSchemas); fs.writeFileSync(openapiTrimmedFile, yaml.dump(openApiSpec)); } function removeODataTypeRecursively(obj) { if (!obj || typeof obj !== 'object') return; if (Array.isArray(obj)) { obj.forEach((item) => removeODataTypeRecursively(item)); return; } Object.keys(obj).forEach((key) => { if (key === '@odata.type') { delete obj[key]; } else { removeODataTypeRecursively(obj[key]); } }); } function simplifyAnyOfInPaths(paths) { Object.entries(paths).forEach(([pathKey, pathItem]) => { if (!pathItem || typeof pathItem !== 'object') return; Object.entries(pathItem).forEach(([method, operation]) => { if (!operation || typeof operation !== 'object') return; if (operation.parameters && Array.isArray(operation.parameters)) { operation.parameters.forEach((param) => { if (param.schema && param.schema.anyOf) { simplifyAnyOfSchema(param.schema, `Path ${pathKey} ${method} parameter`); } }); } if (operation.requestBody && operation.requestBody.content) { Object.entries(operation.requestBody.content).forEach(([mediaType, mediaTypeObj]) => { if (mediaTypeObj.schema && mediaTypeObj.schema.anyOf) { simplifyAnyOfSchema( mediaTypeObj.schema, `Path ${pathKey} ${method} requestBody ${mediaType}` ); } }); } if (operation.responses) { Object.entries(operation.responses).forEach(([statusCode, response]) => { if (response.content) { Object.entries(response.content).forEach(([mediaType, mediaTypeObj]) => { if (mediaTypeObj.schema && mediaTypeObj.schema.anyOf) { simplifyAnyOfSchema( mediaTypeObj.schema, `Path ${pathKey} ${method} response ${statusCode} ${mediaType}` ); } }); } }); } }); }); } function simplifyAnyOfSchema(schema, context) { if (!schema.anyOf || !Array.isArray(schema.anyOf)) return; const anyOfItems = schema.anyOf; if (anyOfItems.length === 2) { const hasRef = anyOfItems.some((item) => item.$ref); const hasNullableObject = anyOfItems.some( (item) => item.type === 'object' && item.nullable === true && Object.keys(item).length <= 2 ); if (hasRef && hasNullableObject) { console.log(`Simplifying anyOf in ${context} (ref + nullable object pattern)`); const refItem = anyOfItems.find((item) => item.$ref); delete schema.anyOf; schema.$ref = refItem.$ref; schema.nullable = true; } } else if (anyOfItems.length > 2) { console.log(`Simplifying anyOf in ${context} (multiple options)`); schema.type = anyOfItems[0].type || 'object'; schema.nullable = true; schema.description = `${schema.description || ''} [Simplified from ${ anyOfItems.length } options]`.trim(); delete schema.anyOf; } } function flattenComplexSchemasRecursively(schemas) { Object.entries(schemas).forEach(([schemaName, schema]) => { if (!schema || typeof schema !== 'object') return; flattenComplexSchema(schema, schemaName); if (schema.allOf) { const flattenedSchema = mergeAllOfSchemas(schema.allOf, schemas); Object.assign(schema, flattenedSchema); delete schema.allOf; } if (schema.properties && shouldReduceProperties(schema)) { reduceProperties(schema, schemaName); } if (schema.properties) { simplifyNestedPropertiesRecursively(schema.properties); } }); } function flattenComplexSchema(schema, schemaName) { if (schema.anyOf && Array.isArray(schema.anyOf)) { if (schema.anyOf.length === 2) { const hasRef = schema.anyOf.some((item) => item.$ref); const hasNullableObject = schema.anyOf.some( (item) => item.type === 'object' && item.nullable === true && Object.keys(item).length <= 2 ); if (hasRef && hasNullableObject) { console.log(`Simplifying anyOf in ${schemaName} (ref + nullable object pattern)`); const refItem = schema.anyOf.find((item) => item.$ref); delete schema.anyOf; schema.$ref = refItem.$ref; schema.nullable = true; } } else if (schema.anyOf.length > 2) { console.log(`Simplifying anyOf in ${schemaName} (${schema.anyOf.length} options)`); const firstOption = schema.anyOf[0]; schema.type = firstOption.type || 'object'; schema.nullable = true; schema.description = `${schema.description || ''} [Simplified from ${ schema.anyOf.length } options]`.trim(); delete schema.anyOf; } } if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 2) { console.log(`Simplifying oneOf in ${schemaName} (${schema.oneOf.length} options)`); const firstOption = schema.oneOf[0]; schema.type = firstOption.type || 'object'; schema.nullable = true; schema.description = `${schema.description || ''} [Simplified from ${ schema.oneOf.length } options]`.trim(); delete schema.oneOf; } } function shouldReduceProperties(schema) { if (!schema.properties) return false; const propertyCount = Object.keys(schema.properties).length; return propertyCount > 25; } function reduceProperties(schema, schemaName) { const properties = schema.properties; const propertyCount = Object.keys(properties).length; if (propertyCount > 25) { console.log(`Reducing properties in ${schemaName} (${propertyCount} -> 25)`); const priorityProperties = [ 'id', 'name', 'displayName', 'description', 'createdDateTime', 'lastModifiedDateTime', 'status', 'state', 'type', 'value', 'email', 'userPrincipalName', 'title', 'content', 'body', 'subject', 'message', 'attachments', 'error', 'code', 'details', 'url', 'href', 'path', 'method', 'enabled', ]; const keptProperties = {}; const propertyKeys = Object.keys(properties); priorityProperties.forEach((key) => { if (properties[key]) { keptProperties[key] = properties[key]; } }); const remainingSlots = 25 - Object.keys(keptProperties).length; const otherKeys = propertyKeys.filter((key) => !keptProperties[key]); otherKeys.slice(0, remainingSlots).forEach((key) => { keptProperties[key] = properties[key]; }); schema.properties = keptProperties; schema.additionalProperties = true; schema.description = `${ schema.description || '' } [Note: Simplified from ${propertyCount} properties to 25 most common ones]`.trim(); } } function mergeAllOfSchemas(allOfArray, allSchemas, visited = new Set()) { const merged = { type: 'object', properties: {}, }; allOfArray.forEach((item) => { if (item.$ref) { const refSchemaName = item.$ref.replace('#/components/schemas/', ''); if (visited.has(refSchemaName)) { return; } visited.add(refSchemaName); const refSchema = allSchemas[refSchemaName]; if (refSchema) { console.log( `Processing ref ${refSchemaName} for ${item.title}, exists: true, has properties: ${!!refSchema.properties}, has allOf: ${!!refSchema.allOf}` ); if (refSchema.allOf) { const nestedMerged = mergeAllOfSchemas(refSchema.allOf, allSchemas, new Set(visited)); Object.assign(merged.properties, nestedMerged.properties); if (nestedMerged.required) { merged.required = [...(merged.required || []), ...nestedMerged.required]; } if (nestedMerged.description && !merged.description) { merged.description = nestedMerged.description; } } if (refSchema.properties) { console.log(`Ensuring ${item.title} has all required properties from ${refSchemaName}`); Object.assign(merged.properties, refSchema.properties); } if (refSchema.required) { merged.required = [...(merged.required || []), ...refSchema.required]; } if (refSchema.description && !merged.description) { merged.description = refSchema.description; } } } else if (item.properties) { Object.assign(merged.properties, item.properties); if (item.required) { merged.required = [...(merged.required || []), ...item.required]; } } }); if (merged.required) { merged.required = [...new Set(merged.required)]; } return merged; } function simplifyNestedPropertiesRecursively(properties, currentDepth = 0, maxDepth = 3) { if (!properties || typeof properties !== 'object' || currentDepth >= maxDepth) { return; } Object.keys(properties).forEach((key) => { const prop = properties[key]; if (prop && typeof prop === 'object') { if (currentDepth === maxDepth - 1 && prop.properties) { console.log(`Flattening nested property at depth ${currentDepth}: ${key}`); prop.type = 'object'; prop.description = `${prop.description || ''} [Simplified: nested object]`.trim(); delete prop.properties; delete prop.additionalProperties; } else if (prop.properties) { simplifyNestedPropertiesRecursively(prop.properties, currentDepth + 1, maxDepth); } if (prop.anyOf && Array.isArray(prop.anyOf)) { if (prop.anyOf.length === 2) { const hasRef = prop.anyOf.some((item) => item.$ref); const hasNullableObject = prop.anyOf.some( (item) => item.type === 'object' && item.nullable === true && Object.keys(item).length <= 2 ); if (hasRef && hasNullableObject) { console.log(`Simplifying anyOf in property ${key} (ref + nullable object pattern)`); const refItem = prop.anyOf.find((item) => item.$ref); delete prop.anyOf; prop.$ref = refItem.$ref; prop.nullable = true; } } else if (prop.anyOf.length > 2) { prop.type = prop.anyOf[0].type || 'object'; prop.nullable = true; prop.description = `${prop.description || ''} [Simplified from ${prop.anyOf.length} options]`.trim(); delete prop.anyOf; } } if (prop.oneOf && Array.isArray(prop.oneOf) && prop.oneOf.length > 2) { prop.type = prop.oneOf[0].type || 'object'; prop.nullable = true; prop.description = `${prop.description || ''} [Simplified from ${prop.oneOf.length} options]`.trim(); delete prop.oneOf; } } }); } function findUsedSchemas(openApiSpec) { const usedSchemas = new Set(); const schemasToProcess = []; const schemas = openApiSpec.components?.schemas || {}; const responses = openApiSpec.components?.responses || {}; const requestBodies = openApiSpec.components?.requestBodies || {}; const paths = openApiSpec.paths || {}; Object.entries(paths).forEach(([, pathItem]) => { Object.entries(pathItem).forEach(([, operation]) => { if (typeof operation !== 'object') return; if (operation.requestBody?.$ref) { const requestBodyName = operation.requestBody.$ref.replace( '#/components/requestBodies/', '' ); const requestBodyDefinition = requestBodies[requestBodyName]; if (requestBodyDefinition?.content) { Object.values(requestBodyDefinition.content).forEach((content) => { if (content.schema?.$ref) { const schemaName = content.schema.$ref.replace('#/components/schemas/', ''); schemasToProcess.push(schemaName); } if (content.schema?.properties) { findRefsInObject(content.schema.properties, (ref) => { const schemaName = ref.replace('#/components/schemas/', ''); schemasToProcess.push(schemaName); }); } }); } } if (operation.requestBody?.content) { Object.values(operation.requestBody.content).forEach((content) => { if (content.schema?.$ref) { const schemaName = content.schema.$ref.replace('#/components/schemas/', ''); schemasToProcess.push(schemaName); } if (content.schema?.properties?.requests?.items?.$ref) { const schemaName = content.schema.properties.requests.items.$ref.replace( '#/components/schemas/', '' ); schemasToProcess.push(schemaName); } }); } if (operation.responses) { Object.entries(operation.responses).forEach(([, response]) => { if (response.$ref) { const responseName = response.$ref.replace('#/components/responses/', ''); const responseDefinition = responses[responseName]; if (responseDefinition?.content) { Object.values(responseDefinition.content).forEach((content) => { if (content.schema?.$ref) { const schemaName = content.schema.$ref.replace('#/components/schemas/', ''); schemasToProcess.push(schemaName); } }); } } if (response.content) { Object.values(response.content).forEach((content) => { if (content.schema?.$ref) { const schemaName = content.schema.$ref.replace('#/components/schemas/', ''); schemasToProcess.push(schemaName); } if (content.schema?.allOf) { content.schema.allOf.forEach((allOfItem) => { if (allOfItem.$ref) { const schemaName = allOfItem.$ref.replace('#/components/schemas/', ''); schemasToProcess.push(schemaName); } if (allOfItem.properties?.value?.items?.$ref) { const schemaName = allOfItem.properties.value.items.$ref.replace( '#/components/schemas/', '' ); schemasToProcess.push(schemaName); } }); } }); } }); } if (operation.parameters) { operation.parameters.forEach((param) => { if (param.schema?.$ref) { const schemaName = param.schema.$ref.replace('#/components/schemas/', ''); schemasToProcess.push(schemaName); } }); } }); }); const visited = new Set(); function processSchema(schemaName) { if (visited.has(schemaName)) return; visited.add(schemaName); const schema = schemas[schemaName]; if (!schema) { console.log(`⚠️ Warning: Schema ${schemaName} not found`); return; } usedSchemas.add(schemaName); findRefsInObject(schema, (ref) => { const refSchemaName = ref.replace('#/components/schemas/', ''); if (schemas[refSchemaName]) { processSchema(refSchemaName); } else { console.log(`⚠️ Schema ${schemaName} references missing schema: ${refSchemaName}`); } }); } schemasToProcess.forEach((schemaName) => processSchema(schemaName)); [ 'microsoft.graph.ODataErrors.ODataError', 'microsoft.graph.ODataErrors.MainError', 'microsoft.graph.ODataErrors.ErrorDetails', 'microsoft.graph.ODataErrors.InnerError', ].forEach((errorSchema) => { if (schemas[errorSchema]) { processSchema(errorSchema); } }); console.log( ` Found ${usedSchemas.size} used schemas out of ${Object.keys(schemas).length} total schemas` ); return usedSchemas; } function findRefsInObject(obj, callback, visited = new Set()) { if (!obj || typeof obj !== 'object' || visited.has(obj)) return; visited.add(obj); if (Array.isArray(obj)) { obj.forEach((item) => findRefsInObject(item, callback, visited)); return; } Object.entries(obj).forEach(([key, value]) => { if (key === '$ref' && typeof value === 'string' && value.startsWith('#/components/schemas/')) { callback(value); } else if (typeof value === 'object') { findRefsInObject(value, callback, visited); } }); } function cleanBrokenRefs(obj, availableSchemas, visited = new Set()) { if (!obj || typeof obj !== 'object' || visited.has(obj)) return; visited.add(obj); if (Array.isArray(obj)) { for (let i = obj.length - 1; i >= 0; i--) { const item = obj[i]; if (item && typeof item === 'object' && item.$ref) { const refSchemaName = item.$ref.replace('#/components/schemas/', ''); if (!availableSchemas[refSchemaName]) { console.log(` Removing broken reference: ${refSchemaName}`); obj.splice(i, 1); } } else if (typeof item === 'object') { cleanBrokenRefs(item, availableSchemas, visited); } } return; } Object.entries(obj).forEach(([key, value]) => { if (key === '$ref' && typeof value === 'string' && value.startsWith('#/components/schemas/')) { const refSchemaName = value.replace('#/components/schemas/', ''); if (!availableSchemas[refSchemaName]) { console.log(` Removing broken $ref: ${refSchemaName}`); delete obj[key]; if (Object.keys(obj).length === 0) { obj.type = 'object'; } } } else if (typeof value === 'object') { cleanBrokenRefs(value, availableSchemas, visited); } }); } function pruneUnusedSchemas(openApiSpec, usedSchemas) { const schemas = openApiSpec.components?.schemas || {}; const originalCount = Object.keys(schemas).length; Object.keys(schemas).forEach((schemaName) => { if (!usedSchemas.has(schemaName)) { delete schemas[schemaName]; } }); Object.values(schemas).forEach((schema) => { if (schema) { cleanBrokenRefs(schema, schemas); } }); if (openApiSpec.components?.responses) { Object.values(openApiSpec.components.responses).forEach((response) => { if (response) { cleanBrokenRefs(response, schemas); } }); } if (openApiSpec.paths) { Object.values(openApiSpec.paths).forEach((pathItem) => { if (pathItem) { cleanBrokenRefs(pathItem, schemas); } }); } const newCount = Object.keys(schemas).length; const reduction = (((originalCount - newCount) / originalCount) * 100).toFixed(1); console.log(` Removed ${originalCount - newCount} unused schemas (${reduction}% reduction)`); console.log(` Final schema count: ${newCount} (from ${originalCount})`); if (openApiSpec.components?.responses) { const usedResponses = new Set(); Object.values(openApiSpec.paths || {}).forEach((pathItem) => { Object.values(pathItem).forEach((operation) => { if (operation.responses) { Object.values(operation.responses).forEach((response) => { if (response.$ref) { const responseName = response.$ref.replace('#/components/responses/', ''); usedResponses.add(responseName); } }); } }); }); usedResponses.add('error'); const responses = openApiSpec.components.responses; const originalResponseCount = Object.keys(responses).length; Object.keys(responses).forEach((responseName) => { if (!usedResponses.has(responseName)) { delete responses[responseName]; } }); const newResponseCount = Object.keys(responses).length; console.log( ` Removed ${originalResponseCount - newResponseCount} unused responses (from ${originalResponseCount} to ${newResponseCount})` ); } if (openApiSpec.components?.requestBodies) { const usedRequestBodies = new Set(); Object.values(openApiSpec.paths || {}).forEach((pathItem) => { Object.values(pathItem).forEach((operation) => { if (operation.requestBody?.$ref) { const requestBodyName = operation.requestBody.$ref.replace( '#/components/requestBodies/', '' ); usedRequestBodies.add(requestBodyName); } }); }); const requestBodies = openApiSpec.components.requestBodies; const originalRequestBodyCount = Object.keys(requestBodies).length; Object.keys(requestBodies).forEach((requestBodyName) => { if (!usedRequestBodies.has(requestBodyName)) { delete requestBodies[requestBodyName]; } }); const newRequestBodyCount = Object.keys(requestBodies).length; console.log( ` Removed ${originalRequestBodyCount - newRequestBodyCount} unused request bodies (from ${originalRequestBodyCount} to ${newRequestBodyCount})` ); } }

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/Softeria/ms-365-mcp-server'

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