Skip to main content
Glama

GenAIScript

Official
by microsoft
MIT License
43
2,820
  • Linux
  • Apple
openapi.ts13.8 kB
import { ScriptFilterOptions } from "../../core/src/ast" import { deleteUndefinedValues, ensureHeadSlash, trimTrailingSlash, } from "../../core/src/cleaners" import { genaiscriptDebug } from "../../core/src/debug" import { nodeTryReadPackage } from "../../core/src/nodepackage" import { toStrictJSONSchema } from "../../core/src/schema" import { logError, logVerbose, logWarn } from "../../core/src/util" import { RemoteOptions, applyRemoteOptions } from "./remote" import { startProjectWatcher } from "./watch" import type { FastifyInstance, FastifyRequest } from "fastify" import { findOpenPort } from "./port" import { OPENAPI_SERVER_PORT } from "../../core/src/constants" import { CORE_VERSION } from "../../core/src/version" import { run } from "./api" import { errorMessage } from "../../core/src/error" import { PromptScriptRunOptions } from "./main" import { ensureDotGenaiscriptPath } from "../../core/src/workdir" import { uniq } from "es-toolkit" const dbg = genaiscriptDebug("openapi") const dbgError = dbg.extend("error") const dbgHandlers = dbg.extend("handlers") export async function startOpenAPIServer( options?: PromptScriptRunOptions & ScriptFilterOptions & RemoteOptions & { port?: string cors?: string network?: boolean startup?: string route?: string } ) { logVerbose(`web api server: starting...`) await ensureDotGenaiscriptPath() await applyRemoteOptions(options) const { startup, cors, network, remote, remoteBranch, remoteForce, remoteInstall, groups, ids, ...runOptions } = options || {} const serverHost = network ? "0.0.0.0" : "127.0.0.1" const route = ensureHeadSlash(trimTrailingSlash(options?.route || "/api")) const docsRoute = `${route}/docs` dbg(`route: %s`, route) dbg(`server host: %s`, serverHost) dbg(`run options: %O`, runOptions) const port = await findOpenPort(OPENAPI_SERVER_PORT, options) const watcher = await startProjectWatcher(options) logVerbose(`openapi server: watching ${watcher.cwd}`) const createFastify = (await import("fastify")).default const swagger = (await import("@fastify/swagger")).default const swaggerUi = (await import("@fastify/swagger-ui")).default const swaggerCors = cors ? (await import("@fastify/cors")).default : undefined let fastifyController: AbortController | undefined let fastify: FastifyInstance | undefined const stopServer = async () => { const s = fastifyController const f = fastify fastifyController = undefined fastify = undefined if (s) { try { logVerbose(`stopping watcher...`) s.abort() } catch (e) { dbg(e) } } if (f) { try { logVerbose(`stopping server...`) await f.close() } catch (e) { dbg(e) } } } const startServer = async () => { await stopServer() logVerbose(`starting server...`) const tools = (await watcher.scripts()).sort((l, r) => l.id.localeCompare(r.id) ) fastifyController = new AbortController() fastify = createFastify({ logger: false }) if (cors) fastify.register(swaggerCors, { origin: cors, methods: ["GET", "POST"], allowedHeaders: ["Content-Type"], }) // infer server metadata from package.json const { name, description = "GenAIScript OpenAPI Server", version = "0.0.0", author, license, homepage, displayName, } = (await nodeTryReadPackage()) || {} const operationPrefix = "" // Register the OpenAPI documentation plugin (Swagger for OpenAPI 3.x) await fastify.register(swagger, { openapi: { openapi: "3.1.1", info: deleteUndefinedValues({ title: displayName || name, description, version, contact: author ? { name: author } : undefined, license: license ? { name: license, } : undefined, }), externalDocs: homepage ? { url: homepage, description: "Homepage", } : undefined, servers: [ { url: `http://127.0.0.1:${port}`, description: "GenAIScript server", }, { url: `http://localhost:${port}`, description: "GenAIScript server", }, { url: `http://${serverHost}:${port}`, description: "GenAIScript server", }, ], tags: uniq([ "default", ...tools.map(({ group }) => group).filter(Boolean), ]).map((name) => ({ name })), }, }) // Dynamically create a POST route for each tool in the tools list const routes = new Set<string>([docsRoute]) for (const tool of tools) { const { id, accept, inputSchema, title: summary, description, group, } = tool const scriptSchema = (inputSchema?.properties .script as JSONSchemaObject) || { type: "object", properties: {}, } const bodySchema = { type: "object", properties: deleteUndefinedValues({ ...(scriptSchema?.properties || {}), files: accept !== "none" ? { type: "array", items: { type: "object", properties: { filename: { type: "string", description: `Filename of the file. Accepts ${accept || "*"}.`, }, content: { type: "string", description: "Content of the file. Use 'base64' encoding for binary files.", }, encoding: { type: "string", description: "Encoding of the file. Binary files should use 'base64'.", enum: ["base64"], }, type: { type: "string", description: "MIME type of the file", }, }, required: ["filename", "content"], }, } : undefined, }), required: scriptSchema?.required || [], } if (!description) logWarn(`${id}: operation must have a description`) if (!group) logWarn(`${id}: operation must have a group`) const operationId = `${operationPrefix}${id}` const schema = deleteUndefinedValues({ operationId, summary, description, tags: [tool.group || "default"].filter(Boolean), body: toStrictJSONSchema(bodySchema, { defaultOptional: true }), response: { 200: toStrictJSONSchema( { type: "object", properties: deleteUndefinedValues({ error: { type: "string", description: "Error message", }, text: { type: "string", description: "Output text", }, data: tool.responseSchema ? toStrictJSONSchema(tool.responseSchema, { defaultOptional: true, }) : undefined, uncertainty: { type: "number", description: "Uncertainty of the response, between 0 and 1", }, perplexity: { type: "number", description: "Perplexity of the response, lower is better", }, }), }, { defaultOptional: true } ), }, 400: { type: "object", properties: { error: { type: "string", description: "Error message", }, }, }, 500: { type: "object", properties: { error: { type: "string", description: "Error message", }, }, }, }) const toolPath = id.replace(/[^a-z\-_]+/gi, "_").replace(/_+$/, "") const url = `${route}/${toolPath}` if (routes.has(url)) { logError(`duplicate route: ${url} for tool ${id}, skipping`) continue } dbg(`script %s: %s\n%O`, id, url, schema) routes.add(url) const handler = async (request: FastifyRequest) => { const { files = [], ...bodyRest } = (request.body || {}) as any dbgHandlers(`query: %O`, request.query) dbgHandlers(`body: %O`, bodyRest) const vars = { ...((request.query as any) || {}), ...bodyRest } dbgHandlers(`vars: %O`, vars) // TODO: parse query params? const res = await run(tool.id, [], { ...runOptions, workspaceFiles: files || [], vars: vars, runTrace: false, outputTrace: false, }) if (!res) throw new Error("Internal Server Error") dbgHandlers(`res: %s`, res.status) if (res.error) { dbgHandlers(`error: %O`, res.error) throw new Error(errorMessage(res.error)) } return deleteUndefinedValues({ ...res, }) } fastify.post(url, { schema }, async (request) => { dbgHandlers(`post %s %O`, tool.id, request.body) return await handler(request) }) } await fastify.register(swaggerUi, { routePrefix: docsRoute, }) // Global error handler for uncaught errors and validation issues fastify.setErrorHandler((error, request, reply) => { dbgError(`%s %s %O`, request.method, request.url, error) if (error.validation) { reply.status(400).send({ error: error.message, }) } else { reply.status(error.statusCode ?? 500).send({ error: `Internal Server Error - ${error.message ?? "An unexpected error occurred"}`, }) } }) console.log(`GenAIScript OpenAPI v${CORE_VERSION}`) console.log(`│ API http://localhost:${port}${route}/`) console.log(`| Console UI: http://localhost:${port}${route}/docs`) console.log( `| OpenAPI Spec: http://localhost:${port}${route}/docs/json` ) await fastify.listen({ port, host: serverHost, signal: fastifyController.signal, }) } if (startup) { logVerbose(`startup script: ${startup}`) await run(startup, [], {}) } // start watcher watcher.addEventListener("change", startServer) await startServer() }

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/microsoft/genaiscript'

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