@mcp-apps/api-tools-mcp-server
Version:
MCP server for interacting with APIs and web services
286 lines • 13.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenApiService = void 0;
const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
const api_service_1 = require("./api-service");
class OpenApiService {
/**
* Checks if an object is a valid OpenAPI schema
*/
static isValidOpenApiSchema(obj) {
// Must be an object
if (!obj || typeof obj !== 'object')
return false;
// OpenAPI 3.x has an "openapi" property
if (obj.openapi && typeof obj.openapi === 'string' && obj.paths && typeof obj.paths === 'object') {
return true;
}
// Swagger 2.x has a "swagger" property
if (obj.swagger && typeof obj.swagger === 'string' && obj.paths && typeof obj.paths === 'object') {
return true;
}
// Check for essential parts that should exist in both versions
if (obj.info && typeof obj.info === 'object' && obj.paths && typeof obj.paths === 'object') {
return true;
}
return false;
}
/**
* Try to find a valid schema by appending common schema paths to the base URL
*/
static async tryDefaultSchemaEndpoints(baseUrl, authType, authConfig) {
// Remove trailing slash if present
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
// If URL path looks like a directory (not ending in .json or containing a file extension),
// we should try looking for schemas both in this directory and the parent directory
const urlEndsWithDirectory = !normalizedBaseUrl.endsWith('.json') &&
!normalizedBaseUrl.split('/').pop()?.includes('.');
// Create an array of base URLs to try
const baseUrls = [normalizedBaseUrl];
// If it looks like a directory path, also try the parent directory
if (urlEndsWithDirectory) {
const parentUrl = normalizedBaseUrl.substring(0, normalizedBaseUrl.lastIndexOf('/'));
if (parentUrl && parentUrl.startsWith('http')) {
baseUrls.push(parentUrl);
}
}
const attemptedUrls = [];
const errors = {};
// Try each base URL with each common schema endpoint path
for (const baseUrl of baseUrls) {
for (const endpoint of this.defaultSchemaEndpoints) {
try {
const schemaUrl = `${baseUrl}${endpoint}`;
console.log(`Auto-discovery: Trying schema URL: ${schemaUrl}`);
attemptedUrls.push(schemaUrl);
const response = await api_service_1.ApiService.callApi({
endpoint: schemaUrl,
method: 'GET',
authType,
authConfig
});
if (response.success) {
// First check if response.data is valid object/JSON
if (response.data && typeof response.data === 'object') {
if (this.isValidOpenApiSchema(response.data)) {
console.log(`Auto-discovery: Valid OpenAPI schema found at: ${schemaUrl}`);
try {
const parsedSchema = await swagger_parser_1.default.dereference(response.data);
return await this.parseSchema(parsedSchema);
}
catch (parseError) {
console.log(`Auto-discovery: Using bundled parsing due to possible circular references at ${schemaUrl}: ${parseError.message}`);
// Fall back to bundled parsing
const bundled = await swagger_parser_1.default.bundle(response.data);
return await this.parseSchema(bundled);
}
}
else {
errors[schemaUrl] = 'Response received but not a valid OpenAPI schema';
}
}
else {
// Handle non-JSON responses
errors[schemaUrl] = 'Response received but not in valid JSON format';
}
}
else {
errors[schemaUrl] = `HTTP ${response.status}: ${response.error || 'Unknown error'}`;
}
}
catch (error) {
// Log error but continue trying other endpoints
errors[`${baseUrl}${endpoint}`] = error.message || 'Unknown error';
continue;
}
}
}
// If we get here, no valid schema was found at any of the default locations
console.error('Auto-discovery failed. Attempted URLs:', attemptedUrls);
console.error('Auto-discovery errors:', JSON.stringify(errors, null, 2));
// Build a more helpful error message
let errorMsg = `No valid OpenAPI schema found at ${baseUrl} or at any standard schema paths.`;
// Display the attempted paths in a more readable format - just show the path parts
const pathsAttempted = attemptedUrls.map(url => {
// Extract just the path part after the domain
const urlObj = new URL(url);
return urlObj.pathname;
});
errorMsg += `\nTried the following paths: ${pathsAttempted.join(', ')}`;
throw new Error(errorMsg);
}
static async fetchSchema(url, authType, authConfig) {
// Check cache first
if (this.cache[url] && (Date.now() - this.cache[url].timestamp < this.CACHE_TTL_MS)) {
console.log(`Using cached OpenAPI schema for ${url}`);
return this.cache[url].schema;
}
// Try to fetch and validate the schema from the primary URL
try {
// Fetch the schema from the URL
const response = await api_service_1.ApiService.callApi({
endpoint: url,
method: 'GET',
authType,
authConfig
});
if (!response.success) {
throw new Error(`Failed to fetch OpenAPI schema: ${response.error}`);
}
const schema = response.data;
// Verify this is actually an OpenAPI schema by checking for required properties
if (this.isValidOpenApiSchema(schema)) {
try {
// Parse the schema - use dereference instead of validate to better handle circular references
console.log(`Valid OpenAPI schema found at ${url}`);
const parsedSchema = await swagger_parser_1.default.dereference(schema);
const result = await this.parseSchema(parsedSchema);
// Store in cache with URL as key for direct access next time
this.cache[url] = {
schema: result,
timestamp: Date.now()
};
return result;
}
catch (parseError) {
// Fall back to a more lenient parse if circular references are causing issues
console.warn('Warning: Using bundled parsing due to possible circular references:', parseError.message);
const bundled = await swagger_parser_1.default.bundle(schema);
const result = await this.parseSchema(bundled);
// Store in cache
this.cache[url] = {
schema: result,
timestamp: Date.now()
};
return result;
}
}
else {
// This URL doesn't contain a valid schema, try default endpoints
console.log(`URL ${url} doesn't contain a valid OpenAPI schema, trying auto-discovery`);
const result = await this.tryDefaultSchemaEndpoints(url, authType, authConfig);
// Store the result in cache with the original URL as key
this.cache[url] = {
schema: result,
timestamp: Date.now()
};
return result;
}
}
catch (error) {
// If the direct fetch fails, try the default endpoints before giving up
try {
console.log(`Failed to fetch from ${url} directly, trying auto-discovery instead`);
const result = await this.tryDefaultSchemaEndpoints(url, authType, authConfig);
// Store the result in cache with the original URL as key
this.cache[url] = {
schema: result,
timestamp: Date.now()
};
return result;
}
catch (fallbackError) {
console.error('Error fetching OpenAPI schema:', error);
console.error('Auto-discovery fallback error:', fallbackError);
throw new Error(`Failed to fetch or parse OpenAPI schema: ${error.message || 'Unknown error'}. Auto-discovery also failed: ${fallbackError.message || 'Unknown error'}`);
}
}
}
/**
* Gets all endpoints from an OpenAPI schema
*/
static async getEndpoints(url, authType, authConfig) {
const schema = await this.fetchSchema(url, authType, authConfig);
return schema.endpoints;
}
/**
* Gets detailed information about operations for a specific endpoint
*/
static async getOperations(url, path, authType, authConfig) {
const schema = await this.fetchSchema(url, authType, authConfig);
const endpoint = schema.endpoints.find(e => e.path === path);
if (!endpoint) {
throw new Error(`Endpoint not found: ${path}`);
}
return endpoint.operations;
}
/**
* Parses an OpenAPI schema into a standardized format
*/
static async parseSchema(schema) {
const parsedSchema = {
info: {
title: schema.info?.title,
version: schema.info?.version,
description: schema.info?.description
},
endpoints: [],
definitions: schema.definitions || schema.components?.schemas,
basePath: schema.basePath,
host: schema.host
};
// Parse paths/endpoints
const paths = schema.paths || {};
for (const path in paths) {
const pathItem = paths[path];
const operations = [];
// HTTP methods (get, post, put, delete, etc.)
const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
for (const method of httpMethods) {
if (pathItem[method]) {
const operation = pathItem[method];
operations.push({
operationId: operation.operationId,
summary: operation.summary,
description: operation.description,
parameters: operation.parameters,
requestBody: operation.requestBody,
responses: operation.responses,
tags: operation.tags,
method: method.toUpperCase()
});
}
}
if (operations.length > 0) {
parsedSchema.endpoints.push({
path,
operations
});
}
}
// Store in cache with title as key
const cacheKey = schema.info?.title || 'schema';
this.cache[cacheKey] = {
schema: parsedSchema,
timestamp: Date.now()
};
return parsedSchema;
}
}
exports.OpenApiService = OpenApiService;
OpenApiService.cache = {};
OpenApiService.CACHE_TTL_MS = 3600000; // 1 hour cache TTL
// Default popular OpenAPI schema URLs to try if the provided URL doesn't contain schema
OpenApiService.defaultSchemaEndpoints = [
'/v2/swagger.json', // Swagger Petstore uses this path
'/swagger.json',
'/api-docs.json',
'/openapi.json',
'/swagger/v1/swagger.json',
'/api/swagger.json',
'/v1/api-docs',
'/v2/api-docs',
'/swagger',
'/swagger/docs/v1',
'/openapi',
'/api/openapi.json',
'/api-docs',
'/docs/api.json',
'/docs/openapi.json',
'/api/v1/swagger.json',
'/api/v1/openapi.json'
];
//# sourceMappingURL=openapi-service.js.map