Skip to main content
Glama

OpenAPI to MCP Server

overlay-applier.ts10.1 kB
/** * Overlay Applier – Reference implementation of the Overlay Specification v1.0.0 * * Features * -------- * • Parses Overlay documents written in JSON or YAML (via `js-yaml`). * • Applies actions sequentially using JSONPath queries (via `jsonpath-plus`). * • Supports `update` (deep merge into objects or append to arrays) and `remove` operations. * • Non‑destructive: works on a deep clone of the target document. * • Zero‑match actions are silently ignored, per spec. * • Provides programmatic API, helper wrappers, and an optional CLI (`ts-node overlay-applier.ts --overlay o.yaml --target api.yaml`). * * Dependencies * ------------ * ```bash * npm install jsonpath-plus js-yaml deepmerge yargs * ``` * * Example * ------- * ```typescript * import { OverlayApplier } from './overlay-applier'; * import fs from 'node:fs'; * * const overlayTxt = fs.readFileSync('overlay.yaml', 'utf8'); * const apiTxt = fs.readFileSync('api.yaml', 'utf8'); * * const applier = new OverlayApplier(); * const result = applier.apply(apiTxt, overlayTxt); * * console.log(applier.stringify(result, { yaml: true })); * ``` */ import { JSONPath } from 'jsonpath-plus'; import { load as yamlLoad, dump as yamlDump } from 'js-yaml'; import deepmerge from 'deepmerge'; /** Overlay → info object */ export interface OverlayInfo { title: string; version: string; /** Specification extensions */ [k: `x-${string}`]: unknown; } /** Single action item */ export interface Action { /** JSONPath expression selecting nodes in the target document */ target: string; /** Human‑readable description (CommonMark allowed) */ description?: string; /** Object or value to merge into the selected node(s) */ update?: unknown; /** Remove selected node(s) */ remove?: boolean; /** Specification extensions */ [k: `x-${string}`]: unknown; } /** Root overlay object */ export interface OverlayDoc { /** Overlay spec version, e.g. `1.0.0` */ overlay: string; info: OverlayInfo; extends?: string; actions: Action[]; /** Specification extensions */ [k: `x-${string}`]: unknown; } /** * OverlayApplier – core engine */ export class OverlayApplier { /** JSONPath‑plus options */ private readonly jpOptions: { resultType: 'all' } = { resultType: 'all' as const }; /** Deep‑clone helper using structuredClone where available */ private clone<T>(obj: T): T { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return typeof structuredClone === 'function' ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)); } /** Parse YAML or JSON text into an OverlayDoc */ parseOverlay(text: string): OverlayDoc { try { if (/^\s*[\[{]/.test(text)) { return JSON.parse(text) as OverlayDoc; } return yamlLoad(text) as OverlayDoc; } catch (err) { throw new Error(`Failed to parse overlay: ${(err as Error).message}`); } } /** Parse YAML or JSON text into a plain JS object */ parseAny(text: string): unknown { try { if (/^\s*[\[{]/.test(text)) { return JSON.parse(text); } return yamlLoad(text); } catch (err) { throw new Error(`Failed to parse document: ${(err as Error).message}`); } } /** Serialize object back to text */ stringify(obj: unknown, opts: { yaml?: boolean } = {}): string { return opts.yaml ? yamlDump(obj, { lineWidth: 120, noRefs: true }) : JSON.stringify(obj, null, 2); } /** Public API: apply overlay to target (both can be objects or text) */ apply(target: string | unknown, overlay: string | OverlayDoc): unknown { const overlayDoc: OverlayDoc = typeof overlay === 'string' ? this.parseOverlay(overlay) : overlay; if (!overlayDoc.overlay) throw new Error('Overlay document missing required "overlay" version field'); const targetObj: any = typeof target === 'string' ? this.parseAny(target) : target; let workingDoc: any = this.clone(targetObj); for (const action of overlayDoc.actions) { workingDoc = this.applyAction(workingDoc, action); } return workingDoc; } /** Apply a single action and return the mutated document */ private applyAction(doc: any, action: Action): any { const { target: expr, update, remove = false } = action; if (!expr) throw new Error('Action is missing required "target" field'); try { // First, find all matches in the document using JSONPath const matches = JSONPath({ path: expr, json: doc, resultType: 'all' }); // Per spec: if zero matches found, silently ignore (no-op) if (!matches.length) return doc; // Special case for info section which gives direct property access if (expr === '$.info' && update && typeof update === 'object') { doc.info = deepmerge(doc.info, update as object); return doc; } // Special case for path operations that need direct access if (expr.startsWith("$.paths[") && expr.endsWith("].get") && update && typeof update === 'object') { // Extract the path from the expression const pathMatch = expr.match(/\$\.paths\['([^']+)'\]/); if (pathMatch) { const path = pathMatch[1]; doc.paths[path].get = deepmerge(doc.paths[path].get, update as object); return doc; } } // Special case for parameter updates that need to find by name if (expr.includes('parameters[?(@.name==') && update && typeof update === 'object') { // Extract path and param name from expression const pathMatch = expr.match(/\$\.paths\['([^']+)'\]/); const nameMatch = expr.match(/name=='([^']+)'/); if (pathMatch && nameMatch) { const path = pathMatch[1]; const paramName = nameMatch[1]; const method = expr.includes('.get.') ? 'get' : expr.includes('.post.') ? 'post' : expr.includes('.put.') ? 'put' : expr.includes('.delete.') ? 'delete' : 'get'; // Find the parameter and update it const params = doc.paths[path][method].parameters; const paramIndex = params.findIndex((p: any) => p.name === paramName); if (paramIndex !== -1) { params[paramIndex] = deepmerge(params[paramIndex], update as object); } return doc; } } // Process matches using standard JSONPath for other cases for (const match of matches) { if (!match.path || !Array.isArray(match.path)) continue; // Get the parent path and key const parentPath = match.path.slice(0, -1); const key = match.path[match.path.length - 1]; // Find the parent object/array that contains the node we want to modify const parentRef = JSONPath({ path: this.toJsonPath(parentPath), json: doc, resultType: 'value' })[0]; if (!parentRef) continue; // Handle 'remove' action - delete the node from its parent if (remove === true) { if (Array.isArray(parentRef)) { parentRef.splice(Number(key), 1); } else if (typeof parentRef === 'object' && parentRef !== null) { delete parentRef[key]; } continue; } // Handle 'update' action if remove is false if (update !== undefined) { const targetNode = match.value; if (Array.isArray(targetNode)) { // For arrays, append the update value targetNode.push(update); } else if (typeof targetNode === 'object' && targetNode !== null) { // For objects, deep merge the properties const merged = deepmerge(targetNode, update as object, { arrayMerge: (dest, src) => dest.concat(src), }); // Update the parent reference to the merged object if (Array.isArray(parentRef)) { parentRef[Number(key)] = merged; } else { parentRef[key] = merged; } } else { // For primitive values, just replace them with the update if (Array.isArray(parentRef)) { parentRef[Number(key)] = update; } else { parentRef[key] = update; } } } } } catch (err) { // Log error but don't throw - non-fatal per spec console.error(`Error applying overlay action with target ${expr}:`, err); } return doc; } /** Remove property or array element from parent */ private removeFromParent(parent: any, key: string | number) { if (Array.isArray(parent)) { parent.splice(Number(key), 1); } else if (parent && typeof parent === 'object') { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete parent[key as keyof typeof parent]; } } /** Assign helper that works for object or array parents */ private assignToParent(parent: any, key: string | number, value: unknown) { if (Array.isArray(parent)) { parent[Number(key)] = value; } else { parent[key as keyof typeof parent] = value; } } /** Convert JSONPath array back to string form */ private toJsonPath(arr: (string | number)[]): string { if (!arr.length) return '$'; return arr .map((seg, idx) => { if (idx === 0) return '$'; if (typeof seg === 'number') return `[${seg}]`; return /^[a-zA-Z_][\w$]*$/.test(seg as string) ? `.${seg}` : `["${String(seg).replace(/"/g, '\\"')}"]`; }) .join(''); } } /** Convenience helper wrapping parse‑apply‑stringify for CLI use */ export function applyOverlayText( targetText: string, overlayText: string, opts: { yamlOutput?: boolean } = {}, ): string { const engine = new OverlayApplier(); const result = engine.apply(targetText, overlayText); return engine.stringify(result, { yaml: !!opts.yamlOutput }); }

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/TykTechnologies/api-to-mcp'

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