Compare commits

...

25 Commits

Author SHA1 Message Date
Harsha v
8ee9fc383d
Create mcpcat.md 2025-09-30 12:15:47 -07:00
Harsha v
f29c277860
Merge pull request #39 from harsha-iiiv/copilot/fix-33b9d782-1c7b-44d1-89a9-adbd77a32aa8
Upgrade package to version 3.2.0 and address code quality improvements based on review feedback
2025-08-24 22:03:57 -07:00
copilot-swe-agent[bot]
33220c1e82 Address coderabbitai review feedback: improve docs, error handling, and boolean normalization
Co-authored-by: harsha-iiiv <31560965+harsha-iiiv@users.noreply.github.com>
2025-08-25 04:59:26 +00:00
copilot-swe-agent[bot]
eda4505a63 Update CHANGELOG.md with corrected version details and proper categorization
Co-authored-by: harsha-iiiv <31560965+harsha-iiiv@users.noreply.github.com>
2025-08-25 04:41:52 +00:00
copilot-swe-agent[bot]
4bf66d9efd Upgrade package version to 3.2.0 and update CHANGELOG.md
Co-authored-by: harsha-iiiv <31560965+harsha-iiiv@users.noreply.github.com>
2025-08-24 23:14:41 +00:00
copilot-swe-agent[bot]
7a31e1f6e9 Initial plan 2025-08-24 23:09:33 +00:00
Harsha v
82ff2b726d
Merge pull request #37 from atomicpages/main
feat: allow folks to BYO OpenAPIV3.Document harsha-iiiv/openapi-mcp-generator#35
2025-08-24 16:04:56 -07:00
Harsha v
1c806b8dab
Merge pull request #38 from FabriBorgobello/feature/endpoint-filtering
feature: Add endpoint filtering
2025-08-24 16:04:01 -07:00
Fabricio Borgobello
c9015f395e
Update src/utils/helpers.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-22 11:58:27 +02:00
Fabricio Borgobello
af1b664653
Adjust boolean validation 2025-08-22 11:57:58 +02:00
Fabricio Borgobello
1f001bb47a
Remove comment 2025-08-22 11:04:08 +02:00
Fabricio Borgobello
b1e29c22de
feature/endpoint-filtering 2025-08-22 11:00:13 +02:00
Dennis Thompson
e6352d13b6 chore: fix clerical error on docs 2025-08-17 21:48:46 -07:00
Dennis Thompson
26307f26ad
Update src/api.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-17 21:48:06 -07:00
Dennis Thompson
b7bc67e444 fix: addresses harsha-iiiv/openapi-mcp-generator#35 2025-08-17 21:26:27 -07:00
Harsha v
47292c89cf
Merge pull request #30 from oneWalker/dev
feat: style and ci optimization
2025-06-19 22:00:22 +05:30
Brian,Kun Liu
2cab8aeada style: format the code with the prettier config 2025-06-19 11:31:27 +08:00
Brian,Kun Liu
ea89e0498e ci: add build and check on PR before merged 2025-06-19 11:31:07 +08:00
Brian,Kun Liu
34a1f6df12 perf: keep code with the same style and format command 2025-06-19 11:28:24 +08:00
Harsha v
e17ab1a3d0
Merge pull request #29 from oneWalker/dev
feat: remove the lowercase transfer to make toolName readable and versionNo from package.json
2025-06-18 15:30:57 +05:30
Brian,Kun Liu
0ca1310c56 perf: remove the lowercase transfer to make toolName readable 2025-06-16 16:12:00 +08:00
Brian,Kun Liu
aa388035ad fix: use the versionNo in package.json 2025-06-16 16:11:10 +08:00
Harsha v
c98d652933
Merge pull request #27 from oneWalker/dev
fix: cannot find the package after building and the problem during th…
2025-06-15 00:19:04 +05:30
Brian,Kun Liu
9c6fed9beb
Update the version serial No 2025-06-12 19:17:09 +08:00
Brian,Kun Liu
67e06c3c34
fix: cannot find the package after building and the problem during the building 2025-06-12 19:00:34 +08:00
40 changed files with 9331 additions and 7752 deletions

View File

@ -1,9 +1,6 @@
{ {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["@typescript-eslint"], "plugins": ["@typescript-eslint"],
"env": { "env": {
"node": true, "node": true,

View File

@ -4,7 +4,6 @@ about: Create a report to help us improve
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@ -24,11 +24,13 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS] - OS: [e.g. iOS]
- Browser [e.g. chrome, safari] - Browser [e.g. chrome, safari]
- Version [e.g. 22] - Version [e.g. 22]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6] - Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1] - OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari] - Browser [e.g. stock browser, safari]

View File

@ -4,7 +4,4 @@ about: Describe this issue template's purpose here.
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---

View File

@ -4,7 +4,6 @@ about: Suggest an idea for this project
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**

44
.github/PULL_REQUEST_TEMPLATE/mcpcat.md vendored Normal file
View 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` (01)
### 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
View 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

View File

@ -5,26 +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/), 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). 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 ## [3.1.2] - 2025-06-08
### Fixed ### Fixed
- Prevent stack overflow (RangeError: Maximum call stack size exceeded) when processing recursive or cyclic OpenAPI schemas (e.g., self-referencing objects). - 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. - Added cycle detection to schema mapping, ensuring robust handling of recursive structures.
## [3.1.1] - 2025-05-26 ## [3.1.1] - 2025-05-26
### Added ### Added
- Introduced a new executable command-line script for easier usage in Unix-like environments. - Introduced a new executable command-line script for easier usage in Unix-like environments.
### Changed ### Changed
- Use new CLI entry point to use the new `bin/openapi-mcp-generator.js` file. - 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. - Updated build script to ensure the new CLI file has the correct permissions.
- Refactored `index.ts` to streamline argument parsing and error handling. - Refactored `index.ts` to streamline argument parsing and error handling.
## [3.1.0] - 2025-05-18 ## [3.1.0] - 2025-05-18
### Added ### Added
- Programmatic API to extract MCP tool definitions from OpenAPI specs - Programmatic API to extract MCP tool definitions from OpenAPI specs
- New exportable `getToolsFromOpenApi` function for direct integration in code - New exportable `getToolsFromOpenApi` function for direct integration in code
- Advanced filtering capabilities for programmatic tool extraction - Advanced filtering capabilities for programmatic tool extraction
@ -32,20 +70,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated README with programmatic API usage examples - Updated README with programmatic API usage examples
### Changed ### Changed
- Improved module structure with better exports - Improved module structure with better exports
- Enhanced detection of module execution context - Enhanced detection of module execution context
## [3.0.0] - 2025-04-26 ## [3.0.0] - 2025-04-26
### Added ### Added
- Streamable HTTP support for OpenAPI MCP generator, enabling efficient handling of large payloads and real-time data transfer. - 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. - Major architectural refactor to support streaming responses and requests.
### Fixed ### Fixed
- Multiple bugs related to HTTP/HTTPS connection handling, stream closure, and error propagation in streaming scenarios. - 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. - Fixed resource leak issues on server aborts and client disconnects during streaming.
### Changed ### Changed
- Major version bump due to breaking changes in API and internal structures to support streaming. - 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. - Updated documentation to reflect new streaming capabilities and usage instructions.
- Enhanced performance and robustness of HTTP/HTTPS transport layers. - Enhanced performance and robustness of HTTP/HTTPS transport layers.
@ -53,6 +95,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2.0.0] - 2025-04-12 ## [2.0.0] - 2025-04-12
### Added ### Added
- Runtime argument validation using Zod - Runtime argument validation using Zod
- JSON Schema to Zod schema conversion - JSON Schema to Zod schema conversion
- Improved error handling and formatting - Improved error handling and formatting
@ -63,6 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for multiple content types - Support for multiple content types
### Changed ### Changed
- Simplified transport layer to only support stdio transport - Simplified transport layer to only support stdio transport
- Removed support for WebSocket and HTTP transports - Removed support for WebSocket and HTTP transports
- Updated to use @modelcontextprotocol/sdk v1.9.0 - Updated to use @modelcontextprotocol/sdk v1.9.0
@ -72,6 +116,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- More robust OpenAPI schema processing - More robust OpenAPI schema processing
### Fixed ### Fixed
- Path parameter resolution in URLs - Path parameter resolution in URLs
- Content-Type header handling - Content-Type header handling
- Response processing for different content types - Response processing for different content types
@ -81,6 +126,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [1.0.0] - Initial Release ## [1.0.0] - Initial Release
### Added ### Added
- Basic OpenAPI to MCP server generation - Basic OpenAPI to MCP server generation
- Support for GET, POST, PUT, DELETE methods - Support for GET, POST, PUT, DELETE methods
- Basic error handling - Basic error handling

View File

@ -21,16 +21,19 @@ import { getToolsFromOpenApi } from 'openapi-mcp-generator';
This function extracts an array of tools from an OpenAPI specification. This function extracts an array of tools from an OpenAPI specification.
**Parameters:** **Parameters:**
- `specPathOrUrl`: Path to a local OpenAPI spec file or URL to a remote spec - `specPathOrUrl`: Path to a local OpenAPI spec file or URL to a remote spec
- `options`: (Optional) Configuration options - `options`: (Optional) Configuration options
**Options:** **Options:**
- `baseUrl`: Override the base URL in the OpenAPI spec - `baseUrl`: Override the base URL in the OpenAPI spec
- `dereference`: Whether to resolve $refs (default: false) - `dereference`: Whether to resolve $refs (default: false)
- `excludeOperationIds`: Array of operation IDs to exclude from the results - `excludeOperationIds`: Array of operation IDs to exclude from the results
- `filterFn`: Custom function to filter tools (receives tool, returns boolean) - `filterFn`: Custom function to filter tools (receives tool, returns boolean)
**Returns:** **Returns:**
- Promise that resolves to an array of McpToolDefinition objects - Promise that resolves to an array of McpToolDefinition objects
**Example:** **Example:**
@ -42,12 +45,15 @@ import { getToolsFromOpenApi } from 'openapi-mcp-generator';
const tools = await getToolsFromOpenApi('./petstore.json'); const tools = await getToolsFromOpenApi('./petstore.json');
// With options // With options
const filteredTools = await getToolsFromOpenApi('https://petstore3.swagger.io/api/v3/openapi.json', { const filteredTools = await getToolsFromOpenApi(
'https://petstore3.swagger.io/api/v3/openapi.json',
{
baseUrl: 'https://petstore3.swagger.io/api/v3', baseUrl: 'https://petstore3.swagger.io/api/v3',
dereference: true, dereference: true,
excludeOperationIds: ['addPet', 'updatePet'], excludeOperationIds: ['addPet', 'updatePet'],
filterFn: (tool) => tool.method.toLowerCase() === 'get' filterFn: (tool) => tool.method.toLowerCase() === 'get',
}); }
);
// Process the results // Process the results
for (const tool of filteredTools) { 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 ## Tool Definition Structure
Each tool definition (`McpToolDefinition`) has the following properties: Each tool definition (`McpToolDefinition`) has the following properties:
@ -105,7 +125,7 @@ interface McpToolDefinition {
```typescript ```typescript
const getTools = await getToolsFromOpenApi(specUrl, { 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 ```typescript
const secureTools = await getToolsFromOpenApi(specUrl, { 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 ```typescript
const userTools = await getToolsFromOpenApi(specUrl, { 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 ```typescript
const safeUserTools = await getToolsFromOpenApi(specUrl, { const safeUserTools = await getToolsFromOpenApi(specUrl, {
excludeOperationIds: ['deleteUser', 'updateUser'], excludeOperationIds: ['deleteUser', 'updateUser'],
filterFn: (tool) => filterFn: (tool) => tool.pathTemplate.includes('/user') && tool.method.toLowerCase() === 'get',
tool.pathTemplate.includes('/user') &&
tool.method.toLowerCase() === 'get'
}); });
``` ```

View File

@ -49,7 +49,7 @@ openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir -
### CLI Options ### CLI Options
| Option | Alias | Description | Default | | Option | Alias | Description | Default |
|--------------------|-------|-----------------------------------------------------------------------------------------------------|---------------------------------| | ------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| `--input` | `-i` | Path or URL to OpenAPI specification (YAML or JSON) | **Required** | | `--input` | `-i` | Path or URL to OpenAPI specification (YAML or JSON) | **Required** |
| `--output` | `-o` | Directory to output the generated MCP project | **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-name` | `-n` | Name of the MCP server (`package.json:name`) | OpenAPI title or `mcp-api-server` |
@ -57,6 +57,7 @@ openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir -
| `--base-url` | `-b` | Base URL for API requests. Required if OpenAPI `servers` missing or ambiguous. | Auto-detected if possible | | `--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"` | | `--transport` | `-t` | Transport mode: `"stdio"` (default), `"web"`, or `"streamable-http"` | `"stdio"` |
| `--port` | `-p` | Port for web-based transports | `3000` | | `--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` | | `--force` | | Overwrite existing files in the output directory without confirmation | `false` |
## 📦 Programmatic API ## 📦 Programmatic API
@ -74,7 +75,7 @@ const filteredTools = await getToolsFromOpenApi('https://example.com/api-spec.js
baseUrl: 'https://api.example.com', baseUrl: 'https://api.example.com',
dereference: true, dereference: true,
excludeOperationIds: ['deletePet'], excludeOperationIds: ['deletePet'],
filterFn: (tool) => tool.method.toLowerCase() === 'get' filterFn: (tool) => tool.method.toLowerCase() === 'get',
}); });
``` ```
@ -100,6 +101,7 @@ The generated project includes:
``` ```
Core dependencies: Core dependencies:
- `@modelcontextprotocol/sdk` - MCP protocol implementation - `@modelcontextprotocol/sdk` - MCP protocol implementation
- `axios` - HTTP client for API requests - `axios` - HTTP client for API requests
- `zod` - Runtime validation - `zod` - Runtime validation
@ -139,7 +141,7 @@ Implements the MCP StreamableHTTP transport which offers:
### Transport Comparison ### Transport Comparison
| Feature | stdio | web (SSE) | streamable-http | | Feature | stdio | web (SSE) | streamable-http |
|---------|-------|-----------|----------------| | ------------------ | ------------------- | ----------------- | ------------------ |
| Protocol | JSON-RPC over stdio | JSON-RPC over SSE | JSON-RPC over HTTP | | Protocol | JSON-RPC over stdio | JSON-RPC over SSE | JSON-RPC over HTTP |
| Connection | Persistent | Persistent | Request/response | | Connection | Persistent | Persistent | Request/response |
| Bidirectional | Yes | Yes | Yes (stateful) | | Bidirectional | Yes | Yes | Yes (stateful) |
@ -158,7 +160,7 @@ Implements the MCP StreamableHTTP transport which offers:
Configure auth credentials in your environment: Configure auth credentials in your environment:
| Auth Type | Variable Format | | Auth Type | Variable Format |
|-------------|----------------------------------------------------------| | ---------- | -------------------------------------------------------------------------------------------------- |
| API Key | `API_KEY_<SCHEME_NAME>` | | API Key | `API_KEY_<SCHEME_NAME>` |
| Bearer | `BEARER_TOKEN_<SCHEME_NAME>` | | Bearer | `BEARER_TOKEN_<SCHEME_NAME>` |
| Basic Auth | `BASIC_USERNAME_<SCHEME_NAME>`, `BASIC_PASSWORD_<SCHEME_NAME>` | | Basic Auth | `BASIC_USERNAME_<SCHEME_NAME>`, `BASIC_PASSWORD_<SCHEME_NAME>` |
@ -166,6 +168,38 @@ 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](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.
---
## ▶️ Running the Generated Server ## ▶️ Running the Generated Server
```bash ```bash
@ -214,8 +248,9 @@ Contributions are welcome!
1. Fork the repo 1. Fork the repo
2. Create a feature branch: `git checkout -b feature/amazing-feature` 2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Commit your changes: `git commit -m "Add amazing feature"` 3. Run `npm run format.write` to format your code
4. Push and open a PR 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) 📌 Repository: [github.com/harsha-iiiv/openapi-mcp-generator](https://github.com/harsha-iiiv/openapi-mcp-generator)

View File

@ -1,12 +1,7 @@
{ {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"eslint:recommended", "plugins": ["@typescript-eslint"],
"plugin:@typescript-eslint/recommended"
],
"plugins": [
"@typescript-eslint"
],
"env": { "env": {
"node": true, "node": true,
"es2022": true "es2022": true
@ -15,10 +10,7 @@
"no-console": [ "no-console": [
"error", "error",
{ {
"allow": [ "allow": ["error", "warn"]
"error",
"warn"
]
} }
], ],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",

View File

@ -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_ID_PETSTORE_AUTH`: Your OAuth client ID
- `OAUTH_CLIENT_SECRET_PETSTORE_AUTH`: Your OAuth client secret - `OAUTH_CLIENT_SECRET_PETSTORE_AUTH`: Your OAuth client secret
## Token Caching ## 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. 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: When making API requests, the server will:
1. Check for a cached token that's still valid 1. Check for a cached token that's still valid
2. Use the cached token if available 2. Use the cached token if available
3. Request a new token if no valid cached token exists 3. Request a new token if no valid cached token exists

View File

@ -1,18 +1,26 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>swagger-petstore---openapi-3-0 MCP Test Client</title> <title>swagger-petstore---openapi-3-0 MCP Test Client</title>
<style> <style>
body { body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
line-height: 1.5; line-height: 1.5;
} }
h1 { margin-bottom: 10px; } h1 {
margin-bottom: 10px;
}
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -39,13 +47,15 @@
} }
#sendButton { #sendButton {
padding: 8px 16px; padding: 8px 16px;
background-color: #4CAF50; background-color: #4caf50;
color: white; color: white;
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: 0 5px 5px 0; border-radius: 0 5px 5px 0;
} }
#sendButton:hover { background-color: #45a049; } #sendButton:hover {
background-color: #45a049;
}
.message { .message {
margin-bottom: 10px; margin-bottom: 10px;
padding: 8px 12px; padding: 8px 12px;
@ -124,7 +134,7 @@
<div id="conversation"></div> <div id="conversation"></div>
<div class="input-area"> <div class="input-area">
<input type="text" id="userInput" placeholder="Type a message..." disabled> <input type="text" id="userInput" placeholder="Type a message..." disabled />
<button id="sendButton" disabled>Send</button> <button id="sendButton" disabled>Send</button>
</div> </div>
</div> </div>
@ -294,8 +304,8 @@
method: 'callTool', method: 'callTool',
params: { params: {
name: toolName, name: toolName,
arguments: parseArguments(text) arguments: parseArguments(text),
} },
}; };
log('REQUEST', JSON.stringify(requestBody)); log('REQUEST', JSON.stringify(requestBody));
@ -307,15 +317,18 @@
const response = await fetch(fullEndpoint, { const response = await fetch(fullEndpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify(requestBody) body: JSON.stringify(requestBody),
}); });
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
log('ERROR', `Error response: ${response.status} ${response.statusText} ${errorText}`); log('ERROR', `Error response: ${response.status} ${response.statusText} ${errorText}`);
appendMessage('system', `Error: ${response.status} ${response.statusText}\n${errorText}`); appendMessage(
'system',
`Error: ${response.status} ${response.statusText}\n${errorText}`
);
} else { } else {
log('INFO', `Request sent successfully`); log('INFO', `Request sent successfully`);
// Note: We don't handle the response content here because the response // Note: We don't handle the response content here because the response

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
/** /**
* Web server setup for HTTP-based MCP communication using Hono * Web server setup for HTTP-based MCP communication using Hono
*/ */
@ -7,11 +6,11 @@ import { cors } from 'hono/cors';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { streamSSE } from 'hono/streaming'; import { streamSSE } from 'hono/streaming';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { JSONRPCMessage, JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js"; import { JSONRPCMessage, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
import type { Context } from 'hono'; import type { Context } from 'hono';
import type { SSEStreamingApi } from 'hono/streaming'; import type { SSEStreamingApi } from 'hono/streaming';
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
// Import server configuration constants // Import server configuration constants
import { SERVER_NAME, SERVER_VERSION } from './index.js'; import { SERVER_NAME, SERVER_VERSION } from './index.js';
@ -52,7 +51,7 @@ async start(): Promise<void> {
// Send the endpoint information // Send the endpoint information
await this.stream.writeSSE({ await this.stream.writeSSE({
event: 'endpoint', event: 'endpoint',
data: `${this.messageUrl}?sessionId=${this._sessionId}` data: `${this.messageUrl}?sessionId=${this._sessionId}`,
}); });
// Send session ID and connection info in a format the client can understand // Send session ID and connection info in a format the client can understand
@ -60,22 +59,22 @@ async start(): Promise<void> {
event: 'session', event: 'session',
data: JSON.stringify({ data: JSON.stringify({
type: 'session_id', type: 'session_id',
session_id: this._sessionId session_id: this._sessionId,
}) }),
}); });
// Send a welcome notification // Send a welcome notification
await this.send({ await this.send({
jsonrpc: "2.0", jsonrpc: '2.0',
method: "notification", method: 'notification',
params: { params: {
type: "welcome", type: 'welcome',
clientInfo: { clientInfo: {
sessionId: this._sessionId, sessionId: this._sessionId,
serverName: SERVER_NAME, serverName: SERVER_NAME,
serverVersion: SERVER_VERSION serverVersion: SERVER_VERSION,
} },
} },
}); });
} }
@ -132,7 +131,7 @@ async send(message: JSONRPCMessage): Promise<void> {
await this.stream.writeSSE({ await this.stream.writeSSE({
event: 'message', event: 'message',
data: JSON.stringify(message) data: JSON.stringify(message),
}); });
} }
} }
@ -160,7 +159,7 @@ app.get('/health', (c) => {
}); });
// SSE endpoint for clients to connect to // SSE endpoint for clients to connect to
app.get("/sse", (c) => { app.get('/sse', (c) => {
return streamSSE(c, async (stream) => { return streamSSE(c, async (stream) => {
// Create SSE transport // Create SSE transport
const transport = new SSETransport('/api/messages', stream); const transport = new SSETransport('/api/messages', stream);
@ -202,7 +201,7 @@ app.get("/sse", (c) => {
}); });
// API endpoint for clients to send messages // API endpoint for clients to send messages
app.post("/api/messages", async (c) => { app.post('/api/messages', async (c) => {
const sessionId = c.req.query('sessionId'); const sessionId = c.req.query('sessionId');
if (!sessionId) { if (!sessionId) {
@ -246,17 +245,31 @@ app.get('/*', async (c) => {
let contentType = 'text/plain'; let contentType = 'text/plain';
switch (ext) { switch (ext) {
case '.html': contentType = 'text/html'; break; case '.html':
case '.css': contentType = 'text/css'; break; contentType = 'text/html';
case '.js': contentType = 'text/javascript'; break; break;
case '.json': contentType = 'application/json'; break; case '.css':
case '.png': contentType = 'image/png'; break; contentType = 'text/css';
case '.jpg': contentType = 'image/jpeg'; break; break;
case '.svg': contentType = 'image/svg+xml'; 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, { return new Response(content, {
headers: { 'Content-Type': contentType } headers: { 'Content-Type': contentType },
}); });
} }
} catch (err) { } catch (err) {
@ -272,15 +285,20 @@ app.get('/*', async (c) => {
}); });
// Start the server // Start the server
serve({ serve(
{
fetch: app.fetch, fetch: app.fetch,
port port,
}, (info) => { },
(info) => {
console.error(`MCP Web Server running at http://localhost:${info.port}`); console.error(`MCP Web Server running at http://localhost:${info.port}`);
console.error(`- SSE Endpoint: http://localhost:${info.port}/sse`); console.error(`- SSE Endpoint: http://localhost:${info.port}/sse`);
console.error(`- Messages Endpoint: http://localhost:${info.port}/api/messages?sessionId=YOUR_SESSION_ID`); console.error(
`- Messages Endpoint: http://localhost:${info.port}/api/messages?sessionId=YOUR_SESSION_ID`
);
console.error(`- Health Check: http://localhost:${info.port}/health`); console.error(`- Health Check: http://localhost:${info.port}/health`);
}); }
);
return app; return app;
} }

View File

@ -17,12 +17,6 @@
"sourceMap": true, "sourceMap": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["src/**/*"],
"src/**/*" "exclude": ["node_modules", "build", "**/*.test.ts"]
],
"exclude": [
"node_modules",
"build",
"**/*.test.ts"
]
} }

View File

@ -1,12 +1,7 @@
{ {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"eslint:recommended", "plugins": ["@typescript-eslint"],
"plugin:@typescript-eslint/recommended"
],
"plugins": [
"@typescript-eslint"
],
"env": { "env": {
"node": true, "node": true,
"es2022": true "es2022": true
@ -15,10 +10,7 @@
"no-console": [ "no-console": [
"error", "error",
{ {
"allow": [ "allow": ["error", "warn"]
"error",
"warn"
]
} }
], ],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",

View File

@ -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_ID_PETSTORE_AUTH`: Your OAuth client ID
- `OAUTH_CLIENT_SECRET_PETSTORE_AUTH`: Your OAuth client secret - `OAUTH_CLIENT_SECRET_PETSTORE_AUTH`: Your OAuth client secret
## Token Caching ## 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. 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: When making API requests, the server will:
1. Check for a cached token that's still valid 1. Check for a cached token that's still valid
2. Use the cached token if available 2. Use the cached token if available
3. Request a new token if no valid cached token exists 3. Request a new token if no valid cached token exists

View File

@ -12,6 +12,8 @@
"scripts": { "scripts": {
"start": "node build/index.js", "start": "node build/index.js",
"build": "tsc && chmod 755 build/index.js", "build": "tsc && chmod 755 build/index.js",
"format.check": "prettier --check .",
"format.write": "prettier --write .",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"prestart": "npm run build", "prestart": "npm run build",
"start:http": "node build/index.js --transport=streamable-http" "start:http": "node build/index.js --transport=streamable-http"

View File

@ -1,18 +1,26 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>swagger-petstore---openapi-3-0 MCP StreamableHTTP Test Client</title> <title>swagger-petstore---openapi-3-0 MCP StreamableHTTP Test Client</title>
<style> <style>
body { body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
line-height: 1.5; line-height: 1.5;
} }
h1 { margin-bottom: 10px; } h1 {
margin-bottom: 10px;
}
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -39,13 +47,15 @@
} }
#sendButton { #sendButton {
padding: 8px 16px; padding: 8px 16px;
background-color: #4CAF50; background-color: #4caf50;
color: white; color: white;
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: 0 5px 5px 0; border-radius: 0 5px 5px 0;
} }
#sendButton:hover { background-color: #45a049; } #sendButton:hover {
background-color: #45a049;
}
.message { .message {
margin-bottom: 10px; margin-bottom: 10px;
padding: 8px 12px; padding: 8px 12px;
@ -124,7 +134,7 @@
<div id="conversation"></div> <div id="conversation"></div>
<div class="input-area"> <div class="input-area">
<input type="text" id="userInput" placeholder="Type a message..." disabled> <input type="text" id="userInput" placeholder="Type a message..." disabled />
<button id="sendButton" disabled>Send</button> <button id="sendButton" disabled>Send</button>
</div> </div>
</div> </div>
@ -191,8 +201,8 @@
params: { params: {
clientName: 'MCP StreamableHTTP Test Client', clientName: 'MCP StreamableHTTP Test Client',
clientVersion: '1.0.0', clientVersion: '1.0.0',
capabilities: {} capabilities: {},
} },
}; };
log('REQUEST', JSON.stringify(requestBody)); log('REQUEST', JSON.stringify(requestBody));
@ -200,15 +210,18 @@
const response = await fetch('/mcp', { const response = await fetch('/mcp', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify(requestBody) body: JSON.stringify(requestBody),
}); });
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
log('ERROR', `Error response: ${response.status} ${response.statusText} ${errorText}`); log('ERROR', `Error response: ${response.status} ${response.statusText} ${errorText}`);
appendMessage('system', `Error: ${response.status} ${response.statusText}\n${errorText}`); appendMessage(
'system',
`Error: ${response.status} ${response.statusText}\n${errorText}`
);
statusEl.textContent = 'Connection error. Try again.'; statusEl.textContent = 'Connection error. Try again.';
return; return;
} }
@ -254,7 +267,7 @@
jsonrpc: '2.0', jsonrpc: '2.0',
id: messageId++, id: messageId++,
method: 'listTools', method: 'listTools',
params: {} params: {},
}; };
log('REQUEST', JSON.stringify(requestBody)); log('REQUEST', JSON.stringify(requestBody));
@ -263,14 +276,17 @@
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'mcp-session-id': sessionId 'mcp-session-id': sessionId,
}, },
body: JSON.stringify(requestBody) body: JSON.stringify(requestBody),
}); });
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
log('ERROR', `Error listing tools: ${response.status} ${response.statusText} ${errorText}`); log(
'ERROR',
`Error listing tools: ${response.status} ${response.statusText} ${errorText}`
);
return; return;
} }
@ -278,7 +294,10 @@
log('TOOLS', JSON.stringify(data)); log('TOOLS', JSON.stringify(data));
if (data.result?.tools && Array.isArray(data.result.tools)) { if (data.result?.tools && Array.isArray(data.result.tools)) {
appendMessage('system', `Available tools: ${data.result.tools.map(t => t.name).join(', ')}`); appendMessage(
'system',
`Available tools: ${data.result.tools.map((t) => t.name).join(', ')}`
);
} }
} catch (error) { } catch (error) {
log('ERROR', `Error listing tools: ${error.message}`); log('ERROR', `Error listing tools: ${error.message}`);
@ -305,8 +324,8 @@
method: 'callTool', method: 'callTool',
params: { params: {
name: toolName, name: toolName,
arguments: parseArguments(text) arguments: parseArguments(text),
} },
}; };
log('REQUEST', JSON.stringify(requestBody)); log('REQUEST', JSON.stringify(requestBody));
@ -315,15 +334,18 @@
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'mcp-session-id': sessionId 'mcp-session-id': sessionId,
}, },
body: JSON.stringify(requestBody) body: JSON.stringify(requestBody),
}); });
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
log('ERROR', `Error response: ${response.status} ${response.statusText} ${errorText}`); log('ERROR', `Error response: ${response.status} ${response.statusText} ${errorText}`);
appendMessage('system', `Error: ${response.status} ${response.statusText}\n${errorText}`); appendMessage(
'system',
`Error: ${response.status} ${response.statusText}\n${errorText}`
);
return; return;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
/** /**
* StreamableHTTP server setup for HTTP-based MCP communication using Hono * StreamableHTTP server setup for HTTP-based MCP communication using Hono
*/ */
@ -6,17 +5,17 @@ import { Hono } from 'hono';
import { cors } from 'hono/cors'; import { cors } from 'hono/cors';
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { InitializeRequestSchema, JSONRPCError } from "@modelcontextprotocol/sdk/types.js"; import { InitializeRequestSchema, JSONRPCError } from '@modelcontextprotocol/sdk/types.js';
import { toReqRes, toFetchResponse } from 'fetch-to-node'; import { toReqRes, toFetchResponse } from 'fetch-to-node';
// Import server configuration constants // Import server configuration constants
import { SERVER_NAME, SERVER_VERSION } from './index.js'; import { SERVER_NAME, SERVER_VERSION } from './index.js';
// Constants // Constants
const SESSION_ID_HEADER_NAME = "mcp-session-id"; const SESSION_ID_HEADER_NAME = 'mcp-session-id';
const JSON_RPC = "2.0"; const JSON_RPC = '2.0';
/** /**
* StreamableHTTP MCP Server handler * StreamableHTTP MCP Server handler
@ -34,9 +33,9 @@ class MCPStreamableHttpServer {
* Handle GET requests (typically used for static files) * Handle GET requests (typically used for static files)
*/ */
async handleGetRequest(c: any) { 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, { return c.text('Method Not Allowed', 405, {
'Allow': 'POST' Allow: 'POST',
}); });
} }
@ -45,7 +44,9 @@ class MCPStreamableHttpServer {
*/ */
async handlePostRequest(c: any) { async handlePostRequest(c: any) {
const sessionId = c.req.header(SESSION_ID_HEADER_NAME); 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 { try {
const body = await c.req.json(); const body = await c.req.json();
@ -71,7 +72,7 @@ class MCPStreamableHttpServer {
// Create new transport for initialize requests // Create new transport for initialize requests
if (!sessionId && this.isInitializeRequest(body)) { 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({ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => uuid(), sessionIdGenerator: () => uuid(),
@ -111,16 +112,10 @@ class MCPStreamableHttpServer {
} }
// Invalid request (no session ID and not initialize) // Invalid request (no session ID and not initialize)
return c.json( return c.json(this.createErrorResponse('Bad Request: invalid session ID or method.'), 400);
this.createErrorResponse("Bad Request: invalid session ID or method."),
400
);
} catch (error) { } catch (error) {
console.error('Error handling MCP request:', error); console.error('Error handling MCP request:', error);
return c.json( return c.json(this.createErrorResponse('Internal server error.'), 500);
this.createErrorResponse("Internal server error."),
500
);
} }
} }
@ -148,7 +143,7 @@ class MCPStreamableHttpServer {
}; };
if (Array.isArray(body)) { if (Array.isArray(body)) {
return body.some(request => isInitial(request)); return body.some((request) => isInitial(request));
} }
return isInitial(body); return isInitial(body);
@ -178,8 +173,8 @@ export async function setupStreamableHttpServer(server: Server, port = 3000) {
}); });
// Main MCP endpoint supporting both GET and POST // Main MCP endpoint supporting both GET and POST
app.get("/mcp", (c) => mcpHandler.handleGetRequest(c)); app.get('/mcp', (c) => mcpHandler.handleGetRequest(c));
app.post("/mcp", (c) => mcpHandler.handlePostRequest(c)); app.post('/mcp', (c) => mcpHandler.handlePostRequest(c));
// Static files for the web client (if any) // Static files for the web client (if any)
app.get('/*', async (c) => { app.get('/*', async (c) => {
@ -209,17 +204,31 @@ export async function setupStreamableHttpServer(server: Server, port = 3000) {
let contentType = 'text/plain'; let contentType = 'text/plain';
switch (ext) { switch (ext) {
case '.html': contentType = 'text/html'; break; case '.html':
case '.css': contentType = 'text/css'; break; contentType = 'text/html';
case '.js': contentType = 'text/javascript'; break; break;
case '.json': contentType = 'application/json'; break; case '.css':
case '.png': contentType = 'image/png'; break; contentType = 'text/css';
case '.jpg': contentType = 'image/jpeg'; break; break;
case '.svg': contentType = 'image/svg+xml'; 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, { return new Response(content, {
headers: { 'Content-Type': contentType } headers: { 'Content-Type': contentType },
}); });
} }
} catch (err) { } catch (err) {
@ -235,14 +244,17 @@ export async function setupStreamableHttpServer(server: Server, port = 3000) {
}); });
// Start the server // Start the server
serve({ serve(
{
fetch: app.fetch, fetch: app.fetch,
port port,
}, (info) => { },
(info) => {
console.error(`MCP StreamableHTTP Server running at http://localhost:${info.port}`); console.error(`MCP StreamableHTTP Server running at http://localhost:${info.port}`);
console.error(`- MCP Endpoint: http://localhost:${info.port}/mcp`); console.error(`- MCP Endpoint: http://localhost:${info.port}/mcp`);
console.error(`- Health Check: http://localhost:${info.port}/health`); console.error(`- Health Check: http://localhost:${info.port}/health`);
}); }
);
return app; return app;
} }

View File

@ -17,12 +17,6 @@
"sourceMap": true, "sourceMap": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["src/**/*"],
"src/**/*" "exclude": ["node_modules", "build", "**/*.test.ts"]
],
"exclude": [
"node_modules",
"build",
"**/*.test.ts"
]
} }

File diff suppressed because it is too large Load Diff

View File

@ -17,11 +17,6 @@
"sourceMap": true, "sourceMap": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["src/**/*"],
"src/**/*" "exclude": ["node_modules", "build"]
],
"exclude": [
"node_modules",
"build"
]
} }

353
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "openapi-mcp-generator", "name": "openapi-mcp-generator",
"version": "3.1.2", "version": "3.2.0",
"description": "Generates MCP server code from OpenAPI specifications", "description": "Generates MCP server code from OpenAPI specifications",
"license": "MIT", "license": "MIT",
"author": "Harsha", "author": "Harsha",
@ -11,17 +11,21 @@
"bin": { "bin": {
"openapi-mcp-generator": "./bin/openapi-mcp-generator.js" "openapi-mcp-generator": "./bin/openapi-mcp-generator.js"
}, },
"main": "dist/index.js",
"files": [ "files": [
"dist", "dist",
"bin", "bin",
"README.md", "README.md",
"LICENSE" "LICENSE"
], ],
"types": "./dist/index.d.ts",
"scripts": { "scripts": {
"start": "node dist/index.js", "start": "node dist/index.js",
"clean": "rimraf dist", "clean": "rimraf dist",
"format.check": "prettier --check .",
"format.write": "prettier --write .",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"build": "tsc && chmod 755 dist/index.js && chmod 755 bin/openapi-mcp-generator.js", "build": "tsc && chmod 755 dist/index.js bin/openapi-mcp-generator.js",
"version:patch": "npm version patch", "version:patch": "npm version patch",
"version:minor": "npm version minor", "version:minor": "npm version minor",
"version:major": "npm version major" "version:major": "npm version major"
@ -49,16 +53,16 @@
"openapi-types": "^12.1.3" "openapi-types": "^12.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.15.2", "@types/node": "^22.17.2",
"@typescript-eslint/eslint-plugin": "^8.31.0", "@typescript-eslint/eslint-plugin": "^8.39.1",
"@typescript-eslint/parser": "^8.31.0", "@typescript-eslint/parser": "^8.39.1",
"eslint": "^9.25.1", "eslint": "^9.33.0",
"prettier": "^3.5.3", "prettier": "^3.6.2",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"typescript": "^5.8.3" "typescript": "^5.9.2"
}, },
"peerDependencies": { "peerDependencies": {
"@modelcontextprotocol/sdk": "^1.10.0", "@modelcontextprotocol/sdk": "^1.10.2",
"json-schema-to-zod": "^2.6.1", "json-schema-to-zod": "^2.6.1",
"zod": "^3.24.3" "zod": "^3.24.3"
} }

View File

@ -24,6 +24,13 @@ export interface GetToolsOptions {
/** Optional filter function to exclude tools based on custom criteria */ /** Optional filter function to exclude tools based on custom criteria */
filterFn?: (tool: McpToolDefinition) => boolean; 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;
} }
/** /**
@ -34,17 +41,19 @@ export interface GetToolsOptions {
* @returns Promise that resolves to an array of tool definitions * @returns Promise that resolves to an array of tool definitions
*/ */
export async function getToolsFromOpenApi( export async function getToolsFromOpenApi(
specPathOrUrl: string, specPathOrUrl: string | OpenAPIV3.Document,
options: GetToolsOptions = {} options: GetToolsOptions = {}
): Promise<McpToolDefinition[]> { ): Promise<McpToolDefinition[]> {
try { try {
// Parse the OpenAPI spec // Parse the OpenAPI spec
const api = options.dereference const api = isOpenApiDocument(specPathOrUrl)
? (await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document ? specPathOrUrl
: (await SwaggerParser.parse(specPathOrUrl)) as OpenAPIV3.Document; : options.dereference
? ((await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document)
: ((await SwaggerParser.parse(specPathOrUrl)) as OpenAPIV3.Document);
// Extract tools from the API // Extract tools from the API
const allTools = extractToolsFromApi(api); const allTools = extractToolsFromApi(api, options.defaultInclude ?? true);
// Add base URL to each tool // Add base URL to each tool
const baseUrl = determineBaseUrl(api, options.baseUrl); const baseUrl = determineBaseUrl(api, options.baseUrl);
@ -55,7 +64,7 @@ export async function getToolsFromOpenApi(
// Filter by excluded operation IDs if provided // Filter by excluded operation IDs if provided
if (options.excludeOperationIds && options.excludeOperationIds.length > 0) { if (options.excludeOperationIds && options.excludeOperationIds.length > 0) {
const excludeSet = new Set(options.excludeOperationIds); 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 // Apply custom filter function if provided
@ -64,14 +73,15 @@ export async function getToolsFromOpenApi(
} }
// Return the filtered tools with base URL added // Return the filtered tools with base URL added
return filteredTools.map(tool => ({ return filteredTools.map((tool) => ({
...tool, ...tool,
baseUrl: baseUrl || '', baseUrl: baseUrl || '',
})); }));
} catch (error) { } catch (error) {
// Provide more context for the error // Provide more context for the error
if (error instanceof 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; throw error;
} }

View File

@ -25,7 +25,7 @@ export function generateMcpServerCode(
serverVersion: string serverVersion: string
): string { ): string {
// Extract tools from API // Extract tools from API
const tools = extractToolsFromApi(api); const tools = extractToolsFromApi(api, options.defaultInclude ?? true);
// Determine base URL // Determine base URL
const determinedBaseUrl = determineBaseUrl(api, options.baseUrl); const determinedBaseUrl = determineBaseUrl(api, options.baseUrl);

View File

@ -1,4 +1,3 @@
/** /**
* Generator for StreamableHTTP server code for the MCP server using Hono * Generator for StreamableHTTP server code for the MCP server using Hono
*/ */

View File

@ -30,6 +30,8 @@ import {
// Import types // Import types
import { CliOptions, TransportType } from './types/index.js'; 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 programmatic API
export { getToolsFromOpenApi, McpToolDefinition, GetToolsOptions } from './api.js'; export { getToolsFromOpenApi, McpToolDefinition, GetToolsOptions } from './api.js';
@ -71,11 +73,23 @@ program
'Port for web or streamable-http transport (default: 3000)', 'Port for web or streamable-http transport (default: 3000)',
(val) => parseInt(val, 10) (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') .option('--force', 'Overwrite existing files without prompting')
.version('3.1.2') // Match package.json version .version(pkg.version) // Match package.json version
.action(options => { .action((options) => {
runGenerator(options) runGenerator(options).catch((error) => {
.catch((error) => {
console.error('Unhandled error:', error); console.error('Unhandled error:', error);
process.exit(1); process.exit(1);
}); });

View File

@ -5,6 +5,7 @@ import { OpenAPIV3 } from 'openapi-types';
import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema'; import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema';
import { generateOperationId } from '../utils/code-gen.js'; import { generateOperationId } from '../utils/code-gen.js';
import { McpToolDefinition } from '../types/index.js'; import { McpToolDefinition } from '../types/index.js';
import { shouldIncludeOperationForMcp } from '../utils/helpers.js';
/** /**
* Extracts tool definitions from an OpenAPI document * Extracts tool definitions from an OpenAPI document
@ -12,7 +13,10 @@ import { McpToolDefinition } from '../types/index.js';
* @param api OpenAPI document * @param api OpenAPI document
* @returns Array of MCP tool definitions * @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 tools: McpToolDefinition[] = [];
const usedNames = new Set<string>(); const usedNames = new Set<string>();
const globalSecurity = api.security || []; const globalSecurity = api.security || [];
@ -26,15 +30,43 @@ export function extractToolsFromApi(api: OpenAPIV3.Document): McpToolDefinition[
const operation = pathItem[method]; const operation = pathItem[method];
if (!operation) continue; 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 // Generate a unique name for the tool
let baseName = operation.operationId || generateOperationId(method, path); let baseName = operation.operationId || generateOperationId(method, path);
if (!baseName) continue; if (!baseName) continue;
// Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -) // Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -)
baseName = baseName baseName = baseName.replace(/\./g, '_').replace(/[^a-z0-9_-]/gi, '_');
.replace(/\./g, '_')
.replace(/[^a-z0-9_-]/gi, '_')
.toLowerCase();
let finalToolName = baseName; let finalToolName = baseName;
let counter = 1; let counter = 1;
@ -174,7 +206,9 @@ export function mapOpenApiSchemaToJsonSchema(
// Detect cycles // Detect cycles
if (seen.has(schema)) { if (seen.has(schema)) {
console.warn(`Cycle detected in schema${schema.title ? ` "${schema.title}"` : ''}, returning generic object to break recursion.`); console.warn(
`Cycle detected in schema${schema.title ? ` "${schema.title}"` : ''}, returning generic object to break recursion.`
);
return { type: 'object' }; return { type: 'object' };
} }
seen.add(schema); seen.add(schema);
@ -212,7 +246,10 @@ export function mapOpenApiSchemaToJsonSchema(
for (const [key, propSchema] of Object.entries(jsonSchema.properties)) { for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
if (typeof propSchema === 'object' && propSchema !== null) { if (typeof propSchema === 'object' && propSchema !== null) {
mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema as OpenAPIV3.SchemaObject, seen); mappedProps[key] = mapOpenApiSchemaToJsonSchema(
propSchema as OpenAPIV3.SchemaObject,
seen
);
} else if (typeof propSchema === 'boolean') { } else if (typeof propSchema === 'boolean') {
mappedProps[key] = propSchema; mappedProps[key] = propSchema;
} }

View File

@ -29,6 +29,12 @@ export interface CliOptions {
transport?: TransportType; transport?: TransportType;
/** Server port (for web and streamable-http transports) */ /** Server port (for web and streamable-http transports) */
port?: number; 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;
} }
/** /**

View File

@ -1,6 +1,7 @@
/** /**
* General helper utilities for OpenAPI to MCP generator * General helper utilities for OpenAPI to MCP generator
*/ */
import { OpenAPIV3 } from 'openapi-types';
/** /**
* Safely stringify a JSON object with proper error handling * 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 * '); 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;
}