UNPKG

@statsig/mcp-test

Version:

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

195 lines (194 loc) 8.65 kB
import { OpenApiToZod } from "./testing.js"; import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; const DEFAULT_STATSIG_API_URL_BASE = "https://latest.statsigapi.net"; function getUrlBase() { return process.env.STATSIG_HOST || DEFAULT_STATSIG_API_URL_BASE; } function getOpenApiUrl() { return `${getUrlBase()}/openapi/20240601.json`; } function getApiHeaders(requestApiKey) { // Use API key from request if provided, otherwise fall back to environment variable const apiKey = requestApiKey || process.env.STATSIG_API_KEY || "[KEY MISSING]"; return { "Content-Type": "application/json", "STATSIG-API-KEY": apiKey, "STATSIG-API-VERSION": "20240601", }; } // Modified to accept API key parameter async function isWarehouseNative(requestApiKey) { const response = await fetch(`${getUrlBase()}/console/v1/company`, { headers: getApiHeaders(requestApiKey), }); if (!response.ok) { return null; } const json = await response.json(); if (typeof json.data?.isWarehouseNative !== "boolean") { return null; } return json.data.isWarehouseNative; } async function buildTools(server, specUrl, showWarehouseNative, requestApiKey) { const converter = await new OpenApiToZod(specUrl).initialize(); const schema = converter.specToZod(); const toolNames = new Set(); for (const [endpoint, methods] of Object.entries(schema)) { let toolName = endpoint.replace(/[^a-zA-Z0-9]/g, "-").substring(0, 40); if (toolNames.has(toolName)) { toolName = `${toolName}-${toolNames.size}`; } toolNames.add(toolName); // Fixed filter function with return statement const methodsToUse = Object.entries(methods).filter(([_, methodArray]) => { return methodArray.some((method) => method.isWHN === showWarehouseNative); }); if (methodsToUse.length === 0) { continue; } const methodsInfo = methodsToUse .map(([methodName, methodDetails]) => { const subDetailString = methodDetails .map((methodDetailObject) => { if (methodDetailObject.ending) { return `When using parameter ${methodDetailObject.ending} on ${methodName}; (${methodDetailObject.ending}): ${methodDetailObject.summary || "No summary available"}`; } else { return `${methodDetailObject.summary || "No summary available"}`; } }) .join("\n"); return `${methodName}: ${subDetailString || "No summary available for endpoint"}`; }) .join("\n"); const description = `API endpoint: ${endpoint}\n` + `Available methods: ${methodsToUse .map(([method]) => method.toUpperCase()) .join(", ")}\n` + `---\n` + methodsInfo; const methodNames = methodsToUse.map(([method]) => method); let enhancedParameters = { method: z .enum(methodNames) .describe(`HTTP method to use. Available methods: ${methodNames.join(", ")}`), }; // Complete the map function to process method parameters methodsToUse.forEach(([methodName, methodDetails]) => { methodDetails.forEach((detail) => { if (Object.keys(detail.parameters).length > 0) { const paramKey = detail.ending ? `${methodName}_${detail.ending}_params` : `${methodName}_params`; enhancedParameters[paramKey] = z .object(detail.parameters) .optional() .describe(detail.ending ? `Parameters for ${methodName} with '${detail.ending}' ending` : `Parameters for ${methodName}`); } }); }); server.tool(toolName, description, enhancedParameters, async (params) => { const { method, ...otherParams } = params; const methodToUse = method || methodNames[0]; const methodArray = methodsToUse.find(([m]) => m === methodToUse)?.[1] || []; let operationConfig; let methodParams = {}; let endingToUse; if (methodArray.length === 1) { operationConfig = methodArray[0]; const paramKey = operationConfig.ending ? `${methodToUse}_${operationConfig.ending}_params` : `${methodToUse}_params`; methodParams = otherParams[paramKey] || {}; endingToUse = operationConfig.ending; } else { for (const config of methodArray) { if (config.ending) { const paramKey = `${methodToUse}_${config.ending}_params`; if (paramKey in otherParams) { operationConfig = config; methodParams = otherParams[paramKey] || {}; endingToUse = config.ending; break; } } } if (!operationConfig) { operationConfig = methodArray[0]; const paramKey = operationConfig.ending ? `${methodToUse}_${operationConfig.ending}_params` : `${methodToUse}_params`; methodParams = otherParams[paramKey] || {}; endingToUse = operationConfig.ending; } } if (!operationConfig) { throw new Error(`No configuration found for method ${methodToUse}`); } const pathParameters = operationConfig.pathParameters || []; let interpolatedPath = endpoint; // For PUT with endings, we need to append the ending to the path if (methodToUse === "put" && endingToUse) { interpolatedPath = `${interpolatedPath}/${endingToUse}`; } const searchParams = new URLSearchParams(); for (const [paramName, paramValue] of Object.entries(methodParams)) { if (pathParameters.includes(paramName)) { interpolatedPath = interpolatedPath.replace(`{${paramName}}`, encodeURIComponent(String(paramValue))); } else { if (paramValue != null) { searchParams.set(paramName, String(paramValue)); } } } const queryString = searchParams.toString(); const url = `${getUrlBase()}${interpolatedPath}${queryString ? `?${queryString}` : ""}`; // console.debug(`Sending request to ${url}`); try { const response = await fetch(url, { method: methodToUse, headers: getApiHeaders(requestApiKey), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API Error (${response.status}): ${errorText}`); } let responseData; const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { responseData = await response.json(); return { content: [ { type: "text", text: JSON.stringify(responseData, null, 2) }, ], }; } else { responseData = await response.text(); return { content: [{ type: "text", text: responseData }] }; } } catch (error) { throw new Error(`Error calling the API: ${error.message}`); } }); } } export async function createServer(requestApiKey) { const server = new McpServer({ name: "statsig", version: "1.0.0", }); const specUrl = getOpenApiUrl(); // console.debug(`Using API spec from ${specUrl}`); // const isWHN = await isWarehouseNative(); // For now, lets give everyone false here as the WHN flag isn't consistent await buildTools(server, specUrl, false, requestApiKey); return server; }