openapi-processor.js•7.63 kB
import fs from 'fs/promises';
import path from 'path';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
export class OpenAPIProcessor {
constructor() {
this.ajv = new Ajv({ strict: false, allErrors: true });
addFormats(this.ajv);
this.spec = null;
this.tools = new Map();
}
async loadSpec(specPath) {
try {
const fullPath = path.resolve(specPath);
const specContent = await fs.readFile(fullPath, 'utf-8');
this.spec = JSON.parse(specContent);
this.validateSpec();
this.generateTools();
return this.spec;
} catch (error) {
throw new Error(`Failed to load OpenAPI spec: ${error.message}`);
}
}
validateSpec() {
if (!this.spec) {
throw new Error('No OpenAPI specification loaded');
}
if (!this.spec.openapi || !this.spec.openapi.startsWith('3.')) {
throw new Error('Only OpenAPI 3.x specifications are supported');
}
if (!this.spec.paths || Object.keys(this.spec.paths).length === 0) {
throw new Error('OpenAPI specification must contain at least one path');
}
}
generateTools() {
this.tools.clear();
for (const [pathStr, pathObj] of Object.entries(this.spec.paths)) {
for (const [method, operation] of Object.entries(pathObj)) {
if (!this.isValidHttpMethod(method)) continue;
const toolName = this.generateToolName(operation, method, pathStr);
const tool = this.createToolDefinition(toolName, method, pathStr, operation);
this.tools.set(toolName, tool);
}
}
}
isValidHttpMethod(method) {
const validMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
return validMethods.includes(method.toLowerCase());
}
generateToolName(operation, method, pathStr) {
if (operation.operationId) {
return operation.operationId;
}
// Generate name from method and path
const cleanPath = pathStr
.replace(/^\//, '')
.replace(/[{}]/g, '')
.replace(/[^a-zA-Z0-9]/g, '_')
.replace(/_+/g, '_')
.replace(/_$/, '');
return `${method}_${cleanPath}`;
}
createToolDefinition(toolName, method, pathStr, operation) {
const tool = {
name: toolName,
description: operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`,
inputSchema: {
type: 'object',
properties: {},
required: []
}
};
// Add path parameters
this.addPathParameters(tool, pathStr, operation);
// Add query parameters
this.addQueryParameters(tool, operation);
// Add header parameters
this.addHeaderParameters(tool, operation);
// Add request body
this.addRequestBody(tool, operation);
return {
...tool,
method: method.toLowerCase(),
path: pathStr,
operation
};
}
addPathParameters(tool, pathStr, operation) {
const pathParams = pathStr.match(/{([^}]+)}/g) || [];
pathParams.forEach(param => {
const paramName = param.slice(1, -1); // Remove { }
const paramDef = this.findParameterDefinition(operation, paramName, 'path');
tool.inputSchema.properties[paramName] = {
type: 'string',
description: paramDef?.description || `Path parameter: ${paramName}`
};
tool.inputSchema.required.push(paramName);
});
}
addQueryParameters(tool, operation) {
const queryParams = this.getParametersByLocation(operation, 'query');
queryParams.forEach(param => {
const schema = this.resolveSchema(param.schema || { type: 'string' });
tool.inputSchema.properties[param.name] = {
...schema,
description: param.description || `Query parameter: ${param.name}`
};
if (param.required) {
tool.inputSchema.required.push(param.name);
}
});
}
addHeaderParameters(tool, operation) {
const headerParams = this.getParametersByLocation(operation, 'header');
headerParams.forEach(param => {
if (param.name.toLowerCase() === 'authorization') return; // Skip auth headers
const schema = this.resolveSchema(param.schema || { type: 'string' });
tool.inputSchema.properties[`header_${param.name}`] = {
...schema,
description: param.description || `Header parameter: ${param.name}`
};
if (param.required) {
tool.inputSchema.required.push(`header_${param.name}`);
}
});
}
addRequestBody(tool, operation) {
if (!operation.requestBody) return;
const requestBody = this.resolveReference(operation.requestBody);
const jsonContent = requestBody.content?.['application/json'];
if (jsonContent?.schema) {
const schema = this.resolveSchema(jsonContent.schema);
if (schema.type === 'object') {
// Merge request body properties into tool schema
Object.entries(schema.properties || {}).forEach(([propName, propSchema]) => {
tool.inputSchema.properties[propName] = propSchema;
});
// Add required properties
if (schema.required) {
tool.inputSchema.required.push(...schema.required);
}
} else {
// Non-object request body
tool.inputSchema.properties.requestBody = {
...schema,
description: requestBody.description || 'Request body'
};
if (requestBody.required !== false) {
tool.inputSchema.required.push('requestBody');
}
}
}
}
getParametersByLocation(operation, location) {
const params = operation.parameters || [];
return params
.map(param => this.resolveReference(param))
.filter(param => param.in === location);
}
findParameterDefinition(operation, name, location) {
const params = operation.parameters || [];
return params
.map(param => this.resolveReference(param))
.find(param => param.name === name && param.in === location);
}
resolveReference(obj) {
if (!obj || !obj.$ref) return obj;
const refPath = obj.$ref.replace('#/', '').split('/');
let resolved = this.spec;
for (const segment of refPath) {
resolved = resolved[segment];
if (!resolved) {
throw new Error(`Could not resolve reference: ${obj.$ref}`);
}
}
return resolved;
}
resolveSchema(schema) {
if (!schema) return { type: 'string' };
const resolved = this.resolveReference(schema);
// Handle array schemas
if (resolved.type === 'array' && resolved.items) {
return {
...resolved,
items: this.resolveSchema(resolved.items)
};
}
// Handle object schemas
if (resolved.type === 'object' && resolved.properties) {
const properties = {};
for (const [propName, propSchema] of Object.entries(resolved.properties)) {
properties[propName] = this.resolveSchema(propSchema);
}
return {
...resolved,
properties
};
}
return resolved;
}
getTools() {
return Array.from(this.tools.values());
}
getTool(name) {
return this.tools.get(name);
}
getBaseUrl() {
if (this.spec.servers && this.spec.servers.length > 0) {
return this.spec.servers[0].url;
}
return '';
}
getSecuritySchemes() {
return this.spec.components?.securitySchemes || {};
}
hasGlobalSecurity() {
return this.spec.security && this.spec.security.length > 0;
}
getGlobalSecurity() {
return this.spec.security || [];
}
}