REST API Support and OpenAPI Schema Generation

- Introduced REST API endpoints for session management and tool invocation, allowing HTTP clients to interact with MCP servers.
- Updated `README.md` to document new REST API features, including usage examples and OpenAPI schema generation.
- Added `@types/yargs` and `uuid` as dependencies in `package.json` and `package-lock.json` for improved command-line argument parsing and unique session ID generation.
- Enhanced session management with automatic cleanup of expired sessions in the gateway.
This commit is contained in:
Acehoss
2024-12-19 17:55:55 +00:00
parent c3ae0ac87e
commit fd9a8c999c
4 changed files with 573 additions and 11 deletions

View File

@@ -1,6 +1,6 @@
# MCP Gateway # 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 ## Features
@@ -12,6 +12,54 @@ A flexible gateway server that bridges Model Context Protocol (MCP) STDIO server
- YAML-based configuration - YAML-based configuration
- Optional Basic and Bearer token authentication - Optional Basic and Bearer token authentication
- Configurable debug logging levels - 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": "<generated-id>"}
```
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 ## Purpose
@@ -225,4 +273,9 @@ Issues and PRs are welcome, but in all honesty they could languish a while.
## License ## License
MIT License MIT License
curl -X POST "http://localhost:3000/api/filesystem/directory_tree?sessionId=randomSession12345" -H "Content-Type: application/json" -d '{
"path": "/home/aaron/Clara"
}'

210
package-lock.json generated
View File

@@ -11,10 +11,13 @@
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "latest", "@modelcontextprotocol/sdk": "latest",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/yargs": "^17.0.33",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"uuid": "^11.0.3",
"winston": "^3.17.0", "winston": "^3.17.0",
"yaml": "^2.6.1" "yaml": "^2.6.1",
"yargs": "^17.7.2"
}, },
"devDependencies": { "devDependencies": {
"tsx": "^4.19.2" "tsx": "^4.19.2"
@@ -535,6 +538,21 @@
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT" "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": { "node_modules/acorn": {
"version": "8.14.0", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -559,6 +577,48 @@
"node": ">=0.4.0" "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": { "node_modules/arg": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -580,6 +640,20 @@
"node": ">= 0.8" "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": { "node_modules/color": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
@@ -658,6 +732,12 @@
"node": ">=0.3.1" "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": { "node_modules/enabled": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
@@ -704,6 +784,15 @@
"@esbuild/win32-x64": "0.23.1" "@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": { "node_modules/fecha": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "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": "^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": { "node_modules/get-tsconfig": {
"version": "4.8.1", "version": "4.8.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
@@ -784,6 +882,15 @@
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT" "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": { "node_modules/is-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -869,6 +976,15 @@
"node": ">= 6" "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": { "node_modules/resolve-pkg-maps": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -956,6 +1072,32 @@
"safe-buffer": "~5.2.0" "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": { "node_modules/text-hex": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -1077,6 +1219,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "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": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "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": ">= 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": { "node_modules/yaml": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
@@ -1131,6 +1312,33 @@
"node": ">= 14" "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": { "node_modules/yn": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@@ -12,10 +12,13 @@
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "latest", "@modelcontextprotocol/sdk": "latest",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/yargs": "^17.0.33",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"uuid": "^11.0.3",
"winston": "^3.17.0", "winston": "^3.17.0",
"yaml": "^2.6.1" "yaml": "^2.6.1",
"yargs": "^17.7.2"
}, },
"devDependencies": { "devDependencies": {
"tsx": "^4.19.2" "tsx": "^4.19.2"

View File

@@ -4,6 +4,10 @@ import { createLogger, format, transports } from 'winston';
import { parse } from 'yaml'; import { parse } from 'yaml';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import http from "http"; 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'; type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose';
@@ -46,6 +50,57 @@ class MCPServer {
) {} ) {}
} }
// Well 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<string, SessionData> = 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<string, string>
});
await stdioTransport.start();
// Store in sessions
sessions.set(sessionId, {
serverProcess: stdioTransport,
lastActive: Date.now()
});
return stdioTransport;
}
class MCPGateway { class MCPGateway {
private servers: Map<string, MCPServer> = new Map(); private servers: Map<string, MCPServer> = new Map();
private logger: ReturnType<typeof createLogger>; private logger: ReturnType<typeof createLogger>;
@@ -121,6 +176,13 @@ class MCPGateway {
return; return;
} }
// If it starts with /api/, its 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] || ""; const serverName = req.url?.split('/')[1].split('?')[0] || "";
this.logger.debug('Incoming request:', { this.logger.debug('Incoming request:', {
fullUrl: req.url, fullUrl: req.url,
@@ -219,7 +281,7 @@ class MCPGateway {
}); });
httpServer.listen(this.config.port, this.config.hostname); 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'); throw new Error('At least one server must be configured');
} }
// Start gateway with loaded config // 1) We'll parse arguments for schemaDump, schemaFormat
const gateway = new MCPGateway(config); const argv = await yargs(hideBin(process.argv))
gateway.start().catch(error => { .option('schemaDump', { type: 'boolean', default: false })
console.error('Failed to start gateway:', error); .option('schemaFormat', { type: 'string', default: 'yaml' })
process.exit(1); .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) { } catch (error) {
console.error('Failed to load configuration:', error); console.error('Failed to load configuration:', error);
process.exit(1); 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/<serverName>/<toolName>?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); // users 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
// Youd typically have a queue in real usage; for simplicity, well
// 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<any> {
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<string, string>
});
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);
}
} }