@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
JavaScript
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;
}
}