Merge pull request #30 from oneWalker/dev

feat: style and ci optimization
This commit is contained in:
Harsha v 2025-06-19 22:00:22 +05:30 committed by GitHub
commit 47292c89cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 8928 additions and 7670 deletions

View File

@ -1,9 +1,6 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"plugins": ["@typescript-eslint"],
"env": {
"node": true,
@ -15,4 +12,4 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
}
}
}

View File

@ -4,7 +4,6 @@ about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
@ -12,6 +11,7 @@ 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 '....'
@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen.
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]
- 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]
- 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.

View File

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

View File

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

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

@ -4,4 +4,4 @@
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2
}
}

View File

@ -8,23 +8,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [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
@ -32,20 +35,24 @@ 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.
@ -53,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2.0.0] - 2025-04-12
### Added
- Runtime argument validation using Zod
- JSON Schema to Zod schema conversion
- Improved error handling and formatting
@ -63,6 +71,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
@ -72,6 +81,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
@ -81,6 +91,7 @@ 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

View File

@ -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', {
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'
});
filterFn: (tool) => tool.method.toLowerCase() === 'get',
}
);
// Process the results
for (const tool of filteredTools) {
@ -105,7 +111,7 @@ interface McpToolDefinition {
```typescript
const getTools = await getToolsFromOpenApi(specUrl, {
filterFn: (tool) => tool.method.toLowerCase() === 'get'
filterFn: (tool) => tool.method.toLowerCase() === 'get',
});
```
@ -113,7 +119,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 +127,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 +136,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',
});
```

View File

@ -49,7 +49,7 @@ 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` |
@ -74,7 +74,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 +100,7 @@ The generated project includes:
```
Core dependencies:
- `@modelcontextprotocol/sdk` - MCP protocol implementation
- `axios` - HTTP client for API requests
- `zod` - Runtime validation
@ -139,7 +140,7 @@ 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) |
@ -158,7 +159,7 @@ 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>` |
@ -214,8 +215,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)

View File

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

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

View File

@ -1,18 +1,26 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>swagger-petstore---openapi-3-0 MCP Test Client</title>
<style>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>swagger-petstore---openapi-3-0 MCP Test Client</title>
<style>
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;
margin: 0 auto;
padding: 20px;
line-height: 1.5;
}
h1 { margin-bottom: 10px; }
h1 {
margin-bottom: 10px;
}
.container {
display: flex;
flex-direction: column;
@ -39,13 +47,15 @@
}
#sendButton {
padding: 8px 16px;
background-color: #4CAF50;
background-color: #4caf50;
color: white;
border: none;
cursor: pointer;
border-radius: 0 5px 5px 0;
}
#sendButton:hover { background-color: #45a049; }
#sendButton:hover {
background-color: #45a049;
}
.message {
margin-bottom: 10px;
padding: 8px 12px;
@ -114,32 +124,32 @@
border-radius: 3px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>swagger-petstore---openapi-3-0 MCP Test Client</h1>
<p class="status" id="status">Disconnected</p>
</style>
</head>
<body>
<h1>swagger-petstore---openapi-3-0 MCP Test Client</h1>
<p class="status" id="status">Disconnected</p>
<div class="container">
<div class="container">
<div id="conversation"></div>
<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>
</div>
</div>
</div>
<button id="showDebug">Show Debug Console</button>
<button id="showDebug">Show Debug Console</button>
<div id="debug">
<div id="debug">
<div class="debug-controls">
<h3>Debug Console</h3>
<button class="clear-debug" id="clearDebug">Clear</button>
</div>
<div id="debugLog"></div>
</div>
</div>
<script>
<script>
const conversation = document.getElementById('conversation');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
@ -294,8 +304,8 @@
method: 'callTool',
params: {
name: toolName,
arguments: parseArguments(text)
}
arguments: parseArguments(text),
},
};
log('REQUEST', JSON.stringify(requestBody));
@ -307,15 +317,18 @@
const response = await fetch(fullEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
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 {
log('INFO', `Request sent successfully`);
// Note: We don't handle the response content here because the response
@ -388,6 +401,6 @@
window.addEventListener('beforeunload', () => {
if (eventSource) eventSource.close();
});
</script>
</body>
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,33 @@
/**
* Web server setup for HTTP-based MCP communication using Hono
*/
* Web server setup for HTTP-based MCP communication using Hono
*/
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { serve } from '@hono/node-server';
import { streamSSE } from 'hono/streaming';
import { v4 as uuid } from 'uuid';
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { JSONRPCMessage, JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js";
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { JSONRPCMessage, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
import type { Context } from 'hono';
import type { SSEStreamingApi } from 'hono/streaming';
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
// Import server configuration constants
import { SERVER_NAME, SERVER_VERSION } from './index.js';
/**
* Custom SSE Transport implementation using Hono's streaming API
*/
* Custom SSE Transport implementation using Hono's streaming API
*/
class SSETransport implements Transport {
private _sessionId: string;
private stream: SSEStreamingApi;
private messageUrl: string;
private _sessionId: string;
private stream: SSEStreamingApi;
private messageUrl: string;
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
constructor(messageUrl: string, stream: SSEStreamingApi) {
constructor(messageUrl: string, stream: SSEStreamingApi) {
this._sessionId = uuid();
this.stream = stream;
this.messageUrl = messageUrl;
@ -38,13 +37,13 @@ constructor(messageUrl: string, stream: SSEStreamingApi) {
console.error(`SSE connection aborted for session ${this._sessionId}`);
this.close();
});
}
}
get sessionId(): string {
get sessionId(): string {
return this._sessionId;
}
}
async start(): Promise<void> {
async start(): Promise<void> {
if (this.stream.closed) {
throw new Error('SSE transport already closed!');
}
@ -52,7 +51,7 @@ async start(): Promise<void> {
// Send the endpoint information
await this.stream.writeSSE({
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
@ -60,26 +59,26 @@ async start(): Promise<void> {
event: 'session',
data: JSON.stringify({
type: 'session_id',
session_id: this._sessionId
})
session_id: this._sessionId,
}),
});
// Send a welcome notification
await this.send({
jsonrpc: "2.0",
method: "notification",
jsonrpc: '2.0',
method: 'notification',
params: {
type: "welcome",
type: 'welcome',
clientInfo: {
sessionId: this._sessionId,
serverName: SERVER_NAME,
serverVersion: SERVER_VERSION
}
}
serverVersion: SERVER_VERSION,
},
},
});
}
}
async handlePostMessage(c: Context): Promise<Response> {
async handlePostMessage(c: Context): Promise<Response> {
if (this.stream?.closed) {
return c.text('SSE connection closed', 400);
}
@ -113,9 +112,9 @@ async handlePostMessage(c: Context): Promise<Response> {
console.error('Error processing request:', error);
return c.text('Error processing message', 400);
}
}
}
async close(): Promise<void> {
async close(): Promise<void> {
if (this.stream && !this.stream.closed) {
this.stream.abort();
}
@ -123,44 +122,44 @@ async close(): Promise<void> {
if (this.onclose) {
this.onclose();
}
}
}
async send(message: JSONRPCMessage): Promise<void> {
async send(message: JSONRPCMessage): Promise<void> {
if (this.stream.closed) {
throw new Error('Not connected');
}
await this.stream.writeSSE({
event: 'message',
data: JSON.stringify(message)
data: JSON.stringify(message),
});
}
}
}
/**
* Sets up a web server for the MCP server using Server-Sent Events (SSE)
*
* @param server The MCP Server instance
* @param port The port to listen on (default: 3000)
* @returns The Hono app instance
*/
* Sets up a web server for the MCP server using Server-Sent Events (SSE)
*
* @param server The MCP Server instance
* @param port The port to listen on (default: 3000)
* @returns The Hono app instance
*/
export async function setupWebServer(server: Server, port = 3000) {
// Create Hono app
const app = new Hono();
// Create Hono app
const app = new Hono();
// Enable CORS
app.use('*', cors());
// Enable CORS
app.use('*', cors());
// Store active SSE transports by session ID
const transports: {[sessionId: string]: SSETransport} = {};
// Store active SSE transports by session ID
const transports: { [sessionId: string]: SSETransport } = {};
// Add a simple health check endpoint
app.get('/health', (c) => {
// Add a simple health check endpoint
app.get('/health', (c) => {
return c.json({ status: 'OK', server: SERVER_NAME, version: SERVER_VERSION });
});
});
// SSE endpoint for clients to connect to
app.get("/sse", (c) => {
// SSE endpoint for clients to connect to
app.get('/sse', (c) => {
return streamSSE(c, async (stream) => {
// Create SSE transport
const transport = new SSETransport('/api/messages', stream);
@ -199,10 +198,10 @@ app.get("/sse", (c) => {
await stream.sleep(1000);
}
});
});
});
// API endpoint for clients to send messages
app.post("/api/messages", async (c) => {
// API endpoint for clients to send messages
app.post('/api/messages', async (c) => {
const sessionId = c.req.query('sessionId');
if (!sessionId) {
@ -216,10 +215,10 @@ app.post("/api/messages", async (c) => {
}
return transport.handlePostMessage(c);
});
});
// Static files for the web client (if any)
app.get('/*', async (c) => {
// Static files for the web client (if any)
app.get('/*', async (c) => {
const filePath = c.req.path === '/' ? '/index.html' : c.req.path;
try {
// Use Node.js fs to serve static files
@ -246,17 +245,31 @@ app.get('/*', async (c) => {
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) {
@ -269,18 +282,23 @@ app.get('/*', async (c) => {
}
return c.text('Not Found', 404);
});
});
// Start the server
serve({
// Start the server
serve(
{
fetch: app.fetch,
port
}, (info) => {
port,
},
(info) => {
console.error(`MCP Web Server running at http://localhost:${info.port}`);
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`);
});
}
);
return app;
return app;
}

View File

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

View File

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

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

View File

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

View File

@ -1,18 +1,26 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>swagger-petstore---openapi-3-0 MCP StreamableHTTP Test Client</title>
<style>
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;
margin: 0 auto;
padding: 20px;
line-height: 1.5;
}
h1 { margin-bottom: 10px; }
h1 {
margin-bottom: 10px;
}
.container {
display: flex;
flex-direction: column;
@ -39,13 +47,15 @@
}
#sendButton {
padding: 8px 16px;
background-color: #4CAF50;
background-color: #4caf50;
color: white;
border: none;
cursor: pointer;
border-radius: 0 5px 5px 0;
}
#sendButton:hover { background-color: #45a049; }
#sendButton:hover {
background-color: #45a049;
}
.message {
margin-bottom: 10px;
padding: 8px 12px;
@ -115,8 +125,8 @@
cursor: pointer;
}
</style>
</head>
<body>
</head>
<body>
<h1>swagger-petstore---openapi-3-0 MCP StreamableHTTP Test Client</h1>
<p class="status" id="status">Disconnected</p>
@ -124,7 +134,7 @@
<div id="conversation"></div>
<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>
</div>
</div>
@ -191,8 +201,8 @@
params: {
clientName: 'MCP StreamableHTTP Test Client',
clientVersion: '1.0.0',
capabilities: {}
}
capabilities: {},
},
};
log('REQUEST', JSON.stringify(requestBody));
@ -200,15 +210,18 @@
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
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.';
return;
}
@ -254,7 +267,7 @@
jsonrpc: '2.0',
id: messageId++,
method: 'listTools',
params: {}
params: {},
};
log('REQUEST', JSON.stringify(requestBody));
@ -263,14 +276,17 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
'mcp-session-id': sessionId
'mcp-session-id': sessionId,
},
body: JSON.stringify(requestBody)
body: JSON.stringify(requestBody),
});
if (!response.ok) {
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;
}
@ -278,7 +294,10 @@
log('TOOLS', JSON.stringify(data));
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) {
log('ERROR', `Error listing tools: ${error.message}`);
@ -305,8 +324,8 @@
method: 'callTool',
params: {
name: toolName,
arguments: parseArguments(text)
}
arguments: parseArguments(text),
},
};
log('REQUEST', JSON.stringify(requestBody));
@ -315,15 +334,18 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
'mcp-session-id': sessionId
'mcp-session-id': sessionId,
},
body: JSON.stringify(requestBody)
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
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;
}
@ -398,5 +420,5 @@
appendMessage('system', 'Initializing MCP connection...');
initialize();
</script>
</body>
</body>
</html>

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
*/
@ -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,7 +23,7 @@ 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;
@ -34,9 +33,9 @@ class MCPStreamableHttpServer {
* 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',
});
}
@ -45,7 +44,9 @@ class MCPStreamableHttpServer {
*/
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();
@ -71,7 +72,7 @@ class MCPStreamableHttpServer {
// 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(),
@ -111,16 +112,10 @@ class MCPStreamableHttpServer {
}
// 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);
}
}
@ -148,7 +143,7 @@ class MCPStreamableHttpServer {
};
if (Array.isArray(body)) {
return body.some(request => isInitial(request));
return body.some((request) => isInitial(request));
}
return isInitial(body);
@ -178,8 +173,8 @@ export async function setupStreamableHttpServer(server: Server, port = 3000) {
});
// 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) => {
@ -209,17 +204,31 @@ export async function setupStreamableHttpServer(server: Server, port = 3000) {
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) {
@ -235,14 +244,17 @@ export async function setupStreamableHttpServer(server: Server, port = 3000) {
});
// Start the server
serve({
serve(
{
fetch: app.fetch,
port
}, (info) => {
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;
}

View File

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

View File

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

View File

@ -22,6 +22,8 @@
"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 && chmod 755 bin/openapi-mcp-generator.js",
"version:patch": "npm version patch",

View File

@ -40,8 +40,8 @@ export async function getToolsFromOpenApi(
try {
// Parse the OpenAPI spec
const api = options.dereference
? (await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document
: (await SwaggerParser.parse(specPathOrUrl)) as OpenAPIV3.Document;
? ((await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document)
: ((await SwaggerParser.parse(specPathOrUrl)) as OpenAPIV3.Document);
// Extract tools from the API
const allTools = extractToolsFromApi(api);
@ -55,7 +55,7 @@ export async function getToolsFromOpenApi(
// 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
@ -64,7 +64,7 @@ export async function getToolsFromOpenApi(
}
// Return the filtered tools with base URL added
return filteredTools.map(tool => ({
return filteredTools.map((tool) => ({
...tool,
baseUrl: baseUrl || '',
}));

View File

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

View File

@ -15,4 +15,4 @@
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}
}