diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a69546..4368f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- CLI option `--passthrough-auth` to forward authentication headers from MCP requests to the downstream API, per the OpenAPI security scheme. Supports http (bearer/basic), apiKey (header/query/cookie), and OpenID Connect bearer tokens. Works for SSE and StreamableHTTP transports. Requires `@modelcontextprotocol/sdk` ^1.17.4. + + ## [3.2.0] - 2025-08-24 ### Added diff --git a/README.md b/README.md index 7ba4323..dea22ee 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,18 @@ openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir - ### CLI Options -| Option | Alias | Description | Default | -| ------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -| `--input` | `-i` | Path or URL to OpenAPI specification (YAML or JSON) | **Required** | -| `--output` | `-o` | Directory to output the generated MCP project | **Required** | -| `--server-name` | `-n` | Name of the MCP server (`package.json:name`) | OpenAPI title or `mcp-api-server` | -| `--server-version` | `-v` | Version of the MCP server (`package.json:version`) | OpenAPI version or `1.0.0` | -| `--base-url` | `-b` | Base URL for API requests. Required if OpenAPI `servers` missing or ambiguous. | Auto-detected if possible | -| `--transport` | `-t` | Transport mode: `"stdio"` (default), `"web"`, or `"streamable-http"` | `"stdio"` | -| `--port` | `-p` | Port for web-based transports | `3000` | -| `--default-include` | | Default behavior for x-mcp filtering. Accepts `true` or `false` (case-insensitive). `true` = include by default, `false` = exclude by default. | `true` | -| `--force` | | Overwrite existing files in the output directory without confirmation | `false` | +| Option | Alias | Description | Default | +| -------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `--input` | `-i` | Path or URL to OpenAPI specification (YAML or JSON) | **Required** | +| `--output` | `-o` | Directory to output the generated MCP project | **Required** | +| `--server-name` | `-n` | Name of the MCP server (`package.json:name`) | OpenAPI title or `mcp-api-server` | +| `--server-version` | `-v` | Version of the MCP server (`package.json:version`) | OpenAPI version or `1.0.0` | +| `--base-url` | `-b` | Base URL for API requests. Required if OpenAPI `servers` missing or ambiguous. | Auto-detected if possible | +| `--transport` | `-t` | Transport mode: `"stdio"` (default), `"web"`, or `"streamable-http"` | `"stdio"` | +| `--port` | `-p` | Port for web-based transports | `3000` | +| `--default-include` | | Default behavior for x-mcp filtering. Accepts `true` or `false` (case-insensitive). `true` = include by default, `false` = exclude by default. | `true` | +| `--force` | | Overwrite existing files in the output directory without confirmation | `false` | +| `--passthrough-auth` | | Forward auth headers in MCP requests to the downstream API, as specified by the OpenAPI spec. | `false` | ## 📦 Programmatic API @@ -125,6 +126,7 @@ Launches a fully functional HTTP server with: - In-browser test client UI - Multi-connection support - Built with lightweight Hono framework +- Optional pass-through auth headers ### StreamableHTTP @@ -137,6 +139,7 @@ Implements the MCP StreamableHTTP transport which offers: - Compatibility with MCP StreamableHTTPClientTransport - In-browser test client UI - Built with lightweight Hono framework +- Optional pass-through auth headers ### Transport Comparison @@ -151,6 +154,7 @@ Implements the MCP StreamableHTTP transport which offers: | Load balancing | No | Limited | Yes | | Status codes | No | Limited | Full HTTP codes | | Headers | No | Limited | Full HTTP headers | +| Pass-through Auth | No | Optional | Optional | | Test client | No | Yes | Yes | --- @@ -168,6 +172,33 @@ Configure auth credentials in your environment: --- +## 🔐 Pass-through Headers for Authentication + +Use the CLI option `--passthrough-auth` to have the server pass-through client auth headers to the downstream API. The headers forwarded are for the auth schemes defined in the OpenAPI spec. Scheme types http (bearer or basic), apiKey (header, query param, or cookie), and openIdConnect bearer tokens are supported. + +The client should configure the auth credentials to be sent, for example: + +``` +"mcpServers": { + "my-api": { + "transport": "HTTP", + "url": "http://localhost:3000/sse", + "headers": { + "Authorization": "Bearer MY_TOKEN" + } + }, + "my-other-api": { + "transport": "Streamable-HTTP", + "url": "http://localhost:4000/mcp", + "headers": { + "X-API-Key": "MY_API_KEY" + } + }, +} +``` + +--- + ## 🔎 Filtering Endpoints with OpenAPI Extensions You can control which operations are exposed as MCP tools using a vendor extension flag `x-mcp`. This extension is supported at the root, path, and operation levels. By default, endpoints are included unless explicitly excluded. diff --git a/examples/pet-store-sse/package-lock.json b/examples/pet-store-sse/package-lock.json index 7dcd70d..ae813e4 100644 --- a/examples/pet-store-sse/package-lock.json +++ b/examples/pet-store-sse/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.26", "dependencies": { "@hono/node-server": "^1.14.1", - "@modelcontextprotocol/sdk": "^1.10.0", + "@modelcontextprotocol/sdk": "^1.17.4", "axios": "^1.9.0", "dotenv": "^16.4.5", "hono": "^4.7.7", @@ -39,15 +39,16 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", - "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", - "license": "MIT", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.4.tgz", + "integrity": "sha512-zq24hfuAmmlNZvik0FLI58uE5sriN0WWsQzIlYnzSuKDAHFqJtBFrl/LfB1NLgJT5Y7dEBzaX4yAKqOPrcetaw==", "dependencies": { + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -89,6 +90,21 @@ "node": ">= 0.6" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -456,6 +472,16 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -717,6 +743,11 @@ "json-schema-to-zod": "dist/cjs/cli.js" } }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -880,6 +911,14 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -1159,6 +1198,14 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/examples/pet-store-sse/package.json b/examples/pet-store-sse/package.json index d94f636..de9c201 100644 --- a/examples/pet-store-sse/package.json +++ b/examples/pet-store-sse/package.json @@ -20,7 +20,7 @@ "node": ">=20.0.0" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.10.0", + "@modelcontextprotocol/sdk": "^1.17.4", "axios": "^1.9.0", "dotenv": "^16.4.5", "zod": "^3.24.3", diff --git a/examples/pet-store-streamable-http/package-lock.json b/examples/pet-store-streamable-http/package-lock.json index 14a6448..fd08f14 100644 --- a/examples/pet-store-streamable-http/package-lock.json +++ b/examples/pet-store-streamable-http/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.26", "dependencies": { "@hono/node-server": "^1.14.1", - "@modelcontextprotocol/sdk": "^1.10.0", + "@modelcontextprotocol/sdk": "^1.17.4", "axios": "^1.9.0", "dotenv": "^16.4.5", "fetch-to-node": "^2.1.0", @@ -40,15 +40,16 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", - "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", - "license": "MIT", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.4.tgz", + "integrity": "sha512-zq24hfuAmmlNZvik0FLI58uE5sriN0WWsQzIlYnzSuKDAHFqJtBFrl/LfB1NLgJT5Y7dEBzaX4yAKqOPrcetaw==", "dependencies": { + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -90,6 +91,21 @@ "node": ">= 0.6" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -457,6 +473,16 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, "node_modules/fetch-to-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fetch-to-node/-/fetch-to-node-2.1.0.tgz", @@ -724,6 +750,11 @@ "json-schema-to-zod": "dist/cjs/cli.js" } }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -887,6 +918,14 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -1166,6 +1205,14 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/examples/pet-store-streamable-http/package.json b/examples/pet-store-streamable-http/package.json index efa8fa6..da110bf 100644 --- a/examples/pet-store-streamable-http/package.json +++ b/examples/pet-store-streamable-http/package.json @@ -22,7 +22,7 @@ "node": ">=20.0.0" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.10.0", + "@modelcontextprotocol/sdk": "^1.17.4", "axios": "^1.9.0", "dotenv": "^16.4.5", "zod": "^3.24.3", diff --git a/examples/petstore-mcp/package.json b/examples/petstore-mcp/package.json index cbb49da..9c9edbf 100644 --- a/examples/petstore-mcp/package.json +++ b/examples/petstore-mcp/package.json @@ -19,7 +19,7 @@ "node": ">=18.0.0" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.9.0", + "@modelcontextprotocol/sdk": "^1.17.4", "axios": "^1.8.4", "json-schema-to-zod": "^2.6.1", "zod": "^3.24.2" diff --git a/package-lock.json b/package-lock.json index ca49844..079b444 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.10.2", + "@modelcontextprotocol/sdk": "^1.17.4", "json-schema-to-zod": "^2.6.1", "zod": "^3.24.3" } @@ -385,16 +385,17 @@ "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", - "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", - "license": "MIT", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.4.tgz", + "integrity": "sha512-zq24hfuAmmlNZvik0FLI58uE5sriN0WWsQzIlYnzSuKDAHFqJtBFrl/LfB1NLgJT5Y7dEBzaX4yAKqOPrcetaw==", "peer": true, "dependencies": { + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -406,6 +407,28 @@ "node": ">=18" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "peer": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1540,7 +1563,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -2495,7 +2517,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3091,7 +3112,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" diff --git a/package.json b/package.json index c05d2ce..628530d 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "typescript": "^5.9.2" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.10.2", + "@modelcontextprotocol/sdk": "^1.17.4", "json-schema-to-zod": "^2.6.1", "zod": "^3.24.3" } diff --git a/src/generator/package-json.ts b/src/generator/package-json.ts index ef97985..b42c491 100644 --- a/src/generator/package-json.ts +++ b/src/generator/package-json.ts @@ -31,7 +31,7 @@ export function generatePackageJson( node: '>=20.0.0', }, dependencies: { - '@modelcontextprotocol/sdk': '^1.10.0', + '@modelcontextprotocol/sdk': '^1.17.4', axios: '^1.9.0', dotenv: '^16.4.5', zod: '^3.24.3', diff --git a/src/generator/server-code.ts b/src/generator/server-code.ts index 6b6a447..c558f28 100644 --- a/src/generator/server-code.ts +++ b/src/generator/server-code.ts @@ -39,7 +39,7 @@ export function generateMcpServerCode( ); // Generate code for request handlers - const callToolHandlerCode = generateCallToolHandler(); + const callToolHandlerCode = generateCallToolHandler(options.passthroughAuth); const listToolsHandlerCode = generateListToolsHandler(); // Determine which transport to include @@ -99,8 +99,12 @@ import { ListToolsRequestSchema, type Tool, type CallToolResult, - type CallToolRequest + type CallToolRequest, + ServerRequest, + ServerNotification, + IsomorphicHeaders } from "@modelcontextprotocol/sdk/types.js";${transportImport} +import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import { z, ZodError } from 'zod'; import { jsonSchemaToZod } from 'json-schema-to-zod'; diff --git a/src/generator/web-server.ts b/src/generator/web-server.ts index 489a563..cab51ce 100644 --- a/src/generator/web-server.ts +++ b/src/generator/web-server.ts @@ -19,7 +19,7 @@ import { serve } from '@hono/node-server'; import { streamSSE } from 'hono/streaming'; import { v4 as uuid } from 'uuid'; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { JSONRPCMessage, JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js"; +import { JSONRPCMessage, JSONRPCMessageSchema, MessageExtraInfo } from "@modelcontextprotocol/sdk/types.js"; import type { Context } from 'hono'; import type { SSEStreamingApi } from 'hono/streaming'; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; @@ -37,7 +37,7 @@ private messageUrl: string; onclose?: () => void; onerror?: (error: Error) => void; -onmessage?: (message: JSONRPCMessage) => void; +onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; constructor(messageUrl: string, stream: SSEStreamingApi) { this._sessionId = uuid(); @@ -105,7 +105,7 @@ async handlePostMessage(c: Context): Promise { // Forward to the message handler if (this.onmessage) { - this.onmessage(parsedMessage); + this.onmessage(parsedMessage, {requestInfo: {headers: c.req.header()}}); return c.text('Accepted', 202); } else { return c.text('No message handler defined', 500); diff --git a/src/index.ts b/src/index.ts index d588a24..47e1c43 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,7 @@ program }, true ) + .option('--passthrough-auth', 'Pass through authentication headers to the API') .option('--force', 'Overwrite existing files without prompting') .version(pkg.version) // Match package.json version .action((options) => { diff --git a/src/types/index.ts b/src/types/index.ts index e36eb3b..c4c8246 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,6 +35,8 @@ export interface CliOptions { * false = exclude by default unless x-mcp explicitly enables. */ defaultInclude?: boolean; + /** Whether to pass through authentication headers to the API. Defaults to false. */ + passthroughAuth?: boolean; } /** diff --git a/src/utils/code-gen.ts b/src/utils/code-gen.ts index ae48f1d..0f5ff48 100644 --- a/src/utils/code-gen.ts +++ b/src/utils/code-gen.ts @@ -87,16 +87,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { * * @returns Generated code for the call tool handler */ -export function generateCallToolHandler(): string { +export function generateCallToolHandler(passthroughAuth: boolean | undefined): string { return ` -server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise => { +server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest, extra: RequestHandlerExtra): Promise => { const { name: toolName, arguments: toolArgs } = request.params; const toolDefinition = toolDefinitionMap.get(toolName); if (!toolDefinition) { console.error(\`Error: Unknown tool requested: \${toolName}\`); return { content: [{ type: "text", text: \`Error: Unknown tool requested: \${toolName}\` }] }; } - return await executeApiTool(toolName, toolDefinition, toolArgs ?? {}, securitySchemes); + let sessionHeaders = ${passthroughAuth ? 'extra.requestInfo?.headers' : 'undefined'}; + return await executeApiTool(toolName, toolDefinition, toolArgs ?? {}, securitySchemes, sessionHeaders); }); `; } diff --git a/src/utils/security.ts b/src/utils/security.ts index c6ecbc9..6dbd1f9 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -208,6 +208,11 @@ export function generateExecuteApiToolFunction( // Generate security handling code for checking, applying security const securityCode = ` + function getHeaderValue(headers: IsomorphicHeaders | undefined, key: string): string | undefined { + const value = headers?.[key]; + return Array.isArray(value) ? value[0] : value; + } + // Apply security requirements if available // Security requirements use OR between array items and AND within each object const appliedSecurity = definition.securityRequirements?.find(req => { @@ -218,17 +223,18 @@ export function generateExecuteApiToolFunction( // API Key security (header, query, cookie) if (scheme.type === 'apiKey') { - return !!process.env[\`API_KEY_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + return !!(getHeaderValue(sessionHeaders,scheme.name.toLowerCase()) || process.env[\`API_KEY_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]); } // HTTP security (basic, bearer) if (scheme.type === 'http') { if (scheme.scheme?.toLowerCase() === 'bearer') { - return !!process.env[\`BEARER_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + return !!(getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ') || process.env[\`BEARER_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]); } else if (scheme.scheme?.toLowerCase() === 'basic') { - return !!process.env[\`BASIC_USERNAME_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`] && - !!process.env[\`BASIC_PASSWORD_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + return (!!(getHeaderValue(sessionHeaders,'authorization')?.startsWith('Basic ') || + !!process.env[\`BASIC_USERNAME_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`] && + !!process.env[\`BASIC_PASSWORD_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`])); } } @@ -253,7 +259,7 @@ export function generateExecuteApiToolFunction( // OpenID Connect if (scheme.type === 'openIdConnect') { - return !!process.env[\`OPENID_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + return !!(getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ') || process.env[\`OPENID_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]); } return false; @@ -268,7 +274,7 @@ export function generateExecuteApiToolFunction( // API Key security if (scheme?.type === 'apiKey') { - const apiKey = process.env[\`API_KEY_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + const apiKey = getHeaderValue(sessionHeaders,scheme.name.toLowerCase()) ?? process.env[\`API_KEY_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; if (apiKey) { if (scheme.in === 'header') { headers[scheme.name.toLowerCase()] = apiKey; @@ -288,18 +294,28 @@ export function generateExecuteApiToolFunction( // HTTP security (Bearer or Basic) else if (scheme?.type === 'http') { if (scheme.scheme?.toLowerCase() === 'bearer') { - const token = process.env[\`BEARER_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; - if (token) { - headers['authorization'] = \`Bearer \${token}\`; - console.error(\`Applied Bearer token for '\${schemeName}'\`); + if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')) { + headers['authorization'] = getHeaderValue(sessionHeaders,'authorization')!; + console.error(\`Applied Bearer token for '\${schemeName}' from session headers\`); + } else { + const token = process.env[\`BEARER_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + if (token) { + headers['authorization'] = \`Bearer \${token}\`; + console.error(\`Applied Bearer token for '\${schemeName}'\`); + } } } else if (scheme.scheme?.toLowerCase() === 'basic') { - const username = process.env[\`BASIC_USERNAME_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; - const password = process.env[\`BASIC_PASSWORD_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; - if (username && password) { - headers['authorization'] = \`Basic \${Buffer.from(\`\${username}:\${password}\`).toString('base64')}\`; - console.error(\`Applied Basic authentication for '\${schemeName}'\`); + if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Basic ')) { + headers['authorization'] = getHeaderValue(sessionHeaders,'authorization')!; + console.error(\`Applied Basic authentication for '\${schemeName}' from session headers\`); + } else { + const username = process.env[\`BASIC_USERNAME_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + const password = process.env[\`BASIC_PASSWORD_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + if (username && password) { + headers['authorization'] = \`Basic \${Buffer.from(\`\${username}:\${password}\`).toString('base64')}\`; + console.error(\`Applied Basic authentication for '\${schemeName}'\`); + } } } } @@ -328,15 +344,19 @@ export function generateExecuteApiToolFunction( } // OpenID Connect else if (scheme?.type === 'openIdConnect') { - const token = process.env[\`OPENID_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; - if (token) { - headers['authorization'] = \`Bearer \${token}\`; - console.error(\`Applied OpenID Connect token for '\${schemeName}'\`); - - // List the scopes that were requested, if any - const scopes = scopesArray as string[]; - if (scopes && scopes.length > 0) { - console.error(\`Requested scopes: \${scopes.join(', ')}\`); + if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')) { + headers['authorization'] = getHeaderValue(sessionHeaders,'authorization')!; + console.error(\`Applied OpenID Connect token for '\${schemeName}' from session headers\`); + } else { + const token = process.env[\`OPENID_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]; + if (token) { + headers['authorization'] = \`Bearer \${token}\`; + console.error(\`Applied OpenID Connect token for '\${schemeName}'\`); + // List the scopes that were requested, if any + const scopes = scopesArray as string[]; + if (scopes && scopes.length > 0) { + console.error(\`Requested scopes: \${scopes.join(', ')}\`); + } } } } @@ -379,7 +399,8 @@ async function executeApiTool( toolName: string, definition: McpToolDefinition, toolArgs: JsonObject, - allSecuritySchemes: Record + allSecuritySchemes: Record, + sessionHeaders: IsomorphicHeaders | undefined ): Promise { try { // Validate arguments against the input schema