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