style: format the code with the prettier config

This commit is contained in:
Brian,Kun Liu 2025-06-19 11:31:27 +08:00
parent ea89e0498e
commit 2cab8aeada
31 changed files with 8862 additions and 7638 deletions

View File

@ -1,18 +1,15 @@
{ {
"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" "env": {
], "node": true,
"plugins": ["@typescript-eslint"], "es2022": true
"env": { },
"node": true, "rules": {
"es2022": true "no-console": "off",
}, "@typescript-eslint/explicit-function-return-type": "off",
"rules": { "@typescript-eslint/no-explicit-any": "off",
"no-console": "off", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
"@typescript-eslint/explicit-function-return-type": "off",
"@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: '' 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,15 +24,17 @@ 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]
- Browser [e.g. chrome, safari] - OS: [e.g. iOS]
- Version [e.g. 22] - Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1] - Device: [e.g. iPhone6]
- Browser [e.g. stock browser, safari] - OS: [e.g. iOS8.1]
- Version [e.g. 22] - Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

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.**

View File

@ -23,7 +23,7 @@ jobs:
with: with:
node-version: '20' node-version: '20'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
cache: 'npm' # Enables npm dependency caching cache: 'npm' # Enables npm dependency caching
cache-dependency-path: '**/package-lock.json' # Cache key based on lockfile cache-dependency-path: '**/package-lock.json' # Cache key based on lockfile
- name: Install dependencies - name: Install dependencies

View File

@ -1,7 +1,7 @@
{ {
"semi": true, "semi": true,
"trailingComma": "es5", "trailingComma": "es5",
"singleQuote": true, "singleQuote": true,
"printWidth": 100, "printWidth": 100,
"tabWidth": 2 "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 ## [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 +35,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 +60,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 +71,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 +81,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 +91,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(
baseUrl: 'https://petstore3.swagger.io/api/v3', 'https://petstore3.swagger.io/api/v3/openapi.json',
dereference: true, {
excludeOperationIds: ['addPet', 'updatePet'], baseUrl: 'https://petstore3.swagger.io/api/v3',
filterFn: (tool) => tool.method.toLowerCase() === 'get' dereference: true,
}); excludeOperationIds: ['addPet', 'updatePet'],
filterFn: (tool) => tool.method.toLowerCase() === 'get',
}
);
// Process the results // Process the results
for (const tool of filteredTools) { for (const tool of filteredTools) {
@ -105,7 +111,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 +119,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 +127,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 +136,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

@ -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,393 +1,406 @@
<!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:
max-width: 800px; system-ui,
margin: 0 auto; -apple-system,
padding: 20px; BlinkMacSystemFont,
line-height: 1.5; 'Segoe UI',
} Roboto,
h1 { margin-bottom: 10px; } sans-serif;
.container { max-width: 800px;
display: flex; margin: 0 auto;
flex-direction: column; padding: 20px;
height: calc(100vh - 150px); line-height: 1.5;
}
#conversation {
flex: 1;
border: 1px solid #ccc;
overflow-y: auto;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.input-area {
display: flex;
margin-bottom: 20px;
}
#userInput {
flex: 1;
padding: 8px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
#sendButton {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
border-radius: 0 5px 5px 0;
}
#sendButton:hover { background-color: #45a049; }
.message {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 5px;
}
.user {
background-color: #e7f4ff;
align-self: flex-end;
}
.server {
background-color: #f1f1f1;
}
.system {
background-color: #fffde7;
color: #795548;
font-style: italic;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
code {
background-color: #f8f8f8;
padding: 2px 4px;
border-radius: 3px;
}
.status {
color: #666;
font-style: italic;
margin-bottom: 10px;
}
#debug {
margin-top: 20px;
background-color: #f8f8f8;
padding: 10px;
border-radius: 5px;
display: none;
}
.debug-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
#showDebug {
margin-top: 10px;
padding: 5px 10px;
cursor: pointer;
background-color: #f1f1f1;
border: 1px solid #ccc;
border-radius: 3px;
}
#debugLog {
max-height: 200px;
overflow-y: auto;
background-color: #111;
color: #0f0;
font-family: monospace;
padding: 5px;
margin-top: 10px;
}
.clear-debug {
padding: 3px 8px;
background-color: #f44336;
color: white;
border: none;
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>
<div class="container">
<div id="conversation"></div>
<div class="input-area">
<input type="text" id="userInput" placeholder="Type a message..." disabled>
<button id="sendButton" disabled>Send</button>
</div>
</div>
<button id="showDebug">Show Debug Console</button>
<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>
<script>
const conversation = document.getElementById('conversation');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
const statusEl = document.getElementById('status');
const showDebugBtn = document.getElementById('showDebug');
const debugDiv = document.getElementById('debug');
const debugLog = document.getElementById('debugLog');
const clearDebugBtn = document.getElementById('clearDebug');
let sessionId = null;
let messageId = 1;
let eventSource = null;
let apiEndpoint = 'http://localhost:3000/api/messages'; // default endpoint
// Debug logging
function log(type, message) {
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
const entry = document.createElement('div');
entry.innerHTML = `<span style="color:#aaa;">${timestamp}</span> <span style="color:#58a6ff;">${type}:</span> ${message}`;
debugLog.appendChild(entry);
debugLog.scrollTop = debugLog.scrollHeight;
console.log(`${type}: ${message}`);
}
// Toggle debug console
showDebugBtn.addEventListener('click', () => {
if (debugDiv.style.display === 'block') {
debugDiv.style.display = 'none';
showDebugBtn.textContent = 'Show Debug Console';
} else {
debugDiv.style.display = 'block';
showDebugBtn.textContent = 'Hide Debug Console';
}
});
// Clear debug logs
clearDebugBtn.addEventListener('click', () => {
debugLog.innerHTML = '';
});
// Connect to SSE endpoint
function connect() {
statusEl.textContent = 'Connecting...';
log('INFO', 'Connecting to SSE endpoint...');
// Close existing connection if any
if (eventSource) {
eventSource.close();
log('INFO', 'Closed existing connection');
}
eventSource = new EventSource('http://localhost:3000/sse');
eventSource.onopen = () => {
log('INFO', 'SSE connection opened');
statusEl.textContent = 'Connected, waiting for session ID...';
};
eventSource.onerror = (error) => {
log('ERROR', `SSE connection error: ${error}`);
statusEl.textContent = 'Connection error. Reconnecting in 3s...';
setTimeout(connect, 3000);
};
// Listen for the endpoint event
eventSource.addEventListener('endpoint', (event) => {
apiEndpoint = event.data;
log('INFO', `API endpoint received: ${apiEndpoint}`);
});
// Listen for the session event
eventSource.addEventListener('session', (event) => {
log('INFO', `Session data received: ${event.data}`);
try {
const data = JSON.parse(event.data);
if (data.type === 'session_id') {
sessionId = data.session_id;
statusEl.textContent = `Connected (Session ID: ${sessionId})`;
userInput.disabled = false;
sendButton.disabled = false;
userInput.focus();
appendMessage('system', `Connected with session ID: ${sessionId}`);
log('INFO', `Received session ID: ${sessionId}`);
}
} catch (error) {
log('ERROR', `Error parsing session data: ${error.message}`);
} }
}); h1 {
margin-bottom: 10px;
}
.container {
display: flex;
flex-direction: column;
height: calc(100vh - 150px);
}
#conversation {
flex: 1;
border: 1px solid #ccc;
overflow-y: auto;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.input-area {
display: flex;
margin-bottom: 20px;
}
#userInput {
flex: 1;
padding: 8px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
#sendButton {
padding: 8px 16px;
background-color: #4caf50;
color: white;
border: none;
cursor: pointer;
border-radius: 0 5px 5px 0;
}
#sendButton:hover {
background-color: #45a049;
}
.message {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 5px;
}
.user {
background-color: #e7f4ff;
align-self: flex-end;
}
.server {
background-color: #f1f1f1;
}
.system {
background-color: #fffde7;
color: #795548;
font-style: italic;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
code {
background-color: #f8f8f8;
padding: 2px 4px;
border-radius: 3px;
}
.status {
color: #666;
font-style: italic;
margin-bottom: 10px;
}
#debug {
margin-top: 20px;
background-color: #f8f8f8;
padding: 10px;
border-radius: 5px;
display: none;
}
.debug-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
#showDebug {
margin-top: 10px;
padding: 5px 10px;
cursor: pointer;
background-color: #f1f1f1;
border: 1px solid #ccc;
border-radius: 3px;
}
#debugLog {
max-height: 200px;
overflow-y: auto;
background-color: #111;
color: #0f0;
font-family: monospace;
padding: 5px;
margin-top: 10px;
}
.clear-debug {
padding: 3px 8px;
background-color: #f44336;
color: white;
border: none;
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>
// Listen for regular messages <div class="container">
eventSource.addEventListener('message', (event) => { <div id="conversation"></div>
log('RAW', event.data);
try { <div class="input-area">
const data = JSON.parse(event.data); <input type="text" id="userInput" placeholder="Type a message..." disabled />
<button id="sendButton" disabled>Send</button>
</div>
</div>
// The MCP SSE transport sends messages in jsonrpc format <button id="showDebug">Show Debug Console</button>
// Check if this is a notification with clientInfo containing sessionId
if (data.method === 'notification' && data.params?.clientInfo?.sessionId) {
if (!sessionId) {
sessionId = data.params.clientInfo.sessionId;
statusEl.textContent = `Connected (Session ID: ${sessionId})`;
userInput.disabled = false;
sendButton.disabled = false;
userInput.focus();
appendMessage('system', `Connected with session ID: ${sessionId}`);
log('INFO', `Received session ID from MCP notification: ${sessionId}`);
}
return;
}
// Handle jsonrpc responses <div id="debug">
if (data.jsonrpc === '2.0' && data.result) { <div class="debug-controls">
appendMessage('server', JSON.stringify(data.result, null, 2)); <h3>Debug Console</h3>
userInput.focus(); <button class="clear-debug" id="clearDebug">Clear</button>
return; </div>
} <div id="debugLog"></div>
</div>
// Handle normal server messages with content <script>
if (data.content) { const conversation = document.getElementById('conversation');
appendMessage('server', JSON.stringify(data, null, 2)); const userInput = document.getElementById('userInput');
userInput.focus(); const sendButton = document.getElementById('sendButton');
const statusEl = document.getElementById('status');
const showDebugBtn = document.getElementById('showDebug');
const debugDiv = document.getElementById('debug');
const debugLog = document.getElementById('debugLog');
const clearDebugBtn = document.getElementById('clearDebug');
let sessionId = null;
let messageId = 1;
let eventSource = null;
let apiEndpoint = 'http://localhost:3000/api/messages'; // default endpoint
// Debug logging
function log(type, message) {
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
const entry = document.createElement('div');
entry.innerHTML = `<span style="color:#aaa;">${timestamp}</span> <span style="color:#58a6ff;">${type}:</span> ${message}`;
debugLog.appendChild(entry);
debugLog.scrollTop = debugLog.scrollHeight;
console.log(`${type}: ${message}`);
}
// Toggle debug console
showDebugBtn.addEventListener('click', () => {
if (debugDiv.style.display === 'block') {
debugDiv.style.display = 'none';
showDebugBtn.textContent = 'Show Debug Console';
} else { } else {
log('INFO', `Received other message: ${JSON.stringify(data)}`); debugDiv.style.display = 'block';
showDebugBtn.textContent = 'Hide Debug Console';
} }
} catch (error) {
log('ERROR', `Error parsing SSE message: ${error.message}`);
appendMessage('system', `Error parsing message: ${event.data}`);
}
});
return eventSource;
}
// Send a message to the server
async function sendMessage() {
const text = userInput.value.trim();
if (!text || !sessionId) return;
appendMessage('user', text);
userInput.value = '';
log('INFO', `Sending message: ${text}`);
try {
const parts = text.split(' ');
const toolName = parts[0];
const requestBody = {
jsonrpc: '2.0',
id: messageId++,
method: 'callTool',
params: {
name: toolName,
arguments: parseArguments(text)
}
};
log('REQUEST', JSON.stringify(requestBody));
// Use the endpoint provided by the server, or fall back to the default
const fullEndpoint = `http://localhost:3000/api/messages?sessionId=${sessionId}`;
console.log('fullEndpoint', fullEndpoint);
const response = await fetch(fullEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
}); });
if (!response.ok) { // Clear debug logs
const errorText = await response.text(); clearDebugBtn.addEventListener('click', () => {
log('ERROR', `Error response: ${response.status} ${response.statusText} ${errorText}`); debugLog.innerHTML = '';
appendMessage('system', `Error: ${response.status} ${response.statusText}\n${errorText}`); });
} else {
log('INFO', `Request sent successfully`); // Connect to SSE endpoint
// Note: We don't handle the response content here because the response function connect() {
// will come through the SSE connection, not this fetch response statusEl.textContent = 'Connecting...';
log('INFO', 'Connecting to SSE endpoint...');
// Close existing connection if any
if (eventSource) {
eventSource.close();
log('INFO', 'Closed existing connection');
}
eventSource = new EventSource('http://localhost:3000/sse');
eventSource.onopen = () => {
log('INFO', 'SSE connection opened');
statusEl.textContent = 'Connected, waiting for session ID...';
};
eventSource.onerror = (error) => {
log('ERROR', `SSE connection error: ${error}`);
statusEl.textContent = 'Connection error. Reconnecting in 3s...';
setTimeout(connect, 3000);
};
// Listen for the endpoint event
eventSource.addEventListener('endpoint', (event) => {
apiEndpoint = event.data;
log('INFO', `API endpoint received: ${apiEndpoint}`);
});
// Listen for the session event
eventSource.addEventListener('session', (event) => {
log('INFO', `Session data received: ${event.data}`);
try {
const data = JSON.parse(event.data);
if (data.type === 'session_id') {
sessionId = data.session_id;
statusEl.textContent = `Connected (Session ID: ${sessionId})`;
userInput.disabled = false;
sendButton.disabled = false;
userInput.focus();
appendMessage('system', `Connected with session ID: ${sessionId}`);
log('INFO', `Received session ID: ${sessionId}`);
}
} catch (error) {
log('ERROR', `Error parsing session data: ${error.message}`);
}
});
// Listen for regular messages
eventSource.addEventListener('message', (event) => {
log('RAW', event.data);
try {
const data = JSON.parse(event.data);
// The MCP SSE transport sends messages in jsonrpc format
// Check if this is a notification with clientInfo containing sessionId
if (data.method === 'notification' && data.params?.clientInfo?.sessionId) {
if (!sessionId) {
sessionId = data.params.clientInfo.sessionId;
statusEl.textContent = `Connected (Session ID: ${sessionId})`;
userInput.disabled = false;
sendButton.disabled = false;
userInput.focus();
appendMessage('system', `Connected with session ID: ${sessionId}`);
log('INFO', `Received session ID from MCP notification: ${sessionId}`);
}
return;
}
// Handle jsonrpc responses
if (data.jsonrpc === '2.0' && data.result) {
appendMessage('server', JSON.stringify(data.result, null, 2));
userInput.focus();
return;
}
// Handle normal server messages with content
if (data.content) {
appendMessage('server', JSON.stringify(data, null, 2));
userInput.focus();
} else {
log('INFO', `Received other message: ${JSON.stringify(data)}`);
}
} catch (error) {
log('ERROR', `Error parsing SSE message: ${error.message}`);
appendMessage('system', `Error parsing message: ${event.data}`);
}
});
return eventSource;
} }
} catch (error) {
log('ERROR', `Error sending message: ${error.message}`);
appendMessage('system', `Error sending message: ${error.message}`);
}
}
// Try to parse arguments from user input // Send a message to the server
// Format: toolName param1=value1 param2=value2 async function sendMessage() {
function parseArguments(text) { const text = userInput.value.trim();
const parts = text.split(' '); if (!text || !sessionId) return;
if (parts.length <= 1) return {};
const args = {}; appendMessage('user', text);
// Skip the first part (tool name) and process the rest userInput.value = '';
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
const equalsIndex = part.indexOf('=');
if (equalsIndex > 0) { log('INFO', `Sending message: ${text}`);
const key = part.substring(0, equalsIndex);
const value = part.substring(equalsIndex + 1);
// Try to parse as number or boolean if possible try {
if (value === 'true') args[key] = true; const parts = text.split(' ');
else if (value === 'false') args[key] = false; const toolName = parts[0];
else if (!isNaN(Number(value))) args[key] = Number(value);
else args[key] = value; const requestBody = {
jsonrpc: '2.0',
id: messageId++,
method: 'callTool',
params: {
name: toolName,
arguments: parseArguments(text),
},
};
log('REQUEST', JSON.stringify(requestBody));
// Use the endpoint provided by the server, or fall back to the default
const fullEndpoint = `http://localhost:3000/api/messages?sessionId=${sessionId}`;
console.log('fullEndpoint', fullEndpoint);
const response = await fetch(fullEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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}`
);
} else {
log('INFO', `Request sent successfully`);
// Note: We don't handle the response content here because the response
// will come through the SSE connection, not this fetch response
}
} catch (error) {
log('ERROR', `Error sending message: ${error.message}`);
appendMessage('system', `Error sending message: ${error.message}`);
}
} }
}
return args; // Try to parse arguments from user input
} // Format: toolName param1=value1 param2=value2
function parseArguments(text) {
const parts = text.split(' ');
if (parts.length <= 1) return {};
// Add a message to the conversation const args = {};
function appendMessage(sender, text) { // Skip the first part (tool name) and process the rest
const messageDiv = document.createElement('div'); for (let i = 1; i < parts.length; i++) {
messageDiv.className = `message ${sender}`; const part = parts[i];
const equalsIndex = part.indexOf('=');
// Format as code block if it looks like JSON if (equalsIndex > 0) {
if (text.trim().startsWith('{') || text.trim().startsWith('[')) { const key = part.substring(0, equalsIndex);
const pre = document.createElement('pre'); const value = part.substring(equalsIndex + 1);
const code = document.createElement('code');
code.textContent = text;
pre.appendChild(code);
messageDiv.appendChild(pre);
} else {
messageDiv.textContent = text;
}
conversation.appendChild(messageDiv); // Try to parse as number or boolean if possible
conversation.scrollTop = conversation.scrollHeight; if (value === 'true') args[key] = true;
} else if (value === 'false') args[key] = false;
else if (!isNaN(Number(value))) args[key] = Number(value);
else args[key] = value;
}
}
// Event listeners return args;
sendButton.addEventListener('click', sendMessage); }
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Connect on page load // Add a message to the conversation
appendMessage('system', 'Connecting to server...'); function appendMessage(sender, text) {
connect(); const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}`;
// Clean up on page unload // Format as code block if it looks like JSON
window.addEventListener('beforeunload', () => { if (text.trim().startsWith('{') || text.trim().startsWith('[')) {
if (eventSource) eventSource.close(); const pre = document.createElement('pre');
}); const code = document.createElement('code');
</script> code.textContent = text;
</body> pre.appendChild(code);
messageDiv.appendChild(pre);
} else {
messageDiv.textContent = text;
}
conversation.appendChild(messageDiv);
conversation.scrollTop = conversation.scrollHeight;
}
// Event listeners
sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Connect on page load
appendMessage('system', 'Connecting to server...');
connect();
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (eventSource) eventSource.close();
});
</script>
</body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -1,286 +1,304 @@
/** /**
* 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 { 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 { 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';
/** /**
* Custom SSE Transport implementation using Hono's streaming API * Custom SSE Transport implementation using Hono's streaming API
*/ */
class SSETransport implements Transport { class SSETransport implements Transport {
private _sessionId: string; private _sessionId: string;
private stream: SSEStreamingApi; private stream: SSEStreamingApi;
private messageUrl: string; private messageUrl: string;
onclose?: () => void; onclose?: () => void;
onerror?: (error: Error) => void; onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void; onmessage?: (message: JSONRPCMessage) => void;
constructor(messageUrl: string, stream: SSEStreamingApi) { constructor(messageUrl: string, stream: SSEStreamingApi) {
this._sessionId = uuid(); this._sessionId = uuid();
this.stream = stream; this.stream = stream;
this.messageUrl = messageUrl; this.messageUrl = messageUrl;
// Set up stream abort handler // Set up stream abort handler
this.stream.onAbort(() => { this.stream.onAbort(() => {
console.error(`SSE connection aborted for session ${this._sessionId}`); console.error(`SSE connection aborted for session ${this._sessionId}`);
this.close(); this.close();
}); });
}
get sessionId(): string {
return this._sessionId;
}
async start(): Promise<void> {
if (this.stream.closed) {
throw new Error('SSE transport already closed!');
} }
// Send the endpoint information get sessionId(): string {
await this.stream.writeSSE({ return this._sessionId;
event: 'endpoint', }
data: `${this.messageUrl}?sessionId=${this._sessionId}`
});
// Send session ID and connection info in a format the client can understand async start(): Promise<void> {
await this.stream.writeSSE({ if (this.stream.closed) {
event: 'session', throw new Error('SSE transport already closed!');
data: JSON.stringify({
type: 'session_id',
session_id: this._sessionId
})
});
// Send a welcome notification
await this.send({
jsonrpc: "2.0",
method: "notification",
params: {
type: "welcome",
clientInfo: {
sessionId: this._sessionId,
serverName: SERVER_NAME,
serverVersion: SERVER_VERSION
}
} }
});
}
async handlePostMessage(c: Context): Promise<Response> { // Send the endpoint information
if (this.stream?.closed) { await this.stream.writeSSE({
return c.text('SSE connection closed', 400); event: 'endpoint',
data: `${this.messageUrl}?sessionId=${this._sessionId}`,
});
// Send session ID and connection info in a format the client can understand
await this.stream.writeSSE({
event: 'session',
data: JSON.stringify({
type: 'session_id',
session_id: this._sessionId,
}),
});
// Send a welcome notification
await this.send({
jsonrpc: '2.0',
method: 'notification',
params: {
type: 'welcome',
clientInfo: {
sessionId: this._sessionId,
serverName: SERVER_NAME,
serverVersion: SERVER_VERSION,
},
},
});
} }
try { async handlePostMessage(c: Context): Promise<Response> {
// Parse and validate the message if (this.stream?.closed) {
const body = await c.req.json(); return c.text('SSE connection closed', 400);
}
try { try {
// Parse and validate the message // Parse and validate the message
const parsedMessage = JSONRPCMessageSchema.parse(body); const body = await c.req.json();
// Forward to the message handler try {
if (this.onmessage) { // Parse and validate the message
this.onmessage(parsedMessage); const parsedMessage = JSONRPCMessageSchema.parse(body);
return c.text('Accepted', 202);
} else { // Forward to the message handler
return c.text('No message handler defined', 500); if (this.onmessage) {
this.onmessage(parsedMessage);
return c.text('Accepted', 202);
} else {
return c.text('No message handler defined', 500);
}
} catch (error) {
if (this.onerror) {
this.onerror(error instanceof Error ? error : new Error(String(error)));
}
console.error('Error parsing message:', error);
return c.text('Invalid message format', 400);
} }
} catch (error) { } catch (error) {
if (this.onerror) { if (this.onerror) {
this.onerror(error instanceof Error ? error : new Error(String(error))); this.onerror(error instanceof Error ? error : new Error(String(error)));
} }
console.error('Error parsing message:', error); console.error('Error processing request:', error);
return c.text('Invalid message format', 400); return c.text('Error processing message', 400);
} }
} catch (error) { }
if (this.onerror) {
this.onerror(error instanceof Error ? error : new Error(String(error))); async close(): Promise<void> {
if (this.stream && !this.stream.closed) {
this.stream.abort();
} }
console.error('Error processing request:', error);
return c.text('Error processing message', 400);
}
}
async close(): Promise<void> { if (this.onclose) {
if (this.stream && !this.stream.closed) { this.onclose();
this.stream.abort(); }
} }
if (this.onclose) { async send(message: JSONRPCMessage): Promise<void> {
this.onclose(); if (this.stream.closed) {
} throw new Error('Not connected');
} }
async send(message: JSONRPCMessage): Promise<void> { await this.stream.writeSSE({
if (this.stream.closed) { event: 'message',
throw new Error('Not connected'); data: JSON.stringify(message),
});
} }
await this.stream.writeSSE({
event: 'message',
data: JSON.stringify(message)
});
}
} }
/** /**
* Sets up a web server for the MCP server using Server-Sent Events (SSE) * Sets up a web server for the MCP server using Server-Sent Events (SSE)
* *
* @param server The MCP Server instance * @param server The MCP Server instance
* @param port The port to listen on (default: 3000) * @param port The port to listen on (default: 3000)
* @returns The Hono app instance * @returns The Hono app instance
*/ */
export async function setupWebServer(server: Server, port = 3000) { export async function setupWebServer(server: Server, port = 3000) {
// Create Hono app // Create Hono app
const app = new Hono(); const app = new Hono();
// Enable CORS // Enable CORS
app.use('*', cors()); app.use('*', cors());
// Store active SSE transports by session ID // Store active SSE transports by session ID
const transports: {[sessionId: string]: SSETransport} = {}; const transports: { [sessionId: string]: SSETransport } = {};
// Add a simple health check endpoint // Add a simple health check endpoint
app.get('/health', (c) => { app.get('/health', (c) => {
return c.json({ status: 'OK', server: SERVER_NAME, version: SERVER_VERSION }); return c.json({ status: 'OK', server: SERVER_NAME, version: SERVER_VERSION });
}); });
// 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);
const sessionId = transport.sessionId; const sessionId = transport.sessionId;
console.error(`New SSE connection established: ${sessionId}`); console.error(`New SSE connection established: ${sessionId}`);
// Store the transport // Store the transport
transports[sessionId] = transport; transports[sessionId] = transport;
// Set up cleanup on transport close // Set up cleanup on transport close
transport.onclose = () => { transport.onclose = () => {
console.error(`SSE connection closed for session ${sessionId}`); console.error(`SSE connection closed for session ${sessionId}`);
delete transports[sessionId]; delete transports[sessionId];
};
// Make the transport available to the MCP server
try {
transport.onmessage = async (message: JSONRPCMessage) => {
try {
// The server will automatically send a response via the transport
// if the message has an ID (i.e., it's a request, not a notification)
} catch (error) {
console.error('Error handling MCP message:', error);
}
}; };
// Connect to the MCP server // Make the transport available to the MCP server
await server.connect(transport); try {
} catch (error) { transport.onmessage = async (message: JSONRPCMessage) => {
console.error(`Error connecting transport for session ${sessionId}:`, error); try {
} // The server will automatically send a response via the transport
// if the message has an ID (i.e., it's a request, not a notification)
} catch (error) {
console.error('Error handling MCP message:', error);
}
};
// Keep the stream open until aborted // Connect to the MCP server
while (!stream.closed) { await server.connect(transport);
await stream.sleep(1000); } catch (error) {
} console.error(`Error connecting transport for session ${sessionId}:`, error);
}
// Keep the stream open until aborted
while (!stream.closed) {
await stream.sleep(1000);
}
});
}); });
});
// 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) {
return c.json({ error: 'Missing sessionId query parameter' }, 400); return c.json({ error: 'Missing sessionId query parameter' }, 400);
}
const transport = transports[sessionId];
if (!transport) {
return c.json({ error: 'No active session found with the provided sessionId' }, 404);
}
return transport.handlePostMessage(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
const fs = await import('fs');
const path = await import('path');
const { fileURLToPath } = await import('url');
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const publicPath = path.join(__dirname, '..', '..', 'public');
const fullPath = path.join(publicPath, filePath);
// Simple security check to prevent directory traversal
if (!fullPath.startsWith(publicPath)) {
return c.text('Forbidden', 403);
} }
const transport = transports[sessionId];
if (!transport) {
return c.json({ error: 'No active session found with the provided sessionId' }, 404);
}
return transport.handlePostMessage(c);
});
// Static files for the web client (if any)
app.get('/*', async (c) => {
const filePath = c.req.path === '/' ? '/index.html' : c.req.path;
try { try {
const stat = fs.statSync(fullPath); // Use Node.js fs to serve static files
if (stat.isFile()) { const fs = await import('fs');
const content = fs.readFileSync(fullPath); const path = await import('path');
const { fileURLToPath } = await import('url');
// Set content type based on file extension const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ext = path.extname(fullPath).toLowerCase(); const publicPath = path.join(__dirname, '..', '..', 'public');
let contentType = 'text/plain'; const fullPath = path.join(publicPath, filePath);
switch (ext) { // Simple security check to prevent directory traversal
case '.html': contentType = 'text/html'; break; if (!fullPath.startsWith(publicPath)) {
case '.css': contentType = 'text/css'; break; return c.text('Forbidden', 403);
case '.js': contentType = 'text/javascript'; break; }
case '.json': contentType = 'application/json'; break;
case '.png': contentType = 'image/png'; break; try {
case '.jpg': contentType = 'image/jpeg'; break; const stat = fs.statSync(fullPath);
case '.svg': contentType = 'image/svg+xml'; break; if (stat.isFile()) {
const content = fs.readFileSync(fullPath);
// Set content type based on file extension
const ext = path.extname(fullPath).toLowerCase();
let contentType = 'text/plain';
switch (ext) {
case '.html':
contentType = 'text/html';
break;
case '.css':
contentType = 'text/css';
break;
case '.js':
contentType = 'text/javascript';
break;
case '.json':
contentType = 'application/json';
break;
case '.png':
contentType = 'image/png';
break;
case '.jpg':
contentType = 'image/jpeg';
break;
case '.svg':
contentType = 'image/svg+xml';
break;
}
return new Response(content, {
headers: { 'Content-Type': contentType },
});
} }
} catch (err) {
return new Response(content, { // File not found or other error
headers: { 'Content-Type': contentType } return c.text('Not Found', 404);
});
} }
} catch (err) { } catch (err) {
// File not found or other error console.error('Error serving static file:', err);
return c.text('Not Found', 404); return c.text('Internal Server Error', 500);
} }
} catch (err) {
console.error('Error serving static file:', err);
return c.text('Internal Server Error', 500);
}
return c.text('Not Found', 404); return c.text('Not Found', 404);
}); });
// Start the server // Start the server
serve({ serve(
fetch: app.fetch, {
port fetch: app.fetch,
}, (info) => { port,
console.error(`MCP Web Server running at http://localhost:${info.port}`); },
console.error(`- SSE Endpoint: http://localhost:${info.port}/sse`); (info) => {
console.error(`- Messages Endpoint: http://localhost:${info.port}/api/messages?sessionId=YOUR_SESSION_ID`); console.error(`MCP Web Server running at http://localhost:${info.port}`);
console.error(`- Health Check: http://localhost:${info.port}/health`); 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(`- 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

@ -1,402 +1,424 @@
<!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:
max-width: 800px; system-ui,
margin: 0 auto; -apple-system,
padding: 20px; BlinkMacSystemFont,
line-height: 1.5; 'Segoe UI',
} Roboto,
h1 { margin-bottom: 10px; } sans-serif;
.container { max-width: 800px;
display: flex; margin: 0 auto;
flex-direction: column; padding: 20px;
height: calc(100vh - 150px); line-height: 1.5;
}
#conversation {
flex: 1;
border: 1px solid #ccc;
overflow-y: auto;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.input-area {
display: flex;
margin-bottom: 20px;
}
#userInput {
flex: 1;
padding: 8px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
#sendButton {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
border-radius: 0 5px 5px 0;
}
#sendButton:hover { background-color: #45a049; }
.message {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 5px;
}
.user {
background-color: #e7f4ff;
align-self: flex-end;
}
.server {
background-color: #f1f1f1;
}
.system {
background-color: #fffde7;
color: #795548;
font-style: italic;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
code {
background-color: #f8f8f8;
padding: 2px 4px;
border-radius: 3px;
}
.status {
color: #666;
font-style: italic;
margin-bottom: 10px;
}
#debug {
margin-top: 20px;
background-color: #f8f8f8;
padding: 10px;
border-radius: 5px;
display: none;
}
.debug-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
#showDebug {
margin-top: 10px;
padding: 5px 10px;
cursor: pointer;
background-color: #f1f1f1;
border: 1px solid #ccc;
border-radius: 3px;
}
#debugLog {
max-height: 200px;
overflow-y: auto;
background-color: #111;
color: #0f0;
font-family: monospace;
padding: 5px;
margin-top: 10px;
}
.clear-debug {
padding: 3px 8px;
background-color: #f44336;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>swagger-petstore---openapi-3-0 MCP StreamableHTTP Test Client</h1>
<p class="status" id="status">Disconnected</p>
<div class="container">
<div id="conversation"></div>
<div class="input-area">
<input type="text" id="userInput" placeholder="Type a message..." disabled>
<button id="sendButton" disabled>Send</button>
</div>
</div>
<button id="showDebug">Show Debug Console</button>
<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>
<script>
const conversation = document.getElementById('conversation');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
const statusEl = document.getElementById('status');
const showDebugBtn = document.getElementById('showDebug');
const debugDiv = document.getElementById('debug');
const debugLog = document.getElementById('debugLog');
const clearDebugBtn = document.getElementById('clearDebug');
let sessionId = null;
let messageId = 1;
// Debug logging
function log(type, message) {
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
const entry = document.createElement('div');
entry.innerHTML = `<span style="color:#aaa;">${timestamp}</span> <span style="color:#58a6ff;">${type}:</span> ${message}`;
debugLog.appendChild(entry);
debugLog.scrollTop = debugLog.scrollHeight;
console.log(`${type}: ${message}`);
}
// Toggle debug console
showDebugBtn.addEventListener('click', () => {
if (debugDiv.style.display === 'block') {
debugDiv.style.display = 'none';
showDebugBtn.textContent = 'Show Debug Console';
} else {
debugDiv.style.display = 'block';
showDebugBtn.textContent = 'Hide Debug Console';
} }
}); h1 {
margin-bottom: 10px;
}
.container {
display: flex;
flex-direction: column;
height: calc(100vh - 150px);
}
#conversation {
flex: 1;
border: 1px solid #ccc;
overflow-y: auto;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.input-area {
display: flex;
margin-bottom: 20px;
}
#userInput {
flex: 1;
padding: 8px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
#sendButton {
padding: 8px 16px;
background-color: #4caf50;
color: white;
border: none;
cursor: pointer;
border-radius: 0 5px 5px 0;
}
#sendButton:hover {
background-color: #45a049;
}
.message {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 5px;
}
.user {
background-color: #e7f4ff;
align-self: flex-end;
}
.server {
background-color: #f1f1f1;
}
.system {
background-color: #fffde7;
color: #795548;
font-style: italic;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
code {
background-color: #f8f8f8;
padding: 2px 4px;
border-radius: 3px;
}
.status {
color: #666;
font-style: italic;
margin-bottom: 10px;
}
#debug {
margin-top: 20px;
background-color: #f8f8f8;
padding: 10px;
border-radius: 5px;
display: none;
}
.debug-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
#showDebug {
margin-top: 10px;
padding: 5px 10px;
cursor: pointer;
background-color: #f1f1f1;
border: 1px solid #ccc;
border-radius: 3px;
}
#debugLog {
max-height: 200px;
overflow-y: auto;
background-color: #111;
color: #0f0;
font-family: monospace;
padding: 5px;
margin-top: 10px;
}
.clear-debug {
padding: 3px 8px;
background-color: #f44336;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>swagger-petstore---openapi-3-0 MCP StreamableHTTP Test Client</h1>
<p class="status" id="status">Disconnected</p>
// Clear debug logs <div class="container">
clearDebugBtn.addEventListener('click', () => { <div id="conversation"></div>
debugLog.innerHTML = '';
});
// Initialize the MCP connection <div class="input-area">
async function initialize() { <input type="text" id="userInput" placeholder="Type a message..." disabled />
statusEl.textContent = 'Connecting...'; <button id="sendButton" disabled>Send</button>
log('INFO', 'Initializing MCP connection...'); </div>
</div>
try { <button id="showDebug">Show Debug Console</button>
const requestBody = {
jsonrpc: '2.0', <div id="debug">
id: messageId++, <div class="debug-controls">
method: 'initialize', <h3>Debug Console</h3>
params: { <button class="clear-debug" id="clearDebug">Clear</button>
clientName: 'MCP StreamableHTTP Test Client', </div>
clientVersion: '1.0.0', <div id="debugLog"></div>
capabilities: {} </div>
<script>
const conversation = document.getElementById('conversation');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
const statusEl = document.getElementById('status');
const showDebugBtn = document.getElementById('showDebug');
const debugDiv = document.getElementById('debug');
const debugLog = document.getElementById('debugLog');
const clearDebugBtn = document.getElementById('clearDebug');
let sessionId = null;
let messageId = 1;
// Debug logging
function log(type, message) {
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
const entry = document.createElement('div');
entry.innerHTML = `<span style="color:#aaa;">${timestamp}</span> <span style="color:#58a6ff;">${type}:</span> ${message}`;
debugLog.appendChild(entry);
debugLog.scrollTop = debugLog.scrollHeight;
console.log(`${type}: ${message}`);
}
// Toggle debug console
showDebugBtn.addEventListener('click', () => {
if (debugDiv.style.display === 'block') {
debugDiv.style.display = 'none';
showDebugBtn.textContent = 'Show Debug Console';
} else {
debugDiv.style.display = 'block';
showDebugBtn.textContent = 'Hide Debug Console';
}
});
// Clear debug logs
clearDebugBtn.addEventListener('click', () => {
debugLog.innerHTML = '';
});
// Initialize the MCP connection
async function initialize() {
statusEl.textContent = 'Connecting...';
log('INFO', 'Initializing MCP connection...');
try {
const requestBody = {
jsonrpc: '2.0',
id: messageId++,
method: 'initialize',
params: {
clientName: 'MCP StreamableHTTP Test Client',
clientVersion: '1.0.0',
capabilities: {},
},
};
log('REQUEST', JSON.stringify(requestBody));
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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}`
);
statusEl.textContent = 'Connection error. Try again.';
return;
} }
};
log('REQUEST', JSON.stringify(requestBody)); // Get session ID from response headers
sessionId = response.headers.get('mcp-session-id');
const response = await fetch('/mcp', { if (!sessionId) {
method: 'POST', log('ERROR', 'No session ID in response headers');
headers: { appendMessage('system', 'Error: No session ID in response headers');
'Content-Type': 'application/json' statusEl.textContent = 'Connection error. Try again.';
}, return;
body: JSON.stringify(requestBody) }
});
if (!response.ok) { // Process response body
const errorText = await response.text(); const data = await response.json();
log('ERROR', `Error response: ${response.status} ${response.statusText} ${errorText}`); log('RESPONSE', JSON.stringify(data));
appendMessage('system', `Error: ${response.status} ${response.statusText}\n${errorText}`);
if (data.result) {
appendMessage('server', JSON.stringify(data.result, null, 2));
}
// Enable UI
statusEl.textContent = `Connected (Session ID: ${sessionId})`;
userInput.disabled = false;
sendButton.disabled = false;
userInput.focus();
appendMessage('system', `Connected with session ID: ${sessionId}`);
// Get list of tools
await listTools();
} catch (error) {
log('ERROR', `Error during initialization: ${error.message}`);
appendMessage('system', `Error during initialization: ${error.message}`);
statusEl.textContent = 'Connection error. Try again.'; statusEl.textContent = 'Connection error. Try again.';
return;
} }
// Get session ID from response headers
sessionId = response.headers.get('mcp-session-id');
if (!sessionId) {
log('ERROR', 'No session ID in response headers');
appendMessage('system', 'Error: No session ID in response headers');
statusEl.textContent = 'Connection error. Try again.';
return;
}
// Process response body
const data = await response.json();
log('RESPONSE', JSON.stringify(data));
if (data.result) {
appendMessage('server', JSON.stringify(data.result, null, 2));
}
// Enable UI
statusEl.textContent = `Connected (Session ID: ${sessionId})`;
userInput.disabled = false;
sendButton.disabled = false;
userInput.focus();
appendMessage('system', `Connected with session ID: ${sessionId}`);
// Get list of tools
await listTools();
} catch (error) {
log('ERROR', `Error during initialization: ${error.message}`);
appendMessage('system', `Error during initialization: ${error.message}`);
statusEl.textContent = 'Connection error. Try again.';
} }
}
// Get list of available tools // Get list of available tools
async function listTools() { async function listTools() {
try { try {
const requestBody = { const requestBody = {
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));
const response = await fetch('http://localhost:3000/mcp', { const response = await fetch('http://localhost:3000/mcp', {
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(
return; 'ERROR',
`Error listing tools: ${response.status} ${response.statusText} ${errorText}`
);
return;
}
const data = await response.json();
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(', ')}`
);
}
} catch (error) {
log('ERROR', `Error listing tools: ${error.message}`);
} }
const data = await response.json();
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(', ')}`);
}
} catch (error) {
log('ERROR', `Error listing tools: ${error.message}`);
} }
}
// Send a message to the server // Send a message to the server
async function sendMessage() { async function sendMessage() {
const text = userInput.value.trim(); const text = userInput.value.trim();
if (!text || !sessionId) return; if (!text || !sessionId) return;
appendMessage('user', text); appendMessage('user', text);
userInput.value = ''; userInput.value = '';
log('INFO', `Sending message: ${text}`); log('INFO', `Sending message: ${text}`);
try { try {
const parts = text.split(' ');
const toolName = parts[0];
const requestBody = {
jsonrpc: '2.0',
id: messageId++,
method: 'callTool',
params: {
name: toolName,
arguments: parseArguments(text),
},
};
log('REQUEST', JSON.stringify(requestBody));
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'mcp-session-id': sessionId,
},
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}`
);
return;
}
const data = await response.json();
log('RESPONSE', JSON.stringify(data));
if (data.error) {
appendMessage('system', `Error: ${data.error.code} - ${data.error.message}`);
} else if (data.result) {
appendMessage('server', JSON.stringify(data.result, null, 2));
}
} catch (error) {
log('ERROR', `Error sending message: ${error.message}`);
appendMessage('system', `Error sending message: ${error.message}`);
}
}
// Try to parse arguments from user input
// Format: toolName param1=value1 param2=value2
function parseArguments(text) {
const parts = text.split(' '); const parts = text.split(' ');
const toolName = parts[0]; if (parts.length <= 1) return {};
const requestBody = { const args = {};
jsonrpc: '2.0', // Skip the first part (tool name) and process the rest
id: messageId++, for (let i = 1; i < parts.length; i++) {
method: 'callTool', const part = parts[i];
params: { const equalsIndex = part.indexOf('=');
name: toolName,
arguments: parseArguments(text) if (equalsIndex > 0) {
const key = part.substring(0, equalsIndex);
const value = part.substring(equalsIndex + 1);
// Try to parse as number or boolean if possible
if (value === 'true') args[key] = true;
else if (value === 'false') args[key] = false;
else if (!isNaN(Number(value))) args[key] = Number(value);
else args[key] = value;
} }
};
log('REQUEST', JSON.stringify(requestBody));
const response = await fetch('/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'mcp-session-id': sessionId
},
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}`);
return;
} }
const data = await response.json(); return args;
log('RESPONSE', JSON.stringify(data)); }
if (data.error) { // Add a message to the conversation
appendMessage('system', `Error: ${data.error.code} - ${data.error.message}`); function appendMessage(sender, text) {
} else if (data.result) { const messageDiv = document.createElement('div');
appendMessage('server', JSON.stringify(data.result, null, 2)); messageDiv.className = `message ${sender}`;
// Format as code block if it looks like JSON
if (text.trim().startsWith('{') || text.trim().startsWith('[')) {
const pre = document.createElement('pre');
const code = document.createElement('code');
code.textContent = text;
pre.appendChild(code);
messageDiv.appendChild(pre);
} else {
messageDiv.textContent = text;
} }
} catch (error) {
log('ERROR', `Error sending message: ${error.message}`);
appendMessage('system', `Error sending message: ${error.message}`);
}
}
// Try to parse arguments from user input conversation.appendChild(messageDiv);
// Format: toolName param1=value1 param2=value2 conversation.scrollTop = conversation.scrollHeight;
function parseArguments(text) {
const parts = text.split(' ');
if (parts.length <= 1) return {};
const args = {};
// Skip the first part (tool name) and process the rest
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
const equalsIndex = part.indexOf('=');
if (equalsIndex > 0) {
const key = part.substring(0, equalsIndex);
const value = part.substring(equalsIndex + 1);
// Try to parse as number or boolean if possible
if (value === 'true') args[key] = true;
else if (value === 'false') args[key] = false;
else if (!isNaN(Number(value))) args[key] = Number(value);
else args[key] = value;
}
} }
return args; // Event listeners
} sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Add a message to the conversation // Initialize on page load
function appendMessage(sender, text) { appendMessage('system', 'Initializing MCP connection...');
const messageDiv = document.createElement('div'); initialize();
messageDiv.className = `message ${sender}`; </script>
</body>
// Format as code block if it looks like JSON
if (text.trim().startsWith('{') || text.trim().startsWith('[')) {
const pre = document.createElement('pre');
const code = document.createElement('code');
code.textContent = text;
pre.appendChild(code);
messageDiv.appendChild(pre);
} else {
messageDiv.textContent = text;
}
conversation.appendChild(messageDiv);
conversation.scrollTop = conversation.scrollHeight;
}
// Event listeners
sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Initialize on page load
appendMessage('system', 'Initializing MCP connection...');
initialize();
</script>
</body>
</html> </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 * 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
@ -24,7 +23,7 @@ const JSON_RPC = "2.0";
class MCPStreamableHttpServer { class MCPStreamableHttpServer {
server: Server; server: Server;
// Store active transports by session ID // Store active transports by session ID
transports: {[sessionId: string]: StreamableHTTPServerTransport} = {}; transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
constructor(server: Server) { constructor(server: Server) {
this.server = server; this.server = server;
@ -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, {
port fetch: app.fetch,
}, (info) => { port,
console.error(`MCP StreamableHTTP Server running at http://localhost:${info.port}`); },
console.error(`- MCP Endpoint: http://localhost:${info.port}/mcp`); (info) => {
console.error(`- Health Check: http://localhost:${info.port}/health`); 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; 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"
]
} }

6404
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -40,8 +40,8 @@ export async function getToolsFromOpenApi(
try { try {
// Parse the OpenAPI spec // Parse the OpenAPI spec
const api = options.dereference const api = options.dereference
? (await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document ? ((await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document)
: (await SwaggerParser.parse(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);
@ -55,7 +55,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,7 +64,7 @@ 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 || '',
})); }));

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

@ -1,18 +1,18 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"] "exclude": ["node_modules", "dist", "test"]
} }