From f2a580a0a896e23ee2cd6d754823a73a648e55fb Mon Sep 17 00:00:00 2001 From: iwyrkore Date: Tue, 26 Aug 2025 15:56:40 -0400 Subject: [PATCH 1/5] Added passthroughAuth option, --passthrough-auth on the commandline. If passthroughAuth is true, auth headers in MCP call tool requests are passed through to the API requests as specified by the OpenAPI spec. Scheme types http (bearer or basic), apiKey (header, query param, or cookie), and openIdConnect bearer tokens (passed through) as supported. --- CHANGELOG.md | 4 ++++ README.md | 23 +++++++++++---------- package-lock.json | 38 ++++++++++++++++++++++++++--------- package.json | 2 +- src/generator/package-json.ts | 2 +- src/generator/server-code.ts | 8 ++++++-- src/generator/web-server.ts | 6 +++--- src/index.ts | 1 + src/types/index.ts | 2 ++ src/utils/code-gen.ts | 7 ++++--- src/utils/security.ts | 30 ++++++++++++++++++++------- 11 files changed, 86 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a69546..9408801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ 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). +### Added + +- passthrough-auth option to pass through auth info in MCP request headers to the API, as specified by the openapi spec. + ## [3.2.0] - 2025-08-24 ### Added diff --git a/README.md b/README.md index 7ba4323..b8e32f6 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` | | pass through auth info in MCP request headers to the API, as specified by the openapi spec. | `false` | ## 📦 Programmatic API 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..8a4826e 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 */ + 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..1964158 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 !!(process.env[\`API_KEY_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`] || getHeaderValue(sessionHeaders,scheme.name.toLowerCase())); } // 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 !!(process.env[\`BEARER_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`] || getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')); } 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 (!!(process.env[\`BASIC_USERNAME_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`] && + !!process.env[\`BASIC_PASSWORD_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]) || + getHeaderValue(sessionHeaders,'authorization')?.startsWith('Basic ')); } } @@ -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 !!(process.env[\`OPENID_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`] || getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')); } 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 = (process.env[\`API_KEY_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`] || getHeaderValue(sessionHeaders,scheme.name.toLowerCase())); if (apiKey) { if (scheme.in === 'header') { headers[scheme.name.toLowerCase()] = apiKey; @@ -292,6 +298,9 @@ export function generateExecuteApiToolFunction( if (token) { headers['authorization'] = \`Bearer \${token}\`; console.error(\`Applied Bearer token for '\${schemeName}'\`); + } else if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')) { + headers['authorization'] = getHeaderValue(sessionHeaders,'authorization')!; + console.error(\`Applied Bearer token for '\${schemeName}' from session headers\`); } } else if (scheme.scheme?.toLowerCase() === 'basic') { @@ -300,6 +309,9 @@ export function generateExecuteApiToolFunction( if (username && password) { headers['authorization'] = \`Basic \${Buffer.from(\`\${username}:\${password}\`).toString('base64')}\`; console.error(\`Applied Basic authentication for '\${schemeName}'\`); + } else if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Basic ')) { + headers['authorization'] = getHeaderValue(sessionHeaders,'authorization')!; + console.error(\`Applied Basic authentication for '\${schemeName}' from session headers\`); } } } @@ -338,6 +350,9 @@ export function generateExecuteApiToolFunction( if (scopes && scopes.length > 0) { console.error(\`Requested scopes: \${scopes.join(', ')}\`); } + } else if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')) { + headers['authorization'] = getHeaderValue(sessionHeaders,'authorization')!; + console.error(\`Applied OpenID Connect token for '\${schemeName}' from session headers\`); } } } @@ -379,7 +394,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 From 0fa27c308195502e64e2a2d22a1bba746475df1f Mon Sep 17 00:00:00 2001 From: iwyrkore Date: Tue, 26 Aug 2025 17:07:41 -0400 Subject: [PATCH 2/5] Updated examples modelcontextprotocol/sdk to version 1.17.4. --- examples/pet-store-sse/package-lock.json | 59 +++++++++++++++++-- examples/pet-store-sse/package.json | 2 +- .../package-lock.json | 59 +++++++++++++++++-- .../pet-store-streamable-http/package.json | 2 +- examples/petstore-mcp/package.json | 2 +- 5 files changed, 109 insertions(+), 15 deletions(-) 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" From d3059af8d8acf807092dd053af34494d523cbb7c Mon Sep 17 00:00:00 2001 From: iwyrkore Date: Tue, 26 Aug 2025 17:26:06 -0400 Subject: [PATCH 3/5] Minor changes recommended by coderabbitai. --- CHANGELOG.md | 5 ++++- README.md | 2 +- src/types/index.ts | 2 +- src/utils/security.ts | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9408801..4368f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,12 @@ 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 -- passthrough-auth option to pass through auth info in MCP request headers to the API, as specified by the openapi spec. +- 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 diff --git a/README.md b/README.md index b8e32f6..27da799 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir - | `--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` | | pass through auth info in MCP request headers to the API, as specified by the openapi spec. | `false` | +| `--passthrough-auth` | | Forward auth headers in MCP requests to the downstream API, as specified by the OpenAPI spec. | `false` | ## 📦 Programmatic API diff --git a/src/types/index.ts b/src/types/index.ts index 8a4826e..c4c8246 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,7 +35,7 @@ export interface CliOptions { * false = exclude by default unless x-mcp explicitly enables. */ defaultInclude?: boolean; - /** Whether to pass through authentication headers to the API */ + /** Whether to pass through authentication headers to the API. Defaults to false. */ passthroughAuth?: boolean; } diff --git a/src/utils/security.ts b/src/utils/security.ts index 1964158..17003b1 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -274,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()}\`] || getHeaderValue(sessionHeaders,scheme.name.toLowerCase())); + const apiKey = process.env[\`API_KEY_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`] ?? getHeaderValue(sessionHeaders,scheme.name.toLowerCase()); if (apiKey) { if (scheme.in === 'header') { headers[scheme.name.toLowerCase()] = apiKey; From 3b1700c36752dc20b51b89097a81acabacec8dee Mon Sep 17 00:00:00 2001 From: iwyrkore Date: Tue, 26 Aug 2025 17:53:55 -0400 Subject: [PATCH 4/5] Updated README. --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 27da799..dea22ee 100644 --- a/README.md +++ b/README.md @@ -126,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 @@ -138,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 @@ -152,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 | --- @@ -169,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. From 04a2c9c7bfe7e52cab261d790e707af5802b7357 Mon Sep 17 00:00:00 2001 From: iwyrkore Date: Tue, 26 Aug 2025 22:31:17 -0400 Subject: [PATCH 5/5] Auth headers (if they exist) take precedence over env var auth. --- src/utils/security.ts | 63 +++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/src/utils/security.ts b/src/utils/security.ts index 17003b1..6dbd1f9 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -223,18 +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()}\`] || getHeaderValue(sessionHeaders,scheme.name.toLowerCase())); + 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()}\`] || getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')); + 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()}\`]) || - getHeaderValue(sessionHeaders,'authorization')?.startsWith('Basic ')); + 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()}\`])); } } @@ -259,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()}\`] || getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')); + return !!(getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ') || process.env[\`OPENID_TOKEN_\${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}\`]); } return false; @@ -274,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()}\`] ?? getHeaderValue(sessionHeaders,scheme.name.toLowerCase()); + 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; @@ -294,24 +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}'\`); - } else if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')) { + 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}'\`); - } else if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Basic ')) { + 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}'\`); + } } } } @@ -340,19 +344,20 @@ 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(', ')}\`); - } - } else if (getHeaderValue(sessionHeaders,'authorization')?.startsWith('Bearer ')) { + 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(', ')}\`); + } + } } } }