Skip to main content
Glama

Notion ReadOnly MCP Server

by Taewoong1378
parser.ts18.7 kB
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types' import type { JSONSchema7 as IJsonSchema } from 'json-schema' import type { ChatCompletionTool } from 'openai/resources/chat/completions' import type { Tool } from '@anthropic-ai/sdk/resources/messages/messages' type NewToolMethod = { name: string description: string inputSchema: IJsonSchema & { type: 'object' } returnSchema?: IJsonSchema } type FunctionParameters = { type: 'object' properties?: Record<string, unknown> required?: string[] [key: string]: unknown } export class OpenAPIToMCPConverter { private schemaCache: Record<string, IJsonSchema> = {} private nameCounter: number = 0 constructor(private openApiSpec: OpenAPIV3.Document | OpenAPIV3_1.Document) {} /** * Resolve a $ref reference to its schema in the openApiSpec. * Returns the raw OpenAPI SchemaObject or null if not found. */ private internalResolveRef(ref: string, resolvedRefs: Set<string>): OpenAPIV3.SchemaObject | null { if (!ref.startsWith('#/')) { return null } if (resolvedRefs.has(ref)) { return null } const parts = ref.replace(/^#\//, '').split('/') let current: any = this.openApiSpec for (const part of parts) { current = current[part] if (!current) return null } resolvedRefs.add(ref) return current as OpenAPIV3.SchemaObject } /** * Convert an OpenAPI schema (or reference) into a JSON Schema object. * Uses caching and handles cycles by returning $ref nodes. */ convertOpenApiSchemaToJsonSchema( schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, resolvedRefs: Set<string>, resolveRefs: boolean = false, ): IJsonSchema { if ('$ref' in schema) { const ref = schema.$ref if (!resolveRefs) { if (ref.startsWith('#/components/schemas/')) { return { $ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'), ...('description' in schema ? { description: schema.description as string } : {}), } } console.error(`Attempting to resolve ref ${ref} not found in components collection.`) // deliberate fall through } // Create base schema with $ref and description if present const refSchema: IJsonSchema = { $ref: ref } if ('description' in schema && schema.description) { refSchema.description = schema.description as string } // If already cached, return immediately with description if (this.schemaCache[ref]) { return this.schemaCache[ref] } const resolved = this.internalResolveRef(ref, resolvedRefs) if (!resolved) { // TODO: need extensive tests for this and we definitely need to handle the case of self references console.error(`Failed to resolve ref ${ref}`) return { $ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'), description: 'description' in schema ? ((schema.description as string) ?? '') : '', } } else { const converted = this.convertOpenApiSchemaToJsonSchema(resolved, resolvedRefs, resolveRefs) this.schemaCache[ref] = converted return converted } } // Handle inline schema const result: IJsonSchema = {} if (schema.type) { result.type = schema.type as IJsonSchema['type'] } // Convert binary format to uri-reference and enhance description if (schema.format === 'binary') { result.format = 'uri-reference' const binaryDesc = 'absolute paths to local files' result.description = schema.description ? `${schema.description} (${binaryDesc})` : binaryDesc } else { if (schema.format) { result.format = schema.format } if (schema.description) { result.description = schema.description } } if (schema.enum) { result.enum = schema.enum } if (schema.default !== undefined) { result.default = schema.default } // Handle object properties if (schema.type === 'object') { result.type = 'object' if (schema.properties) { result.properties = {} for (const [name, propSchema] of Object.entries(schema.properties)) { result.properties[name] = this.convertOpenApiSchemaToJsonSchema(propSchema, resolvedRefs, resolveRefs) } } if (schema.required) { result.required = schema.required } if (schema.additionalProperties === true || schema.additionalProperties === undefined) { result.additionalProperties = true } else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') { result.additionalProperties = this.convertOpenApiSchemaToJsonSchema(schema.additionalProperties, resolvedRefs, resolveRefs) } else { result.additionalProperties = false } } // Handle arrays - ensure binary format conversion happens for array items too if (schema.type === 'array' && schema.items) { result.type = 'array' result.items = this.convertOpenApiSchemaToJsonSchema(schema.items, resolvedRefs, resolveRefs) } // oneOf, anyOf, allOf if (schema.oneOf) { result.oneOf = schema.oneOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs)) } if (schema.anyOf) { result.anyOf = schema.anyOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs)) } if (schema.allOf) { result.allOf = schema.allOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs)) } return result } convertToMCPTools(): { tools: Record<string, { methods: NewToolMethod[] }> openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }> zip: Record<string, { openApi: OpenAPIV3.OperationObject & { method: string; path: string }; mcp: NewToolMethod }> } { const apiName = 'API' const openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }> = {} const tools: Record<string, { methods: NewToolMethod[] }> = { [apiName]: { methods: [] }, } const zip: Record<string, { openApi: OpenAPIV3.OperationObject & { method: string; path: string }; mcp: NewToolMethod }> = {} for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) { if (!pathItem) continue for (const [method, operation] of Object.entries(pathItem)) { if (!this.isOperation(method, operation)) continue const mcpMethod = this.convertOperationToMCPMethod(operation, method, path) if (mcpMethod) { const uniqueName = this.ensureUniqueName(mcpMethod.name) mcpMethod.name = uniqueName tools[apiName]!.methods.push(mcpMethod) openApiLookup[apiName + '-' + uniqueName] = { ...operation, method, path } zip[apiName + '-' + uniqueName] = { openApi: { ...operation, method, path }, mcp: mcpMethod } } } } return { tools, openApiLookup, zip } } /** * Convert the OpenAPI spec to OpenAI's ChatCompletionTool format */ convertToOpenAITools(): ChatCompletionTool[] { const tools: ChatCompletionTool[] = [] for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) { if (!pathItem) continue for (const [method, operation] of Object.entries(pathItem)) { if (!this.isOperation(method, operation)) continue const parameters = this.convertOperationToJsonSchema(operation, method, path) const tool: ChatCompletionTool = { type: 'function', function: { name: operation.operationId!, description: operation.summary || operation.description || '', parameters: parameters as FunctionParameters, }, } tools.push(tool) } } return tools } /** * Convert the OpenAPI spec to Anthropic's Tool format */ convertToAnthropicTools(): Tool[] { const tools: Tool[] = [] for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) { if (!pathItem) continue for (const [method, operation] of Object.entries(pathItem)) { if (!this.isOperation(method, operation)) continue const parameters = this.convertOperationToJsonSchema(operation, method, path) const tool: Tool = { name: operation.operationId!, description: operation.summary || operation.description || '', input_schema: parameters as Tool['input_schema'], } tools.push(tool) } } return tools } private convertComponentsToJsonSchema(): Record<string, IJsonSchema> { const components = this.openApiSpec.components || {} const schema: Record<string, IJsonSchema> = {} for (const [key, value] of Object.entries(components.schemas || {})) { schema[key] = this.convertOpenApiSchemaToJsonSchema(value, new Set()) } return schema } /** * Helper method to convert an operation to a JSON Schema for parameters */ private convertOperationToJsonSchema( operation: OpenAPIV3.OperationObject, method: string, path: string, ): IJsonSchema & { type: 'object' } { const schema: IJsonSchema & { type: 'object' } = { type: 'object', properties: {}, required: [], $defs: this.convertComponentsToJsonSchema(), } // Handle parameters (path, query, header, cookie) if (operation.parameters) { for (const param of operation.parameters) { const paramObj = this.resolveParameter(param) if (paramObj && paramObj.schema) { const paramSchema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set()) // Merge parameter-level description if available if (paramObj.description) { paramSchema.description = paramObj.description } schema.properties![paramObj.name] = paramSchema if (paramObj.required) { schema.required!.push(paramObj.name) } } } } // Handle requestBody if (operation.requestBody) { const bodyObj = this.resolveRequestBody(operation.requestBody) if (bodyObj?.content) { if (bodyObj.content['application/json']?.schema) { const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set()) if (bodySchema.type === 'object' && bodySchema.properties) { for (const [name, propSchema] of Object.entries(bodySchema.properties)) { schema.properties![name] = propSchema } if (bodySchema.required) { schema.required!.push(...bodySchema.required) } } } } } return schema } private isOperation(method: string, operation: any): operation is OpenAPIV3.OperationObject { return ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase()) } private isParameterObject(param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): param is OpenAPIV3.ParameterObject { return !('$ref' in param) } private isRequestBodyObject(body: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject): body is OpenAPIV3.RequestBodyObject { return !('$ref' in body) } private resolveParameter(param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): OpenAPIV3.ParameterObject | null { if (this.isParameterObject(param)) { return param } else { const resolved = this.internalResolveRef(param.$ref, new Set()) if (resolved && (resolved as OpenAPIV3.ParameterObject).name) { return resolved as OpenAPIV3.ParameterObject } } return null } private resolveRequestBody(body: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject): OpenAPIV3.RequestBodyObject | null { if (this.isRequestBodyObject(body)) { return body } else { const resolved = this.internalResolveRef(body.$ref, new Set()) if (resolved) { return resolved as OpenAPIV3.RequestBodyObject } } return null } private resolveResponse(response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject): OpenAPIV3.ResponseObject | null { if ('$ref' in response) { const resolved = this.internalResolveRef(response.$ref, new Set()) if (resolved) { return resolved as OpenAPIV3.ResponseObject } else { return null } } return response } private convertOperationToMCPMethod(operation: OpenAPIV3.OperationObject, method: string, path: string): NewToolMethod | null { if (!operation.operationId) { console.warn(`Operation without operationId at ${method} ${path}`) return null } const methodName = operation.operationId const inputSchema: IJsonSchema & { type: 'object' } = { $defs: this.convertComponentsToJsonSchema(), type: 'object', properties: {}, required: [], } // Handle parameters (path, query, header, cookie) if (operation.parameters) { for (const param of operation.parameters) { const paramObj = this.resolveParameter(param) if (paramObj && paramObj.schema) { const schema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set(), false) // Merge parameter-level description if available if (paramObj.description) { schema.description = paramObj.description } inputSchema.properties![paramObj.name] = schema if (paramObj.required) { inputSchema.required!.push(paramObj.name) } } } } // Handle requestBody if (operation.requestBody) { const bodyObj = this.resolveRequestBody(operation.requestBody) if (bodyObj?.content) { // Handle multipart/form-data for file uploads // We convert the multipart/form-data schema to a JSON schema and we require // that the user passes in a string for each file that points to the local file if (bodyObj.content['multipart/form-data']?.schema) { const formSchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['multipart/form-data'].schema, new Set(), false) if (formSchema.type === 'object' && formSchema.properties) { for (const [name, propSchema] of Object.entries(formSchema.properties)) { inputSchema.properties![name] = propSchema } if (formSchema.required) { inputSchema.required!.push(...formSchema.required!) } } } // Handle application/json else if (bodyObj.content['application/json']?.schema) { const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set(), false) // Merge body schema into the inputSchema's properties if (bodySchema.type === 'object' && bodySchema.properties) { for (const [name, propSchema] of Object.entries(bodySchema.properties)) { inputSchema.properties![name] = propSchema } if (bodySchema.required) { inputSchema.required!.push(...bodySchema.required!) } } else { // If the request body is not an object, just put it under "body" inputSchema.properties!['body'] = bodySchema inputSchema.required!.push('body') } } } } // Build description including error responses let description = operation.summary || operation.description || '' if (operation.responses) { const errorResponses = Object.entries(operation.responses) .filter(([code]) => code.startsWith('4') || code.startsWith('5')) .map(([code, response]) => { const responseObj = this.resolveResponse(response) let errorDesc = responseObj?.description || '' return `${code}: ${errorDesc}` }) if (errorResponses.length > 0) { description += '\nError Responses:\n' + errorResponses.join('\n') } } // Extract return type (response schema) const returnSchema = this.extractResponseType(operation.responses) // Generate Zod schema from input schema try { // const zodSchemaStr = jsonSchemaToZod(inputSchema, { module: "cjs" }) // console.log(zodSchemaStr) // // Execute the function with the zod instance // const zodSchema = eval(zodSchemaStr) as z.ZodType return { name: methodName, description, inputSchema, ...(returnSchema ? { returnSchema } : {}), } } catch (error) { console.warn(`Failed to generate Zod schema for ${methodName}:`, error) // Fallback to a basic object schema return { name: methodName, description, inputSchema, ...(returnSchema ? { returnSchema } : {}), } } } private extractResponseType(responses: OpenAPIV3.ResponsesObject | undefined): IJsonSchema | null { // Look for a success response const successResponse = responses?.['200'] || responses?.['201'] || responses?.['202'] || responses?.['204'] if (!successResponse) return null const responseObj = this.resolveResponse(successResponse) if (!responseObj || !responseObj.content) return null if (responseObj.content['application/json']?.schema) { const returnSchema = this.convertOpenApiSchemaToJsonSchema(responseObj.content['application/json'].schema, new Set(), false) returnSchema['$defs'] = this.convertComponentsToJsonSchema() // Preserve the response description if available and not already set if (responseObj.description && !returnSchema.description) { returnSchema.description = responseObj.description } return returnSchema } // If no JSON response, fallback to a generic string or known formats if (responseObj.content['image/png'] || responseObj.content['image/jpeg']) { return { type: 'string', format: 'binary', description: responseObj.description || '' } } // Fallback return { type: 'string', description: responseObj.description || '' } } private ensureUniqueName(name: string): string { if (name.length <= 64) { return name } const truncatedName = name.slice(0, 64 - 5) // Reserve space for suffix const uniqueSuffix = this.generateUniqueSuffix() return `${truncatedName}-${uniqueSuffix}` } private generateUniqueSuffix(): string { this.nameCounter += 1 return this.nameCounter.toString().padStart(4, '0') } }

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/Taewoong1378/notion-readonly-mcp-server'

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