openapi-directory-mcp
Version:
Model Context Protocol server for accessing enhanced triple-source OpenAPI directory (APIs.guru + additional APIs + custom imports)
291 lines • 9.53 kB
JavaScript
/**
* OpenAPI specification processor for custom specs
* Handles YAML-to-JSON conversion, validation, and normalization
*/
/* eslint-disable no-console */
import { readFileSync } from "fs";
import { resolve } from "path";
import { homedir } from "os";
import * as yaml from "js-yaml";
import SwaggerParser from "@apidevtools/swagger-parser";
import axios from "axios";
import { SecurityScanner } from "./security-scanner.js";
export class SpecProcessor {
constructor() {
this.securityScanner = new SecurityScanner();
}
/**
* Process an OpenAPI spec from file or URL
*/
async processSpec(source, skipSecurity = false) {
let content;
let originalFormat;
// Fetch content
if (this.isUrl(source)) {
content = await this.fetchFromUrl(source);
}
else {
content = this.readFromFile(source);
}
// Detect format
originalFormat = this.detectFormat(content);
// Parse to object
const specObject = this.parseContent(content, originalFormat);
// Validate OpenAPI spec
const validation = await this.validateOpenAPISpec(specObject);
if (!validation.valid) {
throw new Error(`Invalid OpenAPI specification: ${validation.errors.join(", ")}`);
}
// Convert to ApiGuruAPI format
const apiGuruSpec = this.convertToApiGuruFormat(specObject);
// Run security scan
const securityScan = skipSecurity
? this.createEmptySecurityScan()
: await this.securityScanner.scanSpec(specObject);
// Check if security issues block import
if (securityScan.blocked) {
throw new Error(`Import blocked by critical security issues:\n${this.securityScanner.generateReport(securityScan)}`);
}
// Extract metadata - use byte length instead of string length
const metadata = this.extractMetadata(specObject, Buffer.byteLength(content, "utf8"));
return {
spec: apiGuruSpec,
originalFormat,
securityScan,
metadata,
};
}
/**
* Check if source is a URL
*/
isUrl(source) {
return source.startsWith("http://") || source.startsWith("https://");
}
/**
* Fetch spec from URL
*/
async fetchFromUrl(url) {
try {
const response = await axios.get(url, {
timeout: 30000,
headers: {
Accept: "application/json, application/yaml, text/yaml, text/plain",
"User-Agent": "openapi-directory-mcp/1.2.0",
},
maxContentLength: 10 * 1024 * 1024, // 10MB limit
});
if (typeof response.data === "string") {
return response.data;
}
else {
return JSON.stringify(response.data, null, 2);
}
}
catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Failed to fetch spec from URL: ${error.message}`);
}
throw new Error(`Unexpected error fetching spec: ${error}`);
}
}
/**
* Read spec from file (with tilde expansion)
*/
readFromFile(filePath) {
try {
// Expand tilde to home directory
const expandedPath = filePath.startsWith("~/")
? resolve(homedir(), filePath.slice(2))
: filePath;
return readFileSync(expandedPath, "utf-8");
}
catch (error) {
throw new Error(`Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Detect if content is YAML or JSON
*/
detectFormat(content) {
const trimmed = content.trim();
// Check if it starts with JSON indicators
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
return "json";
}
// Check for YAML indicators
if (trimmed.includes("openapi:") ||
trimmed.includes("swagger:") ||
trimmed.match(/^[a-zA-Z_][a-zA-Z0-9_]*:\s*$/m)) {
return "yaml";
}
// Try to parse as JSON first
try {
JSON.parse(content);
return "json";
}
catch {
// Assume YAML if JSON parse fails
return "yaml";
}
}
/**
* Parse content based on format
*/
parseContent(content, format) {
try {
if (format === "json") {
return JSON.parse(content);
}
else {
return yaml.load(content, {
onWarning: (warning) => {
console.warn(`YAML warning: ${warning.message}`);
},
// Security: Don't allow arbitrary code execution
schema: yaml.CORE_SCHEMA,
});
}
}
catch (error) {
throw new Error(`Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Validate OpenAPI specification
*/
async validateOpenAPISpec(spec) {
const errors = [];
const warnings = [];
try {
// Check for OpenAPI version
const version = spec.openapi || spec.swagger;
if (!version) {
errors.push("Missing OpenAPI/Swagger version field");
return { valid: false, errors, warnings };
}
// Use swagger-parser for validation
await SwaggerParser.validate(spec);
// Additional custom validations
if (!spec.info) {
errors.push('Missing required "info" section');
}
else {
if (!spec.info.title) {
errors.push('Missing required "info.title" field');
}
if (!spec.info.version) {
warnings.push('Missing "info.version" field');
}
}
if (!spec.paths && !spec.components) {
warnings.push("No paths or components defined - this might be an incomplete spec");
}
return {
valid: errors.length === 0,
version,
errors,
warnings,
};
}
catch (error) {
const message = error instanceof Error ? error.message : "Unknown validation error";
errors.push(message);
return {
valid: false,
errors,
warnings,
};
}
}
/**
* Convert OpenAPI spec to ApiGuruAPI format
*/
convertToApiGuruFormat(spec) {
const info = spec.info || {};
const added = new Date().toISOString();
const version = info.version || "1.0.0";
// Determine file extensions based on original format
const basePath = "custom/spec";
const apiVersion = {
added,
updated: added,
info: {
...info,
"x-providerName": "custom",
"x-apisguru-categories": info["x-apisguru-categories"] || ["custom"],
},
swaggerUrl: `${basePath}.json`,
swaggerYamlUrl: `${basePath}.yaml`,
openapiVer: spec.openapi || spec.swagger || "3.0.0",
link: info.contact?.url || "",
externalDocs: spec.externalDocs,
};
return {
added,
preferred: version,
versions: {
[version]: apiVersion,
},
};
}
/**
* Extract metadata from spec
*/
extractMetadata(spec, fileSize) {
const info = spec.info || {};
return {
title: info.title || "Untitled API",
description: info.description || "",
version: info.version || "1.0.0",
fileSize,
};
}
/**
* Create empty security scan for when security is skipped
*/
createEmptySecurityScan() {
return {
scannedAt: new Date().toISOString(),
issues: [],
summary: { critical: 0, high: 0, medium: 0, low: 0 },
blocked: false,
};
}
/**
* Get security scanner instance for external use
*/
getSecurityScanner() {
return this.securityScanner;
}
/**
* Validate a spec without full processing (for quick checks)
*/
async quickValidate(source) {
try {
let content;
if (this.isUrl(source)) {
content = await this.fetchFromUrl(source);
}
else {
content = this.readFromFile(source);
}
const format = this.detectFormat(content);
const spec = this.parseContent(content, format);
return await this.validateOpenAPISpec(spec);
}
catch (error) {
return {
valid: false,
errors: [error instanceof Error ? error.message : "Unknown error"],
warnings: [],
};
}
}
/**
* Convert spec to normalized JSON string
*/
toNormalizedJSON(spec) {
return JSON.stringify(spec, null, 2);
}
}
//# sourceMappingURL=spec-processor.js.map