diff --git a/README.md b/README.md index 03a1d5a..ace1c23 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MCP Gateway -A flexible gateway server that bridges Model Context Protocol (MCP) STDIO servers to HTTP+SSE, enabling multi-instance MCP servers to be exposed over HTTP. +A flexible gateway server that bridges Model Context Protocol (MCP) STDIO servers to MCP HTTP+SSE and REST API, enabling multi-instance MCP servers to be exposed over HTTP. ## Features @@ -12,6 +12,54 @@ A flexible gateway server that bridges Model Context Protocol (MCP) STDIO server - YAML-based configuration - Optional Basic and Bearer token authentication - Configurable debug logging levels +- REST API Support + +## REST API Support + +MCP Gateway now provides a REST API interface to MCP servers, making them accessible to any HTTP client that supports OpenAPI/Swagger specifications. This feature is particularly useful for integrating with OpenAI's custom GPTs and other REST API clients. + +### REST API Endpoints + +Before making tool calls, you need to get a session ID: +```bash +curl "http://localhost:3000/api/sessionid" +# Returns: {"sessionId": ""} +``` + +Each tool exposed by an MCP server is available at: +``` +POST /api/{serverName}/{toolName}?sessionId={session-id} +``` +Note: The `sessionId` query parameter is required for all tool calls. + +For example, to call the `directory_tree` tool on a `filesystem` MCP server: +```bash +# First get a session ID +SESSION_ID=$(curl -s "http://localhost:3000/api/sessionid" | jq -r .sessionId) + +# Then make the tool call +curl -X POST "http://localhost:3000/api/filesystem/directory_tree?sessionId=$SESSION_ID" \ + -H "Content-Type: application/json" \ + -d '{"path": "/some/path"}' +``` + +### OpenAPI Schema Generation + +The gateway can generate OpenAPI schemas for all configured tools, making it easy to integrate with OpenAPI-compatible clients: + +```bash +# Generate YAML format (default) +npm start -- --schemaDump + +# Generate JSON format +npm start -- --schemaDump --schemaFormat json +``` + +The generated schema includes: +- All available endpoints for each configured server +- Tool descriptions and parameter schemas +- Request/response formats +- Authentication requirements ## Purpose @@ -225,4 +273,9 @@ Issues and PRs are welcome, but in all honesty they could languish a while. ## License -MIT License \ No newline at end of file +MIT License + + +curl -X POST "http://localhost:3000/api/filesystem/directory_tree?sessionId=randomSession12345" -H "Content-Type: application/json" -d '{ + "path": "/home/aaron/Clara" +}' diff --git a/package-lock.json b/package-lock.json index 2ade8e4..a7d5fda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,13 @@ "dependencies": { "@modelcontextprotocol/sdk": "latest", "@types/node": "^22.10.2", + "@types/yargs": "^17.0.33", "ts-node": "^10.9.1", "typescript": "^5.0.0", + "uuid": "^11.0.3", "winston": "^3.17.0", - "yaml": "^2.6.1" + "yaml": "^2.6.1", + "yargs": "^17.7.2" }, "devDependencies": { "tsx": "^4.19.2" @@ -535,6 +538,21 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -559,6 +577,48 @@ "node": ">=0.4.0" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -580,6 +640,20 @@ "node": ">= 0.8" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -658,6 +732,12 @@ "node": ">=0.3.1" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -704,6 +784,15 @@ "@esbuild/win32-x64": "0.23.1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -731,6 +820,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-tsconfig": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", @@ -784,6 +882,15 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -869,6 +976,15 @@ "node": ">= 6" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -956,6 +1072,32 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -1077,6 +1219,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -1119,6 +1274,32 @@ "node": ">= 12.0.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", @@ -1131,6 +1312,33 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 23b0db2..68202fa 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,13 @@ "dependencies": { "@modelcontextprotocol/sdk": "latest", "@types/node": "^22.10.2", + "@types/yargs": "^17.0.33", "ts-node": "^10.9.1", "typescript": "^5.0.0", + "uuid": "^11.0.3", "winston": "^3.17.0", - "yaml": "^2.6.1" + "yaml": "^2.6.1", + "yargs": "^17.7.2" }, "devDependencies": { "tsx": "^4.19.2" diff --git a/src/gateway.ts b/src/gateway.ts index 840a2af..4f0a58b 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -4,6 +4,10 @@ import { createLogger, format, transports } from 'winston'; import { parse } from 'yaml'; import { readFileSync } from 'fs'; import http from "http"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { stringify } from "yaml"; +import { randomUUID } from "crypto"; type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose'; @@ -46,6 +50,57 @@ class MCPServer { ) {} } +// We’ll store session info and track timeouts +interface SessionData { + serverProcess: StdioClientTransport; + lastActive: number; // timestamp of last request +} + +const SESSION_TIMEOUT_MS = 10 * 60_000; // 10 minutes +const sessions: Map = new Map(); + +// Periodically clean up expired sessions +setInterval(() => { + cleanupExpiredSessions(); +}, 30_000); // check every 30s + +function cleanupExpiredSessions() { + const now = Date.now(); + for (const [sessionId, data] of sessions.entries()) { + if (now - data.lastActive > SESSION_TIMEOUT_MS) { + data.serverProcess.close().catch(() => {}); + sessions.delete(sessionId); + } + } +} + +// Creates or reuses a session (restarts process if timed out) +async function getOrCreateSession(sessionId: string, serverName: string, config: ServerConfig) { + // If existing session is found and not timed out, reuse + const existing = sessions.get(sessionId); + if (existing) { + existing.lastActive = Date.now(); + return existing.serverProcess; + } + + // Otherwise, create a new STDIO transport + const stdioTransport = new StdioClientTransport({ + command: config.command, + args: config.args, + env: process.env as Record + }); + + await stdioTransport.start(); + + // Store in sessions + sessions.set(sessionId, { + serverProcess: stdioTransport, + lastActive: Date.now() + }); + + return stdioTransport; +} + class MCPGateway { private servers: Map = new Map(); private logger: ReturnType; @@ -121,6 +176,13 @@ class MCPGateway { return; } + // If it starts with /api/, it’s a REST call + if ((req.url || "").startsWith("/api/")) { + await handleRestRequest(req, res, this.config); + return; + } + + // Otherwise, fallback to SSE logic const serverName = req.url?.split('/')[1].split('?')[0] || ""; this.logger.debug('Incoming request:', { fullUrl: req.url, @@ -219,7 +281,7 @@ class MCPGateway { }); httpServer.listen(this.config.port, this.config.hostname); - console.log(`MCP Gateway listening on ${this.config.hostname}:${this.config.port}`); + this.logger.info(`MCP Gateway listening on ${this.config.hostname}:${this.config.port}`); } } @@ -243,13 +305,249 @@ try { throw new Error('At least one server must be configured'); } - // Start gateway with loaded config - const gateway = new MCPGateway(config); - gateway.start().catch(error => { - console.error('Failed to start gateway:', error); - process.exit(1); - }); + // 1) We'll parse arguments for schemaDump, schemaFormat + const argv = await yargs(hideBin(process.argv)) + .option('schemaDump', { type: 'boolean', default: false }) + .option('schemaFormat', { type: 'string', default: 'yaml' }) + .argv; + + // 2) If schemaDump, gather all tools and dump. Then exit. + if (argv.schemaDump) { + dumpSchemas(config, argv.schemaFormat === 'json' ? 'json' : 'yaml') + .then(() => process.exit(0)) + .catch(err => { + console.error(err); + process.exit(1); + }); + } else { + // Otherwise, proceed with normal gateway startup + const gateway = new MCPGateway(config); + gateway.start().catch(error => { + console.error('Failed to start gateway:', error); + process.exit(1); + }); + } } catch (error) { console.error('Failed to load configuration:', error); process.exit(1); +} + +async function handleRestRequest( + req: http.IncomingMessage, + res: http.ServerResponse, + config: GatewayConfig +) { + + // Add endpoint for session ID generation + if (req.url == "/api/sessionid" && req.method === "GET") { + const sessionId = randomUUID(); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ sessionId })); + return; + } + + // Example route: POST /api//?sessionId=123 + const urlParts = (req.url || "").split("?")[0].split("/").filter(Boolean); // skip empty + // UrlParts would look like [ 'api', 'serverName', 'toolName' ] + if (urlParts.length < 3) { + res.writeHead(404).end("Invalid REST route"); + return; + } + const [api, serverName, toolName] = urlParts; + + if (api !== "api") { + res.writeHead(404).end("Invalid REST route"); + return; + } + + // parse query, expecting ?sessionId=... + const query = new URLSearchParams((req.url || "").split("?")[1] || ""); + const sessionId = query.get("sessionId"); + + if (!sessionId) { + res.writeHead(400).end("sessionId query parameter is required"); + return; + } + + // If server not configured + if (!serverName || !config.servers[serverName]) { + res.writeHead(404).end("Server not found"); + return; + } + + // Read body + let body = ""; + req.on("data", chunk => { + body += chunk.toString(); + }); + + req.on("end", async () => { + try { + const parsedBody = JSON.parse(body); // user’s tool input + // Prepare JSON-RPC for MCP + const message = { + jsonrpc: "2.0", + id: Date.now().toString(), + method: "tools/call", + params: { + name: toolName, + arguments: parsedBody + } + }; + + // Create or reuse session & send request + const stdioTransport = await getOrCreateSession(sessionId, serverName, config.servers[serverName]); + + // Wrap in a small function that awaits the next response + // You’d typically have a queue in real usage; for simplicity, we’ll + // just listen for the next message event. + const response = await sendAndWaitForResponse(stdioTransport, message); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(response)); + } catch (error) { + console.error(error); + res.writeHead(500).end(String(error)); + } + }); +} + +// Very simple “send and wait” helper +function sendAndWaitForResponse(stdioTransport: StdioClientTransport, message: any): Promise { + return new Promise((resolve, reject) => { + const onMsg = (msg: any) => { + // console.log("Got message:", msg); + if (msg.id === message.id) { + stdioTransport.onmessage = undefined; + resolve(msg.result ?? msg.error ?? {}); + } + }; + stdioTransport.onmessage = onMsg; + stdioTransport.onerror = reject; + stdioTransport.send(message).then(() => { + // console.log("Sent message:", message) + }).catch(reject); + }); +} + +async function dumpSchemas(config: GatewayConfig, format: 'json' | 'yaml') { + const openApi: any = { + openapi: "3.0.0", + info: { title: "MCP Gateway Tools", version: "1.0.0" }, + paths: {} + }; + + for (const [serverName, serverConfig] of Object.entries(config.servers)) { + // console.log("Dumping tools for server:", serverName); + + // Spin up a temporary session, get tools + const stdio = new StdioClientTransport({ + command: serverConfig.command, + args: serverConfig.args, + env: process.env as Record + }); + await stdio.start(); + + + //TODO: Use the Client object from the SDK to talk to the server + + //// Hack: this didn't work with one of the servers I tried + //// initialize + // await sendAndWaitForResponse(stdio, { + // method: "initialize", + // params: { + // protocolVersion: "2024-11-05", + // capabilities: {}, + // clientInfo: {}, + // }, + // }); + + await stdio.send({ + jsonrpc: "2.0", + method: "notifications/initialized", + }); + + // We'll ask for tools ( "tools/list" ), ignoring advanced flows + const toolsList = await sendAndWaitForResponse(stdio, { + jsonrpc: "2.0", + id: Date.now().toString(), + method: "tools/list", + params: {} + }); + + // console.log("Got tools:", toolsList); + + if (toolsList && Array.isArray(toolsList.tools)) { + for (const tool of toolsList.tools) { + // Create an endpoint: POST /{serverName}/{tool.name}?sessionId= + const pathName = `/${serverName}/${tool.name}`; + openApi.paths[pathName] = { + post: { + operationId: `${serverName}-${tool.name}`, + summary: `Call tool: ${tool.name}`, + description: tool.description || "", + parameters: [ + { + name: "sessionId", + in: "query", + schema: { type: "string" }, + required: true, + description: "Session ID for the tool call. Get one from GET /api/sessionid" + } + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: tool.inputSchema || { type: "object" } + } + } + }, + responses: { + 200: { + description: "Success" + } + } + } + }; + } + } + + // Close the transport + await stdio.close(); + } + + // Add session ID generation endpoint to OpenAPI schema + openApi.paths["/sessionid"] = { + get: { + summary: "Generate a new session ID", + description: "Returns a new session ID that can be used for tool calls", + responses: { + 200: { + description: "A new session ID", + content: { + "application/json": { + schema: { + type: "object", + properties: { + sessionId: { + type: "string", + description: "The generated session ID" + } + }, + required: ["sessionId"] + } + } + } + } + } + } + }; + + // Output + if (format === "json") { + console.log(JSON.stringify(openApi, null, 2)); + } else { + const yamlData = stringify(openApi); + console.log(yamlData); + } } \ No newline at end of file