diff --git a/README.md b/README.md index a70faf2..03a1d5a 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,52 @@ A flexible gateway server that bridges Model Context Protocol (MCP) STDIO server - Clean separation between server instances using session IDs - Automatic cleanup of server resources on connection close - YAML-based configuration +- Optional Basic and Bearer token authentication +- Configurable debug logging levels ## Purpose At the moment, most MCP servers are designed for local execution. MCP Gateway enables HTTP+SSE capable clients to interact with MCP servers running on remote machines. This addresses common deployment scenarios, such as running [LibreChat](https://github.com/LibreChat/LibreChat) in a containerized environment where certain MCP servers, like the Puppeteer server, may have limited functionality. MCP Gateway provides a robust solution for distributing MCP servers across multiple machines while maintaining seamless connectivity. -## Security Warning +## Security Features -MCP Gateway does not yet provide any authentication or authorization. Run MCP Gateway only on a trusted network! +MCP Gateway supports two authentication methods that can be enabled independently: + +1. Basic Authentication: Username/password pairs +2. Bearer Token Authentication: Token-based authentication + +Both methods can be enabled simultaneously, and any valid authentication will grant access. + +### Authentication Configuration + +Add authentication settings to your `config.yaml`: + +```yaml +auth: + basic: + enabled: true + credentials: + - username: "admin" + password: "your-secure-password" + # Add more username/password pairs as needed + bearer: + enabled: true + tokens: + - "your-secure-token" + # Add more tokens as needed +``` + +### Using Authentication + +#### Basic Authentication +```bash +curl -u username:password http://localhost:3000/serverName +``` + +#### Bearer Token Authentication +```bash +curl -H "Authorization: Bearer your-secure-token" http://localhost:3000/serverName +``` ## Installation @@ -29,6 +67,31 @@ npm install The gateway is configured using a YAML file. By default, it looks for `config.yaml` in the current directory, but you can specify a different path using the `CONFIG_PATH` environment variable. +### Debug Configuration + +The gateway uses [Winston](https://github.com/winstonjs/winston) for logging, providing rich formatting and multiple log levels: + +```yaml +debug: + level: "info" # Possible values: "error", "warn", "info", "debug", "verbose" +``` + +Log levels, from least to most verbose: +- `error`: Only show errors +- `warn`: Show warnings and errors +- `info`: Show general information, warnings, and errors (default) +- `debug`: Show debug information and all above +- `verbose`: Show all possible logging information + +The logs include timestamps and are color-coded by level when viewing in a terminal. Additional metadata is included as JSON when relevant. + +Example log output: +``` +2024-01-20T10:15:30.123Z [INFO]: New SSE connection for filesystem +2024-01-20T10:15:30.124Z [DEBUG]: Server instance created with sessionId: /filesystem?sessionId=abc123 +2024-01-20T10:15:30.125Z [VERBOSE]: STDIO message received: {"type":"ready"} +``` + ### Basic Configuration Example ```yaml @@ -89,6 +152,33 @@ servers: - "--some-option" ``` +### Complete Configuration Example + +```yaml +hostname: "0.0.0.0" +port: 3000 + +# Authentication configuration (optional) +auth: + basic: + enabled: true + credentials: + - username: "admin" + password: "your-secure-password" + bearer: + enabled: true + tokens: + - "your-secure-token" + +servers: + filesystem: + command: npx + args: + - -y + - "@modelcontextprotocol/server-filesystem" + - "/path/to/root" +``` + ## Running the Gateway Standard start: diff --git a/config.yaml b/config.yaml index 3d6fc0d..eff31f8 100644 --- a/config.yaml +++ b/config.yaml @@ -1,22 +1,38 @@ hostname: "0.0.0.0" # Listen on all interfaces by default port: 3000 +# Debug configuration (optional) +debug: + level: "info" # Possible values: "error", "warn", "info", "debug", "verbose" + +# Authentication configuration (optional) +# auth: +# basic: +# enabled: true +# credentials: +# - username: "admin" +# password: "your-secure-password" +# bearer: +# enabled: true +# tokens: +# - "your-secure-token" + servers: + # run an MCP server with npx filesystem: command: npx args: - -y - "@modelcontextprotocol/server-filesystem" - "/home/aaron" - - git: - command: npx - args: - - -y - - "@modelcontextprotocol/server-git" + # run a an MCP server from local source puppeteer: - command: npx + command: tsx args: - - -y - - "@modelcontextprotocol/server-puppeteer" + - "/home/aaron/AI/MCP-Servers/src/puppeteer/index.ts" + + youtube: + command: tsx + args: + - "/home/aaron/AI/mcp-server-youtube-transcript/src/index.ts" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ce1502b..2ade8e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,22 @@ "@types/node": "^22.10.2", "ts-node": "^10.9.1", "typescript": "^5.0.0", + "winston": "^3.17.0", "yaml": "^2.6.1" }, "devDependencies": { "tsx": "^4.19.2" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -31,6 +41,17 @@ "node": ">=12" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -508,6 +529,12 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -538,6 +565,12 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -547,6 +580,51 @@ "node": ">= 0.8" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -580,6 +658,12 @@ "node": ">=0.3.1" } }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", @@ -620,6 +704,18 @@ "@esbuild/win32-x64": "0.23.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -682,12 +778,68 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "license": "ISC" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/raw-body": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", @@ -703,6 +855,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -713,6 +879,35 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -725,6 +920,24 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -734,6 +947,21 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -743,6 +971,15 @@ "node": ">=0.6" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -834,12 +1071,54 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "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", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "license": "MIT" }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/yaml": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", diff --git a/package.json b/package.json index c0fc1d6..23b0db2 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/node": "^22.10.2", "ts-node": "^10.9.1", "typescript": "^5.0.0", + "winston": "^3.17.0", "yaml": "^2.6.1" }, "devDependencies": { diff --git a/src/gateway.ts b/src/gateway.ts index 6d87c7c..840a2af 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -1,9 +1,30 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { createLogger, format, transports } from 'winston'; import { parse } from 'yaml'; import { readFileSync } from 'fs'; import http from "http"; +type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose'; + +interface DebugConfig { + level: LogLevel; +} + +interface AuthConfig { + basic?: { + enabled: boolean; + credentials: Array<{ + username: string; + password: string; + }>; + }; + bearer?: { + enabled: boolean; + tokens: string[]; + }; +} + interface ServerConfig { command: string; args: string[]; @@ -13,6 +34,8 @@ interface ServerConfig { interface GatewayConfig { hostname: string; port: number; + debug?: DebugConfig; + auth?: AuthConfig; servers: Record; } @@ -25,13 +48,81 @@ class MCPServer { class MCPGateway { private servers: Map = new Map(); + private logger: ReturnType; - constructor(private config: GatewayConfig) { } + constructor(private config: GatewayConfig) { + this.logger = createLogger({ + level: config.debug?.level || 'info', + format: format.combine( + format.timestamp(), + format.colorize(), + format.printf(({ level, message, timestamp, ...metadata }) => { + let msg = `${timestamp} [${level}]: ${message}`; + if (Object.keys(metadata).length > 0) { + msg += ` ${JSON.stringify(metadata)}`; + } + return msg; + }) + ), + transports: [ + new transports.Console() + ] + }); + } + + private authenticateRequest(req: http.IncomingMessage): boolean { + // If no auth config is present, allow all requests + if (!this.config.auth) { + this.logger.debug('No authentication configured, allowing request'); + return true; + } + + const authHeader = req.headers.authorization; + this.logger.verbose('Processing authentication header:', { header: authHeader }); + + // Check Bearer token authentication + if (this.config.auth.bearer?.enabled) { + if (authHeader?.startsWith('Bearer ')) { + const token = authHeader.substring(7); + const isValid = this.config.auth.bearer.tokens.includes(token); + this.logger.debug('Bearer token authentication result:', { isValid }); + if (isValid) return true; + } + } + + // Check Basic authentication + if (this.config.auth.basic?.enabled) { + if (authHeader?.startsWith('Basic ')) { + const base64Credentials = authHeader.substring(6); + const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); + const [username, password] = credentials.split(':'); + + const validCredentials = this.config.auth.basic.credentials.some( + cred => cred.username === username && cred.password === password + ); + + this.logger.debug('Basic authentication result:', { validCredentials }); + if (validCredentials) return true; + } + } + + this.logger.warn('Authentication failed for request'); + return false; + } async start() { const httpServer = http.createServer(async (req, res) => { + // Check authentication first + if (!this.authenticateRequest(req)) { + this.logger.warn('Unauthorized request rejected'); + res.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="MCP Gateway", Bearer' + }).end('Unauthorized'); + return; + } + const serverName = req.url?.split('/')[1].split('?')[0] || ""; - console.debug({ + this.logger.debug('Incoming request:', { fullUrl: req.url, method: req.method, serverName, @@ -40,14 +131,14 @@ class MCPGateway { }); if (!serverName || !this.config.servers[serverName] || !req.url) { - console.log('404: Server not found or invalid URL path'); + this.logger.warn('404: Server not found or invalid URL path'); res.writeHead(404).end(); return; } // Handle root server path - establish SSE connection if (req.method === "GET") { - console.log(`New SSE connection for ${serverName}`); + this.logger.info(`New SSE connection for ${serverName}`); try { const serverConfig = this.config.servers[serverName]; @@ -64,25 +155,27 @@ class MCPGateway { // Store server info this.servers.set(sessionId, server); + this.logger.debug(`Server instance created with sessionId: ${sessionId}`); // Bridge messages from STDIO to SSE server.stdioTransport.onmessage = (msg) => { + this.logger.verbose('STDIO -> SSE:', msg); sseTransport.send(msg); }; res.on('close', () => { - console.log(`SSE connection closed for ${sessionId}`); + this.logger.info(`SSE connection closed for ${sessionId}`); server.sseTransport.close(); server.stdioTransport.close(); this.servers.delete(sessionId); }); - console.log(`Starting transports for ${sessionId}`); + this.logger.info(`Starting transports for ${sessionId}`); await server.stdioTransport.start(); await server.sseTransport.start(); } catch (error) { - console.error('Error setting up SSE connection:', error); + this.logger.error('Error setting up SSE connection:', error); res.writeHead(500).end(String(error)); } return; @@ -107,6 +200,9 @@ class MCPGateway { try { // Parse and forward message to STDIO server const message = JSON.parse(body); + + this.logger.verbose('SSE -> STDIO:', message); + await server.stdioTransport.send(message); // Send success response