Merge pull request #38 from FabriBorgobello/feature/endpoint-filtering
feature: Add endpoint filtering
This commit is contained in:
commit
1c806b8dab
51
README.md
51
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
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
13
src/index.ts
13
src/index.ts
@ -30,6 +30,7 @@ import {
|
||||
|
||||
// Import types
|
||||
import { CliOptions, TransportType } from './types/index.js';
|
||||
import { normalizeBoolean } from './utils/helpers.js';
|
||||
import pkg from '../package.json' with { type: 'json' };
|
||||
|
||||
// Export programmatic API
|
||||
@ -72,6 +73,18 @@ program
|
||||
'Port for web or streamable-http transport (default: 3000)',
|
||||
(val) => parseInt(val, 10)
|
||||
)
|
||||
.option(
|
||||
'--default-include <boolean>',
|
||||
'Default behavior for x-mcp filtering (default: true = include by default, false = exclude by default)',
|
||||
(val) => {
|
||||
const parsed = normalizeBoolean(val);
|
||||
if (typeof parsed === 'boolean') return parsed;
|
||||
console.warn(
|
||||
`Invalid value for --default-include: "${val}". Expected true/false (case-insensitive). Using default: true.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
)
|
||||
.option('--force', 'Overwrite existing files without prompting')
|
||||
.version(pkg.version) // Match package.json version
|
||||
.action((options) => {
|
||||
|
||||
@ -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<string>();
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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,61 @@ 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 opRaw = (operation as any)['x-mcp'];
|
||||
const opVal = normalizeBoolean(opRaw);
|
||||
if (typeof opVal !== 'undefined') return opVal;
|
||||
if (typeof opRaw !== 'undefined') {
|
||||
console.warn(
|
||||
`Invalid x-mcp value on operation '${operation.operationId ?? '[no operationId]'}':`,
|
||||
opRaw,
|
||||
`-> expected boolean or 'true'/'false'. Falling back to path/root/default.`
|
||||
);
|
||||
}
|
||||
|
||||
const pathRaw = (pathItem as any)['x-mcp'];
|
||||
const pathVal = normalizeBoolean(pathRaw);
|
||||
if (typeof pathVal !== 'undefined') return pathVal;
|
||||
if (typeof pathRaw !== 'undefined') {
|
||||
console.warn(
|
||||
`Invalid x-mcp value on path item:`,
|
||||
pathRaw,
|
||||
`-> expected boolean or 'true'/'false'. Falling back to root/default.`
|
||||
);
|
||||
}
|
||||
|
||||
const rootRaw = (api as any)['x-mcp'];
|
||||
const rootVal = normalizeBoolean(rootRaw);
|
||||
if (typeof rootVal !== 'undefined') return rootVal;
|
||||
if (typeof rootRaw !== 'undefined') {
|
||||
console.warn(
|
||||
`Invalid x-mcp value at API root:`,
|
||||
rootRaw,
|
||||
`-> expected boolean or 'true'/'false'. Falling back to defaultInclude=${defaultInclude}.`
|
||||
);
|
||||
}
|
||||
|
||||
return defaultInclude;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user