From b1e29c22de601f7ec8964ff2a603a9eca466af3a Mon Sep 17 00:00:00 2001 From: Fabricio Borgobello Date: Fri, 22 Aug 2025 11:00:13 +0200 Subject: [PATCH] feature/endpoint-filtering --- README.md | 51 +++++++++++++++++++++++++++++------- package-lock.json | 4 +-- src/api.ts | 5 +++- src/generator/server-code.ts | 2 +- src/index.ts | 5 ++++ src/parser/extract-tools.ts | 28 +++++++++++++++++++- src/types/index.ts | 2 ++ src/utils/helpers.ts | 35 +++++++++++++++++++++++++ 8 files changed, 117 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index bbbaff2..6e26726 100644 --- a/README.md +++ b/README.md @@ -48,16 +48,17 @@ 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` | -| `--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 (true=include by default, false=exclude by default) | `true` | +| `--force` | | Overwrite existing files in the output directory without confirmation | `false` | ## 📦 Programmatic API @@ -167,6 +168,36 @@ Configure auth credentials in your environment: --- +## 🔎 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. + +- Extension: `x-mcp: true | false` +- Default: `true` (include by default) +- Precedence: operation > path > root (first non-undefined wins) +- CLI option: `--default-include false` to change default to exclude by default + +Examples: + +```yaml +# Optional root-level default +x-mcp: true + +paths: + /pets: + x-mcp: false # exclude all ops under /pets + get: + x-mcp: true # include this operation anyway + + /users/{id}: + get: + # no x-mcp -> included by default +``` + +This uses standard OpenAPI extensions (x-… fields). See the OpenAPI Extensions guide for details: https://swagger.io/docs/specification/v3_0/openapi-extensions/ + +--- + ## ▶️ Running the Generated Server ```bash diff --git a/package-lock.json b/package-lock.json index 3ea9773..d5a35d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openapi-mcp-generator", - "version": "3.1.2", + "version": "3.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openapi-mcp-generator", - "version": "3.1.2", + "version": "3.1.4", "license": "MIT", "dependencies": { "@apidevtools/swagger-parser": "^10.1.1", diff --git a/src/api.ts b/src/api.ts index 7194000..835cabd 100644 --- a/src/api.ts +++ b/src/api.ts @@ -24,6 +24,9 @@ export interface GetToolsOptions { /** Optional filter function to exclude tools based on custom criteria */ filterFn?: (tool: McpToolDefinition) => boolean; + + /** Default behavior for x-mcp filtering (default: true = include by default) */ + defaultInclude?: boolean; } /** @@ -44,7 +47,7 @@ export async function getToolsFromOpenApi( : ((await SwaggerParser.parse(specPathOrUrl)) as OpenAPIV3.Document); // Extract tools from the API - const allTools = extractToolsFromApi(api); + const allTools = extractToolsFromApi(api, options.defaultInclude ?? true); // Add base URL to each tool const baseUrl = determineBaseUrl(api, options.baseUrl); diff --git a/src/generator/server-code.ts b/src/generator/server-code.ts index 3059591..6b6a447 100644 --- a/src/generator/server-code.ts +++ b/src/generator/server-code.ts @@ -25,7 +25,7 @@ export function generateMcpServerCode( serverVersion: string ): string { // Extract tools from API - const tools = extractToolsFromApi(api); + const tools = extractToolsFromApi(api, options.defaultInclude ?? true); // Determine base URL const determinedBaseUrl = determineBaseUrl(api, options.baseUrl); diff --git a/src/index.ts b/src/index.ts index a528160..2a0fb60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,6 +72,11 @@ program 'Port for web or streamable-http transport (default: 3000)', (val) => parseInt(val, 10) ) + .option( + '--default-include ', + 'Default behavior for x-mcp filtering (default: true = include by default, false = exclude by default)', + (val) => (val === 'false' ? false : true) + ) .option('--force', 'Overwrite existing files without prompting') .version(pkg.version) // Match package.json version .action((options) => { diff --git a/src/parser/extract-tools.ts b/src/parser/extract-tools.ts index f50232e..9fd5bf5 100644 --- a/src/parser/extract-tools.ts +++ b/src/parser/extract-tools.ts @@ -5,6 +5,7 @@ import { OpenAPIV3 } from 'openapi-types'; import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema'; import { generateOperationId } from '../utils/code-gen.js'; import { McpToolDefinition } from '../types/index.js'; +import { shouldIncludeOperationForMcp } from '../utils/helpers.js'; /** * Extracts tool definitions from an OpenAPI document @@ -12,7 +13,10 @@ import { McpToolDefinition } from '../types/index.js'; * @param api OpenAPI document * @returns Array of MCP tool definitions */ -export function extractToolsFromApi(api: OpenAPIV3.Document): McpToolDefinition[] { +export function extractToolsFromApi( + api: OpenAPIV3.Document, + defaultInclude: boolean = true +): McpToolDefinition[] { const tools: McpToolDefinition[] = []; const usedNames = new Set(); const globalSecurity = api.security || []; @@ -26,6 +30,28 @@ export function extractToolsFromApi(api: OpenAPIV3.Document): McpToolDefinition[ const operation = pathItem[method]; if (!operation) continue; + // Apply x-mcp filtering, precedence: operation > path > root + try { + if ( + !shouldIncludeOperationForMcp( + api, + pathItem as OpenAPIV3.PathItemObject, + operation, + defaultInclude + ) + ) { + continue; + } + } catch (error) { + console.warn( + `Error evaluating x-mcp extension for operation ${operation.operationId || `${method} ${path}`}:`, + error + ); + if (!defaultInclude) { + continue; + } + } + // Generate a unique name for the tool let baseName = operation.operationId || generateOperationId(method, path); if (!baseName) continue; diff --git a/src/types/index.ts b/src/types/index.ts index c945341..d3b5b4b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -29,6 +29,8 @@ export interface CliOptions { transport?: TransportType; /** Server port (for web and streamable-http transports) */ port?: number; + /** Default behavior for x-mcp filtering (default: true = include by default) */ + defaultInclude?: boolean; } /** diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 2d11d5e..cce4425 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,6 +1,7 @@ /** * General helper utilities for OpenAPI to MCP generator */ +import { OpenAPIV3 } from 'openapi-types'; /** * Safely stringify a JSON object with proper error handling @@ -110,3 +111,37 @@ export function formatComment(str: string, maxLineLength: number = 80): string { return lines.join('\n * '); } + +/** + * Normalize a value to boolean if it looks like a boolean; otherwise undefined. + */ +export function normalizeBoolean(value: unknown): boolean | undefined { + if (typeof value === 'boolean') return value; + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + return normalized === 'true' ? true : normalized === 'false' ? false : undefined; + } + return undefined; +} + +/** + * Determine if an operation should be included in MCP generation based on x-mcp. + * Precedence: operation > path > root; uses provided default when all undefined. + */ +export function shouldIncludeOperationForMcp( + api: OpenAPIV3.Document, + pathItem: OpenAPIV3.PathItemObject, + operation: OpenAPIV3.OperationObject, + defaultInclude: boolean = true +): boolean { + const opVal = normalizeBoolean((operation as any)['x-mcp']); + if (typeof opVal !== 'undefined') return opVal; + + const pathVal = normalizeBoolean((pathItem as any)['x-mcp']); + if (typeof pathVal !== 'undefined') return pathVal; + + const rootVal = normalizeBoolean((api as any)['x-mcp']); + if (typeof rootVal !== 'undefined') return rootVal; + + return defaultInclude; // use provided default +}