mirror of
https://github.com/aljazceru/mcp-gateway.git
synced 2025-12-17 05:04:24 +01:00
✨ 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:
57
README.md
57
README.md
@@ -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
210
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
312
src/gateway.ts
312
src/gateway.ts
@@ -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 {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<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/, 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] || "";
|
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); // 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<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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user