UNPKG

openapi-directory-mcp

Version:

Model Context Protocol server for accessing enhanced triple-source OpenAPI directory (APIs.guru + additional APIs + custom imports)

803 lines 35 kB
import axios from "axios"; import { calculateProviderStats, CACHE_KEYS, CACHE_TTL, } from "../utils/version-data.js"; export class ApiClient { constructor(baseURL, cacheManager) { this.http = axios.create({ baseURL, timeout: 30000, headers: { Accept: "application/json", "User-Agent": "openapi-directory-mcp/0.1.0", }, }); this.cache = cacheManager; } async fetchWithCache(key, fetchFn, ttl) { const cached = this.cache.get(key); if (cached) { return cached; } const result = await fetchFn(); this.cache.set(key, result, ttl); return result; } /** * List all providers in the directory */ async getProviders() { return this.fetchWithCache(CACHE_KEYS.PROVIDERS, async () => { const response = await this.http.get("/providers.json"); return response.data; }); } /** * List all APIs for a particular provider */ async getProvider(provider) { return this.fetchWithCache(`${CACHE_KEYS.PROVIDER_PREFIX}${provider}`, async () => { const response = await this.http.get(`/${provider}.json`); return response.data.apis || {}; }); } /** * List all serviceNames for a particular provider */ async getServices(provider) { return this.fetchWithCache(`services:${provider}`, async () => { const response = await this.http.get(`/${provider}/services.json`); return response.data; }); } /** * Retrieve one version of a particular API (without service) */ async getAPI(provider, api) { return this.fetchWithCache(`api:${provider}:${api}`, async () => { const response = await this.http.get(`/specs/${provider}/${api}.json`); return response.data; }); } /** * Retrieve one version of a particular API with a serviceName */ async getServiceAPI(provider, service, api) { return this.fetchWithCache(`api:${provider}:${service}:${api}`, async () => { const response = await this.http.get(`/specs/${provider}:${service}/${api}.json`); return response.data; }); } /** * List all APIs in the directory */ async listAPIs() { return this.fetchWithCache("all_apis", async () => { const response = await this.http.get("/list.json"); return response.data; }); } /** * Get paginated APIs with minimal data for context efficiency */ async getPaginatedAPIs(page = 1, limit = 50) { const cacheKey = `paginated_apis:${page}:${limit}`; return this.fetchWithCache(cacheKey, async () => { const allAPIs = await this.listAPIs(); const apiEntries = Object.entries(allAPIs); // Calculate pagination const total_results = apiEntries.length; const total_pages = Math.ceil(total_results / limit); const offset = (page - 1) * limit; // Get page slice and transform to minimal data const pageEntries = apiEntries.slice(offset, offset + limit); const results = pageEntries.map(([id, api]) => { // Get preferred version info const preferredVersion = api.versions[api.preferred]; return { id, title: preferredVersion?.info.title || "Untitled API", description: (preferredVersion?.info.description || "").substring(0, 200) + (preferredVersion?.info.description && preferredVersion.info.description.length > 200 ? "..." : ""), provider: preferredVersion?.info["x-providerName"] || id.split(":")[0] || "Unknown", preferred: api.preferred, categories: preferredVersion?.info["x-apisguru-categories"] || [], }; }); return { results, pagination: { page, limit, total_results, total_pages, has_next: page < total_pages, has_previous: page > 1, }, }; }, 300000); // Cache for 5 minutes } /** * Get API directory summary for overview */ async getAPISummary() { return this.fetchWithCache("api_summary", async () => { const allAPIs = await this.listAPIs(); const apiEntries = Object.entries(allAPIs); // Calculate summary statistics const providers = new Set(); const categories = new Set(); const apisWithScore = apiEntries.map(([id, api]) => { const preferredVersion = api.versions[api.preferred]; const provider = preferredVersion?.info["x-providerName"] || id.split(":")[0] || "Unknown"; providers.add(provider); // Add categories (preferredVersion?.info["x-apisguru-categories"] || []).forEach((cat) => categories.add(cat)); // Calculate popularity score const versionCount = Object.keys(api.versions).length; const popularity = preferredVersion?.info["x-apisguru-popularity"] || 0; const score = versionCount * 10 + popularity; const latestUpdate = Math.max(...Object.values(api.versions).map((v) => new Date(v.updated).getTime())); return { id, title: preferredVersion?.info.title || "Untitled API", provider, score, updated: latestUpdate, updatedString: new Date(latestUpdate).toISOString().split("T")[0], }; }); // Get top 10 popular APIs const popular_apis = apisWithScore .sort((a, b) => b.score - a.score) .slice(0, 10) .map(({ id, title, provider }) => ({ id, title, provider })); // Get 10 recently updated APIs const recent_updates = apisWithScore .sort((a, b) => b.updated - a.updated) .slice(0, 10) .map(({ id, title, updatedString }) => ({ id, title, updated: updatedString || "Unknown", })); return { total_apis: apiEntries.length, total_providers: providers.size, categories: Array.from(categories).sort(), popular_apis, recent_updates, }; }, 600000); // Cache for 10 minutes } /** * Get basic metrics for the directory */ async getMetrics() { return this.fetchWithCache("metrics", async () => { const response = await this.http.get("/metrics.json"); return response.data; }); } /** * Search APIs by query string with pagination and minimal data */ async searchAPIs(query, provider, page = 1, limit = 20) { // Ensure limit is within bounds limit = Math.min(Math.max(limit, 1), 50); const cacheKey = `search:${query}:${provider || "all"}:${page}:${limit}`; return this.fetchWithCache(cacheKey, async () => { const allAPIs = await this.listAPIs(); const queryLower = query.toLowerCase(); // First, filter APIs based on search criteria const matchingAPIs = []; for (const [apiId, api] of Object.entries(allAPIs)) { // If provider filter is specified, check if API matches if (provider && !apiId.includes(provider)) { continue; } // Search in API ID, title, description, and provider name const matches = apiId.toLowerCase().includes(queryLower) || Object.values(api.versions).some((version) => version.info.title?.toLowerCase().includes(queryLower) || version.info.description?.toLowerCase().includes(queryLower) || version.info["x-providerName"] ?.toLowerCase() .includes(queryLower)); if (matches) { matchingAPIs.push([apiId, api]); } } // Sort results by relevance matchingAPIs.sort(([idA, apiA], [idB, apiB]) => { const versionA = apiA.versions[apiA.preferred]; const versionB = apiB.versions[apiB.preferred]; const providerA = versionA?.info["x-providerName"]?.toLowerCase() || ""; const providerB = versionB?.info["x-providerName"]?.toLowerCase() || ""; const titleA = versionA?.info.title?.toLowerCase() || ""; const titleB = versionB?.info.title?.toLowerCase() || ""; // Score each match based on where the query appears const scoreMatch = (id, provider, title) => { // Exact provider match gets highest score if (provider === queryLower) return 100; // Provider contains query gets high score if (provider.includes(queryLower)) return 80; // ID starts with query gets good score if (id.toLowerCase().startsWith(queryLower)) return 60; // ID contains query gets moderate score if (id.toLowerCase().includes(queryLower)) return 40; // Title contains query gets lower score if (title.includes(queryLower)) return 20; // Description match gets lowest score return 10; }; const scoreA = scoreMatch(idA, providerA, titleA); const scoreB = scoreMatch(idB, providerB, titleB); // Sort by score (descending), then by version date (descending), then alphabetically by ID if (scoreA !== scoreB) { return scoreB - scoreA; } // For same relevance score, prioritize newer versions const dateA = new Date(versionA?.updated || "1970-01-01").getTime(); const dateB = new Date(versionB?.updated || "1970-01-01").getTime(); if (dateA !== dateB) { return dateB - dateA; // Newer dates first } return idA.localeCompare(idB); }); // Calculate pagination const total_results = matchingAPIs.length; const total_pages = Math.ceil(total_results / limit); const offset = (page - 1) * limit; // Get page slice and transform to minimal data const pageEntries = matchingAPIs.slice(offset, offset + limit); const results = pageEntries.map(([id, api]) => { // Get preferred version info const preferredVersion = api.versions[api.preferred]; return { id, title: preferredVersion?.info.title || "Untitled API", description: (preferredVersion?.info.description || "").substring(0, 200) + (preferredVersion?.info.description && preferredVersion.info.description.length > 200 ? "..." : ""), provider: preferredVersion?.info["x-providerName"] || id.split(":")[0] || "Unknown", preferred: api.preferred, categories: preferredVersion?.info["x-apisguru-categories"] || [], }; }); return { results, pagination: { page, limit, total_results, total_pages, has_next: page < total_pages, has_previous: page > 1, }, }; }, 300000); // Cache search results for 5 minutes } /** * Get basic summary information for a specific API */ async getAPISummaryById(apiId) { return this.fetchWithCache(`api_summary:${apiId}`, async () => { const allAPIs = await this.listAPIs(); const api = allAPIs[apiId]; if (!api) { throw new Error(`API not found: ${apiId}`); } const preferredVersion = api.versions[api.preferred]; if (!preferredVersion) { throw new Error(`Preferred version not found for API: ${apiId}`); } const info = preferredVersion.info; const result = { id: apiId, title: info.title || "Untitled API", description: info.description || "No description available", provider: info["x-providerName"] || apiId.split(":")[0] || "Unknown", versions: Object.keys(api.versions), preferred_version: api.preferred, base_url: preferredVersion.swaggerUrl .replace("/swagger.json", "") .replace("/swagger.yaml", ""), categories: info["x-apisguru-categories"] || [], authentication: { type: "See OpenAPI spec", description: "Authentication details available in the full specification", }, updated: preferredVersion.updated, added: preferredVersion.added, }; // Only include optional properties if they exist if (info.contact) { result.contact = info.contact; } if (info.license) { result.license = info.license; } if (preferredVersion.link || info.contact?.url) { result.documentation_url = preferredVersion.link || info.contact?.url; } if (info.contact?.url) { result.homepage_url = info.contact.url; } return result; }, 600000); // Cache for 10 minutes } /** * Get paginated endpoints for a specific API with minimal information */ async getAPIEndpoints(apiId, page = 1, limit = 30, tag) { // Ensure limit is within bounds limit = Math.min(Math.max(limit, 1), 100); const cacheKey = `endpoints:${apiId}:${page}:${limit}:${tag || "all"}`; return this.fetchWithCache(cacheKey, async () => { // First get the API info to get the spec URL const allAPIs = await this.listAPIs(); const api = allAPIs[apiId]; if (!api) { throw new Error(`API not found: ${apiId}`); } const preferredVersion = api.versions[api.preferred]; if (!preferredVersion) { throw new Error(`Preferred version not found for API: ${apiId}`); } // Fetch the OpenAPI spec const spec = await this.getOpenAPISpec(preferredVersion.swaggerUrl); // Extract all endpoints const allEndpoints = []; const availableTags = new Set(); if (spec.paths) { for (const [path, pathItem] of Object.entries(spec.paths)) { if (typeof pathItem !== "object" || pathItem === null) { continue; } const methods = [ "get", "post", "put", "patch", "delete", "head", "options", "trace", ]; for (const method of methods) { const operation = pathItem[method]; if (operation && typeof operation === "object") { const endpointTags = operation.tags || []; // Collect all tags endpointTags.forEach((t) => availableTags.add(t)); const endpoint = { method: method.toUpperCase(), path, summary: operation.summary, operationId: operation.operationId, tags: endpointTags, deprecated: operation.deprecated || false, }; allEndpoints.push(endpoint); } } } } // Filter by tag if specified let filteredEndpoints = allEndpoints; if (tag) { filteredEndpoints = allEndpoints.filter((endpoint) => endpoint.tags.some((t) => t.toLowerCase().includes(tag.toLowerCase()))); } // Calculate pagination const total_results = filteredEndpoints.length; const total_pages = Math.ceil(total_results / limit); const offset = (page - 1) * limit; // Get page slice const results = filteredEndpoints.slice(offset, offset + limit); return { results, pagination: { page, limit, total_results, total_pages, has_next: page < total_pages, has_previous: page > 1, }, available_tags: Array.from(availableTags).sort(), }; }, 600000); // Cache for 10 minutes } /** * Get detailed information about a specific API endpoint */ async getEndpointDetails(apiId, method, path) { const cacheKey = `endpoint_details:${apiId}:${method.toLowerCase()}:${path}`; return this.fetchWithCache(cacheKey, async () => { // First get the API info to get the spec URL const allAPIs = await this.listAPIs(); const api = allAPIs[apiId]; if (!api) { throw new Error(`API not found: ${apiId}`); } const preferredVersion = api.versions[api.preferred]; if (!preferredVersion) { throw new Error(`Preferred version not found for API: ${apiId}`); } // Fetch the OpenAPI spec const spec = await this.getOpenAPISpec(preferredVersion.swaggerUrl); // Find the specific endpoint const pathItem = spec.paths?.[path]; if (!pathItem) { throw new Error(`Path not found: ${path}`); } const operation = pathItem[method.toLowerCase()]; if (!operation) { throw new Error(`Method ${method} not found for path ${path}`); } // Extract parameters const parameters = []; // Combine path-level and operation-level parameters const allParams = [ ...(pathItem.parameters || []), ...(operation.parameters || []), ]; for (const param of allParams) { if (param && typeof param === "object") { parameters.push({ name: param.name || "unnamed", in: param.in || "query", required: param.required || false, type: param.type || param.schema?.type || "string", description: param.description, }); } } // Extract responses const responses = []; if (operation.responses) { for (const [code, response] of Object.entries(operation.responses)) { if (response && typeof response === "object") { const contentTypes = []; if (response.content) { contentTypes.push(...Object.keys(response.content)); } responses.push({ code, description: response.description || "No description", content_types: contentTypes, }); } } } // Extract content types const consumes = []; const produces = []; if (operation.requestBody?.content) { consumes.push(...Object.keys(operation.requestBody.content)); } // From responses for (const response of responses) { produces.push(...response.content_types); } // Extract security requirements const security = []; const securityReqs = operation.security || spec.security || []; for (const secReq of securityReqs) { if (secReq && typeof secReq === "object") { for (const [secName, scopes] of Object.entries(secReq)) { const secScheme = spec.components?.securitySchemes?.[secName]; if (secScheme) { const secItem = { type: secScheme.type || "unknown", }; if (Array.isArray(scopes) && scopes.length > 0) { secItem.scopes = scopes; } security.push(secItem); } } } } const result = { method: method.toUpperCase(), path, tags: operation.tags || [], deprecated: operation.deprecated || false, parameters, responses, consumes: [...new Set(consumes)], produces: [...new Set(produces)], }; // Only add optional properties if they exist if (operation.summary) result.summary = operation.summary; if (operation.description) result.description = operation.description; if (operation.operationId) result.operationId = operation.operationId; if (security.length > 0) result.security = security; return result; }, 600000); // Cache for 10 minutes } /** * Get request and response schemas for a specific API endpoint */ async getEndpointSchema(apiId, method, path) { const cacheKey = `endpoint_schema:${apiId}:${method.toLowerCase()}:${path}`; return this.fetchWithCache(cacheKey, async () => { // First get the API info to get the spec URL const allAPIs = await this.listAPIs(); const api = allAPIs[apiId]; if (!api) { throw new Error(`API not found: ${apiId}`); } const preferredVersion = api.versions[api.preferred]; if (!preferredVersion) { throw new Error(`Preferred version not found for API: ${apiId}`); } // Fetch the OpenAPI spec const spec = await this.getOpenAPISpec(preferredVersion.swaggerUrl); // Find the specific endpoint const pathItem = spec.paths?.[path]; if (!pathItem) { throw new Error(`Path not found: ${path}`); } const operation = pathItem[method.toLowerCase()]; if (!operation) { throw new Error(`Method ${method} not found for path ${path}`); } // Extract request body schema let request_body; if (operation.requestBody) { const content = operation.requestBody.content; if (content) { const contentType = Object.keys(content)[0]; // Get first content type if (contentType && content[contentType]?.schema) { request_body = { content_type: contentType, schema: content[contentType].schema, required: operation.requestBody.required || false, }; } } } // Extract parameter schemas const parameters = []; const allParams = [ ...(pathItem.parameters || []), ...(operation.parameters || []), ]; for (const param of allParams) { if (param && typeof param === "object") { parameters.push({ name: param.name || "unnamed", in: param.in || "query", required: param.required || false, schema: param.schema || { type: param.type || "string" }, }); } } // Extract response schemas const responses = []; if (operation.responses) { for (const [code, response] of Object.entries(operation.responses)) { if (response && typeof response === "object") { const content = response.content; if (content) { for (const [contentType, contentData] of Object.entries(content)) { if (contentData?.schema) { responses.push({ code, content_type: contentType, schema: contentData.schema, }); } } } } } } const result = { method: method.toUpperCase(), path, parameters, responses, }; // Only add request_body if it exists if (request_body) { result.request_body = request_body; } return result; }, 600000); // Cache for 10 minutes } /** * Get request and response examples for a specific API endpoint */ async getEndpointExamples(apiId, method, path) { const cacheKey = `endpoint_examples:${apiId}:${method.toLowerCase()}:${path}`; return this.fetchWithCache(cacheKey, async () => { // First get the API info to get the spec URL const allAPIs = await this.listAPIs(); const api = allAPIs[apiId]; if (!api) { throw new Error(`API not found: ${apiId}`); } const preferredVersion = api.versions[api.preferred]; if (!preferredVersion) { throw new Error(`Preferred version not found for API: ${apiId}`); } // Fetch the OpenAPI spec const spec = await this.getOpenAPISpec(preferredVersion.swaggerUrl); // Find the specific endpoint const pathItem = spec.paths?.[path]; if (!pathItem) { throw new Error(`Path not found: ${path}`); } const operation = pathItem[method.toLowerCase()]; if (!operation) { throw new Error(`Method ${method} not found for path ${path}`); } // Extract request examples const request_examples = []; if (operation.requestBody?.content) { for (const [contentType, contentData] of Object.entries(operation.requestBody.content)) { const data = contentData; if (data.example) { request_examples.push({ content_type: contentType, example: data.example, description: data.description, }); } else if (data.examples) { for (const [exampleName, exampleData] of Object.entries(data.examples)) { const example = exampleData; request_examples.push({ content_type: contentType, example: example.value || example, description: example.description || exampleName, }); } } } } // Extract parameter examples const parameter_examples = []; const allParams = [ ...(pathItem.parameters || []), ...(operation.parameters || []), ]; for (const param of allParams) { if (param && typeof param === "object" && param.example !== undefined) { parameter_examples.push({ name: param.name || "unnamed", example: param.example, }); } } // Extract response examples const response_examples = []; if (operation.responses) { for (const [code, response] of Object.entries(operation.responses)) { if (response && typeof response === "object") { const content = response.content; if (content) { for (const [contentType, contentData] of Object.entries(content)) { const data = contentData; if (data.example) { response_examples.push({ code, content_type: contentType, example: data.example, description: response.description, }); } else if (data.examples) { for (const [exampleName, exampleData] of Object.entries(data.examples)) { const example = exampleData; response_examples.push({ code, content_type: contentType, example: example.value || example, description: example.description || exampleName, }); } } } } } } } return { method: method.toUpperCase(), path, request_examples, response_examples, parameter_examples, }; }, 600000); // Cache for 10 minutes } /** * Get OpenAPI specification for a specific API version */ async getOpenAPISpec(url) { return this.fetchWithCache(`spec:${url}`, async () => { const response = await axios.get(url); return response.data; }); } /** * Get popular APIs (top 20 by some heuristic) */ async getPopularAPIs() { return this.fetchWithCache("popular_apis", async () => { const allAPIs = await this.listAPIs(); // Simple heuristic: prefer APIs with more versions and recent updates const apiEntries = Object.entries(allAPIs); const scored = apiEntries.map(([id, api]) => { const versionCount = Object.keys(api.versions).length; const latestUpdate = Math.max(...Object.values(api.versions).map((v) => new Date(v.updated).getTime())); const score = versionCount * 10 + latestUpdate / 1000000; // Simple scoring return { id, api, score }; }); // Sort by score and take top 20 const popular = scored .sort((a, b) => b.score - a.score) .slice(0, 20) .reduce((acc, { id, api }) => { acc[id] = api; return acc; }, {}); return popular; }, 3600000); // Cache for 1 hour } /** * Get recently updated APIs */ async getRecentlyUpdatedAPIs(limit = 10) { return this.fetchWithCache(`recent:${limit}`, async () => { const allAPIs = await this.listAPIs(); const apiEntries = Object.entries(allAPIs); const withLatestUpdate = apiEntries.map(([id, api]) => { const latestUpdate = Math.max(...Object.values(api.versions).map((v) => new Date(v.updated).getTime())); return { id, api, updated: latestUpdate }; }); const recent = withLatestUpdate .sort((a, b) => b.updated - a.updated) .slice(0, limit) .reduce((acc, { id, api }) => { acc[id] = api; return acc; }, {}); return recent; }, 1800000); // Cache for 30 minutes } /** * Get API statistics for a provider */ async getProviderStats(provider) { return this.fetchWithCache(`${CACHE_KEYS.STATS_PREFIX}${provider}`, async () => { const providerAPIs = await this.getProvider(provider); return calculateProviderStats(providerAPIs); }, CACHE_TTL.PROVIDER_STATS); } } //# sourceMappingURL=client.js.map