UNPKG

@statsig/mcp-test

Version:

Statsig MCP Server (TEST VERSION) - Making your AI context-aware with feature flags, experiments, and analytics

146 lines (145 loc) 5.32 kB
import { z } from "zod"; import { fetchOpenApiSpec } from "./api.js"; export class OpenApiToZod { specUrl; HTTP_METHODS = [ "get", "post", "put", "delete", "patch", "options", "head", "trace", ]; INCLUDE_TAG = "MCP"; WHN_TAG_SUBSTR = "(Warehouse Native)"; openApiSpec = null; constructor(specUrl) { this.specUrl = specUrl; this.specUrl = specUrl; } async initialize() { this.openApiSpec = await fetchOpenApiSpec(this.specUrl); return this; } ensureParameterObject(parameter) { return !("$ref" in parameter); } openApiParameterArrayToZod(parameters) { if (!parameters) { return {}; } return Object.fromEntries(parameters .filter((param) => this.ensureParameterObject(param)) .map((param) => [param.name, this.openApiParameterToZod(param)]) .filter(([_, zod]) => zod !== undefined)); } extractPathParameters(parameters) { if (!parameters) { return []; } return parameters .filter((param) => "in" in param && param.in === "path") .map((param) => param.name); } openApiSchemaToZod(schemaObject) { if ("$ref" in schemaObject) { throw new Error("$ref in param schema not supported"); } if (schemaObject.type) { if (Array.isArray(schemaObject.type)) { throw new Error("Array type not supported"); } let schema = (() => { switch (schemaObject.type) { case "integer": return z.number(); case "number": return z.number(); case "string": return z.string(); case "boolean": return z.boolean(); case "object": throw new Error("object param not supported"); case "null": throw new Error("null param not supported"); case "array": if (!schemaObject.items) { throw new Error("array type schema does not have items"); } return z.array(this.openApiSchemaToZod(schemaObject.items)); default: throw new Error(`Unsupported type: ${schemaObject.type}`); } })(); if (schemaObject.nullable) { schema = schema.nullable(); } return schema; } else if (schemaObject.oneOf) { // zod prefers to know about the array types at compile time. This makes this work, and we can't infer a typescript type which is fine. return z.union(schemaObject.oneOf.map((schema) => this.openApiSchemaToZod(schema))); } else { throw new Error("unsupported schema object"); } } openApiParameterToZod(parameter) { if (!parameter.schema) { return undefined; } let schema = this.openApiSchemaToZod(parameter.schema); if (!parameter.required) { schema = schema.optional(); } if (parameter.description) { schema = schema.describe(parameter.description); } return schema; } specToZod() { const schema = {}; if (this.openApiSpec == null) { return schema; } for (const [path, pathItem] of Object.entries(this.openApiSpec.paths)) { for (const method of this.HTTP_METHODS) { const operation = pathItem[method]; if (!operation || !operation.tags || !operation.tags.includes(this.INCLUDE_TAG)) { continue; } // if the method is PUT (i.e. /console/v1/experiments/{id}/start) trim the last part of the url and turn it into a param let ending; let adjustedPath = path; if (method === "put") { const splitUrl = path.split("/"); ending = splitUrl.pop(); adjustedPath = splitUrl.join("/"); } if (!(adjustedPath in schema)) { schema[adjustedPath] = {}; } if (!(method in schema[adjustedPath])) { schema[adjustedPath][method] = []; } const parameters = this.openApiParameterArrayToZod(operation.parameters); const pathParameters = this.extractPathParameters(operation.parameters); schema[adjustedPath][method].push({ summary: operation.summary, parameters, pathParameters, ending, description: operation.description, isWHN: operation.tags && operation.tags.some((tag) => tag.includes(this.WHN_TAG_SUBSTR)), }); } } return schema; } }