Skip to main content
Glama
worker.ts18.5 kB
/// <reference lib="esnext" /> import { dereferenceSync } from "@trojs/openapi-dereference"; import { load, dump } from "js-yaml"; // ==================== TYPES ==================== type JSONSchema = { type?: string; properties?: { [key: string]: JSONSchema }; items?: JSONSchema; required?: string[]; enum?: any[]; oneOf?: JSONSchema[]; anyOf?: JSONSchema[]; allOf?: JSONSchema[]; $ref?: string; nullable?: boolean; description?: string; }; interface Parameter { name: string; in: "query" | "header" | "path" | "cookie"; required?: boolean; schema?: { type: string; enum?: string[]; default?: any; }; description?: string; } interface RequestBody { required?: boolean; content: { [key: string]: { schema: any; }; }; } interface Operation { operationId?: string; servers?: Server[]; summary?: string; description?: string; parameters?: Parameter[]; requestBody?: RequestBody; responses: { [key: string]: any; }; } interface PathItem { get?: Operation; post?: Operation; put?: Operation; delete?: Operation; patch?: Operation; } type Server = { url: string; description?: string }; interface OpenapiDocument { tags?: any; components?: any; webhooks?: any; openapi?: string; servers?: Server[]; info: { title: string; version: string; description?: string; }; paths: { [key: string]: PathItem; }; } type SchemaObject = { type: string; pattern?: string; properties?: { [key: string]: SchemaObject }; items?: SchemaObject; required?: string[]; enum?: any[]; description?: string; format?: string; default?: any; minimum?: number; maximum?: number; } & { [key: string]: any }; type OperationObject = { operationId?: string; summary?: string; description?: string; tags?: string[]; parameters?: any[]; requestBody?: any; responses?: any; security?: any; servers?: Server[]; }; // ==================== CORS HEADERS ==================== const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, MCP-Protocol-Version", }; // ==================== HELPER FUNCTIONS ==================== function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { return value !== null && value !== undefined; } function tryParseJson<T extends unknown>(text: string): T | null { try { const removeCommentsRegex = /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g; const jsonStringWithoutComments = text.replace( removeCommentsRegex, (m, g) => (g ? "" : m) ); return JSON.parse(jsonStringWithoutComments) as T; } catch (parseError) { return null; } } async function convertSwaggerToOpenapi( swaggerUrl: string ): Promise<OpenapiDocument | undefined> { const abortController = new AbortController(); const timeoutId = setTimeout(() => abortController.abort(), 10000); const result = await fetch( `https://converter.swagger.io/api/convert?url=${swaggerUrl}`, { signal: abortController.signal } ) .then((res) => res.json() as Promise<OpenapiDocument>) .catch((e) => { return undefined; }); clearTimeout(timeoutId); return result; } // ==================== OPENAPI SEARCH ==================== async function searchOpenapi(providerId: string) { const urlResponse = await fetch( `https://openapisearch.com/redirect/${providerId}`, { redirect: "follow" } ); if (!urlResponse.ok) { console.log("Not ok", providerId); return; } const openapiUrl = urlResponse.url; const res = await fetch(openapiUrl); if (!res.ok) { console.log("Not ok", openapiUrl); return; } const text = await res.text(); let json = undefined; try { json = JSON.parse(text); } catch (e) { try { json = load(text); } catch (e) { return; } } const basePath = json.servers?.[0]?.url || new URL(openapiUrl).hostname; return { openapiUrl, openapiJson: json, basePath }; } // ==================== OVERVIEW GENERATOR ==================== function generateOverview(hostname: string, openapi: OpenapiDocument): string { const output: string[] = []; const getServerOrigin = (operation: any, rootServers: any[]): string => { const servers = operation?.servers || rootServers || []; if (servers.length === 0) return ""; try { const url = new URL(servers[0].url); return url.origin; } catch (e) { return servers[0].url.split("/")[0]; } }; if (openapi.info) { const { title, version, description } = openapi.info; const serverOrigin = getServerOrigin(undefined, openapi.servers || []); output.push(`${title} v${version} - ${serverOrigin}`); if (description) output.push(description); output.push(""); } const items: { operationId: string; pathPart: string; summaryPart: string; openapiUrl: string; }[] = []; if (openapi.paths) { for (const [path, pathItem] of Object.entries(openapi.paths)) { const methods = ["get", "post", "put", "patch", "delete"]; for (const method of methods) { const operation = pathItem[method as keyof typeof pathItem]; if (!operation) continue; const serverOrigin = getServerOrigin(operation, openapi.servers || []); const operationId = operation.operationId ? `${operation.operationId}` : ""; const queryParams = operation.parameters ?.filter((p) => p.in === "query") ?.map((p) => `${p.name}=${p.schema?.type || p.name}`) ?.join("&"); const queryString = queryParams ? `?${queryParams}` : ""; const summaryPart = operation.summary ? ` - ${operation.summary}` : ""; const pathPart = `${method.toUpperCase()} ${serverOrigin}${path}${queryString}`; const openapiUrl = `https://oapis.org/openapi/${hostname}${ operationId ? `/` + operationId : path }`; items.push({ operationId, pathPart, summaryPart, openapiUrl }); } } } const isLong = JSON.stringify(items).length > 50000; output.push( ...items.map( (item) => `- ${item.operationId}${isLong ? " " : " " + item.pathPart}${ item.summaryPart } ( Spec: ${item.openapiUrl} )` ) ); const endpointCount = output.length - 3; output.unshift(""); output.unshift( `Below is an overview of the ${hostname} openapi in simple language. This API contains ${endpointCount} endpoints. For more detailed information of an endpoint, visit https://oapis.org/summary/${hostname}/[idOrRoute]` ); return output.join("\n"); } // ==================== OPENAPI OPERATIONS ==================== function matchOperation(openapi: OpenapiDocument, pathname: string) { if (!openapi.paths) { return; } // First try direct path match const directMatch = openapi.paths[pathname]?.get; if (directMatch) { return { operation: directMatch, originalPath: pathname, method: "GET", }; } const normalizedPathname = pathname.slice(1); // Then try operationId match for (const [path, pathItem] of Object.entries(openapi.paths)) { const matchingMethod = ["get", "post", "put", "patch", "delete"].find( (httpMethod) => { const operation = pathItem?.[httpMethod as keyof typeof pathItem]; return operation?.operationId === normalizedPathname; } ); if (matchingMethod) { return { operation: pathItem[matchingMethod as keyof typeof pathItem], originalPath: path, method: matchingMethod.toUpperCase(), }; } } return undefined; } // ==================== MCP HANDLERS ==================== function getInitializeResult() { return { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "openapi-mcp-server", version: "2.2.0", title: "OpenAPI MCP Server", websiteUrl: "https://openapisearch.com", description: "Explore and interact with OpenAPI specifications through MCP. Discover APIs from openapisearch.com or provide your own OpenAPI spec URL.", icons: [ { src: "https://www.google.com/s2/favicons?domain=openapisearch.com&sz=48", sizes: ["32x32", "16x16", "48x48"], }, { src: "https://www.google.com/s2/favicons?domain=openapisearch.com&sz=256", sizes: ["any"], }, ], }, instructions: "This MCP server provides access to OpenAPI specifications. Use 'getApiOverview' first to understand an API's structure, then use 'getApiOperation' to get details about specific endpoints.", }; } async function handleGetApiOverview(args: { id: string }) { const { id } = args; if (!id) { return { content: [ { type: "text", text: "Error: 'id' parameter is required", }, ], isError: true, }; } try { const openapi = await searchOpenapi(id); if (!openapi) { throw new Error("OpenAPI not found"); } const { openapiUrl, openapiJson } = openapi; if (!openapiJson) { throw new Error("Could not parse OpenAPI JSON"); } const isSwagger = (openapiJson as any)?.swagger || !openapiJson?.openapi || !openapiJson?.openapi.startsWith("3."); const convertedOpenapi = isSwagger ? await convertSwaggerToOpenapi(openapiUrl) : (openapiJson as OpenapiDocument); if (!convertedOpenapi?.openapi) { throw new Error("Conversion failed"); } const overview = generateOverview(id, convertedOpenapi); if (overview.length > 250000) { throw new Error( "The OpenAPI specification is too large to process with this MCP. Please try a different OpenAPI." ); } return { content: [{ type: "text", text: overview }], isError: false, }; } catch (error: any) { return { content: [ { type: "text", text: `Error: ${error.message}`, }, ], isError: true, }; } } async function handleGetApiOperation(args: { id: string; operationIdOrRoute: string; }) { const { id, operationIdOrRoute } = args; if (!id || !operationIdOrRoute) { return { content: [ { type: "text", text: "Error: Both 'id' and 'operationIdOrRoute' parameters are required", }, ], isError: true, }; } try { const openapi = await searchOpenapi(id); if (!openapi) { throw new Error("OpenAPI not found"); } const { openapiUrl, openapiJson } = openapi; if (!openapiJson) { throw new Error("Could not parse OpenAPI JSON"); } const isSwagger = (openapiJson as any)?.swagger || !openapiJson?.openapi || !openapiJson?.openapi.startsWith("3."); const convertedOpenapi = isSwagger ? await convertSwaggerToOpenapi(openapiUrl) : (openapiJson as OpenapiDocument); if (!convertedOpenapi?.openapi) { throw new Error("Conversion failed"); } // Match the operation const op = matchOperation(convertedOpenapi, `/${operationIdOrRoute}`); if (!op?.operation) { const operationIds = Object.values(convertedOpenapi.paths) .map((item) => [ item.get?.operationId, item.post?.operationId, item.delete?.operationId, item.put?.operationId, item.patch?.operationId, ] .filter(Boolean) .map((x) => x!) ) .flat(); const routes = Object.keys(convertedOpenapi.paths); throw new Error( `Operation wasn't found. Available IDs: ${operationIds.join( ", " )}. Routes: ${routes.join(", ")}` ); } // Get the subset for this operation const subset = { ...convertedOpenapi, paths: { [op.originalPath]: { [op.method.toLowerCase()]: op.operation, }, }, }; try { const { tags, webhooks, components, ...dereferenced } = dereferenceSync( subset ) as OpenapiDocument; return { content: [{ type: "text", text: dump(dereferenced) }], isError: false, }; } catch { return { content: [{ type: "text", text: dump(subset) }], isError: false, }; } } catch (error: any) { return { content: [ { type: "text", text: `Error: ${error.message}`, }, ], isError: true, }; } } // ==================== MCP HANDLER ==================== async function handleMcp(request: Request) { // Handle SSE rejection if ( request.method === "GET" && request.headers.get("accept")?.includes("text/event-stream") ) { return new Response("Only Streamable HTTP is supported", { status: 405, headers: corsHeaders, }); } if (request.method === "GET") { const initializeResult = getInitializeResult(); return new Response(JSON.stringify(initializeResult, null, 2), { headers: { "Content-Type": "application/json", ...corsHeaders, }, }); } if (request.method !== "POST") { return new Response("Method not allowed", { status: 405, headers: corsHeaders, }); } try { const message = await request.json(); // Handle ping if (message.method === "ping") { return jsonRpcResponse(message.id, {}); } // Handle initialize if (message.method === "initialize") { return jsonRpcResponse(message.id, getInitializeResult()); } // Handle initialized notification if (message.method === "notifications/initialized") { return new Response(null, { status: 202, headers: corsHeaders, }); } // Handle prompts/list if (message.method === "prompts/list") { return jsonRpcResponse(message.id, { prompts: [] }); } // Handle resources/list if (message.method === "resources/list") { return jsonRpcResponse(message.id, { resources: [] }); } // Handle resources/read if (message.method === "resources/read") { const { uri } = message.params; return jsonRpcError(message.id, -32602, `Resource not found: ${uri}`); } // Handle tools/list if (message.method === "tools/list") { const openapiIds = await fetch("https://openapisearch.com/").then((res) => res.text() ); const tools = [ { name: "getApiOverview", title: "Get API Overview", description: `Get an overview of an OpenAPI specification. This should be the first step when working with any API.\n\n${openapiIds}`, inputSchema: { type: "object", properties: { id: { type: "string", description: "API identifier, can be a known ID from openapisearch.com or a URL leading to a raw OpenAPI file", }, }, required: ["id"], }, }, { name: "getApiOperation", title: "Get API Operation", description: `Get details about a specific operation from an OpenAPI specification. Use this after getting an overview.\n\n${openapiIds}`, inputSchema: { type: "object", properties: { id: { type: "string", description: "API identifier, can be a known ID from openapisearch.com or a URL leading to a raw OpenAPI file", }, operationIdOrRoute: { type: "string", description: "Operation ID or route path to retrieve", }, }, required: ["id", "operationIdOrRoute"], }, }, ]; return jsonRpcResponse(message.id, { tools }); } // Handle tools/call if (message.method === "tools/call") { const { name, arguments: args } = message.params; try { let result; if (name === "getApiOverview") { result = await handleGetApiOverview(args); } else if (name === "getApiOperation") { result = await handleGetApiOperation(args); } else { return jsonRpcResponse(message.id, { content: [ { type: "text", text: `Error: Unknown tool: ${name}`, }, ], isError: true, }); } return jsonRpcResponse(message.id, result); } catch (error: any) { return jsonRpcResponse(message.id, { content: [ { type: "text", text: `Error executing tool: ${error.message}`, }, ], isError: true, }); } } // Method not found return jsonRpcError( message.id, -32601, `Method not found: ${message.method}` ); } catch (error) { return jsonRpcError(null, -32700, "Parse error"); } } function jsonRpcResponse(id: any, result: any) { return new Response( JSON.stringify( { jsonrpc: "2.0", id, result, }, null, 2 ), { headers: { "Content-Type": "application/json", ...corsHeaders, }, } ); } function jsonRpcError(id: any, code: number, message: string) { return new Response( JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }, null, 2), { headers: { "Content-Type": "application/json", ...corsHeaders, }, } ); } // ==================== MAIN EXPORT ==================== export default { async fetch(request: Request) { // Handle CORS preflight if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: corsHeaders, }); } const url = new URL(request.url); const path = url.pathname; // Root redirect if (path === "/") { return new Response(null, { status: 302, headers: { Location: "/mcp" } }); } // Only handle /mcp endpoint if (path !== "/mcp") { return new Response("Not found", { status: 404, headers: corsHeaders }); } return handleMcp(request); }, };

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/janwilmake/openapi-mcp-server'

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