Compare commits
34 Commits
coderabbit
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee9fc383d | ||
|
|
f29c277860 | ||
|
|
33220c1e82 | ||
|
|
eda4505a63 | ||
|
|
4bf66d9efd | ||
|
|
7a31e1f6e9 | ||
|
|
82ff2b726d | ||
|
|
1c806b8dab | ||
|
|
c9015f395e | ||
|
|
af1b664653 | ||
|
|
1f001bb47a | ||
|
|
b1e29c22de | ||
|
|
e6352d13b6 | ||
|
|
26307f26ad | ||
|
|
b7bc67e444 | ||
|
|
47292c89cf | ||
|
|
2cab8aeada | ||
|
|
ea89e0498e | ||
|
|
34a1f6df12 | ||
|
|
e17ab1a3d0 | ||
|
|
0ca1310c56 | ||
|
|
aa388035ad | ||
|
|
c98d652933 | ||
|
|
9c6fed9beb | ||
|
|
67e06c3c34 | ||
|
|
df758359e8 | ||
|
|
78c3d91ec2 | ||
|
|
4f7417890f | ||
|
|
aaf8613216 | ||
|
|
4c84306814 | ||
|
|
e2fd79c2ec | ||
|
|
2c6c05988e | ||
|
|
c607d9c759 | ||
|
|
6141d2e8ae |
@ -1,18 +1,15 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2022": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
||||
}
|
||||
}
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2022": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
||||
}
|
||||
}
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
7
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
44
.github/PULL_REQUEST_TEMPLATE/mcpcat.md
vendored
Normal file
44
.github/PULL_REQUEST_TEMPLATE/mcpcat.md
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
# Add optional MCPcat analytics scaffolding (`--with-mcpcat`) to generated MCP servers
|
||||
|
||||
> **TL;DR**: This PR adds an **opt-in** flag to scaffold privacy-safe analytics wiring for MCPcat in projects generated by `openapi-mcp-generator`.
|
||||
|
||||
## Summary
|
||||
This PR introduces a `--with-mcpcat` CLI flag that scaffolds:
|
||||
- A tiny analytics shim to emit initialize/tool-call events.
|
||||
- A default **local redaction** helper to scrub sensitive data before export.
|
||||
- Minimal config via environment variables.
|
||||
No behavior changes unless the flag and env vars are set.
|
||||
|
||||
## Motivation
|
||||
- Make freshly generated MCP servers **observable in minutes**.
|
||||
- Encourage **privacy-by-default** analytics patterns.
|
||||
- Reduce copy/paste wiring; standardize event shape (operationId, path, duration, status).
|
||||
|
||||
## Changes
|
||||
### CLI
|
||||
- `generate` accepts `--with-mcpcat` (default: off).
|
||||
|
||||
### Template files (added conditionally)
|
||||
- `src/analytics/mcpcat.ts` – lazy import + safe no-op if SDK absent.
|
||||
- `src/analytics/redact.ts` – OpenAPI-aware heuristics (e.g., `*token*`, `password`, `apiKey`, `authorization`, `email`).
|
||||
- `src/analytics/config.ts` – reads env:
|
||||
- `MCPCAT_ENABLED=true|false` (default `false`)
|
||||
- `MCPCAT_PROJECT_ID=<id>`
|
||||
- `MCPCAT_ENDPOINT=<optional override>`
|
||||
- `MCPCAT_SAMPLE_RATE=1.0` (0–1)
|
||||
|
||||
### Server wiring
|
||||
- Hooks server `.initialize` and each tool invocation to record:
|
||||
- `operationId`, HTTP `method`, `path`
|
||||
- redacted `args`
|
||||
- `outcome` (`ok`/`error`) + truncated error message
|
||||
- `duration_ms`
|
||||
|
||||
### Docs
|
||||
- Adds a “Enable analytics (MCPcat)” section to generated README with privacy notes and quickstart.
|
||||
|
||||
## Implementation Notes
|
||||
- **Compile-time optional**: no imports unless flag is used.
|
||||
- **Runtime safe**: try/catch around SDK import → graceful no-op if not installed.
|
||||
- **Transport-agnostic**: compatible with stdio, SSE/web, and StreamableHTTP templates.
|
||||
- **Edge-friendly**: avoids Node-only APIs in scaffolding to support edge runtimes (e.g., Workers).
|
||||
30
.github/workflows/check.yml
vendored
Normal file
30
.github/workflows/check.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
format-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Format check
|
||||
run: npm run format.check
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
5
.github/workflows/npm-publish.yml
vendored
5
.github/workflows/npm-publish.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm' # Enables npm dependency caching
|
||||
cache: 'npm' # Enables npm dependency caching
|
||||
cache-dependency-path: '**/package-lock.json' # Cache key based on lockfile
|
||||
|
||||
- name: Install dependencies
|
||||
@ -35,9 +35,8 @@ jobs:
|
||||
- name: Check version change
|
||||
id: check_version
|
||||
run: |
|
||||
git fetch origin main
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
PREV_VERSION=$(git show origin/main:package.json | grep '"version":' | sed -E 's/.*"version": *"([^"]+)".*/\1/')
|
||||
PREV_VERSION=$(git show HEAD^:package.json | grep '"version":' | sed -E 's/.*"version": *"([^"]+)".*/\1/')
|
||||
if [ "$CURRENT_VERSION" != "$PREV_VERSION" ]; then
|
||||
echo "Version changed from $PREV_VERSION to $CURRENT_VERSION"
|
||||
echo "version_changed=true" >> $GITHUB_OUTPUT
|
||||
|
||||
12
.prettierrc
12
.prettierrc
@ -1,7 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
|
||||
80
CHANGELOG.md
80
CHANGELOG.md
@ -5,9 +5,64 @@ 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).
|
||||
|
||||
## [3.2.0] - 2025-08-24
|
||||
|
||||
### Added
|
||||
|
||||
- Endpoint filtering using `x-mcp` OpenAPI extension to control which operations are exposed as MCP tools
|
||||
- CLI option `--default-include` to change default behavior for endpoint inclusion
|
||||
- Precedence rules for `x-mcp` extension (operation > path > root level)
|
||||
- Enhanced programmatic API with `defaultInclude` option in `getToolsFromOpenApi`
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved documentation with examples for endpoint filtering and OpenAPI extensions.
|
||||
- Version bump to next minor release
|
||||
- Updated package version to reflect accumulated features and improvements
|
||||
|
||||
## [3.1.4] - 2025-06-18
|
||||
|
||||
### Chores
|
||||
|
||||
- Updated the application version to 3.1.4 and ensured the CLI displays the version dynamically.
|
||||
|
||||
### Style
|
||||
|
||||
- Improved code formatting for better readability.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Tool names now retain their original casing during extraction.
|
||||
|
||||
## [3.1.3] - 2025-06-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Cannot find the package after building and the problem during the building.
|
||||
|
||||
## [3.1.2] - 2025-06-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent stack overflow (RangeError: Maximum call stack size exceeded) when processing recursive or cyclic OpenAPI schemas (e.g., self-referencing objects).
|
||||
- Added cycle detection to schema mapping, ensuring robust handling of recursive structures.
|
||||
|
||||
## [3.1.1] - 2025-05-26
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced a new executable command-line script for easier usage in Unix-like environments.
|
||||
|
||||
### Changed
|
||||
|
||||
- Use new CLI entry point to use the new `bin/openapi-mcp-generator.js` file.
|
||||
- Updated build script to ensure the new CLI file has the correct permissions.
|
||||
- Refactored `index.ts` to streamline argument parsing and error handling.
|
||||
|
||||
## [3.1.0] - 2025-05-18
|
||||
|
||||
### Added
|
||||
|
||||
- Programmatic API to extract MCP tool definitions from OpenAPI specs
|
||||
- New exportable `getToolsFromOpenApi` function for direct integration in code
|
||||
- Advanced filtering capabilities for programmatic tool extraction
|
||||
@ -15,12 +70,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Updated README with programmatic API usage examples
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved module structure with better exports
|
||||
- Enhanced detection of module execution context
|
||||
|
||||
## [3.0.0] - 2025-04-26
|
||||
|
||||
### Added
|
||||
|
||||
- Streamable HTTP support for OpenAPI MCP generator, enabling efficient handling of large payloads and real-time data transfer.
|
||||
- Major architectural refactor to support streaming responses and requests.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Multiple bugs related to HTTP/HTTPS connection handling, stream closure, and error propagation in streaming scenarios.
|
||||
- Fixed resource leak issues on server aborts and client disconnects during streaming.
|
||||
|
||||
### Changed
|
||||
|
||||
- Major version bump due to breaking changes in API and internal structures to support streaming.
|
||||
- Updated documentation to reflect new streaming capabilities and usage instructions.
|
||||
- Enhanced performance and robustness of HTTP/HTTPS transport layers.
|
||||
|
||||
## [2.0.0] - 2025-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Runtime argument validation using Zod
|
||||
- JSON Schema to Zod schema conversion
|
||||
- Improved error handling and formatting
|
||||
@ -31,6 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Support for multiple content types
|
||||
|
||||
### Changed
|
||||
|
||||
- Simplified transport layer to only support stdio transport
|
||||
- Removed support for WebSocket and HTTP transports
|
||||
- Updated to use @modelcontextprotocol/sdk v1.9.0
|
||||
@ -40,6 +116,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- More robust OpenAPI schema processing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Path parameter resolution in URLs
|
||||
- Content-Type header handling
|
||||
- Response processing for different content types
|
||||
@ -49,8 +126,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [1.0.0] - Initial Release
|
||||
|
||||
### Added
|
||||
|
||||
- Basic OpenAPI to MCP server generation
|
||||
- Support for GET, POST, PUT, DELETE methods
|
||||
- Basic error handling
|
||||
- Simple CLI interface
|
||||
- Basic TypeScript support
|
||||
- Basic TypeScript support
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 harsha-iiiv
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -21,16 +21,19 @@ import { getToolsFromOpenApi } from 'openapi-mcp-generator';
|
||||
This function extracts an array of tools from an OpenAPI specification.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `specPathOrUrl`: Path to a local OpenAPI spec file or URL to a remote spec
|
||||
- `options`: (Optional) Configuration options
|
||||
|
||||
**Options:**
|
||||
|
||||
- `baseUrl`: Override the base URL in the OpenAPI spec
|
||||
- `dereference`: Whether to resolve $refs (default: false)
|
||||
- `excludeOperationIds`: Array of operation IDs to exclude from the results
|
||||
- `filterFn`: Custom function to filter tools (receives tool, returns boolean)
|
||||
|
||||
**Returns:**
|
||||
|
||||
- Promise that resolves to an array of McpToolDefinition objects
|
||||
|
||||
**Example:**
|
||||
@ -42,12 +45,15 @@ import { getToolsFromOpenApi } from 'openapi-mcp-generator';
|
||||
const tools = await getToolsFromOpenApi('./petstore.json');
|
||||
|
||||
// With options
|
||||
const filteredTools = await getToolsFromOpenApi('https://petstore3.swagger.io/api/v3/openapi.json', {
|
||||
baseUrl: 'https://petstore3.swagger.io/api/v3',
|
||||
dereference: true,
|
||||
excludeOperationIds: ['addPet', 'updatePet'],
|
||||
filterFn: (tool) => tool.method.toLowerCase() === 'get'
|
||||
});
|
||||
const filteredTools = await getToolsFromOpenApi(
|
||||
'https://petstore3.swagger.io/api/v3/openapi.json',
|
||||
{
|
||||
baseUrl: 'https://petstore3.swagger.io/api/v3',
|
||||
dereference: true,
|
||||
excludeOperationIds: ['addPet', 'updatePet'],
|
||||
filterFn: (tool) => tool.method.toLowerCase() === 'get',
|
||||
}
|
||||
);
|
||||
|
||||
// Process the results
|
||||
for (const tool of filteredTools) {
|
||||
@ -58,6 +64,20 @@ for (const tool of filteredTools) {
|
||||
}
|
||||
```
|
||||
|
||||
you can also provide a `OpenAPIV3.Document` to the parser:
|
||||
|
||||
```typescript
|
||||
import { parser } from '@readme/openapi-parser';
|
||||
|
||||
const api = await parser('https://petstore3.swagger.io/api/v3/openapi.json', {
|
||||
dereference: {
|
||||
circular: true,
|
||||
},
|
||||
});
|
||||
|
||||
const tools = await getToolsFromOpenApi(api);
|
||||
```
|
||||
|
||||
## Tool Definition Structure
|
||||
|
||||
Each tool definition (`McpToolDefinition`) has the following properties:
|
||||
@ -66,34 +86,34 @@ Each tool definition (`McpToolDefinition`) has the following properties:
|
||||
interface McpToolDefinition {
|
||||
/** Name of the tool, must be unique */
|
||||
name: string;
|
||||
|
||||
|
||||
/** Human-readable description of the tool */
|
||||
description: string;
|
||||
|
||||
|
||||
/** JSON Schema that defines the input parameters */
|
||||
inputSchema: JSONSchema7 | boolean;
|
||||
|
||||
|
||||
/** HTTP method for the operation (get, post, etc.) */
|
||||
method: string;
|
||||
|
||||
|
||||
/** URL path template with parameter placeholders */
|
||||
pathTemplate: string;
|
||||
|
||||
|
||||
/** OpenAPI parameter objects for this operation */
|
||||
parameters: OpenAPIV3.ParameterObject[];
|
||||
|
||||
|
||||
/** Parameter names and locations for execution */
|
||||
executionParameters: { name: string; in: string }[];
|
||||
|
||||
|
||||
/** Content type for request body, if applicable */
|
||||
requestBodyContentType?: string;
|
||||
|
||||
|
||||
/** Security requirements for this operation */
|
||||
securityRequirements: OpenAPIV3.SecurityRequirementObject[];
|
||||
|
||||
|
||||
/** Original operation ID from the OpenAPI spec */
|
||||
operationId: string;
|
||||
|
||||
|
||||
/** Base URL for the API (if available) */
|
||||
baseUrl?: string;
|
||||
}
|
||||
@ -105,7 +125,7 @@ interface McpToolDefinition {
|
||||
|
||||
```typescript
|
||||
const getTools = await getToolsFromOpenApi(specUrl, {
|
||||
filterFn: (tool) => tool.method.toLowerCase() === 'get'
|
||||
filterFn: (tool) => tool.method.toLowerCase() === 'get',
|
||||
});
|
||||
```
|
||||
|
||||
@ -113,7 +133,7 @@ const getTools = await getToolsFromOpenApi(specUrl, {
|
||||
|
||||
```typescript
|
||||
const secureTools = await getToolsFromOpenApi(specUrl, {
|
||||
filterFn: (tool) => tool.securityRequirements.length > 0
|
||||
filterFn: (tool) => tool.securityRequirements.length > 0,
|
||||
});
|
||||
```
|
||||
|
||||
@ -121,7 +141,7 @@ const secureTools = await getToolsFromOpenApi(specUrl, {
|
||||
|
||||
```typescript
|
||||
const userTools = await getToolsFromOpenApi(specUrl, {
|
||||
filterFn: (tool) => tool.pathTemplate.includes('/user')
|
||||
filterFn: (tool) => tool.pathTemplate.includes('/user'),
|
||||
});
|
||||
```
|
||||
|
||||
@ -130,8 +150,6 @@ const userTools = await getToolsFromOpenApi(specUrl, {
|
||||
```typescript
|
||||
const safeUserTools = await getToolsFromOpenApi(specUrl, {
|
||||
excludeOperationIds: ['deleteUser', 'updateUser'],
|
||||
filterFn: (tool) =>
|
||||
tool.pathTemplate.includes('/user') &&
|
||||
tool.method.toLowerCase() === 'get'
|
||||
filterFn: (tool) => tool.pathTemplate.includes('/user') && tool.method.toLowerCase() === 'get',
|
||||
});
|
||||
```
|
||||
```
|
||||
|
||||
97
README.md
97
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. 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` |
|
||||
|
||||
## 📦 Programmatic API
|
||||
|
||||
@ -74,7 +75,7 @@ const filteredTools = await getToolsFromOpenApi('https://example.com/api-spec.js
|
||||
baseUrl: 'https://api.example.com',
|
||||
dereference: true,
|
||||
excludeOperationIds: ['deletePet'],
|
||||
filterFn: (tool) => tool.method.toLowerCase() === 'get'
|
||||
filterFn: (tool) => tool.method.toLowerCase() === 'get',
|
||||
});
|
||||
```
|
||||
|
||||
@ -100,6 +101,7 @@ The generated project includes:
|
||||
```
|
||||
|
||||
Core dependencies:
|
||||
|
||||
- `@modelcontextprotocol/sdk` - MCP protocol implementation
|
||||
- `axios` - HTTP client for API requests
|
||||
- `zod` - Runtime validation
|
||||
@ -138,18 +140,18 @@ Implements the MCP StreamableHTTP transport which offers:
|
||||
|
||||
### Transport Comparison
|
||||
|
||||
| Feature | stdio | web (SSE) | streamable-http |
|
||||
|---------|-------|-----------|----------------|
|
||||
| Protocol | JSON-RPC over stdio | JSON-RPC over SSE | JSON-RPC over HTTP |
|
||||
| Connection | Persistent | Persistent | Request/response |
|
||||
| Bidirectional | Yes | Yes | Yes (stateful) |
|
||||
| Multiple clients | No | Yes | Yes |
|
||||
| Browser compatible | No | Yes | Yes |
|
||||
| Firewall friendly | No | Yes | Yes |
|
||||
| Load balancing | No | Limited | Yes |
|
||||
| Status codes | No | Limited | Full HTTP codes |
|
||||
| Headers | No | Limited | Full HTTP headers |
|
||||
| Test client | No | Yes | Yes |
|
||||
| Feature | stdio | web (SSE) | streamable-http |
|
||||
| ------------------ | ------------------- | ----------------- | ------------------ |
|
||||
| Protocol | JSON-RPC over stdio | JSON-RPC over SSE | JSON-RPC over HTTP |
|
||||
| Connection | Persistent | Persistent | Request/response |
|
||||
| Bidirectional | Yes | Yes | Yes (stateful) |
|
||||
| Multiple clients | No | Yes | Yes |
|
||||
| Browser compatible | No | Yes | Yes |
|
||||
| Firewall friendly | No | Yes | Yes |
|
||||
| Load balancing | No | Limited | Yes |
|
||||
| Status codes | No | Limited | Full HTTP codes |
|
||||
| Headers | No | Limited | Full HTTP headers |
|
||||
| Test client | No | Yes | Yes |
|
||||
|
||||
---
|
||||
|
||||
@ -157,12 +159,44 @@ Implements the MCP StreamableHTTP transport which offers:
|
||||
|
||||
Configure auth credentials in your environment:
|
||||
|
||||
| Auth Type | Variable Format |
|
||||
|-------------|----------------------------------------------------------|
|
||||
| API Key | `API_KEY_<SCHEME_NAME>` |
|
||||
| Bearer | `BEARER_TOKEN_<SCHEME_NAME>` |
|
||||
| Basic Auth | `BASIC_USERNAME_<SCHEME_NAME>`, `BASIC_PASSWORD_<SCHEME_NAME>` |
|
||||
| OAuth2 | `OAUTH_CLIENT_ID_<SCHEME_NAME>`, `OAUTH_CLIENT_SECRET_<SCHEME_NAME>`, `OAUTH_SCOPES_<SCHEME_NAME>` |
|
||||
| Auth Type | Variable Format |
|
||||
| ---------- | -------------------------------------------------------------------------------------------------- |
|
||||
| API Key | `API_KEY_<SCHEME_NAME>` |
|
||||
| Bearer | `BEARER_TOKEN_<SCHEME_NAME>` |
|
||||
| Basic Auth | `BASIC_USERNAME_<SCHEME_NAME>`, `BASIC_PASSWORD_<SCHEME_NAME>` |
|
||||
| OAuth2 | `OAUTH_CLIENT_ID_<SCHEME_NAME>`, `OAUTH_CLIENT_SECRET_<SCHEME_NAME>`, `OAUTH_SCOPES_<SCHEME_NAME>` |
|
||||
|
||||
---
|
||||
|
||||
## 🔎 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](https://swagger.io/docs/specification/v3_0/openapi-extensions/) for details.
|
||||
|
||||
Note: `x-mcp` must be a boolean or the strings `"true"`/`"false"` (case-insensitive). Other values are ignored in favor of higher-precedence or default behavior.
|
||||
|
||||
---
|
||||
|
||||
@ -214,8 +248,9 @@ Contributions are welcome!
|
||||
|
||||
1. Fork the repo
|
||||
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||
3. Commit your changes: `git commit -m "Add amazing feature"`
|
||||
4. Push and open a PR
|
||||
3. Run `npm run format.write` to format your code
|
||||
4. Commit your changes: `git commit -m "Add amazing feature"`
|
||||
5. Push and open a PR
|
||||
|
||||
📌 Repository: [github.com/harsha-iiiv/openapi-mcp-generator](https://github.com/harsha-iiiv/openapi-mcp-generator)
|
||||
|
||||
|
||||
6
bin/openapi-mcp-generator.js
Executable file
6
bin/openapi-mcp-generator.js
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { program } from '../dist/index.js';
|
||||
|
||||
// Parse CLI arguments and run the program
|
||||
program.parse(process.argv);
|
||||
@ -1,12 +1,7 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2022": true
|
||||
@ -15,10 +10,7 @@
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"error",
|
||||
"warn"
|
||||
]
|
||||
"allow": ["error", "warn"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
@ -30,4 +22,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,4 +4,4 @@
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,11 +13,13 @@ This API uses OAuth2 for authentication. The MCP server can handle OAuth2 authen
|
||||
|
||||
- `OAUTH_CLIENT_ID_PETSTORE_AUTH`: Your OAuth client ID
|
||||
- `OAUTH_CLIENT_SECRET_PETSTORE_AUTH`: Your OAuth client secret
|
||||
|
||||
## Token Caching
|
||||
|
||||
The MCP server automatically caches OAuth tokens obtained via client credentials flow. Tokens are cached for their lifetime (as specified by the `expires_in` parameter in the token response) minus 60 seconds as a safety margin.
|
||||
|
||||
When making API requests, the server will:
|
||||
|
||||
1. Check for a cached token that's still valid
|
||||
2. Use the cached token if available
|
||||
3. Request a new token if no valid cached token exists
|
||||
|
||||
@ -34,4 +34,4 @@
|
||||
"typescript": "^5.8.3",
|
||||
"@types/uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -17,12 +17,6 @@
|
||||
"sourceMap": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "build", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2022": true
|
||||
@ -15,10 +10,7 @@
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"error",
|
||||
"warn"
|
||||
]
|
||||
"allow": ["error", "warn"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
@ -30,4 +22,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,4 +4,4 @@
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,11 +13,13 @@ This API uses OAuth2 for authentication. The MCP server can handle OAuth2 authen
|
||||
|
||||
- `OAUTH_CLIENT_ID_PETSTORE_AUTH`: Your OAuth client ID
|
||||
- `OAUTH_CLIENT_SECRET_PETSTORE_AUTH`: Your OAuth client secret
|
||||
|
||||
## Token Caching
|
||||
|
||||
The MCP server automatically caches OAuth tokens obtained via client credentials flow. Tokens are cached for their lifetime (as specified by the `expires_in` parameter in the token response) minus 60 seconds as a safety margin.
|
||||
|
||||
When making API requests, the server will:
|
||||
|
||||
1. Check for a cached token that's still valid
|
||||
2. Use the cached token if available
|
||||
3. Request a new token if no valid cached token exists
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
"scripts": {
|
||||
"start": "node build/index.js",
|
||||
"build": "tsc && chmod 755 build/index.js",
|
||||
"format.check": "prettier --check .",
|
||||
"format.write": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prestart": "npm run build",
|
||||
"start:http": "node build/index.js --transport=streamable-http"
|
||||
@ -35,4 +37,4 @@
|
||||
"typescript": "^5.8.3",
|
||||
"@types/uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* StreamableHTTP server setup for HTTP-based MCP communication using Hono
|
||||
*/
|
||||
@ -6,17 +5,17 @@ import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { serve } from '@hono/node-server';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { InitializeRequestSchema, JSONRPCError } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { InitializeRequestSchema, JSONRPCError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { toReqRes, toFetchResponse } from 'fetch-to-node';
|
||||
|
||||
// Import server configuration constants
|
||||
import { SERVER_NAME, SERVER_VERSION } from './index.js';
|
||||
|
||||
// Constants
|
||||
const SESSION_ID_HEADER_NAME = "mcp-session-id";
|
||||
const JSON_RPC = "2.0";
|
||||
const SESSION_ID_HEADER_NAME = 'mcp-session-id';
|
||||
const JSON_RPC = '2.0';
|
||||
|
||||
/**
|
||||
* StreamableHTTP MCP Server handler
|
||||
@ -24,106 +23,102 @@ const JSON_RPC = "2.0";
|
||||
class MCPStreamableHttpServer {
|
||||
server: Server;
|
||||
// Store active transports by session ID
|
||||
transports: {[sessionId: string]: StreamableHTTPServerTransport} = {};
|
||||
|
||||
transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
|
||||
|
||||
constructor(server: Server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle GET requests (typically used for static files)
|
||||
*/
|
||||
async handleGetRequest(c: any) {
|
||||
console.error("GET request received - StreamableHTTP transport only supports POST");
|
||||
console.error('GET request received - StreamableHTTP transport only supports POST');
|
||||
return c.text('Method Not Allowed', 405, {
|
||||
'Allow': 'POST'
|
||||
Allow: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle POST requests (all MCP communication)
|
||||
*/
|
||||
async handlePostRequest(c: any) {
|
||||
const sessionId = c.req.header(SESSION_ID_HEADER_NAME);
|
||||
console.error(`POST request received ${sessionId ? 'with session ID: ' + sessionId : 'without session ID'}`);
|
||||
|
||||
console.error(
|
||||
`POST request received ${sessionId ? 'with session ID: ' + sessionId : 'without session ID'}`
|
||||
);
|
||||
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
|
||||
|
||||
// Convert Fetch Request to Node.js req/res
|
||||
const { req, res } = toReqRes(c.req.raw);
|
||||
|
||||
|
||||
// Reuse existing transport if we have a session ID
|
||||
if (sessionId && this.transports[sessionId]) {
|
||||
const transport = this.transports[sessionId];
|
||||
|
||||
|
||||
// Handle the request with the transport
|
||||
await transport.handleRequest(req, res, body);
|
||||
|
||||
|
||||
// Cleanup when the response ends
|
||||
res.on('close', () => {
|
||||
console.error(`Request closed for session ${sessionId}`);
|
||||
});
|
||||
|
||||
|
||||
// Convert Node.js response back to Fetch Response
|
||||
return toFetchResponse(res);
|
||||
}
|
||||
|
||||
|
||||
// Create new transport for initialize requests
|
||||
if (!sessionId && this.isInitializeRequest(body)) {
|
||||
console.error("Creating new StreamableHTTP transport for initialize request");
|
||||
|
||||
console.error('Creating new StreamableHTTP transport for initialize request');
|
||||
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => uuid(),
|
||||
});
|
||||
|
||||
|
||||
// Add error handler for debug purposes
|
||||
transport.onerror = (err) => {
|
||||
console.error('StreamableHTTP transport error:', err);
|
||||
};
|
||||
|
||||
|
||||
// Connect the transport to the MCP server
|
||||
await this.server.connect(transport);
|
||||
|
||||
|
||||
// Handle the request with the transport
|
||||
await transport.handleRequest(req, res, body);
|
||||
|
||||
|
||||
// Store the transport if we have a session ID
|
||||
const newSessionId = transport.sessionId;
|
||||
if (newSessionId) {
|
||||
console.error(`New session established: ${newSessionId}`);
|
||||
this.transports[newSessionId] = transport;
|
||||
|
||||
|
||||
// Set up clean-up for when the transport is closed
|
||||
transport.onclose = () => {
|
||||
console.error(`Session closed: ${newSessionId}`);
|
||||
delete this.transports[newSessionId];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Cleanup when the response ends
|
||||
res.on('close', () => {
|
||||
console.error(`Request closed for new session`);
|
||||
});
|
||||
|
||||
|
||||
// Convert Node.js response back to Fetch Response
|
||||
return toFetchResponse(res);
|
||||
}
|
||||
|
||||
|
||||
// Invalid request (no session ID and not initialize)
|
||||
return c.json(
|
||||
this.createErrorResponse("Bad Request: invalid session ID or method."),
|
||||
400
|
||||
);
|
||||
return c.json(this.createErrorResponse('Bad Request: invalid session ID or method.'), 400);
|
||||
} catch (error) {
|
||||
console.error('Error handling MCP request:', error);
|
||||
return c.json(
|
||||
this.createErrorResponse("Internal server error."),
|
||||
500
|
||||
);
|
||||
return c.json(this.createErrorResponse('Internal server error.'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a JSON-RPC error response
|
||||
*/
|
||||
@ -137,7 +132,7 @@ class MCPStreamableHttpServer {
|
||||
id: uuid(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the request is an initialize request
|
||||
*/
|
||||
@ -146,18 +141,18 @@ class MCPStreamableHttpServer {
|
||||
const result = InitializeRequestSchema.safeParse(data);
|
||||
return result.success;
|
||||
};
|
||||
|
||||
|
||||
if (Array.isArray(body)) {
|
||||
return body.some(request => isInitial(request));
|
||||
return body.some((request) => isInitial(request));
|
||||
}
|
||||
|
||||
|
||||
return isInitial(body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a web server for the MCP server using StreamableHTTP transport
|
||||
*
|
||||
*
|
||||
* @param server The MCP Server instance
|
||||
* @param port The port to listen on (default: 3000)
|
||||
* @returns The Hono app instance
|
||||
@ -165,22 +160,22 @@ class MCPStreamableHttpServer {
|
||||
export async function setupStreamableHttpServer(server: Server, port = 3000) {
|
||||
// Create Hono app
|
||||
const app = new Hono();
|
||||
|
||||
|
||||
// Enable CORS
|
||||
app.use('*', cors());
|
||||
|
||||
|
||||
// Create MCP handler
|
||||
const mcpHandler = new MCPStreamableHttpServer(server);
|
||||
|
||||
|
||||
// Add a simple health check endpoint
|
||||
app.get('/health', (c) => {
|
||||
return c.json({ status: 'OK', server: SERVER_NAME, version: SERVER_VERSION });
|
||||
});
|
||||
|
||||
|
||||
// Main MCP endpoint supporting both GET and POST
|
||||
app.get("/mcp", (c) => mcpHandler.handleGetRequest(c));
|
||||
app.post("/mcp", (c) => mcpHandler.handlePostRequest(c));
|
||||
|
||||
app.get('/mcp', (c) => mcpHandler.handleGetRequest(c));
|
||||
app.post('/mcp', (c) => mcpHandler.handlePostRequest(c));
|
||||
|
||||
// Static files for the web client (if any)
|
||||
app.get('/*', async (c) => {
|
||||
const filePath = c.req.path === '/' ? '/index.html' : c.req.path;
|
||||
@ -189,37 +184,51 @@ export async function setupStreamableHttpServer(server: Server, port = 3000) {
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const { fileURLToPath } = await import('url');
|
||||
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const publicPath = path.join(__dirname, '..', '..', 'public');
|
||||
const fullPath = path.join(publicPath, filePath);
|
||||
|
||||
|
||||
// Simple security check to prevent directory traversal
|
||||
if (!fullPath.startsWith(publicPath)) {
|
||||
return c.text('Forbidden', 403);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isFile()) {
|
||||
const content = fs.readFileSync(fullPath);
|
||||
|
||||
|
||||
// Set content type based on file extension
|
||||
const ext = path.extname(fullPath).toLowerCase();
|
||||
let contentType = 'text/plain';
|
||||
|
||||
|
||||
switch (ext) {
|
||||
case '.html': contentType = 'text/html'; break;
|
||||
case '.css': contentType = 'text/css'; break;
|
||||
case '.js': contentType = 'text/javascript'; break;
|
||||
case '.json': contentType = 'application/json'; break;
|
||||
case '.png': contentType = 'image/png'; break;
|
||||
case '.jpg': contentType = 'image/jpeg'; break;
|
||||
case '.svg': contentType = 'image/svg+xml'; break;
|
||||
case '.html':
|
||||
contentType = 'text/html';
|
||||
break;
|
||||
case '.css':
|
||||
contentType = 'text/css';
|
||||
break;
|
||||
case '.js':
|
||||
contentType = 'text/javascript';
|
||||
break;
|
||||
case '.json':
|
||||
contentType = 'application/json';
|
||||
break;
|
||||
case '.png':
|
||||
contentType = 'image/png';
|
||||
break;
|
||||
case '.jpg':
|
||||
contentType = 'image/jpeg';
|
||||
break;
|
||||
case '.svg':
|
||||
contentType = 'image/svg+xml';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return new Response(content, {
|
||||
headers: { 'Content-Type': contentType }
|
||||
headers: { 'Content-Type': contentType },
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@ -230,19 +239,22 @@ export async function setupStreamableHttpServer(server: Server, port = 3000) {
|
||||
console.error('Error serving static file:', err);
|
||||
return c.text('Internal Server Error', 500);
|
||||
}
|
||||
|
||||
|
||||
return c.text('Not Found', 404);
|
||||
});
|
||||
|
||||
|
||||
// Start the server
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port
|
||||
}, (info) => {
|
||||
console.error(`MCP StreamableHTTP Server running at http://localhost:${info.port}`);
|
||||
console.error(`- MCP Endpoint: http://localhost:${info.port}/mcp`);
|
||||
console.error(`- Health Check: http://localhost:${info.port}/health`);
|
||||
});
|
||||
|
||||
serve(
|
||||
{
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
},
|
||||
(info) => {
|
||||
console.error(`MCP StreamableHTTP Server running at http://localhost:${info.port}`);
|
||||
console.error(`- MCP Endpoint: http://localhost:${info.port}/mcp`);
|
||||
console.error(`- Health Check: http://localhost:${info.port}/health`);
|
||||
}
|
||||
);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@ -17,12 +17,6 @@
|
||||
"sourceMap": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "build", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -17,11 +17,6 @@
|
||||
"sourceMap": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build"
|
||||
]
|
||||
}
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "build"]
|
||||
}
|
||||
|
||||
6469
package-lock.json
generated
6469
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
129
package.json
129
package.json
@ -1,64 +1,69 @@
|
||||
{
|
||||
"name": "openapi-mcp-generator",
|
||||
"version": "3.1.0",
|
||||
"description": "Generates MCP server code from OpenAPI specifications",
|
||||
"license": "MIT",
|
||||
"author": "Harsha",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"openapi-mcp-generator": "./dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc && chmod 755 dist/index.js",
|
||||
"version:patch": "npm version patch",
|
||||
"version:minor": "npm version minor",
|
||||
"version:major": "npm version major"
|
||||
},
|
||||
"keywords": [
|
||||
"openapi",
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"generator",
|
||||
"llm",
|
||||
"ai",
|
||||
"api"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues"
|
||||
},
|
||||
"homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.1",
|
||||
"commander": "^13.1.0",
|
||||
"openapi-types": "^12.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"eslint": "^9.25.1",
|
||||
"prettier": "^3.5.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.10.0",
|
||||
"json-schema-to-zod": "^2.6.1",
|
||||
"zod": "^3.24.3"
|
||||
}
|
||||
"name": "openapi-mcp-generator",
|
||||
"version": "3.2.0",
|
||||
"description": "Generates MCP server code from OpenAPI specifications",
|
||||
"license": "MIT",
|
||||
"author": "Harsha",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"openapi-mcp-generator": "./bin/openapi-mcp-generator.js"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"clean": "rimraf dist",
|
||||
"format.check": "prettier --check .",
|
||||
"format.write": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc && chmod 755 dist/index.js bin/openapi-mcp-generator.js",
|
||||
"version:patch": "npm version patch",
|
||||
"version:minor": "npm version minor",
|
||||
"version:major": "npm version major"
|
||||
},
|
||||
"keywords": [
|
||||
"openapi",
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"generator",
|
||||
"llm",
|
||||
"ai",
|
||||
"api"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues"
|
||||
},
|
||||
"homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.1",
|
||||
"commander": "^13.1.0",
|
||||
"openapi-types": "^12.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.17.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"eslint": "^9.33.0",
|
||||
"prettier": "^3.6.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"json-schema-to-zod": "^2.6.1",
|
||||
"zod": "^3.24.3"
|
||||
}
|
||||
}
|
||||
|
||||
46
src/api.ts
46
src/api.ts
@ -15,67 +15,77 @@ import { determineBaseUrl } from './utils/url.js';
|
||||
export interface GetToolsOptions {
|
||||
/** Optional base URL to override the one in the OpenAPI spec */
|
||||
baseUrl?: string;
|
||||
|
||||
|
||||
/** Whether to dereference the OpenAPI spec */
|
||||
dereference?: boolean;
|
||||
|
||||
|
||||
/** Array of operation IDs to exclude from the tools list */
|
||||
excludeOperationIds?: string[];
|
||||
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
function isOpenApiDocument(spec: string | OpenAPIV3.Document): spec is OpenAPIV3.Document {
|
||||
return typeof spec === 'object' && spec !== null && 'openapi' in spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of tools from an OpenAPI specification
|
||||
*
|
||||
*
|
||||
* @param specPathOrUrl Path or URL to the OpenAPI specification
|
||||
* @param options Options for generating the tools
|
||||
* @returns Promise that resolves to an array of tool definitions
|
||||
*/
|
||||
export async function getToolsFromOpenApi(
|
||||
specPathOrUrl: string,
|
||||
specPathOrUrl: string | OpenAPIV3.Document,
|
||||
options: GetToolsOptions = {}
|
||||
): Promise<McpToolDefinition[]> {
|
||||
try {
|
||||
// Parse the OpenAPI spec
|
||||
const api = options.dereference
|
||||
? (await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document
|
||||
: (await SwaggerParser.parse(specPathOrUrl)) as OpenAPIV3.Document;
|
||||
const api = isOpenApiDocument(specPathOrUrl)
|
||||
? specPathOrUrl
|
||||
: options.dereference
|
||||
? ((await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document)
|
||||
: ((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);
|
||||
|
||||
|
||||
// Apply filters to exclude specified operationIds and custom filter function
|
||||
let filteredTools = allTools;
|
||||
|
||||
|
||||
// Filter by excluded operation IDs if provided
|
||||
if (options.excludeOperationIds && options.excludeOperationIds.length > 0) {
|
||||
const excludeSet = new Set(options.excludeOperationIds);
|
||||
filteredTools = filteredTools.filter(tool => !excludeSet.has(tool.operationId));
|
||||
filteredTools = filteredTools.filter((tool) => !excludeSet.has(tool.operationId));
|
||||
}
|
||||
|
||||
|
||||
// Apply custom filter function if provided
|
||||
if (options.filterFn) {
|
||||
filteredTools = filteredTools.filter(options.filterFn);
|
||||
}
|
||||
|
||||
|
||||
// Return the filtered tools with base URL added
|
||||
return filteredTools.map(tool => ({
|
||||
return filteredTools.map((tool) => ({
|
||||
...tool,
|
||||
baseUrl: baseUrl || '',
|
||||
}));
|
||||
} catch (error) {
|
||||
// Provide more context for the error
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to extract tools from OpenAPI: ${error.message}`);
|
||||
// Preserve original stack/context
|
||||
throw new Error(`Failed to extract tools from OpenAPI: ${error.message}`, { cause: error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Export types for convenience
|
||||
export { McpToolDefinition };
|
||||
export { McpToolDefinition };
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Generator for StreamableHTTP server code for the MCP server using Hono
|
||||
*/
|
||||
|
||||
37
src/index.ts
37
src/index.ts
@ -30,6 +30,8 @@ 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
|
||||
export { getToolsFromOpenApi, McpToolDefinition, GetToolsOptions } from './api.js';
|
||||
@ -71,23 +73,30 @@ program
|
||||
'Port for web or streamable-http transport (default: 3000)',
|
||||
(val) => parseInt(val, 10)
|
||||
)
|
||||
.option(
|
||||
'--default-include <boolean>',
|
||||
'Default behavior for x-mcp filtering (true|false, case-insensitive). 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;
|
||||
},
|
||||
true
|
||||
)
|
||||
.option('--force', 'Overwrite existing files without prompting')
|
||||
.version('3.1.0'); // Match package.json version
|
||||
|
||||
// Check if module is being run directly (not imported)
|
||||
const isMainModule = process.argv[1] === new URL(import.meta.url).pathname;
|
||||
|
||||
if (isMainModule) {
|
||||
// Parse arguments explicitly from process.argv
|
||||
program.parse(process.argv);
|
||||
|
||||
// Run with the parsed options
|
||||
runGenerator(program.opts<CliOptions & { force?: boolean }>())
|
||||
.catch((error) => {
|
||||
.version(pkg.version) // Match package.json version
|
||||
.action((options) => {
|
||||
runGenerator(options).catch((error) => {
|
||||
console.error('Unhandled error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Export the program object for use in bin stub
|
||||
export { program };
|
||||
|
||||
/**
|
||||
* Main function to run the generator
|
||||
@ -288,4 +297,4 @@ async function runGenerator(options: CliOptions & { force?: boolean }) {
|
||||
}
|
||||
|
||||
// Export the run function for programmatic usage
|
||||
export { runGenerator as generateMcpServer };
|
||||
export { runGenerator as generateMcpServer };
|
||||
|
||||
@ -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,15 +30,43 @@ 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) {
|
||||
const loc = operation.operationId || `${method} ${path}`;
|
||||
const extVal =
|
||||
(operation as any)['x-mcp'] ?? (pathItem as any)['x-mcp'] ?? (api as any)['x-mcp'];
|
||||
let extPreview: string;
|
||||
try {
|
||||
extPreview = JSON.stringify(extVal);
|
||||
} catch {
|
||||
extPreview = String(extVal);
|
||||
}
|
||||
console.warn(
|
||||
`Error evaluating x-mcp extension for operation ${loc} (x-mcp=${extPreview}):`,
|
||||
error
|
||||
);
|
||||
if (!defaultInclude) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a unique name for the tool
|
||||
let baseName = operation.operationId || generateOperationId(method, path);
|
||||
if (!baseName) continue;
|
||||
|
||||
// Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -)
|
||||
baseName = baseName
|
||||
.replace(/\./g, '_')
|
||||
.replace(/[^a-z0-9_-]/gi, '_')
|
||||
.toLowerCase();
|
||||
baseName = baseName.replace(/\./g, '_').replace(/[^a-z0-9_-]/gi, '_');
|
||||
|
||||
let finalToolName = baseName;
|
||||
let counter = 1;
|
||||
@ -153,13 +185,15 @@ export function generateInputSchemaAndDetails(operation: OpenAPIV3.OperationObje
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an OpenAPI schema to a JSON Schema
|
||||
* Maps an OpenAPI schema to a JSON Schema with cycle protection.
|
||||
*
|
||||
* @param schema OpenAPI schema object or reference
|
||||
* @param seen WeakSet tracking already visited schema objects
|
||||
* @returns JSON Schema representation
|
||||
*/
|
||||
export function mapOpenApiSchemaToJsonSchema(
|
||||
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject
|
||||
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
||||
seen: WeakSet<object> = new WeakSet()
|
||||
): JSONSchema7 | boolean {
|
||||
// Handle reference objects
|
||||
if ('$ref' in schema) {
|
||||
@ -170,57 +204,73 @@ export function mapOpenApiSchemaToJsonSchema(
|
||||
// Handle boolean schemas
|
||||
if (typeof schema === 'boolean') return schema;
|
||||
|
||||
// Create a copy of the schema to modify
|
||||
const jsonSchema: JSONSchema7 = { ...schema } as any;
|
||||
|
||||
// Convert integer type to number (JSON Schema compatible)
|
||||
if (schema.type === 'integer') jsonSchema.type = 'number';
|
||||
|
||||
// Remove OpenAPI-specific properties that aren't in JSON Schema
|
||||
delete (jsonSchema as any).nullable;
|
||||
delete (jsonSchema as any).example;
|
||||
delete (jsonSchema as any).xml;
|
||||
delete (jsonSchema as any).externalDocs;
|
||||
delete (jsonSchema as any).deprecated;
|
||||
delete (jsonSchema as any).readOnly;
|
||||
delete (jsonSchema as any).writeOnly;
|
||||
|
||||
// Handle nullable properties by adding null to the type
|
||||
if (schema.nullable) {
|
||||
if (Array.isArray(jsonSchema.type)) {
|
||||
if (!jsonSchema.type.includes('null')) jsonSchema.type.push('null');
|
||||
} else if (typeof jsonSchema.type === 'string') {
|
||||
jsonSchema.type = [jsonSchema.type as JSONSchema7TypeName, 'null'];
|
||||
} else if (!jsonSchema.type) {
|
||||
jsonSchema.type = 'null';
|
||||
}
|
||||
// Detect cycles
|
||||
if (seen.has(schema)) {
|
||||
console.warn(
|
||||
`Cycle detected in schema${schema.title ? ` "${schema.title}"` : ''}, returning generic object to break recursion.`
|
||||
);
|
||||
return { type: 'object' };
|
||||
}
|
||||
seen.add(schema);
|
||||
|
||||
// Recursively process object properties
|
||||
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
||||
const mappedProps: { [key: string]: JSONSchema7 | boolean } = {};
|
||||
try {
|
||||
// Create a copy of the schema to modify
|
||||
const jsonSchema: JSONSchema7 = { ...schema } as any;
|
||||
|
||||
for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
|
||||
if (typeof propSchema === 'object' && propSchema !== null) {
|
||||
mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema as OpenAPIV3.SchemaObject);
|
||||
} else if (typeof propSchema === 'boolean') {
|
||||
mappedProps[key] = propSchema;
|
||||
// Convert integer type to number (JSON Schema compatible)
|
||||
if (schema.type === 'integer') jsonSchema.type = 'number';
|
||||
|
||||
// Remove OpenAPI-specific properties that aren't in JSON Schema
|
||||
delete (jsonSchema as any).nullable;
|
||||
delete (jsonSchema as any).example;
|
||||
delete (jsonSchema as any).xml;
|
||||
delete (jsonSchema as any).externalDocs;
|
||||
delete (jsonSchema as any).deprecated;
|
||||
delete (jsonSchema as any).readOnly;
|
||||
delete (jsonSchema as any).writeOnly;
|
||||
|
||||
// Handle nullable properties by adding null to the type
|
||||
if (schema.nullable) {
|
||||
if (Array.isArray(jsonSchema.type)) {
|
||||
if (!jsonSchema.type.includes('null')) jsonSchema.type.push('null');
|
||||
} else if (typeof jsonSchema.type === 'string') {
|
||||
jsonSchema.type = [jsonSchema.type as JSONSchema7TypeName, 'null'];
|
||||
} else if (!jsonSchema.type) {
|
||||
jsonSchema.type = 'null';
|
||||
}
|
||||
}
|
||||
|
||||
jsonSchema.properties = mappedProps;
|
||||
}
|
||||
// Recursively process object properties
|
||||
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
||||
const mappedProps: { [key: string]: JSONSchema7 | boolean } = {};
|
||||
|
||||
// Recursively process array items
|
||||
if (
|
||||
jsonSchema.type === 'array' &&
|
||||
typeof jsonSchema.items === 'object' &&
|
||||
jsonSchema.items !== null
|
||||
) {
|
||||
jsonSchema.items = mapOpenApiSchemaToJsonSchema(
|
||||
jsonSchema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject
|
||||
);
|
||||
}
|
||||
for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
|
||||
if (typeof propSchema === 'object' && propSchema !== null) {
|
||||
mappedProps[key] = mapOpenApiSchemaToJsonSchema(
|
||||
propSchema as OpenAPIV3.SchemaObject,
|
||||
seen
|
||||
);
|
||||
} else if (typeof propSchema === 'boolean') {
|
||||
mappedProps[key] = propSchema;
|
||||
}
|
||||
}
|
||||
|
||||
return jsonSchema;
|
||||
jsonSchema.properties = mappedProps;
|
||||
}
|
||||
|
||||
// Recursively process array items
|
||||
if (
|
||||
jsonSchema.type === 'array' &&
|
||||
typeof jsonSchema.items === 'object' &&
|
||||
jsonSchema.items !== null
|
||||
) {
|
||||
jsonSchema.items = mapOpenApiSchemaToJsonSchema(
|
||||
jsonSchema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
||||
seen
|
||||
);
|
||||
}
|
||||
return jsonSchema;
|
||||
} finally {
|
||||
seen.delete(schema);
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,12 @@ export interface CliOptions {
|
||||
transport?: TransportType;
|
||||
/** Server port (for web and streamable-http transports) */
|
||||
port?: number;
|
||||
/**
|
||||
* Default behavior for x-mcp filtering.
|
||||
* true (default) = include by default when x-mcp is missing or invalid;
|
||||
* false = exclude by default unless x-mcp explicitly enables.
|
||||
*/
|
||||
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,63 @@ 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();
|
||||
if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
|
||||
if (['false', '0', 'no', 'off'].includes(normalized)) return false;
|
||||
return 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;
|
||||
}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user