Skip to main content
Glama
with-mcp.ts19.2 kB
/// <reference types="@cloudflare/workers-types" /> /// <reference lib="esnext" /> interface McpConfig { /** defaults to 2025-03-26 */ protocolVersion?: string; /** GET endpoint that returns MCP-compatible 401 if authentication isn't valid. * * e.g. '/me' * * Will be used before responding with "initialize" and '.../list' methods */ authEndpoint?: string; serverInfo?: { name: string; version: string; }; promptOperationIds?: string[]; toolOperationIds?: string[]; resourceOperationIds?: string[]; } interface OpenAPIOperation { operationId: string; summary?: string; description?: string; parameters?: Array<{ name: string; in: string; required?: boolean; description?: string; schema?: any; }>; requestBody?: { content?: { [mediaType: string]: { schema?: any; }; }; }; responses?: { [statusCode: string]: { description?: string; content?: { [mediaType: string]: { schema?: any; }; }; }; }; } interface OpenAPISpec { paths: { [path: string]: { [method: string]: OpenAPIOperation; }; }; components?: { schemas?: { [name: string]: any }; }; } export function withMcp<TEnv = {}>( handler: ExportedHandlerFetchHandler, openapi: OpenAPISpec, config: McpConfig ) { // Extract operations by operationId const allOperations = new Map< string, { path: string; method: string; operation: OpenAPIOperation } >(); for (const [path, methods] of Object.entries(openapi.paths)) { for (const [method, operation] of Object.entries(methods)) { if (operation.operationId) { allOperations.set(operation.operationId, { path, method, operation }); } } } return async ( request: Request, env: TEnv, ctx: ExecutionContext ): Promise<Response> => { const url = new URL(request.url); // Handle MCP endpoint if (url.pathname === "/mcp") { // Handle preflight OPTIONS request if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, MCP-Protocol-Version", "Access-Control-Max-Age": "86400", }, }); } if (request.method === "GET") { return new Response("Only Streamable HTTP is supported", { status: 405, }); } if (request.method === "POST") { const response = await handleMcp( request, env, ctx, allOperations, config, handler ); // Add CORS headers to the response const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, MCP-Protocol-Version", }; // Clone the response to add headers return new Response(response.body, { status: response.status, statusText: response.statusText, headers: { ...Object.fromEntries(response.headers.entries()), ...corsHeaders, }, }); } } // Pass through to original handler return handler(request, env, ctx); }; } async function checkAuth( config: McpConfig, originalRequest: Request, originalHandler: ExportedHandlerFetchHandler, env: any, ctx: any ): Promise<Response | null> { if (!config.authEndpoint) { return null; // No auth required } // Build auth request const baseUrl = new URL(originalRequest.url).origin; const authUrl = new URL(config.authEndpoint, baseUrl); const origHeaders = Object.fromEntries( originalRequest.headers .entries() .map(([key, val]) => [key.toLowerCase(), val]) ); const authRequest = new Request(authUrl.toString(), { method: "GET", headers: origHeaders, }) as Request<unknown, IncomingRequestCfProperties<unknown>>; const authResponse = await originalHandler(authRequest, env, ctx); // If auth failed, return the auth response if (authResponse.status === 401 || authResponse.status === 402) { return authResponse; } return null; // Auth passed } async function handleMcp( request: Request, env: any, ctx: any, allOperations: Map< string, { path: string; method: string; operation: OpenAPIOperation } >, config: McpConfig, originalHandler: ExportedHandlerFetchHandler ): Promise<Response> { try { const message: any = await request.json(); // Handle initialize if (message.method === "ping") { return new Response( JSON.stringify({ jsonrpc: "2.0", id: message.id, result: {}, }), { headers: { "Content-Type": "application/json" } } ); } if (message.method === "initialize") { // Check auth if configured const authResult = await checkAuth( config, request, originalHandler, env, ctx ); if (authResult) { console.log("initialize", { status: authResult.status, authResult: await authResult.clone().text(), }); return authResult; } const initializeResult = { jsonrpc: "2.0", id: message.id, result: { protocolVersion: config.protocolVersion || "2025-03-26", capabilities: { ...(config.promptOperationIds && config.promptOperationIds.length > 0 && { prompts: {} }), ...(config.resourceOperationIds && config.resourceOperationIds.length > 0 && { resources: {} }), ...(config.toolOperationIds && config.toolOperationIds.length > 0 && { tools: {} }), }, serverInfo: config.serverInfo || { name: "OpenAPI-MCP-Server", version: "1.0.0", }, }, }; console.log("initialize", { initializeResult }); return new Response(JSON.stringify(initializeResult), { headers: { "Content-Type": "application/json" }, }); } // Handle initialized notification if (message.method === "notifications/initialized") { console.log("notifications/initialized"); return new Response(null, { status: 202 }); } // Handle prompts/list if (message.method === "prompts/list") { // Check auth if configured const authResult = await checkAuth( config, request, originalHandler, env, ctx ); if (authResult) { return authResult; } const prompts = (config.promptOperationIds || []) .map((opId) => { const op = allOperations.get(opId); if (!op) return null; return { name: opId, title: op.operation.summary || opId, description: op.operation.description, arguments: extractArguments(op.operation), }; }) .filter(Boolean); return new Response( JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { prompts }, }), { headers: { "Content-Type": "application/json" } } ); } // Handle prompts/get if (message.method === "prompts/get") { const { name, arguments: args } = message.params; const op = allOperations.get(name); if (!op || !(config.promptOperationIds || []).includes(name)) { return createError(message.id, -32602, `Unknown prompt: ${name}`); } // Execute the operation (which includes its own auth check) const apiResponse = await executeOperation( op, args, originalHandler, request, env, ctx ); // If 401 or 402, proxy the response as-is if (apiResponse.status === 401 || apiResponse.status === 402) { return apiResponse; } // Convert to prompt messages const messages = await convertResponseToPromptMessages(apiResponse); return new Response( JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { description: op.operation.description, messages, }, }), { headers: { "Content-Type": "application/json" } } ); } // Handle resources/list if (message.method === "resources/list") { // Check auth if configured const authResult = await checkAuth( config, request, originalHandler, env, ctx ); if (authResult) { return authResult; } const resources = (config.resourceOperationIds || []) .map((opId) => { const op = allOperations.get(opId); if (!op) return null; return { uri: `resource://${opId}`, name: opId, title: op.operation.summary || opId, description: op.operation.description, mimeType: inferMimeType(op.operation), }; }) .filter(Boolean); return new Response( JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { resources }, }), { headers: { "Content-Type": "application/json" } } ); } // Handle resources/read if (message.method === "resources/read") { const { uri } = message.params; const opId = uri.replace("resource://", ""); const op = allOperations.get(opId); if (!op || !(config.resourceOperationIds || []).includes(opId)) { return createError(message.id, -32002, `Resource not found: ${uri}`); } // Execute the operation (which includes its own auth check) const apiResponse = await executeOperation( op, {}, originalHandler, request, env, ctx ); // If 401 or 402, proxy the response as-is if (apiResponse.status === 401 || apiResponse.status === 402) { return apiResponse; } // Convert to resource content const contents = await convertResponseToResourceContents( apiResponse, uri ); return new Response( JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { contents }, }), { headers: { "Content-Type": "application/json" } } ); } // Handle tools/list if (message.method === "tools/list") { // Check auth if configured const authResult = await checkAuth( config, request, originalHandler, env, ctx ); if (authResult) { console.log("tools/list", { status: authResult.status, authResult: await authResult.clone().text(), }); return authResult; } const tools = (config.toolOperationIds || []) .map((opId) => { const op = allOperations.get(opId); if (!op) return null; return { name: opId, title: op.operation.summary || opId, description: op.operation.description, inputSchema: extractInputSchema(op.operation), }; }) .filter(Boolean); const toolsListResult = { jsonrpc: "2.0", id: message.id, result: { tools }, }; console.log({ toolsListResult }); return new Response(JSON.stringify(toolsListResult), { headers: { "Content-Type": "application/json" }, }); } // Handle tools/call if (message.method === "tools/call") { const { name, arguments: args } = message.params; const op = allOperations.get(name); if (!op || !(config.toolOperationIds || []).includes(name)) { return createError(message.id, -32602, `Unknown tool: ${name}`); } try { // Execute the operation (which includes its own auth check) const apiResponse = await executeOperation( op, args, originalHandler, request, env, ctx ); // If 401 or 402, proxy the response as-is if (apiResponse.status === 401 || apiResponse.status === 402) { return apiResponse; } const content = await convertResponseToToolContent(apiResponse); return new Response( JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { content, isError: !apiResponse.ok, }, }), { headers: { "Content-Type": "application/json" } } ); } catch (error) { return new Response( JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { content: [ { type: "text", text: `Error executing tool: ${error.message}`, }, ], isError: true, }, }), { headers: { "Content-Type": "application/json" }, } ); } } return createError( message.id, -32601, `Method not found: ${message.method}` ); } catch (error) { return createError(null, -32700, "Parse error"); } } function extractArguments(operation: OpenAPIOperation) { const args = []; // Extract from parameters if (operation.parameters) { for (const param of operation.parameters) { args.push({ name: param.name, description: param.description, required: param.required || false, }); } } // Extract from request body schema properties if ( operation.requestBody?.content?.["application/json"]?.schema?.properties ) { const props = operation.requestBody.content["application/json"].schema.properties; const required = operation.requestBody.content["application/json"].schema.required || []; for (const [name, schema] of Object.entries(props)) { args.push({ name, description: (schema as any).description, required: required.includes(name), }); } } return args; } function extractInputSchema(operation: OpenAPIOperation) { // Start with basic object schema const schema: any = { type: "object", properties: {}, required: [], }; // Add parameters as properties if (operation.parameters) { for (const param of operation.parameters) { schema.properties[param.name] = param.schema || { type: "string" }; if (param.required) { schema.required.push(param.name); } } } // Merge request body schema if (operation.requestBody?.content?.["application/json"]?.schema) { const bodySchema = operation.requestBody.content["application/json"].schema; if (bodySchema.properties) { Object.assign(schema.properties, bodySchema.properties); } if (bodySchema.required) { schema.required.push(...bodySchema.required); } } return schema; } function inferMimeType(operation: OpenAPIOperation): string { // Check response content types const responses = operation.responses; if (responses) { for (const response of Object.values(responses)) { if (response.content) { const contentTypes = Object.keys(response.content); if (contentTypes.length > 0) { const preferred = ["text/plain", "text/markdown"]; const pref = contentTypes.find((x) => preferred.includes(x)); if (pref) { return pref; } return contentTypes[0]; } } } } return "application/json"; } async function executeOperation( op: { path: string; method: string; operation: OpenAPIOperation }, args: any, originalHandler: ExportedHandlerFetchHandler, originalRequest: Request, env: any, ctx: any ): Promise<Response> { // Build the API request URL let url = op.path; const queryParams = new URLSearchParams(); const bodyData: any = {}; // query params in the URL will be input into the API as well, regardless of whether or not they appear in the openapi // needed for https://smithery.ai/docs/build/session-config#well-known-endpoint-approach const originalRequestUrl = new URL(originalRequest.url); originalRequestUrl.searchParams.forEach((value, key) => { queryParams.set(key, value); }); // Handle parameters if (op.operation.parameters) { for (const param of op.operation.parameters) { const value = args[param.name]; if (value !== undefined) { if (param.in === "path") { url = url.replace(`{${param.name}}`, encodeURIComponent(value)); } else if (param.in === "query") { queryParams.set(param.name, value); } // Note: header params would need special handling } } } // Add remaining args to body Object.assign(bodyData, args); // Build the final URL const baseUrl = new URL(originalRequest.url).origin; const finalUrl = new URL(url, baseUrl); if (queryParams.toString()) { finalUrl.search = queryParams.toString(); } const origHeaders = Object.fromEntries( originalRequest.headers .entries() .map(([key, val]) => [key.toLowerCase(), val]) ); const headers = { ...origHeaders, accept: inferMimeType(op.operation), "content-type": "application/json", }; // Create the API request const apiRequest = new Request(finalUrl.toString(), { method: op.method.toUpperCase(), headers, ...(op.method.toUpperCase() !== "GET" && Object.keys(bodyData).length > 0 && { body: JSON.stringify(bodyData), }), }) as Request<unknown, IncomingRequestCfProperties<unknown>>; return originalHandler(apiRequest, env, ctx); } async function convertResponseToPromptMessages(response: Response) { const text = await response.text(); return [ { role: "user" as const, content: { type: "text" as const, text: response.ok ? text : `Error: ${response.status} ${response.statusText}\n${text}`, }, }, ]; } async function convertResponseToResourceContents( response: Response, uri: string ) { const text = await response.text(); const contentType = response.headers.get("content-type") || "text/plain"; return [ { uri, mimeType: contentType, text, }, ]; } async function convertResponseToToolContent(response: Response) { const text = await response.text(); return [{ type: "text" as const, text }]; } function createError(id: any, code: number, message: string) { return new Response( JSON.stringify({ jsonrpc: "2.0", id, error: { code, message }, }), { status: 200, // JSON-RPC errors use 200 status headers: { "Content-Type": "application/json" }, } ); }

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/with-mcp'

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