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.
This commit is contained in:
iwyrkore 2025-08-26 15:56:40 -04:00
parent f29c277860
commit f2a580a0a8
11 changed files with 86 additions and 37 deletions

View File

@ -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

View File

@ -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

38
package-lock.json generated
View File

@ -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"

View File

@ -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"
}

View File

@ -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',

View File

@ -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';

View File

@ -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<Response> {
// 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);

View File

@ -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) => {

View File

@ -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;
}
/**

View File

@ -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<CallToolResult> => {
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest, extra: RequestHandlerExtra<ServerRequest, ServerNotification>): Promise<CallToolResult> => {
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);
});
`;
}

View File

@ -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<string, any>
allSecuritySchemes: Record<string, any>,
sessionHeaders: IsomorphicHeaders | undefined
): Promise<CallToolResult> {
try {
// Validate arguments against the input schema