UNPKG

@lonu/stc

Version:

A tool for converting OpenApi/Swagger/Apifox into code.

194 lines (193 loc) 6.51 kB
const PARAMETER_CATEGORIES = [ "path", "query", "body", "formData", "header", ]; const sanitizeToolName = (name) => { const normalized = name .replace(/[^a-zA-Z0-9_-]+/g, "-") .replace(/^-+|-+$/g, ""); return normalized || "openapi-tool"; }; const schemaType = (property) => { const type = Array.isArray(property.type) ? property.type[0] : property.type; if (property.typeX === "array" || type === "array") return "array"; if (type === "integer") return "integer"; if (type === "number") return "number"; if (type === "boolean" || type === "bool") return "boolean"; if (type === "file") return "string"; if (type === "object" || property.properties?.length || property.ref) { return "object"; } return "string"; }; const definitionToSchema = (name, definitions, visitedRefs) => { const definition = definitions.get(name); if (!definition || visitedRefs.has(name)) return undefined; visitedRefs.add(name); const schema = Array.isArray(definition) ? propertiesToSchema(definition, definitions, visitedRefs) : toJsonSchema(definition, definitions, visitedRefs); visitedRefs.delete(name); return schema; }; const propertiesToSchema = (properties, definitions, visitedRefs) => { const schema = { type: "object", properties: {} }; const required = []; properties.forEach((child) => { const name = child.originalName ?? child.name; schema.properties[name] = toJsonSchema(child, definitions, visitedRefs); if (child.required) required.push(name); }); if (required.length) schema.required = required; return schema; }; const toJsonSchema = (property, definitions, visitedRefs = new Set()) => { const type = schemaType(property); const schema = { type }; if (property.description) schema.description = property.description; if (property.enumOption?.length) schema.enum = property.enumOption; if (property.default !== undefined) schema.default = property.default; if (property.format) schema.format = property.format; if (type === "string" && property.type === "file") schema.format = "binary"; if (property.nullable) schema.type = [type, "null"]; if (type === "array") { const item = property.items ?? { name: "item", type: property.typeX ?? property.ref ?? "string", ref: property.ref, }; schema.items = property.ref ? definitionToSchema(property.ref, definitions, visitedRefs) ?? toJsonSchema(item, definitions, visitedRefs) : toJsonSchema(item, definitions, visitedRefs); } if (type === "object") { const resolved = property.properties?.length ? propertiesToSchema(property.properties, definitions, visitedRefs) : definitionToSchema(property.ref ?? "", definitions, visitedRefs); if (resolved) Object.assign(schema, resolved); if (property.additionalRef) { schema.additionalProperties = definitionToSchema(property.additionalRef, definitions, visitedRefs) ?? { type: "object" }; } } return schema; }; const uniqueInputName = (preferred, category, usedNames) => { let name = preferred; let index = 2; if (usedNames.has(name)) name = `${category}_${preferred}`; const baseName = name; while (usedNames.has(name)) { name = `${baseName}_${index}`; index += 1; } usedNames.add(name); return name; }; const createInput = (data, definitions) => { const properties = {}; const required = []; const parameters = []; const usedNames = new Set(); PARAMETER_CATEGORIES.forEach((category) => { data.parameters[category].forEach((parameter) => { const parameterName = parameter.originalName ?? parameter.name; const preferredName = category === "body" ? "body" : parameterName; const inputName = uniqueInputName(preferredName, category, usedNames); properties[inputName] = toJsonSchema(parameter, definitions); if (parameter.required) required.push(inputName); parameters.push({ in: category, name: category === "body" ? "body" : parameterName, input: inputName, required: Boolean(parameter.required), }); }); }); const schema = { type: "object", properties, additionalProperties: false, }; if (required.length) schema.required = required; return { schema, parameters }; }; const getToolName = (key, usedNames) => { const rawName = key.slice(key.indexOf("@") + 1); const baseName = sanitizeToolName(rawName); let name = baseName; let index = 2; while (usedNames.has(name)) { name = `${baseName}-${index}`; index += 1; } usedNames.add(name); return name; }; export const McpPlugin = { name: "stc:McpPlugin", lang: "mcp", setup() { return { unknownType: "string", typeMap() { return {}; }, template: { enum: "mcp", definitionHeader: "mcp", definitionBody: "mcp", definitionFooter: "mcp", actionImport: "mcp", actionMethod: "mcp", }, langDirectoryName: "mcp", }; }, onTransform(definitions, action) { const usedNames = new Set(); const tools = []; action.forEach((data, key) => { const input = createInput(data, definitions); tools.push({ name: getToolName(key, usedNames), description: data.summary || data.description || `${data.method} ${data.url}`, inputSchema: input.schema, "x-stc-http": { method: data.method.toUpperCase(), path: data.url, parameters: input.parameters, }, }); }); return { definition: { filename: "mcp-tools.json", content: JSON.stringify({ tools }, null, 2) + "\n", banner: false, }, }; }, };