diff --git a/README.md b/README.md index 738b9f3..61205f5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Generate [Model Context Protocol (MCP)](https://modelcontextprotocol.github.io/) servers from OpenAPI specifications. -This CLI tool automates the generation of MCP-compatible servers that proxy requests to existing REST APIsβ€”enabling AI agents and other MCP clients to seamlessly interact with your APIs using either standard input/output or HTTP-based transport. +This CLI tool automates the generation of MCP-compatible servers that proxy requests to existing REST APIsβ€”enabling AI agents and other MCP clients to seamlessly interact with your APIs using your choice of transport methods. --- @@ -17,9 +17,9 @@ This CLI tool automates the generation of MCP-compatible servers that proxy requ - πŸ” **Authentication Support**: API keys, Bearer tokens, Basic auth, and OAuth2 supported via environment variables. - πŸ§ͺ **Zod Validation**: Automatically generates Zod schemas from OpenAPI definitions for runtime input validation. - βš™οΈ **Typed Server**: Fully typed, maintainable TypeScript code output. -- πŸ’¬ **Stdio & Web Transport**: Communicate over stdio or HTTP (beta, SSE support). +- πŸ”Œ **Multiple Transports**: Communicate over stdio, SSE via Hono, or StreamableHTTP. - 🧰 **Project Scaffold**: Generates a complete Node.js project with `tsconfig.json`, `package.json`, and entry point. -- πŸ§ͺ **Built-in HTML Test Client** *(Web mode)*: Test API interactions visually in your browser. +- πŸ§ͺ **Built-in HTML Test Clients**: Test API interactions visually in your browser (for web-based transports). --- @@ -39,8 +39,11 @@ npm install -g openapi-mcp-generator # Generate an MCP server (stdio) openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir -# Generate an MCP web server (beta) +# Generate an MCP web server with SSE openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir --transport=web --port=3000 + +# Generate an MCP StreamableHTTP server +openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir --transport=streamable-http --port=3000 ``` ### CLI Options @@ -52,8 +55,8 @@ openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir - | `--server-name` | `-n` | Name of the MCP server (`package.json:name`) | OpenAPI title or `mcp-api-server` | | `--server-version` | `-v` | Version of the MCP server (`package.json:version`) | OpenAPI version or `1.0.0` | | `--base-url` | `-b` | Base URL for API requests. Required if OpenAPI `servers` missing or ambiguous. | Auto-detected if possible | -| `--transport` | `-t` | Transport mode: `"stdio"` (default) or `"web"` (beta) | `"stdio"` | -| `--port` | `-p` | Port for web server mode | `3000` | +| `--transport` | `-t` | Transport mode: `"stdio"` (default), `"web"`, or `"streamable-http"` | `"stdio"` | +| `--port` | `-p` | Port for web-based transports | `3000` | | `--force` | | Overwrite existing files in the output directory without confirmation | `false` | --- @@ -66,13 +69,20 @@ The generated project includes: β”œβ”€β”€ .gitignore β”œβ”€β”€ package.json β”œβ”€β”€ tsconfig.json -└── src/ - └── index.ts +β”œβ”€β”€ .env.example +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ index.ts +β”‚ └── [transport-specific-files] +└── public/ # For web-based transports + └── index.html # Test client ``` -- Uses `axios`, `zod`, `@modelcontextprotocol/sdk`, and `json-schema-to-zod` -- Secure API key/tokens via environment variables -- Tool generation for each endpoint +Core dependencies: +- `@modelcontextprotocol/sdk` - MCP protocol implementation +- `axios` - HTTP client for API requests +- `zod` - Runtime validation +- `json-schema-to-zod` - Convert JSON Schema to Zod +- Transport-specific deps (Hono, uuid, etc.) --- @@ -82,7 +92,7 @@ The generated project includes: Communicates with MCP clients via standard input/output. Ideal for local development or integration with LLM tools. -### Web Server Mode (Beta) +### Web Server with SSE Launches a fully functional HTTP server with: @@ -90,8 +100,34 @@ Launches a fully functional HTTP server with: - REST endpoint for client β†’ server communication - In-browser test client UI - Multi-connection support +- Built with lightweight Hono framework -> ⚠️ **Note**: Web mode is experimental and may have breaking changes in future updates. +### StreamableHTTP + +Implements the MCP StreamableHTTP transport which offers: + +- Stateful JSON-RPC over HTTP POST requests +- Session management using HTTP headers +- Proper HTTP response status codes +- Built-in error handling +- Compatibility with MCP StreamableHTTPClientTransport +- In-browser test client UI +- Built with lightweight Hono framework + +### Transport Comparison + +| Feature | stdio | web (SSE) | streamable-http | +|---------|-------|-----------|----------------| +| Protocol | JSON-RPC over stdio | JSON-RPC over SSE | JSON-RPC over HTTP | +| Connection | Persistent | Persistent | Request/response | +| Bidirectional | Yes | Yes | Yes (stateful) | +| Multiple clients | No | Yes | Yes | +| Browser compatible | No | Yes | Yes | +| Firewall friendly | No | Yes | Yes | +| Load balancing | No | Limited | Yes | +| Status codes | No | Limited | Full HTTP codes | +| Headers | No | Limited | Full HTTP headers | +| Test client | No | Yes | Yes | --- @@ -117,15 +153,26 @@ npm install # Run in stdio mode npm start -# Run in web server mode (if generated with --transport=web) +# Run in web server mode npm run start:web + +# Run in StreamableHTTP mode +npm run start:http ``` +### Testing Web-Based Servers + +For web and StreamableHTTP transports, a browser-based test client is automatically generated: + +1. Start the server using the appropriate command +2. Open your browser to `http://localhost:` +3. Use the test client to interact with your MCP server + --- ## ⚠️ Requirements -- Node.js v18 or later +- Node.js v20 or later --- @@ -154,4 +201,4 @@ Contributions are welcome! ## πŸ“„ License -MIT License β€” see [LICENSE](./LICENSE) for full details. +MIT License β€” see [LICENSE](./LICENSE) for full details. \ No newline at end of file diff --git a/examples/pet-store-sse/.env.example b/examples/pet-store-sse/.env.example new file mode 100644 index 0000000..4a7ee65 --- /dev/null +++ b/examples/pet-store-sse/.env.example @@ -0,0 +1,13 @@ +# MCP Server Environment Variables +# Copy this file to .env and fill in the values + +# Server configuration +PORT=3000 +LOG_LEVEL=info + +# API Authentication +# OAuth2 authentication (implicit flow) +OAUTH_TOKEN_PETSTORE_AUTH=your_oauth_token_here +API_KEY_API_KEY=your_api_key_here + +# Add any other environment variables your API might need diff --git a/examples/pet-store-sse/.eslintrc.json b/examples/pet-store-sse/.eslintrc.json new file mode 100644 index 0000000..8cb5c8e --- /dev/null +++ b/examples/pet-store-sse/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "plugins": [ + "@typescript-eslint" + ], + "env": { + "node": true, + "es2022": true + }, + "rules": { + "no-console": [ + "error", + { + "allow": [ + "error", + "warn" + ] + } + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_" + } + ] + } +} \ No newline at end of file diff --git a/examples/pet-store-sse/.gitignore b/examples/pet-store-sse/.gitignore new file mode 100644 index 0000000..61ab6c0 --- /dev/null +++ b/examples/pet-store-sse/.gitignore @@ -0,0 +1,64 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Build outputs +dist +build +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Reports +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage +coverage +*.lcov +.nyc_output + +# Build artifacts +.grunt +bower_components +jspm_packages/ +web_modules/ +.lock-wscript + +# Editor settings +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.idea +*.sublime-workspace +*.sublime-project + +# Caches +.eslintcache +.stylelintcache +.node_repl_history +.browserslistcache + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# OS specific +.DS_Store +Thumbs.db diff --git a/examples/pet-store-sse/.prettierrc b/examples/pet-store-sse/.prettierrc new file mode 100644 index 0000000..494b947 --- /dev/null +++ b/examples/pet-store-sse/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2 +} \ No newline at end of file diff --git a/examples/pet-store-sse/docs/oauth2-configuration.md b/examples/pet-store-sse/docs/oauth2-configuration.md new file mode 100644 index 0000000..9f3625b --- /dev/null +++ b/examples/pet-store-sse/docs/oauth2-configuration.md @@ -0,0 +1,23 @@ +# OAuth2 Configuration + +This API uses OAuth2 for authentication. The MCP server can handle OAuth2 authentication in the following ways: + +1. **Using a pre-acquired token**: You provide a token you've already obtained +2. **Using client credentials flow**: The server automatically acquires a token using your client ID and secret + +## Environment Variables + +### petstore_auth + +**Configuration Variables:** + +- `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 diff --git a/examples/pet-store-sse/jest.config.js b/examples/pet-store-sse/jest.config.js new file mode 100644 index 0000000..71603b2 --- /dev/null +++ b/examples/pet-store-sse/jest.config.js @@ -0,0 +1,16 @@ +export default { + preset: 'ts-jest', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\.{1,2}/.*)\.js$': '$1', + }, + transform: { + '^.+\.tsx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, +}; diff --git a/examples/pet-store-sse/package-lock.json b/examples/pet-store-sse/package-lock.json new file mode 100644 index 0000000..7dcd70d --- /dev/null +++ b/examples/pet-store-sse/package-lock.json @@ -0,0 +1,1224 @@ +{ + "name": "swagger-petstore---openapi-3-0", + "version": "1.0.26", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "swagger-petstore---openapi-3-0", + "version": "1.0.26", + "dependencies": { + "@hono/node-server": "^1.14.1", + "@modelcontextprotocol/sdk": "^1.10.0", + "axios": "^1.9.0", + "dotenv": "^16.4.5", + "hono": "^4.7.7", + "json-schema-to-zod": "^2.6.1", + "uuid": "^11.1.0", + "zod": "^3.24.3" + }, + "devDependencies": { + "@types/node": "^22.15.2", + "@types/uuid": "^10.0.0", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.1.tgz", + "integrity": "sha512-vmbuM+HPinjWzPe7FFPWMMQMsbKE9gDPhaH0FFdqbGpkT5lp++tcWDTxwBl5EgS5y6JVgIaCdjeHRfQ4XRBRjQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", + "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.15.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", + "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.7.tgz", + "integrity": "sha512-2PCpQRbN87Crty8/L/7akZN3UyZIAopSoRxCwRbJgUuV1+MHNFHzYFxZTg4v/03cXUm+jce/qa2VSBZpKBm3Qw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-to-zod": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/json-schema-to-zod/-/json-schema-to-zod-2.6.1.tgz", + "integrity": "sha512-uiHmWH21h9FjKJkRBntfVGTLpYlCZ1n98D0izIlByqQLqpmkQpNTBtfbdP04Na6+43lgsvrShFh2uWLkQDKJuQ==", + "license": "ISC", + "bin": { + "json-schema-to-zod": "dist/cjs/cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/pet-store-sse/package.json b/examples/pet-store-sse/package.json new file mode 100644 index 0000000..13ef081 --- /dev/null +++ b/examples/pet-store-sse/package.json @@ -0,0 +1,37 @@ +{ + "name": "swagger-petstore---openapi-3-0", + "version": "1.0.26", + "description": "MCP Server generated from OpenAPI spec for swagger-petstore---openapi-3-0", + "private": true, + "type": "module", + "main": "build/index.js", + "files": [ + "build", + "src" + ], + "scripts": { + "start": "node build/index.js", + "build": "tsc && chmod 755 build/index.js", + "typecheck": "tsc --noEmit", + "prestart": "npm run build", + "start:web": "node build/index.js --transport=web" + }, + "engines": { + "node": ">=20.0.0" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.10.0", + "axios": "^1.9.0", + "dotenv": "^16.4.5", + "zod": "^3.24.3", + "json-schema-to-zod": "^2.6.1", + "hono": "^4.7.7", + "@hono/node-server": "^1.14.1", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/node": "^22.15.2", + "typescript": "^5.8.3", + "@types/uuid": "^10.0.0" + } +} \ No newline at end of file diff --git a/examples/pet-store-sse/public/index.html b/examples/pet-store-sse/public/index.html new file mode 100644 index 0000000..aa97ea3 --- /dev/null +++ b/examples/pet-store-sse/public/index.html @@ -0,0 +1,393 @@ + + + + + +swagger-petstore---openapi-3-0 MCP Test Client + + + +

swagger-petstore---openapi-3-0 MCP Test Client

+

Disconnected

+ +
+
+ +
+ + +
+
+ + + +
+
+

Debug Console

+ +
+
+
+ + + + \ No newline at end of file diff --git a/examples/pet-store-sse/src/index.ts b/examples/pet-store-sse/src/index.ts new file mode 100644 index 0000000..1a7673a --- /dev/null +++ b/examples/pet-store-sse/src/index.ts @@ -0,0 +1,799 @@ +#!/usr/bin/env node +/** + * MCP Server generated from OpenAPI spec for swagger-petstore---openapi-3-0 v1.0.26 + * Generated on: 2025-04-26T16:32:46.309Z + */ + +// Load environment variables from .env file +import dotenv from 'dotenv'; +dotenv.config(); + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + type Tool, + type CallToolResult, + type CallToolRequest +} from "@modelcontextprotocol/sdk/types.js"; +import { setupWebServer } from "./web-server.js"; + +import { z, ZodError } from 'zod'; +import { jsonSchemaToZod } from 'json-schema-to-zod'; +import axios, { type AxiosRequestConfig, type AxiosError } from 'axios'; + +/** + * Type definition for JSON objects + */ +type JsonObject = Record; + +/** + * Interface for MCP Tool Definition + */ +interface McpToolDefinition { + name: string; + description: string; + inputSchema: any; + method: string; + pathTemplate: string; + executionParameters: { name: string, in: string }[]; + requestBodyContentType?: string; + securityRequirements: any[]; +} + +/** + * Server configuration + */ +export const SERVER_NAME = "swagger-petstore---openapi-3-0"; +export const SERVER_VERSION = "1.0.26"; +export const API_BASE_URL = "https://petstore3.swagger.io/api/v3"; + +/** + * MCP Server instance + */ +const server = new Server( + { name: SERVER_NAME, version: SERVER_VERSION }, + { capabilities: { tools: {} } } +); + +/** + * Map of tool definitions by name + */ +const toolDefinitionMap: Map = new Map([ + + ["updatepet", { + name: "updatepet", + description: `Update an existing pet by Id.`, + inputSchema: {"type":"object","properties":{"requestBody":{"required":["name","photoUrls"],"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"},"category":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"}}},"photoUrls":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"}}}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"description":"Update an existent pet in the store"}},"required":["requestBody"]}, + method: "put", + pathTemplate: "/pet", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["addpet", { + name: "addpet", + description: `Add a new pet to the store.`, + inputSchema: {"type":"object","properties":{"requestBody":{"required":["name","photoUrls"],"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"},"category":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"}}},"photoUrls":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"}}}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"description":"Create a new pet in the store"}},"required":["requestBody"]}, + method: "post", + pathTemplate: "/pet", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["findpetsbystatus", { + name: "findpetsbystatus", + description: `Multiple status values can be provided with comma separated strings.`, + inputSchema: {"type":"object","properties":{"status":{"type":"string","default":"available","enum":["available","pending","sold"],"description":"Status values that need to be considered for filter"}}}, + method: "get", + pathTemplate: "/pet/findByStatus", + executionParameters: [{"name":"status","in":"query"}], + requestBodyContentType: undefined, + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["findpetsbytags", { + name: "findpetsbytags", + description: `Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.`, + inputSchema: {"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"description":"Tags to filter by"}}}, + method: "get", + pathTemplate: "/pet/findByTags", + executionParameters: [{"name":"tags","in":"query"}], + requestBodyContentType: undefined, + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["getpetbyid", { + name: "getpetbyid", + description: `Returns a single pet.`, + inputSchema: {"type":"object","properties":{"petId":{"type":"number","format":"int64","description":"ID of pet to return"}},"required":["petId"]}, + method: "get", + pathTemplate: "/pet/{petId}", + executionParameters: [{"name":"petId","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [{"api_key":[]},{"petstore_auth":["write:pets","read:pets"]}] + }], + ["updatepetwithform", { + name: "updatepetwithform", + description: `Updates a pet resource based on the form data.`, + inputSchema: {"type":"object","properties":{"petId":{"type":"number","format":"int64","description":"ID of pet that needs to be updated"},"name":{"type":"string","description":"Name of pet that needs to be updated"},"status":{"type":"string","description":"Status of pet that needs to be updated"}},"required":["petId"]}, + method: "post", + pathTemplate: "/pet/{petId}", + executionParameters: [{"name":"petId","in":"path"},{"name":"name","in":"query"},{"name":"status","in":"query"}], + requestBodyContentType: undefined, + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["deletepet", { + name: "deletepet", + description: `Delete a pet.`, + inputSchema: {"type":"object","properties":{"api_key":{"type":"string"},"petId":{"type":"number","format":"int64","description":"Pet id to delete"}},"required":["petId"]}, + method: "delete", + pathTemplate: "/pet/{petId}", + executionParameters: [{"name":"api_key","in":"header"},{"name":"petId","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["uploadfile", { + name: "uploadfile", + description: `Upload image of the pet.`, + inputSchema: {"type":"object","properties":{"petId":{"type":"number","format":"int64","description":"ID of pet to update"},"additionalMetadata":{"type":"string","description":"Additional Metadata"},"requestBody":{"type":"string","description":"Request body (content type: application/octet-stream)"}},"required":["petId"]}, + method: "post", + pathTemplate: "/pet/{petId}/uploadImage", + executionParameters: [{"name":"petId","in":"path"},{"name":"additionalMetadata","in":"query"}], + requestBodyContentType: "application/octet-stream", + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["getinventory", { + name: "getinventory", + description: `Returns a map of status codes to quantities.`, + inputSchema: {"type":"object","properties":{}}, + method: "get", + pathTemplate: "/store/inventory", + executionParameters: [], + requestBodyContentType: undefined, + securityRequirements: [{"api_key":[]}] + }], + ["placeorder", { + name: "placeorder", + description: `Place a new order in the store.`, + inputSchema: {"type":"object","properties":{"requestBody":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"petId":{"type":"number","format":"int64"},"quantity":{"type":"number","format":"int32"},"shipDate":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Order Status","enum":["placed","approved","delivered"]},"complete":{"type":"boolean"}},"description":"The JSON request body."}}}, + method: "post", + pathTemplate: "/store/order", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [] + }], + ["getorderbyid", { + name: "getorderbyid", + description: `For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.`, + inputSchema: {"type":"object","properties":{"orderId":{"type":"number","format":"int64","description":"ID of order that needs to be fetched"}},"required":["orderId"]}, + method: "get", + pathTemplate: "/store/order/{orderId}", + executionParameters: [{"name":"orderId","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["deleteorder", { + name: "deleteorder", + description: `For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors.`, + inputSchema: {"type":"object","properties":{"orderId":{"type":"number","format":"int64","description":"ID of the order that needs to be deleted"}},"required":["orderId"]}, + method: "delete", + pathTemplate: "/store/order/{orderId}", + executionParameters: [{"name":"orderId","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["createuser", { + name: "createuser", + description: `This can only be done by the logged in user.`, + inputSchema: {"type":"object","properties":{"requestBody":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"number","description":"User Status","format":"int32"}},"description":"Created user object"}}}, + method: "post", + pathTemplate: "/user", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [] + }], + ["createuserswithlistinput", { + name: "createuserswithlistinput", + description: `Creates list of users with given input array.`, + inputSchema: {"type":"object","properties":{"requestBody":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"number","description":"User Status","format":"int32"}}},"description":"The JSON request body."}}}, + method: "post", + pathTemplate: "/user/createWithList", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [] + }], + ["loginuser", { + name: "loginuser", + description: `Log into the system.`, + inputSchema: {"type":"object","properties":{"username":{"type":"string","description":"The user name for login"},"password":{"type":"string","description":"The password for login in clear text"}}}, + method: "get", + pathTemplate: "/user/login", + executionParameters: [{"name":"username","in":"query"},{"name":"password","in":"query"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["logoutuser", { + name: "logoutuser", + description: `Log user out of the system.`, + inputSchema: {"type":"object","properties":{}}, + method: "get", + pathTemplate: "/user/logout", + executionParameters: [], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["getuserbyname", { + name: "getuserbyname", + description: `Get user detail based on username.`, + inputSchema: {"type":"object","properties":{"username":{"type":"string","description":"The name that needs to be fetched. Use user1 for testing"}},"required":["username"]}, + method: "get", + pathTemplate: "/user/{username}", + executionParameters: [{"name":"username","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["updateuser", { + name: "updateuser", + description: `This can only be done by the logged in user.`, + inputSchema: {"type":"object","properties":{"username":{"type":"string","description":"name that need to be deleted"},"requestBody":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"number","description":"User Status","format":"int32"}},"description":"Update an existent user in the store"}},"required":["username"]}, + method: "put", + pathTemplate: "/user/{username}", + executionParameters: [{"name":"username","in":"path"}], + requestBodyContentType: "application/json", + securityRequirements: [] + }], + ["deleteuser", { + name: "deleteuser", + description: `This can only be done by the logged in user.`, + inputSchema: {"type":"object","properties":{"username":{"type":"string","description":"The name that needs to be deleted"}},"required":["username"]}, + method: "delete", + pathTemplate: "/user/{username}", + executionParameters: [{"name":"username","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], +]); + +/** + * Security schemes from the OpenAPI spec + */ +const securitySchemes = { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }; + + +server.setRequestHandler(ListToolsRequestSchema, async () => { + const toolsForClient: Tool[] = Array.from(toolDefinitionMap.values()).map(def => ({ + name: def.name, + description: def.description, + inputSchema: def.inputSchema + })); + return { tools: toolsForClient }; +}); + + +server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise => { + const { name: toolName, arguments: toolArgs } = request.params; + const toolDefinition = toolDefinitionMap.get(toolName); + if (!toolDefinition) { + console.error(`Error: Unknown tool requested: ${toolName}`); + return { content: [{ type: "text", text: `Error: Unknown tool requested: ${toolName}` }] }; + } + return await executeApiTool(toolName, toolDefinition, toolArgs ?? {}, securitySchemes); +}); + + + +/** + * Type definition for cached OAuth tokens + */ +interface TokenCacheEntry { + token: string; + expiresAt: number; +} + +/** + * Declare global __oauthTokenCache property for TypeScript + */ +declare global { + var __oauthTokenCache: Record | undefined; +} + +/** + * Acquires an OAuth2 token using client credentials flow + * + * @param schemeName Name of the security scheme + * @param scheme OAuth2 security scheme + * @returns Acquired token or null if unable to acquire + */ +async function acquireOAuth2Token(schemeName: string, scheme: any): Promise { + try { + // Check if we have the necessary credentials + const clientId = process.env[`OAUTH_CLIENT_ID_SCHEMENAME`]; + const clientSecret = process.env[`OAUTH_CLIENT_SECRET_SCHEMENAME`]; + const scopes = process.env[`OAUTH_SCOPES_SCHEMENAME`]; + + if (!clientId || !clientSecret) { + console.error(`Missing client credentials for OAuth2 scheme '${schemeName}'`); + return null; + } + + // Initialize token cache if needed + if (typeof global.__oauthTokenCache === 'undefined') { + global.__oauthTokenCache = {}; + } + + // Check if we have a cached token + const cacheKey = `${schemeName}_${clientId}`; + const cachedToken = global.__oauthTokenCache[cacheKey]; + const now = Date.now(); + + if (cachedToken && cachedToken.expiresAt > now) { + console.error(`Using cached OAuth2 token for '${schemeName}' (expires in ${Math.floor((cachedToken.expiresAt - now) / 1000)} seconds)`); + return cachedToken.token; + } + + // Determine token URL based on flow type + let tokenUrl = ''; + if (scheme.flows?.clientCredentials?.tokenUrl) { + tokenUrl = scheme.flows.clientCredentials.tokenUrl; + console.error(`Using client credentials flow for '${schemeName}'`); + } else if (scheme.flows?.password?.tokenUrl) { + tokenUrl = scheme.flows.password.tokenUrl; + console.error(`Using password flow for '${schemeName}'`); + } else { + console.error(`No supported OAuth2 flow found for '${schemeName}'`); + return null; + } + + // Prepare the token request + let formData = new URLSearchParams(); + formData.append('grant_type', 'client_credentials'); + + // Add scopes if specified + if (scopes) { + formData.append('scope', scopes); + } + + console.error(`Requesting OAuth2 token from ${tokenUrl}`); + + // Make the token request + const response = await axios({ + method: 'POST', + url: tokenUrl, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` + }, + data: formData.toString() + }); + + // Process the response + if (response.data?.access_token) { + const token = response.data.access_token; + const expiresIn = response.data.expires_in || 3600; // Default to 1 hour + + // Cache the token + global.__oauthTokenCache[cacheKey] = { + token, + expiresAt: now + (expiresIn * 1000) - 60000 // Expire 1 minute early + }; + + console.error(`Successfully acquired OAuth2 token for '${schemeName}' (expires in ${expiresIn} seconds)`); + return token; + } else { + console.error(`Failed to acquire OAuth2 token for '${schemeName}': No access_token in response`); + return null; + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`Error acquiring OAuth2 token for '${schemeName}':`, errorMessage); + return null; + } +} + + +/** + * Executes an API tool with the provided arguments + * + * @param toolName Name of the tool to execute + * @param definition Tool definition + * @param toolArgs Arguments provided by the user + * @param allSecuritySchemes Security schemes from the OpenAPI spec + * @returns Call tool result + */ +async function executeApiTool( + toolName: string, + definition: McpToolDefinition, + toolArgs: JsonObject, + allSecuritySchemes: Record +): Promise { + try { + // Validate arguments against the input schema + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(definition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + } catch (error: unknown) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + const errorMessage = error instanceof Error ? error.message : String(error); + return { content: [{ type: 'text', text: `Internal error during validation setup: ${errorMessage}` }] }; + } + } + + // Prepare URL, query parameters, headers, and request body + let urlPath = definition.pathTemplate; + const queryParams: Record = {}; + const headers: Record = { 'Accept': 'application/json' }; + let requestBodyData: any = undefined; + + // Apply parameters to the URL path, query, or headers + definition.executionParameters.forEach((param) => { + const value = validatedArgs[param.name]; + if (typeof value !== 'undefined' && value !== null) { + if (param.in === 'path') { + urlPath = urlPath.replace(`{${param.name}}`, encodeURIComponent(String(value))); + } + else if (param.in === 'query') { + queryParams[param.name] = value; + } + else if (param.in === 'header') { + headers[param.name.toLowerCase()] = String(value); + } + } + }); + + // Ensure all path parameters are resolved + if (urlPath.includes('{')) { + throw new Error(`Failed to resolve path parameters: ${urlPath}`); + } + + // Construct the full URL + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + + // Handle request body if needed + if (definition.requestBodyContentType && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + headers['content-type'] = definition.requestBodyContentType; + } + + + // Apply security requirements if available + // Security requirements use OR between array items and AND within each object + const appliedSecurity = definition.securityRequirements?.find(req => { + // Try each security requirement (combined with OR) + return Object.entries(req).every(([schemeName, scopesArray]) => { + const scheme = allSecuritySchemes[schemeName]; + if (!scheme) return false; + + // API Key security (header, query, cookie) + if (scheme.type === 'apiKey') { + return !!process.env[`API_KEY_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + } + + // HTTP security (basic, bearer) + if (scheme.type === 'http') { + if (scheme.scheme?.toLowerCase() === 'bearer') { + return !!process.env[`BEARER_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + } + else if (scheme.scheme?.toLowerCase() === 'basic') { + return !!process.env[`BASIC_USERNAME_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`] && + !!process.env[`BASIC_PASSWORD_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + } + } + + // OAuth2 security + if (scheme.type === 'oauth2') { + // Check for pre-existing token + if (process.env[`OAUTH_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]) { + return true; + } + + // Check for client credentials for auto-acquisition + if (process.env[`OAUTH_CLIENT_ID_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`] && + process.env[`OAUTH_CLIENT_SECRET_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]) { + // Verify we have a supported flow + if (scheme.flows?.clientCredentials || scheme.flows?.password) { + return true; + } + } + + return false; + } + + // OpenID Connect + if (scheme.type === 'openIdConnect') { + return !!process.env[`OPENID_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + } + + return false; + }); + }); + + // If we found matching security scheme(s), apply them + if (appliedSecurity) { + // Apply each security scheme from this requirement (combined with AND) + for (const [schemeName, scopesArray] of Object.entries(appliedSecurity)) { + const scheme = allSecuritySchemes[schemeName]; + + // API Key security + if (scheme?.type === 'apiKey') { + const apiKey = process.env[`API_KEY_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + if (apiKey) { + if (scheme.in === 'header') { + headers[scheme.name.toLowerCase()] = apiKey; + console.error(`Applied API key '${schemeName}' in header '${scheme.name}'`); + } + else if (scheme.in === 'query') { + queryParams[scheme.name] = apiKey; + console.error(`Applied API key '${schemeName}' in query parameter '${scheme.name}'`); + } + else if (scheme.in === 'cookie') { + // Add the cookie, preserving other cookies if they exist + headers['cookie'] = `${scheme.name}=${apiKey}${headers['cookie'] ? `; ${headers['cookie']}` : ''}`; + console.error(`Applied API key '${schemeName}' in cookie '${scheme.name}'`); + } + } + } + // HTTP security (Bearer or Basic) + else if (scheme?.type === 'http') { + if (scheme.scheme?.toLowerCase() === 'bearer') { + const token = process.env[`BEARER_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + if (token) { + headers['authorization'] = `Bearer ${token}`; + console.error(`Applied Bearer token for '${schemeName}'`); + } + } + else if (scheme.scheme?.toLowerCase() === 'basic') { + const username = process.env[`BASIC_USERNAME_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + const password = process.env[`BASIC_PASSWORD_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + if (username && password) { + headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; + console.error(`Applied Basic authentication for '${schemeName}'`); + } + } + } + // OAuth2 security + else if (scheme?.type === 'oauth2') { + // First try to use a pre-provided token + let token = process.env[`OAUTH_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + + // If no token but we have client credentials, try to acquire a token + if (!token && (scheme.flows?.clientCredentials || scheme.flows?.password)) { + console.error(`Attempting to acquire OAuth token for '${schemeName}'`); + token = (await acquireOAuth2Token(schemeName, scheme)) ?? ''; + } + + // Apply token if available + if (token) { + headers['authorization'] = `Bearer ${token}`; + console.error(`Applied OAuth2 token for '${schemeName}'`); + + // List the scopes that were requested, if any + const scopes = scopesArray as string[]; + if (scopes && scopes.length > 0) { + console.error(`Requested scopes: ${scopes.join(', ')}`); + } + } + } + // OpenID Connect + else if (scheme?.type === 'openIdConnect') { + const token = process.env[`OPENID_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + if (token) { + headers['authorization'] = `Bearer ${token}`; + console.error(`Applied OpenID Connect token for '${schemeName}'`); + + // List the scopes that were requested, if any + const scopes = scopesArray as string[]; + if (scopes && scopes.length > 0) { + console.error(`Requested scopes: ${scopes.join(', ')}`); + } + } + } + } + } + // Log warning if security is required but not available + else if (definition.securityRequirements?.length > 0) { + // First generate a more readable representation of the security requirements + const securityRequirementsString = definition.securityRequirements + .map(req => { + const parts = Object.entries(req) + .map(([name, scopesArray]) => { + const scopes = scopesArray as string[]; + if (scopes.length === 0) return name; + return `${name} (scopes: ${scopes.join(', ')})`; + }) + .join(' AND '); + return `[${parts}]`; + }) + .join(' OR '); + + console.warn(`Tool '${toolName}' requires security: ${securityRequirementsString}, but no suitable credentials found.`); + } + + + // Prepare the axios request configuration + const config: AxiosRequestConfig = { + method: definition.method.toUpperCase(), + url: requestUrl, + params: queryParams, + headers: headers, + ...(requestBodyData !== undefined && { data: requestBodyData }), + }; + + // Log request info to stderr (doesn't affect MCP output) + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // Execute the request + const response = await axios(config); + + // Process and format the response + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + + // Handle JSON responses + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { + responseText = JSON.stringify(response.data, null, 2); + } catch (e) { + responseText = "[Stringify Error]"; + } + } + // Handle string responses + else if (typeof response.data === 'string') { + responseText = response.data; + } + // Handle other response types + else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } + // Handle empty responses + else { + responseText = `(Status: ${response.status} - No body content)`; + } + + // Return formatted response + return { + content: [ + { + type: "text", + text: `API Response (Status: ${response.status}):\n${responseText}` + } + ], + }; + + } catch (error: unknown) { + // Handle errors during execution + let errorMessage: string; + + // Format Axios errors specially + if (axios.isAxiosError(error)) { + errorMessage = formatApiError(error); + } + // Handle standard errors + else if (error instanceof Error) { + errorMessage = error.message; + } + // Handle unexpected error types + else { + errorMessage = 'Unexpected error: ' + String(error); + } + + // Log error to stderr + console.error(`Error during execution of tool '${toolName}':`, errorMessage); + + // Return error message to client + return { content: [{ type: "text", text: errorMessage }] }; + } +} + + +/** + * Main function to start the server + */ +async function main() { +// Set up Web Server transport + try { + await setupWebServer(server, 3000); + } catch (error) { + console.error("Error setting up web server:", error); + process.exit(1); + } +} + +/** + * Cleanup function for graceful shutdown + */ +async function cleanup() { + console.error("Shutting down MCP server..."); + process.exit(0); +} + +// Register signal handlers +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +// Start the server +main().catch((error) => { + console.error("Fatal error in main execution:", error); + process.exit(1); +}); + +/** + * Formats API errors for better readability + * + * @param error Axios error + * @returns Formatted error message + */ +function formatApiError(error: AxiosError): string { + let message = 'API request failed.'; + if (error.response) { + message = `API Error: Status ${error.response.status} (${error.response.statusText || 'Status text not available'}). `; + const responseData = error.response.data; + const MAX_LEN = 200; + if (typeof responseData === 'string') { + message += `Response: ${responseData.substring(0, MAX_LEN)}${responseData.length > MAX_LEN ? '...' : ''}`; + } + else if (responseData) { + try { + const jsonString = JSON.stringify(responseData); + message += `Response: ${jsonString.substring(0, MAX_LEN)}${jsonString.length > MAX_LEN ? '...' : ''}`; + } catch { + message += 'Response: [Could not serialize data]'; + } + } + else { + message += 'No response body received.'; + } + } else if (error.request) { + message = 'API Network Error: No response received from server.'; + if (error.code) message += ` (Code: ${error.code})`; + } else { + message += `API Request Setup Error: ${error.message}`; + } + return message; +} + +/** + * Converts a JSON Schema to a Zod schema for runtime validation + * + * @param jsonSchema JSON Schema + * @param toolName Tool name for error reporting + * @returns Zod schema + */ +function getZodSchemaFromJsonSchema(jsonSchema: any, toolName: string): z.ZodTypeAny { + if (typeof jsonSchema !== 'object' || jsonSchema === null) { + return z.object({}).passthrough(); + } + try { + const zodSchemaString = jsonSchemaToZod(jsonSchema); + const zodSchema = eval(zodSchemaString); + if (typeof zodSchema?.parse !== 'function') { + throw new Error('Eval did not produce a valid Zod schema.'); + } + return zodSchema as z.ZodTypeAny; + } catch (err: any) { + console.error(`Failed to generate/evaluate Zod schema for '${toolName}':`, err); + return z.object({}).passthrough(); + } +} diff --git a/examples/pet-store-sse/src/web-server.ts b/examples/pet-store-sse/src/web-server.ts new file mode 100644 index 0000000..36461c5 --- /dev/null +++ b/examples/pet-store-sse/src/web-server.ts @@ -0,0 +1,286 @@ + +/** +* 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 type { Context } from 'hono'; +import type { SSEStreamingApi } from 'hono/streaming'; +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 +*/ +class SSETransport implements Transport { +private _sessionId: string; +private stream: SSEStreamingApi; +private messageUrl: string; + +onclose?: () => void; +onerror?: (error: Error) => void; +onmessage?: (message: JSONRPCMessage) => void; + +constructor(messageUrl: string, stream: SSEStreamingApi) { + this._sessionId = uuid(); + this.stream = stream; + this.messageUrl = messageUrl; + + // Set up stream abort handler + this.stream.onAbort(() => { + console.error(`SSE connection aborted for session ${this._sessionId}`); + this.close(); + }); +} + +get sessionId(): string { + return this._sessionId; +} + +async start(): Promise { + if (this.stream.closed) { + throw new Error('SSE transport already closed!'); + } + + // Send the endpoint information + await this.stream.writeSSE({ + 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 + } + } + }); +} + +async handlePostMessage(c: Context): Promise { + if (this.stream?.closed) { + return c.text('SSE connection closed', 400); + } + + try { + // Parse and validate the message + const body = await c.req.json(); + + try { + // Parse and validate the message + const parsedMessage = JSONRPCMessageSchema.parse(body); + + // Forward to the message handler + 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) { + if (this.onerror) { + this.onerror(error instanceof Error ? error : new Error(String(error))); + } + console.error('Error processing request:', error); + return c.text('Error processing message', 400); + } +} + +async close(): Promise { + if (this.stream && !this.stream.closed) { + this.stream.abort(); + } + + if (this.onclose) { + this.onclose(); + } +} + +async send(message: JSONRPCMessage): Promise { + if (this.stream.closed) { + throw new Error('Not connected'); + } + + await this.stream.writeSSE({ + event: '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 +*/ +export async function setupWebServer(server: Server, port = 3000) { +// Create Hono app +const app = new Hono(); + +// Enable CORS +app.use('*', cors()); + +// Store active SSE transports by session ID +const transports: {[sessionId: string]: SSETransport} = {}; + +// 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) => { + return streamSSE(c, async (stream) => { + // Create SSE transport + const transport = new SSETransport('/api/messages', stream); + const sessionId = transport.sessionId; + + console.error(`New SSE connection established: ${sessionId}`); + + // Store the transport + transports[sessionId] = transport; + + // Set up cleanup on transport close + transport.onclose = () => { + console.error(`SSE connection closed for session ${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 + await server.connect(transport); + } 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 +app.post("/api/messages", async (c) => { + const sessionId = c.req.query('sessionId'); + + if (!sessionId) { + 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); + } + + try { + const stat = fs.statSync(fullPath); + if (stat.isFile()) { + const content = fs.readFileSync(fullPath); + + // Set content type based on file extension + const ext = path.extname(fullPath).toLowerCase(); + let contentType = 'text/plain'; + + switch (ext) { + case '.html': contentType = 'text/html'; break; + case '.css': contentType = 'text/css'; break; + case '.js': contentType = 'text/javascript'; break; + case '.json': contentType = 'application/json'; break; + case '.png': contentType = 'image/png'; break; + case '.jpg': contentType = 'image/jpeg'; break; + case '.svg': contentType = 'image/svg+xml'; break; + } + + return new Response(content, { + headers: { 'Content-Type': contentType } + }); + } + } catch (err) { + // File not found or other error + return c.text('Not Found', 404); + } + } catch (err) { + console.error('Error serving static file:', err); + return c.text('Internal Server Error', 500); + } + + return c.text('Not Found', 404); +}); + +// Start the server +serve({ + fetch: app.fetch, + port +}, (info) => { + console.error(`MCP 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(`- Health Check: http://localhost:${info.port}/health`); +}); + +return app; +} diff --git a/examples/pet-store-sse/tsconfig.json b/examples/pet-store-sse/tsconfig.json new file mode 100644 index 0000000..ed2966d --- /dev/null +++ b/examples/pet-store-sse/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "module": "Node16", + "moduleResolution": "Node16", + "noEmit": false, + "outDir": "./build", + "declaration": true, + "sourceMap": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "build", + "**/*.test.ts" + ] +} \ No newline at end of file diff --git a/examples/pet-store-streamable-http/.env.example b/examples/pet-store-streamable-http/.env.example new file mode 100644 index 0000000..4a7ee65 --- /dev/null +++ b/examples/pet-store-streamable-http/.env.example @@ -0,0 +1,13 @@ +# MCP Server Environment Variables +# Copy this file to .env and fill in the values + +# Server configuration +PORT=3000 +LOG_LEVEL=info + +# API Authentication +# OAuth2 authentication (implicit flow) +OAUTH_TOKEN_PETSTORE_AUTH=your_oauth_token_here +API_KEY_API_KEY=your_api_key_here + +# Add any other environment variables your API might need diff --git a/examples/pet-store-streamable-http/.eslintrc.json b/examples/pet-store-streamable-http/.eslintrc.json new file mode 100644 index 0000000..8cb5c8e --- /dev/null +++ b/examples/pet-store-streamable-http/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "plugins": [ + "@typescript-eslint" + ], + "env": { + "node": true, + "es2022": true + }, + "rules": { + "no-console": [ + "error", + { + "allow": [ + "error", + "warn" + ] + } + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_" + } + ] + } +} \ No newline at end of file diff --git a/examples/pet-store-streamable-http/.gitignore b/examples/pet-store-streamable-http/.gitignore new file mode 100644 index 0000000..61ab6c0 --- /dev/null +++ b/examples/pet-store-streamable-http/.gitignore @@ -0,0 +1,64 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Build outputs +dist +build +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Reports +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage +coverage +*.lcov +.nyc_output + +# Build artifacts +.grunt +bower_components +jspm_packages/ +web_modules/ +.lock-wscript + +# Editor settings +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.idea +*.sublime-workspace +*.sublime-project + +# Caches +.eslintcache +.stylelintcache +.node_repl_history +.browserslistcache + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# OS specific +.DS_Store +Thumbs.db diff --git a/examples/pet-store-streamable-http/.prettierrc b/examples/pet-store-streamable-http/.prettierrc new file mode 100644 index 0000000..494b947 --- /dev/null +++ b/examples/pet-store-streamable-http/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2 +} \ No newline at end of file diff --git a/examples/pet-store-streamable-http/docs/oauth2-configuration.md b/examples/pet-store-streamable-http/docs/oauth2-configuration.md new file mode 100644 index 0000000..9f3625b --- /dev/null +++ b/examples/pet-store-streamable-http/docs/oauth2-configuration.md @@ -0,0 +1,23 @@ +# OAuth2 Configuration + +This API uses OAuth2 for authentication. The MCP server can handle OAuth2 authentication in the following ways: + +1. **Using a pre-acquired token**: You provide a token you've already obtained +2. **Using client credentials flow**: The server automatically acquires a token using your client ID and secret + +## Environment Variables + +### petstore_auth + +**Configuration Variables:** + +- `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 diff --git a/examples/pet-store-streamable-http/jest.config.js b/examples/pet-store-streamable-http/jest.config.js new file mode 100644 index 0000000..71603b2 --- /dev/null +++ b/examples/pet-store-streamable-http/jest.config.js @@ -0,0 +1,16 @@ +export default { + preset: 'ts-jest', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\.{1,2}/.*)\.js$': '$1', + }, + transform: { + '^.+\.tsx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, +}; diff --git a/examples/pet-store-streamable-http/package-lock.json b/examples/pet-store-streamable-http/package-lock.json new file mode 100644 index 0000000..14a6448 --- /dev/null +++ b/examples/pet-store-streamable-http/package-lock.json @@ -0,0 +1,1231 @@ +{ + "name": "swagger-petstore---openapi-3-0", + "version": "1.0.26", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "swagger-petstore---openapi-3-0", + "version": "1.0.26", + "dependencies": { + "@hono/node-server": "^1.14.1", + "@modelcontextprotocol/sdk": "^1.10.0", + "axios": "^1.9.0", + "dotenv": "^16.4.5", + "fetch-to-node": "^2.1.0", + "hono": "^4.7.7", + "json-schema-to-zod": "^2.6.1", + "uuid": "^11.1.0", + "zod": "^3.24.3" + }, + "devDependencies": { + "@types/node": "^22.15.2", + "@types/uuid": "^10.0.0", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.1.tgz", + "integrity": "sha512-vmbuM+HPinjWzPe7FFPWMMQMsbKE9gDPhaH0FFdqbGpkT5lp++tcWDTxwBl5EgS5y6JVgIaCdjeHRfQ4XRBRjQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", + "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.15.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", + "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fetch-to-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fetch-to-node/-/fetch-to-node-2.1.0.tgz", + "integrity": "sha512-Wq05j6LE1GrWpT2t1YbCkyFY6xKRJq3hx/oRJdWEJpZlik3g25MmdJS6RFm49iiMJw6zpZuBOrgihOgy2jGyAA==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.7.tgz", + "integrity": "sha512-2PCpQRbN87Crty8/L/7akZN3UyZIAopSoRxCwRbJgUuV1+MHNFHzYFxZTg4v/03cXUm+jce/qa2VSBZpKBm3Qw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-to-zod": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/json-schema-to-zod/-/json-schema-to-zod-2.6.1.tgz", + "integrity": "sha512-uiHmWH21h9FjKJkRBntfVGTLpYlCZ1n98D0izIlByqQLqpmkQpNTBtfbdP04Na6+43lgsvrShFh2uWLkQDKJuQ==", + "license": "ISC", + "bin": { + "json-schema-to-zod": "dist/cjs/cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/pet-store-streamable-http/package.json b/examples/pet-store-streamable-http/package.json new file mode 100644 index 0000000..c199fe2 --- /dev/null +++ b/examples/pet-store-streamable-http/package.json @@ -0,0 +1,38 @@ +{ + "name": "swagger-petstore---openapi-3-0", + "version": "1.0.26", + "description": "MCP Server generated from OpenAPI spec for swagger-petstore---openapi-3-0", + "private": true, + "type": "module", + "main": "build/index.js", + "files": [ + "build", + "src" + ], + "scripts": { + "start": "node build/index.js", + "build": "tsc && chmod 755 build/index.js", + "typecheck": "tsc --noEmit", + "prestart": "npm run build", + "start:http": "node build/index.js --transport=streamable-http" + }, + "engines": { + "node": ">=20.0.0" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.10.0", + "axios": "^1.9.0", + "dotenv": "^16.4.5", + "zod": "^3.24.3", + "json-schema-to-zod": "^2.6.1", + "hono": "^4.7.7", + "@hono/node-server": "^1.14.1", + "uuid": "^11.1.0", + "fetch-to-node": "^2.1.0" + }, + "devDependencies": { + "@types/node": "^22.15.2", + "typescript": "^5.8.3", + "@types/uuid": "^10.0.0" + } +} \ No newline at end of file diff --git a/examples/pet-store-streamable-http/public/index.html b/examples/pet-store-streamable-http/public/index.html new file mode 100644 index 0000000..ebc29a0 --- /dev/null +++ b/examples/pet-store-streamable-http/public/index.html @@ -0,0 +1,402 @@ + + + + + + swagger-petstore---openapi-3-0 MCP StreamableHTTP Test Client + + + +

swagger-petstore---openapi-3-0 MCP StreamableHTTP Test Client

+

Disconnected

+ +
+
+ +
+ + +
+
+ + + +
+
+

Debug Console

+ +
+
+
+ + + + \ No newline at end of file diff --git a/examples/pet-store-streamable-http/src/index.ts b/examples/pet-store-streamable-http/src/index.ts new file mode 100644 index 0000000..0d92628 --- /dev/null +++ b/examples/pet-store-streamable-http/src/index.ts @@ -0,0 +1,799 @@ +#!/usr/bin/env node +/** + * MCP Server generated from OpenAPI spec for swagger-petstore---openapi-3-0 v1.0.26 + * Generated on: 2025-04-26T16:34:48.638Z + */ + +// Load environment variables from .env file +import dotenv from 'dotenv'; +dotenv.config(); + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + type Tool, + type CallToolResult, + type CallToolRequest +} from "@modelcontextprotocol/sdk/types.js"; +import { setupStreamableHttpServer } from "./streamable-http.js"; + +import { z, ZodError } from 'zod'; +import { jsonSchemaToZod } from 'json-schema-to-zod'; +import axios, { type AxiosRequestConfig, type AxiosError } from 'axios'; + +/** + * Type definition for JSON objects + */ +type JsonObject = Record; + +/** + * Interface for MCP Tool Definition + */ +interface McpToolDefinition { + name: string; + description: string; + inputSchema: any; + method: string; + pathTemplate: string; + executionParameters: { name: string, in: string }[]; + requestBodyContentType?: string; + securityRequirements: any[]; +} + +/** + * Server configuration + */ +export const SERVER_NAME = "swagger-petstore---openapi-3-0"; +export const SERVER_VERSION = "1.0.26"; +export const API_BASE_URL = "https://petstore3.swagger.io/api/v3"; + +/** + * MCP Server instance + */ +const server = new Server( + { name: SERVER_NAME, version: SERVER_VERSION }, + { capabilities: { tools: {} } } +); + +/** + * Map of tool definitions by name + */ +const toolDefinitionMap: Map = new Map([ + + ["updatepet", { + name: "updatepet", + description: `Update an existing pet by Id.`, + inputSchema: {"type":"object","properties":{"requestBody":{"required":["name","photoUrls"],"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"},"category":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"}}},"photoUrls":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"}}}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"description":"Update an existent pet in the store"}},"required":["requestBody"]}, + method: "put", + pathTemplate: "/pet", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["addpet", { + name: "addpet", + description: `Add a new pet to the store.`, + inputSchema: {"type":"object","properties":{"requestBody":{"required":["name","photoUrls"],"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"},"category":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"}}},"photoUrls":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"name":{"type":"string"}}}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"description":"Create a new pet in the store"}},"required":["requestBody"]}, + method: "post", + pathTemplate: "/pet", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["findpetsbystatus", { + name: "findpetsbystatus", + description: `Multiple status values can be provided with comma separated strings.`, + inputSchema: {"type":"object","properties":{"status":{"type":"string","default":"available","enum":["available","pending","sold"],"description":"Status values that need to be considered for filter"}}}, + method: "get", + pathTemplate: "/pet/findByStatus", + executionParameters: [{"name":"status","in":"query"}], + requestBodyContentType: undefined, + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["findpetsbytags", { + name: "findpetsbytags", + description: `Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.`, + inputSchema: {"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"description":"Tags to filter by"}}}, + method: "get", + pathTemplate: "/pet/findByTags", + executionParameters: [{"name":"tags","in":"query"}], + requestBodyContentType: undefined, + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["getpetbyid", { + name: "getpetbyid", + description: `Returns a single pet.`, + inputSchema: {"type":"object","properties":{"petId":{"type":"number","format":"int64","description":"ID of pet to return"}},"required":["petId"]}, + method: "get", + pathTemplate: "/pet/{petId}", + executionParameters: [{"name":"petId","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [{"api_key":[]},{"petstore_auth":["write:pets","read:pets"]}] + }], + ["updatepetwithform", { + name: "updatepetwithform", + description: `Updates a pet resource based on the form data.`, + inputSchema: {"type":"object","properties":{"petId":{"type":"number","format":"int64","description":"ID of pet that needs to be updated"},"name":{"type":"string","description":"Name of pet that needs to be updated"},"status":{"type":"string","description":"Status of pet that needs to be updated"}},"required":["petId"]}, + method: "post", + pathTemplate: "/pet/{petId}", + executionParameters: [{"name":"petId","in":"path"},{"name":"name","in":"query"},{"name":"status","in":"query"}], + requestBodyContentType: undefined, + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["deletepet", { + name: "deletepet", + description: `Delete a pet.`, + inputSchema: {"type":"object","properties":{"api_key":{"type":"string"},"petId":{"type":"number","format":"int64","description":"Pet id to delete"}},"required":["petId"]}, + method: "delete", + pathTemplate: "/pet/{petId}", + executionParameters: [{"name":"api_key","in":"header"},{"name":"petId","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["uploadfile", { + name: "uploadfile", + description: `Upload image of the pet.`, + inputSchema: {"type":"object","properties":{"petId":{"type":"number","format":"int64","description":"ID of pet to update"},"additionalMetadata":{"type":"string","description":"Additional Metadata"},"requestBody":{"type":"string","description":"Request body (content type: application/octet-stream)"}},"required":["petId"]}, + method: "post", + pathTemplate: "/pet/{petId}/uploadImage", + executionParameters: [{"name":"petId","in":"path"},{"name":"additionalMetadata","in":"query"}], + requestBodyContentType: "application/octet-stream", + securityRequirements: [{"petstore_auth":["write:pets","read:pets"]}] + }], + ["getinventory", { + name: "getinventory", + description: `Returns a map of status codes to quantities.`, + inputSchema: {"type":"object","properties":{}}, + method: "get", + pathTemplate: "/store/inventory", + executionParameters: [], + requestBodyContentType: undefined, + securityRequirements: [{"api_key":[]}] + }], + ["placeorder", { + name: "placeorder", + description: `Place a new order in the store.`, + inputSchema: {"type":"object","properties":{"requestBody":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"petId":{"type":"number","format":"int64"},"quantity":{"type":"number","format":"int32"},"shipDate":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Order Status","enum":["placed","approved","delivered"]},"complete":{"type":"boolean"}},"description":"The JSON request body."}}}, + method: "post", + pathTemplate: "/store/order", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [] + }], + ["getorderbyid", { + name: "getorderbyid", + description: `For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.`, + inputSchema: {"type":"object","properties":{"orderId":{"type":"number","format":"int64","description":"ID of order that needs to be fetched"}},"required":["orderId"]}, + method: "get", + pathTemplate: "/store/order/{orderId}", + executionParameters: [{"name":"orderId","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["deleteorder", { + name: "deleteorder", + description: `For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors.`, + inputSchema: {"type":"object","properties":{"orderId":{"type":"number","format":"int64","description":"ID of the order that needs to be deleted"}},"required":["orderId"]}, + method: "delete", + pathTemplate: "/store/order/{orderId}", + executionParameters: [{"name":"orderId","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["createuser", { + name: "createuser", + description: `This can only be done by the logged in user.`, + inputSchema: {"type":"object","properties":{"requestBody":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"number","description":"User Status","format":"int32"}},"description":"Created user object"}}}, + method: "post", + pathTemplate: "/user", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [] + }], + ["createuserswithlistinput", { + name: "createuserswithlistinput", + description: `Creates list of users with given input array.`, + inputSchema: {"type":"object","properties":{"requestBody":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"number","description":"User Status","format":"int32"}}},"description":"The JSON request body."}}}, + method: "post", + pathTemplate: "/user/createWithList", + executionParameters: [], + requestBodyContentType: "application/json", + securityRequirements: [] + }], + ["loginuser", { + name: "loginuser", + description: `Log into the system.`, + inputSchema: {"type":"object","properties":{"username":{"type":"string","description":"The user name for login"},"password":{"type":"string","description":"The password for login in clear text"}}}, + method: "get", + pathTemplate: "/user/login", + executionParameters: [{"name":"username","in":"query"},{"name":"password","in":"query"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["logoutuser", { + name: "logoutuser", + description: `Log user out of the system.`, + inputSchema: {"type":"object","properties":{}}, + method: "get", + pathTemplate: "/user/logout", + executionParameters: [], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["getuserbyname", { + name: "getuserbyname", + description: `Get user detail based on username.`, + inputSchema: {"type":"object","properties":{"username":{"type":"string","description":"The name that needs to be fetched. Use user1 for testing"}},"required":["username"]}, + method: "get", + pathTemplate: "/user/{username}", + executionParameters: [{"name":"username","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], + ["updateuser", { + name: "updateuser", + description: `This can only be done by the logged in user.`, + inputSchema: {"type":"object","properties":{"username":{"type":"string","description":"name that need to be deleted"},"requestBody":{"type":"object","properties":{"id":{"type":"number","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"number","description":"User Status","format":"int32"}},"description":"Update an existent user in the store"}},"required":["username"]}, + method: "put", + pathTemplate: "/user/{username}", + executionParameters: [{"name":"username","in":"path"}], + requestBodyContentType: "application/json", + securityRequirements: [] + }], + ["deleteuser", { + name: "deleteuser", + description: `This can only be done by the logged in user.`, + inputSchema: {"type":"object","properties":{"username":{"type":"string","description":"The name that needs to be deleted"}},"required":["username"]}, + method: "delete", + pathTemplate: "/user/{username}", + executionParameters: [{"name":"username","in":"path"}], + requestBodyContentType: undefined, + securityRequirements: [] + }], +]); + +/** + * Security schemes from the OpenAPI spec + */ +const securitySchemes = { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }; + + +server.setRequestHandler(ListToolsRequestSchema, async () => { + const toolsForClient: Tool[] = Array.from(toolDefinitionMap.values()).map(def => ({ + name: def.name, + description: def.description, + inputSchema: def.inputSchema + })); + return { tools: toolsForClient }; +}); + + +server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise => { + const { name: toolName, arguments: toolArgs } = request.params; + const toolDefinition = toolDefinitionMap.get(toolName); + if (!toolDefinition) { + console.error(`Error: Unknown tool requested: ${toolName}`); + return { content: [{ type: "text", text: `Error: Unknown tool requested: ${toolName}` }] }; + } + return await executeApiTool(toolName, toolDefinition, toolArgs ?? {}, securitySchemes); +}); + + + +/** + * Type definition for cached OAuth tokens + */ +interface TokenCacheEntry { + token: string; + expiresAt: number; +} + +/** + * Declare global __oauthTokenCache property for TypeScript + */ +declare global { + var __oauthTokenCache: Record | undefined; +} + +/** + * Acquires an OAuth2 token using client credentials flow + * + * @param schemeName Name of the security scheme + * @param scheme OAuth2 security scheme + * @returns Acquired token or null if unable to acquire + */ +async function acquireOAuth2Token(schemeName: string, scheme: any): Promise { + try { + // Check if we have the necessary credentials + const clientId = process.env[`OAUTH_CLIENT_ID_SCHEMENAME`]; + const clientSecret = process.env[`OAUTH_CLIENT_SECRET_SCHEMENAME`]; + const scopes = process.env[`OAUTH_SCOPES_SCHEMENAME`]; + + if (!clientId || !clientSecret) { + console.error(`Missing client credentials for OAuth2 scheme '${schemeName}'`); + return null; + } + + // Initialize token cache if needed + if (typeof global.__oauthTokenCache === 'undefined') { + global.__oauthTokenCache = {}; + } + + // Check if we have a cached token + const cacheKey = `${schemeName}_${clientId}`; + const cachedToken = global.__oauthTokenCache[cacheKey]; + const now = Date.now(); + + if (cachedToken && cachedToken.expiresAt > now) { + console.error(`Using cached OAuth2 token for '${schemeName}' (expires in ${Math.floor((cachedToken.expiresAt - now) / 1000)} seconds)`); + return cachedToken.token; + } + + // Determine token URL based on flow type + let tokenUrl = ''; + if (scheme.flows?.clientCredentials?.tokenUrl) { + tokenUrl = scheme.flows.clientCredentials.tokenUrl; + console.error(`Using client credentials flow for '${schemeName}'`); + } else if (scheme.flows?.password?.tokenUrl) { + tokenUrl = scheme.flows.password.tokenUrl; + console.error(`Using password flow for '${schemeName}'`); + } else { + console.error(`No supported OAuth2 flow found for '${schemeName}'`); + return null; + } + + // Prepare the token request + let formData = new URLSearchParams(); + formData.append('grant_type', 'client_credentials'); + + // Add scopes if specified + if (scopes) { + formData.append('scope', scopes); + } + + console.error(`Requesting OAuth2 token from ${tokenUrl}`); + + // Make the token request + const response = await axios({ + method: 'POST', + url: tokenUrl, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` + }, + data: formData.toString() + }); + + // Process the response + if (response.data?.access_token) { + const token = response.data.access_token; + const expiresIn = response.data.expires_in || 3600; // Default to 1 hour + + // Cache the token + global.__oauthTokenCache[cacheKey] = { + token, + expiresAt: now + (expiresIn * 1000) - 60000 // Expire 1 minute early + }; + + console.error(`Successfully acquired OAuth2 token for '${schemeName}' (expires in ${expiresIn} seconds)`); + return token; + } else { + console.error(`Failed to acquire OAuth2 token for '${schemeName}': No access_token in response`); + return null; + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`Error acquiring OAuth2 token for '${schemeName}':`, errorMessage); + return null; + } +} + + +/** + * Executes an API tool with the provided arguments + * + * @param toolName Name of the tool to execute + * @param definition Tool definition + * @param toolArgs Arguments provided by the user + * @param allSecuritySchemes Security schemes from the OpenAPI spec + * @returns Call tool result + */ +async function executeApiTool( + toolName: string, + definition: McpToolDefinition, + toolArgs: JsonObject, + allSecuritySchemes: Record +): Promise { + try { + // Validate arguments against the input schema + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(definition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + } catch (error: unknown) { + if (error instanceof ZodError) { + const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + const errorMessage = error instanceof Error ? error.message : String(error); + return { content: [{ type: 'text', text: `Internal error during validation setup: ${errorMessage}` }] }; + } + } + + // Prepare URL, query parameters, headers, and request body + let urlPath = definition.pathTemplate; + const queryParams: Record = {}; + const headers: Record = { 'Accept': 'application/json' }; + let requestBodyData: any = undefined; + + // Apply parameters to the URL path, query, or headers + definition.executionParameters.forEach((param) => { + const value = validatedArgs[param.name]; + if (typeof value !== 'undefined' && value !== null) { + if (param.in === 'path') { + urlPath = urlPath.replace(`{${param.name}}`, encodeURIComponent(String(value))); + } + else if (param.in === 'query') { + queryParams[param.name] = value; + } + else if (param.in === 'header') { + headers[param.name.toLowerCase()] = String(value); + } + } + }); + + // Ensure all path parameters are resolved + if (urlPath.includes('{')) { + throw new Error(`Failed to resolve path parameters: ${urlPath}`); + } + + // Construct the full URL + const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath; + + // Handle request body if needed + if (definition.requestBodyContentType && typeof validatedArgs['requestBody'] !== 'undefined') { + requestBodyData = validatedArgs['requestBody']; + headers['content-type'] = definition.requestBodyContentType; + } + + + // Apply security requirements if available + // Security requirements use OR between array items and AND within each object + const appliedSecurity = definition.securityRequirements?.find(req => { + // Try each security requirement (combined with OR) + return Object.entries(req).every(([schemeName, scopesArray]) => { + const scheme = allSecuritySchemes[schemeName]; + if (!scheme) return false; + + // API Key security (header, query, cookie) + if (scheme.type === 'apiKey') { + return !!process.env[`API_KEY_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + } + + // HTTP security (basic, bearer) + if (scheme.type === 'http') { + if (scheme.scheme?.toLowerCase() === 'bearer') { + return !!process.env[`BEARER_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + } + else if (scheme.scheme?.toLowerCase() === 'basic') { + return !!process.env[`BASIC_USERNAME_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`] && + !!process.env[`BASIC_PASSWORD_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + } + } + + // OAuth2 security + if (scheme.type === 'oauth2') { + // Check for pre-existing token + if (process.env[`OAUTH_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]) { + return true; + } + + // Check for client credentials for auto-acquisition + if (process.env[`OAUTH_CLIENT_ID_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`] && + process.env[`OAUTH_CLIENT_SECRET_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]) { + // Verify we have a supported flow + if (scheme.flows?.clientCredentials || scheme.flows?.password) { + return true; + } + } + + return false; + } + + // OpenID Connect + if (scheme.type === 'openIdConnect') { + return !!process.env[`OPENID_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + } + + return false; + }); + }); + + // If we found matching security scheme(s), apply them + if (appliedSecurity) { + // Apply each security scheme from this requirement (combined with AND) + for (const [schemeName, scopesArray] of Object.entries(appliedSecurity)) { + const scheme = allSecuritySchemes[schemeName]; + + // API Key security + if (scheme?.type === 'apiKey') { + const apiKey = process.env[`API_KEY_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + if (apiKey) { + if (scheme.in === 'header') { + headers[scheme.name.toLowerCase()] = apiKey; + console.error(`Applied API key '${schemeName}' in header '${scheme.name}'`); + } + else if (scheme.in === 'query') { + queryParams[scheme.name] = apiKey; + console.error(`Applied API key '${schemeName}' in query parameter '${scheme.name}'`); + } + else if (scheme.in === 'cookie') { + // Add the cookie, preserving other cookies if they exist + headers['cookie'] = `${scheme.name}=${apiKey}${headers['cookie'] ? `; ${headers['cookie']}` : ''}`; + console.error(`Applied API key '${schemeName}' in cookie '${scheme.name}'`); + } + } + } + // HTTP security (Bearer or Basic) + else if (scheme?.type === 'http') { + if (scheme.scheme?.toLowerCase() === 'bearer') { + const token = process.env[`BEARER_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + if (token) { + headers['authorization'] = `Bearer ${token}`; + console.error(`Applied Bearer token for '${schemeName}'`); + } + } + else if (scheme.scheme?.toLowerCase() === 'basic') { + const username = process.env[`BASIC_USERNAME_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + const password = process.env[`BASIC_PASSWORD_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + if (username && password) { + headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; + console.error(`Applied Basic authentication for '${schemeName}'`); + } + } + } + // OAuth2 security + else if (scheme?.type === 'oauth2') { + // First try to use a pre-provided token + let token = process.env[`OAUTH_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + + // If no token but we have client credentials, try to acquire a token + if (!token && (scheme.flows?.clientCredentials || scheme.flows?.password)) { + console.error(`Attempting to acquire OAuth token for '${schemeName}'`); + token = (await acquireOAuth2Token(schemeName, scheme)) ?? ''; + } + + // Apply token if available + if (token) { + headers['authorization'] = `Bearer ${token}`; + console.error(`Applied OAuth2 token for '${schemeName}'`); + + // List the scopes that were requested, if any + const scopes = scopesArray as string[]; + if (scopes && scopes.length > 0) { + console.error(`Requested scopes: ${scopes.join(', ')}`); + } + } + } + // OpenID Connect + else if (scheme?.type === 'openIdConnect') { + const token = process.env[`OPENID_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]; + if (token) { + headers['authorization'] = `Bearer ${token}`; + console.error(`Applied OpenID Connect token for '${schemeName}'`); + + // List the scopes that were requested, if any + const scopes = scopesArray as string[]; + if (scopes && scopes.length > 0) { + console.error(`Requested scopes: ${scopes.join(', ')}`); + } + } + } + } + } + // Log warning if security is required but not available + else if (definition.securityRequirements?.length > 0) { + // First generate a more readable representation of the security requirements + const securityRequirementsString = definition.securityRequirements + .map(req => { + const parts = Object.entries(req) + .map(([name, scopesArray]) => { + const scopes = scopesArray as string[]; + if (scopes.length === 0) return name; + return `${name} (scopes: ${scopes.join(', ')})`; + }) + .join(' AND '); + return `[${parts}]`; + }) + .join(' OR '); + + console.warn(`Tool '${toolName}' requires security: ${securityRequirementsString}, but no suitable credentials found.`); + } + + + // Prepare the axios request configuration + const config: AxiosRequestConfig = { + method: definition.method.toUpperCase(), + url: requestUrl, + params: queryParams, + headers: headers, + ...(requestBodyData !== undefined && { data: requestBodyData }), + }; + + // Log request info to stderr (doesn't affect MCP output) + console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`); + + // Execute the request + const response = await axios(config); + + // Process and format the response + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + + // Handle JSON responses + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { + responseText = JSON.stringify(response.data, null, 2); + } catch (e) { + responseText = "[Stringify Error]"; + } + } + // Handle string responses + else if (typeof response.data === 'string') { + responseText = response.data; + } + // Handle other response types + else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } + // Handle empty responses + else { + responseText = `(Status: ${response.status} - No body content)`; + } + + // Return formatted response + return { + content: [ + { + type: "text", + text: `API Response (Status: ${response.status}):\n${responseText}` + } + ], + }; + + } catch (error: unknown) { + // Handle errors during execution + let errorMessage: string; + + // Format Axios errors specially + if (axios.isAxiosError(error)) { + errorMessage = formatApiError(error); + } + // Handle standard errors + else if (error instanceof Error) { + errorMessage = error.message; + } + // Handle unexpected error types + else { + errorMessage = 'Unexpected error: ' + String(error); + } + + // Log error to stderr + console.error(`Error during execution of tool '${toolName}':`, errorMessage); + + // Return error message to client + return { content: [{ type: "text", text: errorMessage }] }; + } +} + + +/** + * Main function to start the server + */ +async function main() { +// Set up StreamableHTTP transport + try { + await setupStreamableHttpServer(server, 3000); + } catch (error) { + console.error("Error setting up StreamableHTTP server:", error); + process.exit(1); + } +} + +/** + * Cleanup function for graceful shutdown + */ +async function cleanup() { + console.error("Shutting down MCP server..."); + process.exit(0); +} + +// Register signal handlers +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +// Start the server +main().catch((error) => { + console.error("Fatal error in main execution:", error); + process.exit(1); +}); + +/** + * Formats API errors for better readability + * + * @param error Axios error + * @returns Formatted error message + */ +function formatApiError(error: AxiosError): string { + let message = 'API request failed.'; + if (error.response) { + message = `API Error: Status ${error.response.status} (${error.response.statusText || 'Status text not available'}). `; + const responseData = error.response.data; + const MAX_LEN = 200; + if (typeof responseData === 'string') { + message += `Response: ${responseData.substring(0, MAX_LEN)}${responseData.length > MAX_LEN ? '...' : ''}`; + } + else if (responseData) { + try { + const jsonString = JSON.stringify(responseData); + message += `Response: ${jsonString.substring(0, MAX_LEN)}${jsonString.length > MAX_LEN ? '...' : ''}`; + } catch { + message += 'Response: [Could not serialize data]'; + } + } + else { + message += 'No response body received.'; + } + } else if (error.request) { + message = 'API Network Error: No response received from server.'; + if (error.code) message += ` (Code: ${error.code})`; + } else { + message += `API Request Setup Error: ${error.message}`; + } + return message; +} + +/** + * Converts a JSON Schema to a Zod schema for runtime validation + * + * @param jsonSchema JSON Schema + * @param toolName Tool name for error reporting + * @returns Zod schema + */ +function getZodSchemaFromJsonSchema(jsonSchema: any, toolName: string): z.ZodTypeAny { + if (typeof jsonSchema !== 'object' || jsonSchema === null) { + return z.object({}).passthrough(); + } + try { + const zodSchemaString = jsonSchemaToZod(jsonSchema); + const zodSchema = eval(zodSchemaString); + if (typeof zodSchema?.parse !== 'function') { + throw new Error('Eval did not produce a valid Zod schema.'); + } + return zodSchema as z.ZodTypeAny; + } catch (err: any) { + console.error(`Failed to generate/evaluate Zod schema for '${toolName}':`, err); + return z.object({}).passthrough(); + } +} diff --git a/examples/pet-store-streamable-http/src/streamable-http.ts b/examples/pet-store-streamable-http/src/streamable-http.ts new file mode 100644 index 0000000..fb3d364 --- /dev/null +++ b/examples/pet-store-streamable-http/src/streamable-http.ts @@ -0,0 +1,248 @@ + +/** + * StreamableHTTP 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 { 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 { 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"; + +/** + * StreamableHTTP MCP Server handler + */ +class MCPStreamableHttpServer { + server: Server; + // Store active transports by session ID + transports: {[sessionId: string]: StreamableHTTPServerTransport} = {}; + + constructor(server: Server) { + this.server = server; + } + + /** + * Handle GET requests (typically used for static files) + */ + async handleGetRequest(c: any) { + console.error("GET request received - StreamableHTTP transport only supports POST"); + return c.text('Method Not Allowed', 405, { + 'Allow': 'POST' + }); + } + + /** + * Handle POST requests (all MCP communication) + */ + async handlePostRequest(c: any) { + const sessionId = c.req.header(SESSION_ID_HEADER_NAME); + console.error(`POST request received ${sessionId ? 'with session ID: ' + sessionId : 'without session ID'}`); + + try { + const body = await c.req.json(); + + // Convert Fetch Request to Node.js req/res + const { req, res } = toReqRes(c.req.raw); + + // Reuse existing transport if we have a session ID + if (sessionId && this.transports[sessionId]) { + const transport = this.transports[sessionId]; + + // Handle the request with the transport + await transport.handleRequest(req, res, body); + + // Cleanup when the response ends + res.on('close', () => { + console.error(`Request closed for session ${sessionId}`); + }); + + // Convert Node.js response back to Fetch Response + return toFetchResponse(res); + } + + // Create new transport for initialize requests + if (!sessionId && this.isInitializeRequest(body)) { + console.error("Creating new StreamableHTTP transport for initialize request"); + + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => uuid(), + }); + + // Add error handler for debug purposes + transport.onerror = (err) => { + console.error('StreamableHTTP transport error:', err); + }; + + // Connect the transport to the MCP server + await this.server.connect(transport); + + // Handle the request with the transport + await transport.handleRequest(req, res, body); + + // Store the transport if we have a session ID + const newSessionId = transport.sessionId; + if (newSessionId) { + console.error(`New session established: ${newSessionId}`); + this.transports[newSessionId] = transport; + + // Set up clean-up for when the transport is closed + transport.onclose = () => { + console.error(`Session closed: ${newSessionId}`); + delete this.transports[newSessionId]; + }; + } + + // Cleanup when the response ends + res.on('close', () => { + console.error(`Request closed for new session`); + }); + + // Convert Node.js response back to Fetch Response + return toFetchResponse(res); + } + + // Invalid request (no session ID and not initialize) + return c.json( + this.createErrorResponse("Bad Request: invalid session ID or method."), + 400 + ); + } catch (error) { + console.error('Error handling MCP request:', error); + return c.json( + this.createErrorResponse("Internal server error."), + 500 + ); + } + } + + /** + * Create a JSON-RPC error response + */ + private createErrorResponse(message: string): JSONRPCError { + return { + jsonrpc: JSON_RPC, + error: { + code: -32000, + message: message, + }, + id: uuid(), + }; + } + + /** + * Check if the request is an initialize request + */ + private isInitializeRequest(body: any): boolean { + const isInitial = (data: any) => { + const result = InitializeRequestSchema.safeParse(data); + return result.success; + }; + + if (Array.isArray(body)) { + return body.some(request => isInitial(request)); + } + + return isInitial(body); + } +} + +/** + * Sets up a web server for the MCP server using StreamableHTTP transport + * + * @param server The MCP Server instance + * @param port The port to listen on (default: 3000) + * @returns The Hono app instance + */ +export async function setupStreamableHttpServer(server: Server, port = 3000) { + // Create Hono app + const app = new Hono(); + + // Enable CORS + app.use('*', cors()); + + // Create MCP handler + const mcpHandler = new MCPStreamableHttpServer(server); + + // Add a simple health check endpoint + app.get('/health', (c) => { + return c.json({ status: 'OK', server: SERVER_NAME, version: SERVER_VERSION }); + }); + + // Main MCP endpoint supporting both GET and POST + app.get("/mcp", (c) => mcpHandler.handleGetRequest(c)); + app.post("/mcp", (c) => mcpHandler.handlePostRequest(c)); + + // 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); + } + + try { + const stat = fs.statSync(fullPath); + if (stat.isFile()) { + const content = fs.readFileSync(fullPath); + + // Set content type based on file extension + const ext = path.extname(fullPath).toLowerCase(); + let contentType = 'text/plain'; + + switch (ext) { + case '.html': contentType = 'text/html'; break; + case '.css': contentType = 'text/css'; break; + case '.js': contentType = 'text/javascript'; break; + case '.json': contentType = 'application/json'; break; + case '.png': contentType = 'image/png'; break; + case '.jpg': contentType = 'image/jpeg'; break; + case '.svg': contentType = 'image/svg+xml'; break; + } + + return new Response(content, { + headers: { 'Content-Type': contentType } + }); + } + } catch (err) { + // File not found or other error + return c.text('Not Found', 404); + } + } catch (err) { + console.error('Error serving static file:', err); + return c.text('Internal Server Error', 500); + } + + return c.text('Not Found', 404); + }); + + // Start the server + serve({ + fetch: app.fetch, + port + }, (info) => { + console.error(`MCP StreamableHTTP Server running at http://localhost:${info.port}`); + console.error(`- MCP Endpoint: http://localhost:${info.port}/mcp`); + console.error(`- Health Check: http://localhost:${info.port}/health`); + }); + + return app; +} diff --git a/examples/pet-store-streamable-http/tsconfig.json b/examples/pet-store-streamable-http/tsconfig.json new file mode 100644 index 0000000..ed2966d --- /dev/null +++ b/examples/pet-store-streamable-http/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "module": "Node16", + "moduleResolution": "Node16", + "noEmit": false, + "outDir": "./build", + "declaration": true, + "sourceMap": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "build", + "**/*.test.ts" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index f2d7684..2123c91 100644 --- a/package.json +++ b/package.json @@ -1,61 +1,61 @@ { "name": "openapi-mcp-generator", - "version": "2.5.0-beta.0", + "version": "3.0.0", "description": "Generates MCP server code from OpenAPI specifications", "license": "MIT", "author": "Harsha", "type": "module", "engines": { - "node": ">=20.0.0" + "node": ">=20.0.0" }, "bin": { - "openapi-mcp-generator": "./dist/index.js" + "openapi-mcp-generator": "./dist/index.js" }, "files": [ - "dist", - "README.md", - "LICENSE" + "dist", + "README.md", + "LICENSE" ], "scripts": { - "start": "node dist/index.js", - "clean": "rimraf dist", - "typecheck": "tsc --noEmit", - "build": "tsc && chmod 755 dist/index.js" + "start": "node dist/index.js", + "clean": "rimraf dist", + "typecheck": "tsc --noEmit", + "build": "tsc && chmod 755 dist/index.js" }, "keywords": [ - "openapi", - "mcp", - "model-context-protocol", - "generator", - "llm", - "ai", - "api" + "openapi", + "mcp", + "model-context-protocol", + "generator", + "llm", + "ai", + "api" ], "repository": { - "type": "git", - "url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git" + "type": "git", + "url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git" }, "bugs": { - "url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues" + "url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues" }, "homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme", "dependencies": { - "@apidevtools/swagger-parser": "^10.1.1", - "commander": "^13.1.0", - "openapi-types": "^12.1.3" - }, - "devDependencies": { - "@types/node": "^22.14.1", - "@typescript-eslint/eslint-plugin": "^8.29.1", - "@typescript-eslint/parser": "^8.29.1", - "eslint": "^9.24.0", - "prettier": "^3.5.3", - "rimraf": "^6.0.1", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.9.0", - "zod": "^3.24.2", - "json-schema-to-zod": "^2.4.1" - } - } \ No newline at end of file + "@apidevtools/swagger-parser": "^10.1.1", + "commander": "^13.1.0", + "openapi-types": "^12.1.3" + }, + "devDependencies": { + "@types/node": "^22.15.2", + "@typescript-eslint/eslint-plugin": "^8.31.0", + "@typescript-eslint/parser": "^8.31.0", + "eslint": "^9.25.1", + "prettier": "^3.5.3", + "rimraf": "^6.0.1", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.10.0", + "json-schema-to-zod": "^2.6.1", + "zod": "^3.24.3" + } +} diff --git a/src/generator/config-files.ts b/src/generator/config-files.ts index 2b6387b..03c60bb 100644 --- a/src/generator/config-files.ts +++ b/src/generator/config-files.ts @@ -4,43 +4,43 @@ /** * Generates the content of tsconfig.json for the MCP server - * + * * @returns JSON string for tsconfig.json */ export function generateTsconfigJson(): string { - const tsconfigData = { - compilerOptions: { - "esModuleInterop": true, - "skipLibCheck": true, - "target": "ES2022", - "allowJs": true, - "resolveJsonModule": true, - "moduleDetection": "force", - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "module": "Node16", - "moduleResolution": "Node16", - "noEmit": false, - "outDir": "./build", - "declaration": true, - "sourceMap": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "build", "**/*.test.ts"] - }; - - return JSON.stringify(tsconfigData, null, 2); + const tsconfigData = { + compilerOptions: { + esModuleInterop: true, + skipLibCheck: true, + target: 'ES2022', + allowJs: true, + resolveJsonModule: true, + moduleDetection: 'force', + strict: true, + noImplicitAny: true, + strictNullChecks: true, + module: 'Node16', + moduleResolution: 'Node16', + noEmit: false, + outDir: './build', + declaration: true, + sourceMap: true, + forceConsistentCasingInFileNames: true, + }, + include: ['src/**/*'], + exclude: ['node_modules', 'build', '**/*.test.ts'], + }; + + return JSON.stringify(tsconfigData, null, 2); } /** * Generates the content of .gitignore for the MCP server - * + * * @returns Content for .gitignore */ export function generateGitignore(): string { - return `# Dependencies + return `# Dependencies node_modules .pnp .pnp.js @@ -109,39 +109,36 @@ Thumbs.db /** * Generates the content of .eslintrc.json for the MCP server - * + * * @returns JSON string for .eslintrc.json */ export function generateEslintConfig(): string { - const eslintConfig = { - "parser": "@typescript-eslint/parser", - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "plugins": ["@typescript-eslint"], - "env": { - "node": true, - "es2022": true - }, - "rules": { - "no-console": ["error", { "allow": ["error", "warn"] }], - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] - } - }; - - return JSON.stringify(eslintConfig, null, 2); + const eslintConfig = { + parser: '@typescript-eslint/parser', + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + plugins: ['@typescript-eslint'], + env: { + node: true, + es2022: true, + }, + rules: { + 'no-console': ['error', { allow: ['error', 'warn'] }], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, + }; + + return JSON.stringify(eslintConfig, null, 2); } /** * Generates the content of jest.config.js for the MCP server - * + * * @returns Content for jest.config.js */ export function generateJestConfig(): string { - return `export default { + return `export default { preset: 'ts-jest', testEnvironment: 'node', extensionsToTreatAsEsm: ['.ts'], @@ -162,17 +159,17 @@ export function generateJestConfig(): string { /** * Generates the content of .prettierrc for the MCP server - * + * * @returns JSON string for .prettierrc */ export function generatePrettierConfig(): string { - const prettierConfig = { - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 100, - "tabWidth": 2 - }; - - return JSON.stringify(prettierConfig, null, 2); -} \ No newline at end of file + const prettierConfig = { + semi: true, + trailingComma: 'es5', + singleQuote: true, + printWidth: 100, + tabWidth: 2, + }; + + return JSON.stringify(prettierConfig, null, 2); +} diff --git a/src/generator/env-file.ts b/src/generator/env-file.ts index 9591028..d1cc591 100644 --- a/src/generator/env-file.ts +++ b/src/generator/env-file.ts @@ -6,12 +6,14 @@ import { getEnvVarName } from '../utils/security.js'; /** * Generates the content of .env.example file for the MCP server - * + * * @param securitySchemes Security schemes from the OpenAPI spec * @returns Content for .env.example file */ -export function generateEnvExample(securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes']): string { - let content = `# MCP Server Environment Variables +export function generateEnvExample( + securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes'] +): string { + let content = `# MCP Server Environment Variables # Copy this file to .env and fill in the values # Server configuration @@ -20,56 +22,53 @@ LOG_LEVEL=info `; - // Add security scheme environment variables with examples - if (securitySchemes && Object.keys(securitySchemes).length > 0) { - content += `# API Authentication\n`; - - for (const [name, schemeOrRef] of Object.entries(securitySchemes)) { - if ('$ref' in schemeOrRef) { - content += `# ${name} - Referenced security scheme (reference not resolved)\n`; - continue; - } - - const scheme = schemeOrRef; - - if (scheme.type === 'apiKey') { - const varName = getEnvVarName(name, 'API_KEY'); - content += `${varName}=your_api_key_here\n`; - } - else if (scheme.type === 'http') { - if (scheme.scheme?.toLowerCase() === 'bearer') { - const varName = getEnvVarName(name, 'BEARER_TOKEN'); - content += `${varName}=your_bearer_token_here\n`; - } - else if (scheme.scheme?.toLowerCase() === 'basic') { - const usernameVar = getEnvVarName(name, 'BASIC_USERNAME'); - const passwordVar = getEnvVarName(name, 'BASIC_PASSWORD'); - content += `${usernameVar}=your_username_here\n`; - content += `${passwordVar}=your_password_here\n`; - } - } - else if (scheme.type === 'oauth2') { - content += `# OAuth2 authentication (${scheme.flows ? Object.keys(scheme.flows).join(', ') : 'unknown'} flow)\n`; - const varName = `OAUTH_TOKEN_${name.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`; - content += `${varName}=your_oauth_token_here\n`; - } - } - } else { - content += `# No API authentication required\n`; - } + // Add security scheme environment variables with examples + if (securitySchemes && Object.keys(securitySchemes).length > 0) { + content += `# API Authentication\n`; - content += `\n# Add any other environment variables your API might need\n`; - - return content; + for (const [name, schemeOrRef] of Object.entries(securitySchemes)) { + if ('$ref' in schemeOrRef) { + content += `# ${name} - Referenced security scheme (reference not resolved)\n`; + continue; + } + + const scheme = schemeOrRef; + + if (scheme.type === 'apiKey') { + const varName = getEnvVarName(name, 'API_KEY'); + content += `${varName}=your_api_key_here\n`; + } else if (scheme.type === 'http') { + if (scheme.scheme?.toLowerCase() === 'bearer') { + const varName = getEnvVarName(name, 'BEARER_TOKEN'); + content += `${varName}=your_bearer_token_here\n`; + } else if (scheme.scheme?.toLowerCase() === 'basic') { + const usernameVar = getEnvVarName(name, 'BASIC_USERNAME'); + const passwordVar = getEnvVarName(name, 'BASIC_PASSWORD'); + content += `${usernameVar}=your_username_here\n`; + content += `${passwordVar}=your_password_here\n`; + } + } else if (scheme.type === 'oauth2') { + content += `# OAuth2 authentication (${scheme.flows ? Object.keys(scheme.flows).join(', ') : 'unknown'} flow)\n`; + const varName = `OAUTH_TOKEN_${name.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`; + content += `${varName}=your_oauth_token_here\n`; + } + } + } else { + content += `# No API authentication required\n`; + } + + content += `\n# Add any other environment variables your API might need\n`; + + return content; } /** * Generates dotenv configuration code for the MCP server - * + * * @returns Code for loading environment variables */ export function generateDotenvConfig(): string { - return ` + return ` /** * Load environment variables from .env file */ @@ -93,4 +92,4 @@ export const config = { logLevel: process.env.LOG_LEVEL || 'info', }; `; -} \ No newline at end of file +} diff --git a/src/generator/index.ts b/src/generator/index.ts index 7652d90..fa16de3 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -6,4 +6,5 @@ export * from './package-json.js'; export * from './config-files.js'; export * from './env-file.js'; export * from './oauth-docs.js'; -export * from './web-server.js'; \ No newline at end of file +export * from './web-server.js'; +export * from './streamable-http.js'; diff --git a/src/generator/oauth-docs.ts b/src/generator/oauth-docs.ts index 26dbd94..9895de0 100644 --- a/src/generator/oauth-docs.ts +++ b/src/generator/oauth-docs.ts @@ -5,34 +5,36 @@ import { OpenAPIV3 } from 'openapi-types'; /** * Generates documentation about OAuth2 configuration - * + * * @param securitySchemes Security schemes from OpenAPI spec * @returns Markdown documentation about OAuth2 configuration */ -export function generateOAuth2Docs(securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes']): string { - if (!securitySchemes) { - return "# OAuth2 Configuration\n\nNo OAuth2 security schemes defined in this API."; - } +export function generateOAuth2Docs( + securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes'] +): string { + if (!securitySchemes) { + return '# OAuth2 Configuration\n\nNo OAuth2 security schemes defined in this API.'; + } - let oauth2Schemes: {name: string, scheme: OpenAPIV3.OAuth2SecurityScheme}[] = []; - - // Find OAuth2 schemes - for (const [name, schemeOrRef] of Object.entries(securitySchemes)) { - if ('$ref' in schemeOrRef) continue; - - if (schemeOrRef.type === 'oauth2') { - oauth2Schemes.push({ - name, - scheme: schemeOrRef - }); - } + let oauth2Schemes: { name: string; scheme: OpenAPIV3.OAuth2SecurityScheme }[] = []; + + // Find OAuth2 schemes + for (const [name, schemeOrRef] of Object.entries(securitySchemes)) { + if ('$ref' in schemeOrRef) continue; + + if (schemeOrRef.type === 'oauth2') { + oauth2Schemes.push({ + name, + scheme: schemeOrRef, + }); } - - if (oauth2Schemes.length === 0) { - return "# OAuth2 Configuration\n\nNo OAuth2 security schemes defined in this API."; - } - - let content = `# OAuth2 Configuration + } + + if (oauth2Schemes.length === 0) { + return '# OAuth2 Configuration\n\nNo OAuth2 security schemes defined in this API.'; + } + + let content = `# OAuth2 Configuration This API uses OAuth2 for authentication. The MCP server can handle OAuth2 authentication in the following ways: @@ -43,59 +45,65 @@ This API uses OAuth2 for authentication. The MCP server can handle OAuth2 authen `; - // Document each OAuth2 scheme - for (const {name, scheme} of oauth2Schemes) { - content += `### ${name}\n\n`; - - if (scheme.description) { - content += `${scheme.description}\n\n`; - } - - content += "**Configuration Variables:**\n\n"; - - const envVarPrefix = name.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase(); - - content += `- \`OAUTH_CLIENT_ID_${envVarPrefix}\`: Your OAuth client ID\n`; - content += `- \`OAUTH_CLIENT_SECRET_${envVarPrefix}\`: Your OAuth client secret\n`; - - if (scheme.flows?.clientCredentials) { - content += `- \`OAUTH_SCOPES_${envVarPrefix}\`: Space-separated list of scopes to request (optional)\n`; - content += `- \`OAUTH_TOKEN_${envVarPrefix}\`: Pre-acquired OAuth token (optional if using client credentials)\n\n`; - - content += "**Client Credentials Flow:**\n\n"; - content += `- Token URL: \`${scheme.flows.clientCredentials.tokenUrl}\`\n`; - - if (scheme.flows.clientCredentials.scopes && Object.keys(scheme.flows.clientCredentials.scopes).length > 0) { - content += "\n**Available Scopes:**\n\n"; - - for (const [scope, description] of Object.entries(scheme.flows.clientCredentials.scopes)) { - content += `- \`${scope}\`: ${description}\n`; - } - } - - content += "\n"; - } - - if (scheme.flows?.authorizationCode) { - content += `- \`OAUTH_TOKEN_${envVarPrefix}\`: Pre-acquired OAuth token (required for authorization code flow)\n\n`; - - content += "**Authorization Code Flow:**\n\n"; - content += `- Authorization URL: \`${scheme.flows.authorizationCode.authorizationUrl}\`\n`; - content += `- Token URL: \`${scheme.flows.authorizationCode.tokenUrl}\`\n`; - - if (scheme.flows.authorizationCode.scopes && Object.keys(scheme.flows.authorizationCode.scopes).length > 0) { - content += "\n**Available Scopes:**\n\n"; - - for (const [scope, description] of Object.entries(scheme.flows.authorizationCode.scopes)) { - content += `- \`${scope}\`: ${description}\n`; - } - } - - content += "\n"; - } + // Document each OAuth2 scheme + for (const { name, scheme } of oauth2Schemes) { + content += `### ${name}\n\n`; + + if (scheme.description) { + content += `${scheme.description}\n\n`; } - - content += `## Token Caching + + content += '**Configuration Variables:**\n\n'; + + const envVarPrefix = name.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase(); + + content += `- \`OAUTH_CLIENT_ID_${envVarPrefix}\`: Your OAuth client ID\n`; + content += `- \`OAUTH_CLIENT_SECRET_${envVarPrefix}\`: Your OAuth client secret\n`; + + if (scheme.flows?.clientCredentials) { + content += `- \`OAUTH_SCOPES_${envVarPrefix}\`: Space-separated list of scopes to request (optional)\n`; + content += `- \`OAUTH_TOKEN_${envVarPrefix}\`: Pre-acquired OAuth token (optional if using client credentials)\n\n`; + + content += '**Client Credentials Flow:**\n\n'; + content += `- Token URL: \`${scheme.flows.clientCredentials.tokenUrl}\`\n`; + + if ( + scheme.flows.clientCredentials.scopes && + Object.keys(scheme.flows.clientCredentials.scopes).length > 0 + ) { + content += '\n**Available Scopes:**\n\n'; + + for (const [scope, description] of Object.entries(scheme.flows.clientCredentials.scopes)) { + content += `- \`${scope}\`: ${description}\n`; + } + } + + content += '\n'; + } + + if (scheme.flows?.authorizationCode) { + content += `- \`OAUTH_TOKEN_${envVarPrefix}\`: Pre-acquired OAuth token (required for authorization code flow)\n\n`; + + content += '**Authorization Code Flow:**\n\n'; + content += `- Authorization URL: \`${scheme.flows.authorizationCode.authorizationUrl}\`\n`; + content += `- Token URL: \`${scheme.flows.authorizationCode.tokenUrl}\`\n`; + + if ( + scheme.flows.authorizationCode.scopes && + Object.keys(scheme.flows.authorizationCode.scopes).length > 0 + ) { + content += '\n**Available Scopes:**\n\n'; + + for (const [scope, description] of Object.entries(scheme.flows.authorizationCode.scopes)) { + content += `- \`${scope}\`: ${description}\n`; + } + } + + content += '\n'; + } + } + + content += `## 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. @@ -105,5 +113,5 @@ When making API requests, the server will: 3. Request a new token if no valid cached token exists `; - return content; -} \ No newline at end of file + return content; +} diff --git a/src/generator/package-json.ts b/src/generator/package-json.ts index 7fe6a34..ef97985 100644 --- a/src/generator/package-json.ts +++ b/src/generator/package-json.ts @@ -1,65 +1,70 @@ /** * Generates the content of package.json for the MCP server - * + * * @param serverName Server name * @param serverVersion Server version - * @param includeWebDeps Whether to include web server dependencies + * @param transportType Type of transport to use (stdio, web, or streamable-http) * @returns JSON string for package.json */ export function generatePackageJson( - serverName: string, - serverVersion: string, - includeWebDeps: boolean = false + serverName: string, + serverVersion: string, + transportType: string = 'stdio' ): string { - const packageData: any = { - name: serverName, - version: serverVersion, - description: `MCP Server generated from OpenAPI spec for ${serverName}`, - private: true, - type: "module", - main: "build/index.js", - files: [ "build", "src" ], - scripts: { - "start": "node build/index.js", - "build": "tsc && chmod 755 build/index.js", - "typecheck": "tsc --noEmit", - "prestart": "npm run build" - }, - engines: { - "node": ">=20.0.0" - }, - dependencies: { - "@modelcontextprotocol/sdk": "^1.9.0", - "axios": "^1.8.4", - "dotenv": "^16.4.5", - "zod": "^3.24.2", - "json-schema-to-zod": "^2.4.1" - }, - devDependencies: { - "@types/node": "^18.19.0", - "typescript": "^5.8.3" - } + const includeWebDeps = transportType === 'web' || transportType === 'streamable-http'; + + const packageData: any = { + name: serverName, + version: serverVersion, + description: `MCP Server generated from OpenAPI spec for ${serverName}`, + private: true, + type: 'module', + main: 'build/index.js', + files: ['build', 'src'], + scripts: { + start: 'node build/index.js', + build: 'tsc && chmod 755 build/index.js', + typecheck: 'tsc --noEmit', + prestart: 'npm run build', + }, + engines: { + node: '>=20.0.0', + }, + dependencies: { + '@modelcontextprotocol/sdk': '^1.10.0', + axios: '^1.9.0', + dotenv: '^16.4.5', + zod: '^3.24.3', + 'json-schema-to-zod': '^2.6.1', + }, + devDependencies: { + '@types/node': '^22.15.2', + typescript: '^5.8.3', + }, + }; + + // Add Hono dependencies for web-based transports + if (includeWebDeps) { + packageData.dependencies = { + ...packageData.dependencies, + hono: '^4.7.7', + '@hono/node-server': '^1.14.1', + uuid: '^11.1.0', }; - - // Add web server dependencies if needed - if (includeWebDeps) { - packageData.dependencies = { - ...packageData.dependencies, - "express": "^4.18.2", - "cors": "^2.8.5", - "uuid": "^11.1.0" - }; - - packageData.devDependencies = { - ...packageData.devDependencies, - "@types/express": "^4.17.21", - "@types/cors": "^2.8.17", - "@types/uuid": "^10.0.0" - }; - - // Add a script to start in web mode - packageData.scripts["start:web"] = "node build/index.js --transport=web"; + + packageData.devDependencies = { + ...packageData.devDependencies, + '@types/uuid': '^10.0.0', + }; + + // Add appropriate start script based on transport type + if (transportType === 'web') { + packageData.scripts['start:web'] = 'node build/index.js --transport=web'; + } else if (transportType === 'streamable-http') { + packageData.scripts['start:http'] = 'node build/index.js --transport=streamable-http'; + packageData.dependencies['fetch-to-node'] = '^2.1.0'; } - - return JSON.stringify(packageData, null, 2); -} \ No newline at end of file + } + + return JSON.stringify(packageData, null, 2); +} diff --git a/src/generator/server-code.ts b/src/generator/server-code.ts index 88b7dce..3059591 100644 --- a/src/generator/server-code.ts +++ b/src/generator/server-code.ts @@ -2,16 +2,16 @@ import { OpenAPIV3 } from 'openapi-types'; import { CliOptions } from '../types/index.js'; import { extractToolsFromApi } from '../parser/extract-tools.js'; import { determineBaseUrl } from '../utils/index.js'; -import { - generateToolDefinitionMap, - generateCallToolHandler, - generateListToolsHandler +import { + generateToolDefinitionMap, + generateCallToolHandler, + generateListToolsHandler, } from '../utils/code-gen.js'; import { generateExecuteApiToolFunction } from '../utils/security.js'; /** * Generates the TypeScript code for the MCP server - * + * * @param api OpenAPI document * @param options CLI options * @param serverName Server name @@ -19,43 +19,57 @@ import { generateExecuteApiToolFunction } from '../utils/security.js'; * @returns Generated TypeScript code */ export function generateMcpServerCode( - api: OpenAPIV3.Document, - options: CliOptions, - serverName: string, - serverVersion: string + api: OpenAPIV3.Document, + options: CliOptions, + serverName: string, + serverVersion: string ): string { - // Extract tools from API - const tools = extractToolsFromApi(api); - - // Determine base URL - const determinedBaseUrl = determineBaseUrl(api, options.baseUrl); - - // Generate code for tool definition map - const toolDefinitionMapCode = generateToolDefinitionMap(tools, api.components?.securitySchemes); - - // Generate code for API tool execution - const executeApiToolFunctionCode = generateExecuteApiToolFunction(api.components?.securitySchemes); - - // Generate code for request handlers - const callToolHandlerCode = generateCallToolHandler(); - const listToolsHandlerCode = generateListToolsHandler(); + // Extract tools from API + const tools = extractToolsFromApi(api); - // Determine if we should include web server code - const includeWebServer = options.transport === 'web'; - const webServerImport = includeWebServer - ? `\nimport { setupWebServer } from "./web-server.js";` - : ''; - - // Define transport based on options - const transportCode = includeWebServer - ? `// Set up Web Server transport + // Determine base URL + const determinedBaseUrl = determineBaseUrl(api, options.baseUrl); + + // Generate code for tool definition map + const toolDefinitionMapCode = generateToolDefinitionMap(tools, api.components?.securitySchemes); + + // Generate code for API tool execution + const executeApiToolFunctionCode = generateExecuteApiToolFunction( + api.components?.securitySchemes + ); + + // Generate code for request handlers + const callToolHandlerCode = generateCallToolHandler(); + const listToolsHandlerCode = generateListToolsHandler(); + + // Determine which transport to include + let transportImport = ''; + let transportCode = ''; + + switch (options.transport) { + case 'web': + transportImport = `\nimport { setupWebServer } from "./web-server.js";`; + transportCode = `// Set up Web Server transport try { await setupWebServer(server, ${options.port || 3000}); } catch (error) { console.error("Error setting up web server:", error); process.exit(1); - }` - : `// Set up stdio transport + }`; + break; + case 'streamable-http': + transportImport = `\nimport { setupStreamableHttpServer } from "./streamable-http.js";`; + transportCode = `// Set up StreamableHTTP transport + try { + await setupStreamableHttpServer(server, ${options.port || 3000}); + } catch (error) { + console.error("Error setting up StreamableHTTP server:", error); + process.exit(1); + }`; + break; + default: // stdio + transportImport = ''; + transportCode = `// Set up stdio transport try { const transport = new StdioServerTransport(); await server.connect(transport); @@ -64,9 +78,11 @@ export function generateMcpServerCode( console.error("Error during server startup:", error); process.exit(1); }`; + break; + } - // Generate the full server code - return `#!/usr/bin/env node + // Generate the full server code + return `#!/usr/bin/env node /** * MCP Server generated from OpenAPI spec for ${serverName} v${serverVersion} * Generated on: ${new Date().toISOString()} @@ -84,7 +100,7 @@ import { type Tool, type CallToolResult, type CallToolRequest -} from "@modelcontextprotocol/sdk/types.js";${webServerImport} +} from "@modelcontextprotocol/sdk/types.js";${transportImport} import { z, ZodError } from 'zod'; import { jsonSchemaToZod } from 'json-schema-to-zod'; @@ -224,4 +240,4 @@ function getZodSchemaFromJsonSchema(jsonSchema: any, toolName: string): z.ZodTyp } } `; -} \ No newline at end of file +} diff --git a/src/generator/streamable-http.ts b/src/generator/streamable-http.ts new file mode 100644 index 0000000..b773a2a --- /dev/null +++ b/src/generator/streamable-http.ts @@ -0,0 +1,674 @@ + +/** + * Generator for StreamableHTTP server code for the MCP server using Hono + */ + +/** + * Generates StreamableHTTP server code for the MCP server + * + * @param port Server port (default: 3000) + * @returns Generated code for the StreamableHTTP server + */ +export function generateStreamableHttpCode(port: number = 3000): string { + return ` +/** + * StreamableHTTP 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 { 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 { 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"; + +/** + * StreamableHTTP MCP Server handler + */ +class MCPStreamableHttpServer { + server: Server; + // Store active transports by session ID + transports: {[sessionId: string]: StreamableHTTPServerTransport} = {}; + + constructor(server: Server) { + this.server = server; + } + + /** + * Handle GET requests (typically used for static files) + */ + async handleGetRequest(c: any) { + console.error("GET request received - StreamableHTTP transport only supports POST"); + return c.text('Method Not Allowed', 405, { + 'Allow': 'POST' + }); + } + + /** + * Handle POST requests (all MCP communication) + */ + async handlePostRequest(c: any) { + const sessionId = c.req.header(SESSION_ID_HEADER_NAME); + console.error(\`POST request received \${sessionId ? 'with session ID: ' + sessionId : 'without session ID'}\`); + + try { + const body = await c.req.json(); + + // Convert Fetch Request to Node.js req/res + const { req, res } = toReqRes(c.req.raw); + + // Reuse existing transport if we have a session ID + if (sessionId && this.transports[sessionId]) { + const transport = this.transports[sessionId]; + + // Handle the request with the transport + await transport.handleRequest(req, res, body); + + // Cleanup when the response ends + res.on('close', () => { + console.error(\`Request closed for session \${sessionId}\`); + }); + + // Convert Node.js response back to Fetch Response + return toFetchResponse(res); + } + + // Create new transport for initialize requests + if (!sessionId && this.isInitializeRequest(body)) { + console.error("Creating new StreamableHTTP transport for initialize request"); + + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => uuid(), + }); + + // Add error handler for debug purposes + transport.onerror = (err) => { + console.error('StreamableHTTP transport error:', err); + }; + + // Connect the transport to the MCP server + await this.server.connect(transport); + + // Handle the request with the transport + await transport.handleRequest(req, res, body); + + // Store the transport if we have a session ID + const newSessionId = transport.sessionId; + if (newSessionId) { + console.error(\`New session established: \${newSessionId}\`); + this.transports[newSessionId] = transport; + + // Set up clean-up for when the transport is closed + transport.onclose = () => { + console.error(\`Session closed: \${newSessionId}\`); + delete this.transports[newSessionId]; + }; + } + + // Cleanup when the response ends + res.on('close', () => { + console.error(\`Request closed for new session\`); + }); + + // Convert Node.js response back to Fetch Response + return toFetchResponse(res); + } + + // Invalid request (no session ID and not initialize) + return c.json( + this.createErrorResponse("Bad Request: invalid session ID or method."), + 400 + ); + } catch (error) { + console.error('Error handling MCP request:', error); + return c.json( + this.createErrorResponse("Internal server error."), + 500 + ); + } + } + + /** + * Create a JSON-RPC error response + */ + private createErrorResponse(message: string): JSONRPCError { + return { + jsonrpc: JSON_RPC, + error: { + code: -32000, + message: message, + }, + id: uuid(), + }; + } + + /** + * Check if the request is an initialize request + */ + private isInitializeRequest(body: any): boolean { + const isInitial = (data: any) => { + const result = InitializeRequestSchema.safeParse(data); + return result.success; + }; + + if (Array.isArray(body)) { + return body.some(request => isInitial(request)); + } + + return isInitial(body); + } +} + +/** + * Sets up a web server for the MCP server using StreamableHTTP transport + * + * @param server The MCP Server instance + * @param port The port to listen on (default: ${port}) + * @returns The Hono app instance + */ +export async function setupStreamableHttpServer(server: Server, port = ${port}) { + // Create Hono app + const app = new Hono(); + + // Enable CORS + app.use('*', cors()); + + // Create MCP handler + const mcpHandler = new MCPStreamableHttpServer(server); + + // Add a simple health check endpoint + app.get('/health', (c) => { + return c.json({ status: 'OK', server: SERVER_NAME, version: SERVER_VERSION }); + }); + + // Main MCP endpoint supporting both GET and POST + app.get("/mcp", (c) => mcpHandler.handleGetRequest(c)); + app.post("/mcp", (c) => mcpHandler.handlePostRequest(c)); + + // 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); + } + + try { + const stat = fs.statSync(fullPath); + if (stat.isFile()) { + const content = fs.readFileSync(fullPath); + + // Set content type based on file extension + const ext = path.extname(fullPath).toLowerCase(); + let contentType = 'text/plain'; + + switch (ext) { + case '.html': contentType = 'text/html'; break; + case '.css': contentType = 'text/css'; break; + case '.js': contentType = 'text/javascript'; break; + case '.json': contentType = 'application/json'; break; + case '.png': contentType = 'image/png'; break; + case '.jpg': contentType = 'image/jpeg'; break; + case '.svg': contentType = 'image/svg+xml'; break; + } + + return new Response(content, { + headers: { 'Content-Type': contentType } + }); + } + } catch (err) { + // File not found or other error + return c.text('Not Found', 404); + } + } catch (err) { + console.error('Error serving static file:', err); + return c.text('Internal Server Error', 500); + } + + return c.text('Not Found', 404); + }); + + // Start the server + serve({ + fetch: app.fetch, + port + }, (info) => { + console.error(\`MCP StreamableHTTP Server running at http://localhost:\${info.port}\`); + console.error(\`- MCP Endpoint: http://localhost:\${info.port}/mcp\`); + console.error(\`- Health Check: http://localhost:\${info.port}/health\`); + }); + + return app; +} +`; +} + +/** + * Generates HTML client page for testing the MCP StreamableHTTP server + * + * @param serverName The name of the MCP server + * @returns HTML content for the test client + */ +export function generateStreamableHttpClientHtml(serverName: string): string { + // HTML client remains the same + return ` + + + + + ${serverName} MCP StreamableHTTP Test Client + + + +

${serverName} MCP StreamableHTTP Test Client

+

Disconnected

+ +
+
+ +
+ + +
+
+ + + +
+
+

Debug Console

+ +
+
+
+ + + +`; +} diff --git a/src/generator/web-server.ts b/src/generator/web-server.ts index 44e4b9a..489a563 100644 --- a/src/generator/web-server.ts +++ b/src/generator/web-server.ts @@ -1,351 +1,553 @@ /** - * Generator for web server code for the MCP server + * Generator for web server code for the MCP server using Hono with SSE streaming */ /** - * Generates web server code for the MCP server (using Express and SSE) - * + * Generates web server code for the MCP server (using Hono and SSE) + * * @param port Server port (default: 3000) * @returns Generated code for the web server */ export function generateWebServerCode(port: number = 3000): string { - return ` + return ` /** - * Web server setup for HTTP-based MCP communication - */ -import express, { Request, Response } from 'express'; -import cors from 'cors'; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +* 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 type { Context } from 'hono'; +import type { SSEStreamingApi } from 'hono/streaming'; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; // Import server configuration constants import { SERVER_NAME, SERVER_VERSION } from './index.js'; /** - * 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: ${port}) - * @returns The Express app instance - */ -export async function setupWebServer(server: Server, port = ${port}) { - // Create Express app - const app = express(); +* Custom SSE Transport implementation using Hono's streaming API +*/ +class SSETransport implements Transport { +private _sessionId: string; +private stream: SSEStreamingApi; +private messageUrl: string; + +onclose?: () => void; +onerror?: (error: Error) => void; +onmessage?: (message: JSONRPCMessage) => void; + +constructor(messageUrl: string, stream: SSEStreamingApi) { + this._sessionId = uuid(); + this.stream = stream; + this.messageUrl = messageUrl; - // Enable CORS - app.use(cors()); + // Set up stream abort handler + this.stream.onAbort(() => { + console.error(\`SSE connection aborted for session \${this._sessionId}\`); + this.close(); + }); +} + +get sessionId(): string { + return this._sessionId; +} + +async start(): Promise { + if (this.stream.closed) { + throw new Error('SSE transport already closed!'); + } - // Parse JSON requests - app.use(express.json()); - - // Add a simple health check endpoint - app.get('/health', (_, res) => { - res.json({ status: 'OK', server: SERVER_NAME, version: SERVER_VERSION }); + // Send the endpoint information + await this.stream.writeSSE({ + event: 'endpoint', + data: \`\${this.messageUrl}?sessionId=\${this._sessionId}\` }); - // Store active SSE transports by session ID - const transports: {[sessionId: string]: SSEServerTransport} = {}; - - // SSE endpoint for clients to connect to - app.get("/sse", async (req: Request, res: Response) => { - // Set headers for SSE - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - - // Enable CORS for SSE - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); - - // Send initial comment to establish connection - res.write(':\\n\\n'); - - // Create new transport for this client - const transport = new SSEServerTransport('/api/messages', res); - const sessionId = transport.sessionId; - - console.error(\`New SSE connection established: \${sessionId}\`); - transports[sessionId] = transport; - - // Clean up on connection close - req.on('close', () => { - console.error(\`SSE connection closed: \${sessionId}\`); - delete transports[sessionId]; - }); - - // Connect the transport to the MCP server - try { - await server.connect(transport); - } catch (error) { - console.error(\`Error connecting transport for session \${sessionId}:\`, error); - // Don't try to send errors to the client here, as headers may already be sent - } + // 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 + }) }); - // API endpoint for clients to send messages - app.post("/api/messages", async (req: Request, res: Response) => { - const sessionId = req.query.sessionId as string; - - if (!sessionId) { - return res.status(400).send('Missing sessionId query parameter'); - } - - const transport = transports[sessionId]; - - if (!transport) { - return res.status(404).send('No active session found with the provided sessionId'); - } - - try { - await transport.handlePostMessage(req, res); - } catch (error) { - console.error(\`Error handling message for session \${sessionId}:\`, error); - - // If the response hasn't been sent yet, send an error response - if (!res.headersSent) { - res.status(500).send('Internal server error processing message'); + // 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 { + if (this.stream?.closed) { + return c.text('SSE connection closed', 400); + } - // Static files for the web client (if any) - app.use(express.static('public')); + try { + // Parse and validate the message + const body = await c.req.json(); + + try { + // Parse and validate the message + const parsedMessage = JSONRPCMessageSchema.parse(body); + + // Forward to the message handler + 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) { + if (this.onerror) { + this.onerror(error instanceof Error ? error : new Error(String(error))); + } + console.error('Error processing request:', error); + return c.text('Error processing message', 400); + } +} + +async close(): Promise { + if (this.stream && !this.stream.closed) { + this.stream.abort(); + } - // Start the server - app.listen(port, () => { - console.error(\`MCP Web Server running at http://localhost:\${port}\`); - console.error(\`- SSE Endpoint: http://localhost:\${port}/sse\`); - console.error(\`- Messages Endpoint: http://localhost:\${port}/api/messages?sessionId=YOUR_SESSION_ID\`); - console.error(\`- Health Check: http://localhost:\${port}/health\`); + if (this.onclose) { + this.onclose(); + } +} + +async send(message: JSONRPCMessage): Promise { + if (this.stream.closed) { + throw new Error('Not connected'); + } + + await this.stream.writeSSE({ + event: '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: ${port}) +* @returns The Hono app instance +*/ +export async function setupWebServer(server: Server, port = ${port}) { +// Create Hono app +const app = new Hono(); + +// Enable CORS +app.use('*', cors()); + +// Store active SSE transports by session ID +const transports: {[sessionId: string]: SSETransport} = {}; + +// 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) => { + return streamSSE(c, async (stream) => { + // Create SSE transport + const transport = new SSETransport('/api/messages', stream); + const sessionId = transport.sessionId; + + console.error(\`New SSE connection established: \${sessionId}\`); + + // Store the transport + transports[sessionId] = transport; + + // Set up cleanup on transport close + transport.onclose = () => { + console.error(\`SSE connection closed for session \${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 + await server.connect(transport); + } 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 +app.post("/api/messages", async (c) => { + const sessionId = c.req.query('sessionId'); - return app; + if (!sessionId) { + 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); + } + + try { + const stat = fs.statSync(fullPath); + if (stat.isFile()) { + const content = fs.readFileSync(fullPath); + + // Set content type based on file extension + const ext = path.extname(fullPath).toLowerCase(); + let contentType = 'text/plain'; + + switch (ext) { + case '.html': contentType = 'text/html'; break; + case '.css': contentType = 'text/css'; break; + case '.js': contentType = 'text/javascript'; break; + case '.json': contentType = 'application/json'; break; + case '.png': contentType = 'image/png'; break; + case '.jpg': contentType = 'image/jpeg'; break; + case '.svg': contentType = 'image/svg+xml'; break; + } + + return new Response(content, { + headers: { 'Content-Type': contentType } + }); + } + } catch (err) { + // File not found or other error + return c.text('Not Found', 404); + } + } catch (err) { + console.error('Error serving static file:', err); + return c.text('Internal Server Error', 500); + } + + return c.text('Not Found', 404); +}); + +// Start the server +serve({ + fetch: app.fetch, + port +}, (info) => { + console.error(\`MCP 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(\`- Health Check: http://localhost:\${info.port}/health\`); +}); + +return app; } `; } /** * Generates HTML client for testing the MCP server - * + * * @param serverName The name of the MCP server * @returns HTML content for the test client */ export function generateTestClientHtml(serverName: string): string { - return ` + // HTML client remains the same + return ` - - - ${serverName} MCP Test Client - + + +${serverName} MCP Test Client + -

${serverName} MCP Test Client

-

Disconnected

+

${serverName} MCP Test Client

+

Disconnected

+ +
+
-
-
- -
- - -
+
+ +
- - - -
-
-

Debug Console

- -
-
+
+ + + +
+
+

Debug Console

+
+
+
+ + + 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(); + }); + `; -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 4d42835..a22d3a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * OpenAPI to MCP Generator - * + * * This tool generates a Model Context Protocol (MCP) server from an OpenAPI specification. * It creates a Node.js project that implements MCP over stdio to proxy API requests. */ @@ -12,38 +12,64 @@ import SwaggerParser from '@apidevtools/swagger-parser'; import { OpenAPIV3 } from 'openapi-types'; // Import generators -import { - generateMcpServerCode, - generatePackageJson, - generateTsconfigJson, - generateGitignore, - generateEslintConfig, - generateJestConfig, - generatePrettierConfig, - generateEnvExample, - generateOAuth2Docs, - generateWebServerCode, - generateTestClientHtml +import { + generateMcpServerCode, + generatePackageJson, + generateTsconfigJson, + generateGitignore, + generateEslintConfig, + generateJestConfig, + generatePrettierConfig, + generateEnvExample, + generateOAuth2Docs, + generateWebServerCode, + generateTestClientHtml, + generateStreamableHttpCode, + generateStreamableHttpClientHtml, } from './generator/index.js'; // Import types -import { CliOptions } from './types/index.js'; +import { CliOptions, TransportType } from './types/index.js'; // Configure CLI const program = new Command(); program - .name('openapi-mcp-generator') - .description('Generates a buildable MCP server project (TypeScript) from an OpenAPI specification') - .requiredOption('-i, --input ', 'Path or URL to the OpenAPI specification file (JSON or YAML)') - .requiredOption('-o, --output ', 'Path to the directory where the MCP server project will be created (e.g., ./petstore-mcp)') - .option('-n, --server-name ', 'Name for the generated MCP server package (default: derived from OpenAPI info title)') - .option('-v, --server-version ', 'Version for the generated MCP server (default: derived from OpenAPI info version or 0.1.0)') - .option('-b, --base-url ', 'Base URL for the target API. Required if not specified in OpenAPI `servers` or if multiple servers exist.') - .option('-t, --transport ', 'Server transport type: "stdio" or "web" (default: "stdio")') - .option('-p, --port ', 'Port for web server (used with --transport=web, default: 3000)', (val) => parseInt(val, 10)) - .option('--force', 'Overwrite existing files without prompting') - .version('2.0.0'); // Match package.json version + .name('openapi-mcp-generator') + .description( + 'Generates a buildable MCP server project (TypeScript) from an OpenAPI specification' + ) + .requiredOption( + '-i, --input ', + 'Path or URL to the OpenAPI specification file (JSON or YAML)' + ) + .requiredOption( + '-o, --output ', + 'Path to the directory where the MCP server project will be created (e.g., ./petstore-mcp)' + ) + .option( + '-n, --server-name ', + 'Name for the generated MCP server package (default: derived from OpenAPI info title)' + ) + .option( + '-v, --server-version ', + 'Version for the generated MCP server (default: derived from OpenAPI info version or 0.1.0)' + ) + .option( + '-b, --base-url ', + 'Base URL for the target API. Required if not specified in OpenAPI `servers` or if multiple servers exist.' + ) + .option( + '-t, --transport ', + 'Server transport type: "stdio", "web", or "streamable-http" (default: "stdio")' + ) + .option( + '-p, --port ', + 'Port for web or streamable-http transport (default: 3000)', + (val) => parseInt(val, 10) + ) + .option('--force', 'Overwrite existing files without prompting') + .version('2.0.0'); // Match package.json version // Parse arguments explicitly from process.argv program.parse(process.argv); @@ -55,172 +81,201 @@ const options = program.opts(); * Main function to run the generator */ async function main() { - // Use the parsed options directly - const outputDir = options.output; - const inputSpec = options.input; + // Use the parsed options directly + const outputDir = options.output; + const inputSpec = options.input; - const srcDir = path.join(outputDir, 'src'); - const serverFilePath = path.join(srcDir, 'index.ts'); - const packageJsonPath = path.join(outputDir, 'package.json'); - const tsconfigPath = path.join(outputDir, 'tsconfig.json'); - const gitignorePath = path.join(outputDir, '.gitignore'); - const eslintPath = path.join(outputDir, '.eslintrc.json'); - const prettierPath = path.join(outputDir, '.prettierrc'); - const jestConfigPath = path.join(outputDir, 'jest.config.js'); - const envExamplePath = path.join(outputDir, '.env.example'); - const docsDir = path.join(outputDir, 'docs'); - const oauth2DocsPath = path.join(docsDir, 'oauth2-configuration.md'); - - // Web server files (if requested) - const webServerPath = path.join(srcDir, 'web-server.ts'); - const publicDir = path.join(outputDir, 'public'); - const indexHtmlPath = path.join(publicDir, 'index.html'); + const srcDir = path.join(outputDir, 'src'); + const serverFilePath = path.join(srcDir, 'index.ts'); + const packageJsonPath = path.join(outputDir, 'package.json'); + const tsconfigPath = path.join(outputDir, 'tsconfig.json'); + const gitignorePath = path.join(outputDir, '.gitignore'); + const eslintPath = path.join(outputDir, '.eslintrc.json'); + const prettierPath = path.join(outputDir, '.prettierrc'); + const jestConfigPath = path.join(outputDir, 'jest.config.js'); + const envExamplePath = path.join(outputDir, '.env.example'); + const docsDir = path.join(outputDir, 'docs'); + const oauth2DocsPath = path.join(docsDir, 'oauth2-configuration.md'); - try { - // Check if output directory exists and is not empty - if (!options.force) { - try { - const dirExists = await fs.stat(outputDir).catch(() => false); - if (dirExists) { - const files = await fs.readdir(outputDir); - if (files.length > 0) { - console.error(`Error: Output directory ${outputDir} already exists and is not empty.`); - console.error('Use --force to overwrite existing files.'); - process.exit(1); - } - } - } catch (err) { - // Directory doesn't exist, which is fine - } + // Web server files (if requested) + const webServerPath = path.join(srcDir, 'web-server.ts'); + const publicDir = path.join(outputDir, 'public'); + const indexHtmlPath = path.join(publicDir, 'index.html'); + + // StreamableHTTP files (if requested) + const streamableHttpPath = path.join(srcDir, 'streamable-http.ts'); + + try { + // Check if output directory exists and is not empty + if (!options.force) { + try { + const dirExists = await fs.stat(outputDir).catch(() => false); + if (dirExists) { + const files = await fs.readdir(outputDir); + if (files.length > 0) { + console.error(`Error: Output directory ${outputDir} already exists and is not empty.`); + console.error('Use --force to overwrite existing files.'); + process.exit(1); + } } - - // Parse OpenAPI spec - console.error(`Parsing OpenAPI spec: ${inputSpec}`); - const api = await SwaggerParser.dereference(inputSpec) as OpenAPIV3.Document; - console.error('OpenAPI spec parsed successfully.'); - - // Determine server name and version - const serverNameRaw = options.serverName || (api.info?.title || 'my-mcp-server'); - const serverName = serverNameRaw.toLowerCase().replace(/[^a-z0-9_-]/g, '-'); - const serverVersion = options.serverVersion || (api.info?.version || '0.1.0'); - - console.error('Generating server code...'); - const serverTsContent = generateMcpServerCode(api, options, serverName, serverVersion); - - console.error('Generating package.json...'); - const packageJsonContent = generatePackageJson(serverName, serverVersion, options.transport === 'web'); - - console.error('Generating tsconfig.json...'); - const tsconfigJsonContent = generateTsconfigJson(); - - console.error('Generating .gitignore...'); - const gitignoreContent = generateGitignore(); - - console.error('Generating ESLint config...'); - const eslintConfigContent = generateEslintConfig(); - - console.error('Generating Prettier config...'); - const prettierConfigContent = generatePrettierConfig(); - - console.error('Generating Jest config...'); - const jestConfigContent = generateJestConfig(); - - console.error('Generating .env.example file...'); - const envExampleContent = generateEnvExample(api.components?.securitySchemes); - - console.error('Generating OAuth2 documentation...'); - const oauth2DocsContent = generateOAuth2Docs(api.components?.securitySchemes); - - console.error(`Creating project directory structure at: ${outputDir}`); - await fs.mkdir(srcDir, { recursive: true }); - - await fs.writeFile(serverFilePath, serverTsContent); - console.error(` -> Created ${serverFilePath}`); - - await fs.writeFile(packageJsonPath, packageJsonContent); - console.error(` -> Created ${packageJsonPath}`); - - await fs.writeFile(tsconfigPath, tsconfigJsonContent); - console.error(` -> Created ${tsconfigPath}`); - - await fs.writeFile(gitignorePath, gitignoreContent); - console.error(` -> Created ${gitignorePath}`); - - await fs.writeFile(eslintPath, eslintConfigContent); - console.error(` -> Created ${eslintPath}`); - - await fs.writeFile(prettierPath, prettierConfigContent); - console.error(` -> Created ${prettierPath}`); - - await fs.writeFile(jestConfigPath, jestConfigContent); - console.error(` -> Created ${jestConfigPath}`); - - await fs.writeFile(envExamplePath, envExampleContent); - console.error(` -> Created ${envExamplePath}`); - - // Only write OAuth2 docs if there are OAuth2 security schemes - if (oauth2DocsContent.includes("No OAuth2 security schemes defined")) { - console.error(` -> No OAuth2 security schemes found, skipping documentation`); - } else { - await fs.mkdir(docsDir, { recursive: true }); - await fs.writeFile(oauth2DocsPath, oauth2DocsContent); - console.error(` -> Created ${oauth2DocsPath}`); - } - - // Generate web server files if web transport is requested - if (options.transport === 'web') { - console.error('Generating web server files...'); - - // Generate web server code - const webServerCode = generateWebServerCode(options.port || 3000); - await fs.writeFile(webServerPath, webServerCode); - console.error(` -> Created ${webServerPath}`); - - // Create public directory and index.html - await fs.mkdir(publicDir, { recursive: true }); - - // Generate test client - const indexHtmlContent = generateTestClientHtml(serverName); - await fs.writeFile(indexHtmlPath, indexHtmlContent); - console.error(` -> Created ${indexHtmlPath}`); - } - - console.error("\n---"); - console.error(`MCP server project '${serverName}' successfully generated at: ${outputDir}`); - console.error("\nNext steps:"); - console.error(`1. Navigate to the directory: cd ${outputDir}`); - console.error(`2. Install dependencies: npm install`); - - if (options.transport === 'web') { - console.error(`3. Build the TypeScript code: npm run build`); - console.error(`4. Run the server in web mode: npm run start:web`); - console.error(` (This will start a web server on port ${options.port || 3000})`); - console.error(` Access the test client at: http://localhost:${options.port || 3000}`); - } else { - console.error(`3. Build the TypeScript code: npm run build`); - console.error(`4. Run the server: npm start`); - console.error(` (This runs the built JavaScript code in build/index.js)`); - } - console.error("---"); - - } catch (error) { - console.error('\nError generating MCP server project:', error); - - // Only attempt cleanup if the directory exists and force option was used - if (options.force) { - try { - await fs.rm(outputDir, { recursive: true, force: true }); - console.error(`Cleaned up partially created directory: ${outputDir}`); - } catch (cleanupError) { - console.error(`Failed to cleanup directory ${outputDir}:`, cleanupError); - } - } - - process.exit(1); + } catch (err) { + // Directory doesn't exist, which is fine + } } + + // Parse OpenAPI spec + console.error(`Parsing OpenAPI spec: ${inputSpec}`); + const api = (await SwaggerParser.dereference(inputSpec)) as OpenAPIV3.Document; + console.error('OpenAPI spec parsed successfully.'); + + // Determine server name and version + const serverNameRaw = options.serverName || api.info?.title || 'my-mcp-server'; + const serverName = serverNameRaw.toLowerCase().replace(/[^a-z0-9_-]/g, '-'); + const serverVersion = options.serverVersion || api.info?.version || '0.1.0'; + + console.error('Generating server code...'); + const serverTsContent = generateMcpServerCode(api, options, serverName, serverVersion); + + console.error('Generating package.json...'); + const packageJsonContent = generatePackageJson( + serverName, + serverVersion, + options.transport as TransportType + ); + + console.error('Generating tsconfig.json...'); + const tsconfigJsonContent = generateTsconfigJson(); + + console.error('Generating .gitignore...'); + const gitignoreContent = generateGitignore(); + + console.error('Generating ESLint config...'); + const eslintConfigContent = generateEslintConfig(); + + console.error('Generating Prettier config...'); + const prettierConfigContent = generatePrettierConfig(); + + console.error('Generating Jest config...'); + const jestConfigContent = generateJestConfig(); + + console.error('Generating .env.example file...'); + const envExampleContent = generateEnvExample(api.components?.securitySchemes); + + console.error('Generating OAuth2 documentation...'); + const oauth2DocsContent = generateOAuth2Docs(api.components?.securitySchemes); + + console.error(`Creating project directory structure at: ${outputDir}`); + await fs.mkdir(srcDir, { recursive: true }); + + await fs.writeFile(serverFilePath, serverTsContent); + console.error(` -> Created ${serverFilePath}`); + + await fs.writeFile(packageJsonPath, packageJsonContent); + console.error(` -> Created ${packageJsonPath}`); + + await fs.writeFile(tsconfigPath, tsconfigJsonContent); + console.error(` -> Created ${tsconfigPath}`); + + await fs.writeFile(gitignorePath, gitignoreContent); + console.error(` -> Created ${gitignorePath}`); + + await fs.writeFile(eslintPath, eslintConfigContent); + console.error(` -> Created ${eslintPath}`); + + await fs.writeFile(prettierPath, prettierConfigContent); + console.error(` -> Created ${prettierPath}`); + + await fs.writeFile(jestConfigPath, jestConfigContent); + console.error(` -> Created ${jestConfigPath}`); + + await fs.writeFile(envExamplePath, envExampleContent); + console.error(` -> Created ${envExamplePath}`); + + // Only write OAuth2 docs if there are OAuth2 security schemes + if (oauth2DocsContent.includes('No OAuth2 security schemes defined')) { + console.error(` -> No OAuth2 security schemes found, skipping documentation`); + } else { + await fs.mkdir(docsDir, { recursive: true }); + await fs.writeFile(oauth2DocsPath, oauth2DocsContent); + console.error(` -> Created ${oauth2DocsPath}`); + } + + // Generate web server files if web transport is requested + if (options.transport === 'web') { + console.error('Generating web server files...'); + + // Generate web server code + const webServerCode = generateWebServerCode(options.port || 3000); + await fs.writeFile(webServerPath, webServerCode); + console.error(` -> Created ${webServerPath}`); + + // Create public directory and index.html + await fs.mkdir(publicDir, { recursive: true }); + + // Generate test client + const indexHtmlContent = generateTestClientHtml(serverName); + await fs.writeFile(indexHtmlPath, indexHtmlContent); + console.error(` -> Created ${indexHtmlPath}`); + } + + // Generate streamable HTTP files if streamable-http transport is requested + if (options.transport === 'streamable-http') { + console.error('Generating StreamableHTTP server files...'); + + // Generate StreamableHTTP server code + const streamableHttpCode = generateStreamableHttpCode(options.port || 3000); + await fs.writeFile(streamableHttpPath, streamableHttpCode); + console.error(` -> Created ${streamableHttpPath}`); + + // Create public directory and index.html + await fs.mkdir(publicDir, { recursive: true }); + + // Generate test client + const indexHtmlContent = generateStreamableHttpClientHtml(serverName); + await fs.writeFile(indexHtmlPath, indexHtmlContent); + console.error(` -> Created ${indexHtmlPath}`); + } + + console.error('\n---'); + console.error(`MCP server project '${serverName}' successfully generated at: ${outputDir}`); + console.error('\nNext steps:'); + console.error(`1. Navigate to the directory: cd ${outputDir}`); + console.error(`2. Install dependencies: npm install`); + + if (options.transport === 'web') { + console.error(`3. Build the TypeScript code: npm run build`); + console.error(`4. Run the server in web mode: npm run start:web`); + console.error(` (This will start a web server on port ${options.port || 3000})`); + console.error(` Access the test client at: http://localhost:${options.port || 3000}`); + } else if (options.transport === 'streamable-http') { + console.error(`3. Build the TypeScript code: npm run build`); + console.error(`4. Run the server in StreamableHTTP mode: npm run start:http`); + console.error(` (This will start a StreamableHTTP server on port ${options.port || 3000})`); + console.error(` Access the test client at: http://localhost:${options.port || 3000}`); + } else { + console.error(`3. Build the TypeScript code: npm run build`); + console.error(`4. Run the server: npm start`); + console.error(` (This runs the built JavaScript code in build/index.js)`); + } + console.error('---'); + } catch (error) { + console.error('\nError generating MCP server project:', error); + + // Only attempt cleanup if the directory exists and force option was used + if (options.force) { + try { + await fs.rm(outputDir, { recursive: true, force: true }); + console.error(`Cleaned up partially created directory: ${outputDir}`); + } catch (cleanupError) { + console.error(`Failed to cleanup directory ${outputDir}:`, cleanupError); + } + } + + process.exit(1); + } } -main().catch(error => { - console.error('Unhandled error:', error); - process.exit(1); -}); \ No newline at end of file +main().catch((error) => { + console.error('Unhandled error:', error); + process.exit(1); +}); diff --git a/src/parser/extract-tools.ts b/src/parser/extract-tools.ts index 248782c..8816095 100644 --- a/src/parser/extract-tools.ts +++ b/src/parser/extract-tools.ts @@ -8,212 +8,219 @@ import { McpToolDefinition } from '../types/index.js'; /** * Extracts tool definitions from an OpenAPI document - * + * * @param api OpenAPI document * @returns Array of MCP tool definitions */ export function extractToolsFromApi(api: OpenAPIV3.Document): McpToolDefinition[] { - const tools: McpToolDefinition[] = []; - const usedNames = new Set(); - const globalSecurity = api.security || []; + const tools: McpToolDefinition[] = []; + const usedNames = new Set(); + const globalSecurity = api.security || []; - if (!api.paths) return tools; + if (!api.paths) return tools; - for (const [path, pathItem] of Object.entries(api.paths)) { - if (!pathItem) continue; - - for (const method of Object.values(OpenAPIV3.HttpMethods)) { - const operation = pathItem[method]; - if (!operation) continue; - - // Generate a unique name for the tool - let baseName = operation.operationId || generateOperationId(method, path); - if (!baseName) continue; + for (const [path, pathItem] of Object.entries(api.paths)) { + if (!pathItem) continue; - // Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -) - baseName = baseName.replace(/\./g, '_').replace(/[^a-z0-9_-]/gi, '_').toLowerCase(); - - let finalToolName = baseName; - let counter = 1; - while (usedNames.has(finalToolName)) { - finalToolName = `${baseName}_${counter++}`; - } - usedNames.add(finalToolName); - - // Get or create a description - const description = operation.description || operation.summary || - `Executes ${method.toUpperCase()} ${path}`; - - // Generate input schema and extract parameters - const { inputSchema, parameters, requestBodyContentType } = generateInputSchemaAndDetails(operation); - - // Extract parameter details for execution - const executionParameters = parameters.map(p => ({ name: p.name, in: p.in })); - - // Determine security requirements - const securityRequirements = operation.security === null ? - globalSecurity : - operation.security || globalSecurity; + for (const method of Object.values(OpenAPIV3.HttpMethods)) { + const operation = pathItem[method]; + if (!operation) continue; - // Create the tool definition - tools.push({ - name: finalToolName, - description, - inputSchema, - method, - pathTemplate: path, - parameters, - executionParameters, - requestBodyContentType, - securityRequirements, - operationId: baseName, - }); - } + // Generate a unique name for the tool + let baseName = operation.operationId || generateOperationId(method, path); + if (!baseName) continue; + + // Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -) + baseName = baseName + .replace(/\./g, '_') + .replace(/[^a-z0-9_-]/gi, '_') + .toLowerCase(); + + let finalToolName = baseName; + let counter = 1; + while (usedNames.has(finalToolName)) { + finalToolName = `${baseName}_${counter++}`; + } + usedNames.add(finalToolName); + + // Get or create a description + const description = + operation.description || operation.summary || `Executes ${method.toUpperCase()} ${path}`; + + // Generate input schema and extract parameters + const { inputSchema, parameters, requestBodyContentType } = + generateInputSchemaAndDetails(operation); + + // Extract parameter details for execution + const executionParameters = parameters.map((p) => ({ name: p.name, in: p.in })); + + // Determine security requirements + const securityRequirements = + operation.security === null ? globalSecurity : operation.security || globalSecurity; + + // Create the tool definition + tools.push({ + name: finalToolName, + description, + inputSchema, + method, + pathTemplate: path, + parameters, + executionParameters, + requestBodyContentType, + securityRequirements, + operationId: baseName, + }); } - - return tools; + } + + return tools; } /** * Generates input schema and extracts parameter details from an operation - * + * * @param operation OpenAPI operation object * @returns Input schema, parameters, and request body content type */ export function generateInputSchemaAndDetails(operation: OpenAPIV3.OperationObject): { - inputSchema: JSONSchema7 | boolean; - parameters: OpenAPIV3.ParameterObject[]; - requestBodyContentType?: string; + inputSchema: JSONSchema7 | boolean; + parameters: OpenAPIV3.ParameterObject[]; + requestBodyContentType?: string; } { - const properties: { [key: string]: JSONSchema7 | boolean } = {}; - const required: string[] = []; - - // Process parameters - const allParameters: OpenAPIV3.ParameterObject[] = Array.isArray(operation.parameters) - ? operation.parameters.map(p => p as OpenAPIV3.ParameterObject) - : []; - - allParameters.forEach(param => { - if (!param.name || !param.schema) return; - - const paramSchema = mapOpenApiSchemaToJsonSchema(param.schema as OpenAPIV3.SchemaObject); - if (typeof paramSchema === 'object') { - paramSchema.description = param.description || paramSchema.description; - } - - properties[param.name] = paramSchema; - if (param.required) required.push(param.name); - }); - - // Process request body (if present) - let requestBodyContentType: string | undefined = undefined; - - if (operation.requestBody) { - const opRequestBody = operation.requestBody as OpenAPIV3.RequestBodyObject; - const jsonContent = opRequestBody.content?.['application/json']; - const firstContent = opRequestBody.content ? Object.entries(opRequestBody.content)[0] : undefined; - - if (jsonContent?.schema) { - requestBodyContentType = 'application/json'; - const bodySchema = mapOpenApiSchemaToJsonSchema(jsonContent.schema as OpenAPIV3.SchemaObject); - - if (typeof bodySchema === 'object') { - bodySchema.description = opRequestBody.description || - bodySchema.description || - 'The JSON request body.'; - } - - properties['requestBody'] = bodySchema; - if (opRequestBody.required) required.push('requestBody'); - } else if (firstContent) { - const [contentType] = firstContent; - requestBodyContentType = contentType; - - properties['requestBody'] = { - type: 'string', - description: opRequestBody.description || `Request body (content type: ${contentType})` - }; - - if (opRequestBody.required) required.push('requestBody'); - } + const properties: { [key: string]: JSONSchema7 | boolean } = {}; + const required: string[] = []; + + // Process parameters + const allParameters: OpenAPIV3.ParameterObject[] = Array.isArray(operation.parameters) + ? operation.parameters.map((p) => p as OpenAPIV3.ParameterObject) + : []; + + allParameters.forEach((param) => { + if (!param.name || !param.schema) return; + + const paramSchema = mapOpenApiSchemaToJsonSchema(param.schema as OpenAPIV3.SchemaObject); + if (typeof paramSchema === 'object') { + paramSchema.description = param.description || paramSchema.description; } - - // Combine everything into a JSON Schema - const inputSchema: JSONSchema7 = { - type: 'object', - properties, - ...(required.length > 0 && { required }) - }; - - return { inputSchema, parameters: allParameters, requestBodyContentType }; + + properties[param.name] = paramSchema; + if (param.required) required.push(param.name); + }); + + // Process request body (if present) + let requestBodyContentType: string | undefined = undefined; + + if (operation.requestBody) { + const opRequestBody = operation.requestBody as OpenAPIV3.RequestBodyObject; + const jsonContent = opRequestBody.content?.['application/json']; + const firstContent = opRequestBody.content + ? Object.entries(opRequestBody.content)[0] + : undefined; + + if (jsonContent?.schema) { + requestBodyContentType = 'application/json'; + const bodySchema = mapOpenApiSchemaToJsonSchema(jsonContent.schema as OpenAPIV3.SchemaObject); + + if (typeof bodySchema === 'object') { + bodySchema.description = + opRequestBody.description || bodySchema.description || 'The JSON request body.'; + } + + properties['requestBody'] = bodySchema; + if (opRequestBody.required) required.push('requestBody'); + } else if (firstContent) { + const [contentType] = firstContent; + requestBodyContentType = contentType; + + properties['requestBody'] = { + type: 'string', + description: opRequestBody.description || `Request body (content type: ${contentType})`, + }; + + if (opRequestBody.required) required.push('requestBody'); + } + } + + // Combine everything into a JSON Schema + const inputSchema: JSONSchema7 = { + type: 'object', + properties, + ...(required.length > 0 && { required }), + }; + + return { inputSchema, parameters: allParameters, requestBodyContentType }; } /** * Maps an OpenAPI schema to a JSON Schema - * + * * @param schema OpenAPI schema object or reference * @returns JSON Schema representation */ -export function mapOpenApiSchemaToJsonSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): JSONSchema7 | boolean { - // Handle reference objects - if ('$ref' in schema) { - console.warn(`Unresolved $ref '${schema.$ref}'.`); - return { type: 'object' }; +export function mapOpenApiSchemaToJsonSchema( + schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject +): JSONSchema7 | boolean { + // Handle reference objects + if ('$ref' in schema) { + console.warn(`Unresolved $ref '${schema.$ref}'.`); + return { type: 'object' }; + } + + // Handle boolean schemas + if (typeof schema === 'boolean') return schema; + + // Create a copy of the schema to modify + const jsonSchema: JSONSchema7 = { ...schema } as any; + + // Convert integer type to number (JSON Schema compatible) + if (schema.type === 'integer') jsonSchema.type = 'number'; + + // Remove OpenAPI-specific properties that aren't in JSON Schema + delete (jsonSchema as any).nullable; + delete (jsonSchema as any).example; + delete (jsonSchema as any).xml; + delete (jsonSchema as any).externalDocs; + delete (jsonSchema as any).deprecated; + delete (jsonSchema as any).readOnly; + delete (jsonSchema as any).writeOnly; + + // Handle nullable properties by adding null to the type + if (schema.nullable) { + if (Array.isArray(jsonSchema.type)) { + if (!jsonSchema.type.includes('null')) jsonSchema.type.push('null'); + } else if (typeof jsonSchema.type === 'string') { + jsonSchema.type = [jsonSchema.type as JSONSchema7TypeName, 'null']; + } else if (!jsonSchema.type) { + jsonSchema.type = 'null'; } - - // Handle boolean schemas - if (typeof schema === 'boolean') return schema; - - // Create a copy of the schema to modify - const jsonSchema: JSONSchema7 = { ...schema } as any; - - // Convert integer type to number (JSON Schema compatible) - if (schema.type === 'integer') jsonSchema.type = 'number'; - - // Remove OpenAPI-specific properties that aren't in JSON Schema - delete (jsonSchema as any).nullable; - delete (jsonSchema as any).example; - delete (jsonSchema as any).xml; - delete (jsonSchema as any).externalDocs; - delete (jsonSchema as any).deprecated; - delete (jsonSchema as any).readOnly; - delete (jsonSchema as any).writeOnly; - - // Handle nullable properties by adding null to the type - if (schema.nullable) { - if (Array.isArray(jsonSchema.type)) { - if (!jsonSchema.type.includes('null')) jsonSchema.type.push('null'); - } - else if (typeof jsonSchema.type === 'string') { - jsonSchema.type = [jsonSchema.type as JSONSchema7TypeName, 'null']; - } - else if (!jsonSchema.type) { - jsonSchema.type = 'null'; - } + } + + // Recursively process object properties + if (jsonSchema.type === 'object' && jsonSchema.properties) { + const mappedProps: { [key: string]: JSONSchema7 | boolean } = {}; + + for (const [key, propSchema] of Object.entries(jsonSchema.properties)) { + if (typeof propSchema === 'object' && propSchema !== null) { + mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema as OpenAPIV3.SchemaObject); + } else if (typeof propSchema === 'boolean') { + mappedProps[key] = propSchema; + } } - - // Recursively process object properties - if (jsonSchema.type === 'object' && jsonSchema.properties) { - const mappedProps: { [key: string]: JSONSchema7 | boolean } = {}; - - for (const [key, propSchema] of Object.entries(jsonSchema.properties)) { - if (typeof propSchema === 'object' && propSchema !== null) { - mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema as OpenAPIV3.SchemaObject); - } - else if (typeof propSchema === 'boolean') { - mappedProps[key] = propSchema; - } - } - - jsonSchema.properties = mappedProps; - } - - // Recursively process array items - if (jsonSchema.type === 'array' && typeof jsonSchema.items === 'object' && jsonSchema.items !== null) { - jsonSchema.items = mapOpenApiSchemaToJsonSchema( - jsonSchema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject - ); - } - - return jsonSchema; -} \ No newline at end of file + + jsonSchema.properties = mappedProps; + } + + // Recursively process array items + if ( + jsonSchema.type === 'array' && + typeof jsonSchema.items === 'object' && + jsonSchema.items !== null + ) { + jsonSchema.items = mapOpenApiSchemaToJsonSchema( + jsonSchema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject + ); + } + + return jsonSchema; +} diff --git a/src/parser/index.ts b/src/parser/index.ts index 0bf5784..16b581c 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,4 +1,4 @@ /** * Parser module exports */ -export * from './extract-tools.js'; \ No newline at end of file +export * from './extract-tools.js'; diff --git a/src/types/index.ts b/src/types/index.ts index 5f25ef9..c945341 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,27 +1,34 @@ +// src/types/index.ts + /** * Core type definitions for the openapi-to-mcp generator */ import { OpenAPIV3 } from 'openapi-types'; -import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema'; +import type { JSONSchema7 } from 'json-schema'; + +/** + * Transport types supported by the MCP server + */ +export type TransportType = 'stdio' | 'web' | 'streamable-http'; /** * CLI options for the generator */ export interface CliOptions { - /** Path to the OpenAPI specification file */ - input: string; - /** Output directory path for generated files */ - output: string; - /** Optional server name to override the one in the OpenAPI spec */ - serverName?: string; - /** Optional server version to override the one in the OpenAPI spec */ - serverVersion?: string; - /** Optional base URL to override the one in the OpenAPI spec */ - baseUrl?: string; - /** Server transport type (stdio or web) */ - transport?: 'stdio' | 'web'; - /** Server port (for web transport) */ - port?: number; + /** Path to the OpenAPI specification file */ + input: string; + /** Output directory path for generated files */ + output: string; + /** Optional server name to override the one in the OpenAPI spec */ + serverName?: string; + /** Optional server version to override the one in the OpenAPI spec */ + serverVersion?: string; + /** Optional base URL to override the one in the OpenAPI spec */ + baseUrl?: string; + /** Server transport type (stdio, web, or streamable-http) */ + transport?: TransportType; + /** Server port (for web and streamable-http transports) */ + port?: number; } /** @@ -29,29 +36,29 @@ export interface CliOptions { * for use in Model Context Protocol server */ export interface McpToolDefinition { - /** Name of the tool, must be unique */ - name: string; - /** Human-readable description of the tool */ - description: string; - /** JSON Schema that defines the input parameters */ - inputSchema: JSONSchema7 | boolean; - /** HTTP method for the operation (get, post, etc.) */ - method: string; - /** URL path template with parameter placeholders */ - pathTemplate: string; - /** OpenAPI parameter objects for this operation */ - parameters: OpenAPIV3.ParameterObject[]; - /** Parameter names and locations for execution */ - executionParameters: { name: string, in: string }[]; - /** Content type for request body, if applicable */ - requestBodyContentType?: string; - /** Security requirements for this operation */ - securityRequirements: OpenAPIV3.SecurityRequirementObject[]; - /** Original operation ID from the OpenAPI spec */ - operationId: string; + /** Name of the tool, must be unique */ + name: string; + /** Human-readable description of the tool */ + description: string; + /** JSON Schema that defines the input parameters */ + inputSchema: JSONSchema7 | boolean; + /** HTTP method for the operation (get, post, etc.) */ + method: string; + /** URL path template with parameter placeholders */ + pathTemplate: string; + /** OpenAPI parameter objects for this operation */ + parameters: OpenAPIV3.ParameterObject[]; + /** Parameter names and locations for execution */ + executionParameters: { name: string; in: string }[]; + /** Content type for request body, if applicable */ + requestBodyContentType?: string; + /** Security requirements for this operation */ + securityRequirements: OpenAPIV3.SecurityRequirementObject[]; + /** Original operation ID from the OpenAPI spec */ + operationId: string; } /** * Helper type for JSON objects */ -export type JsonObject = Record; \ No newline at end of file +export type JsonObject = Record; diff --git a/src/utils/code-gen.ts b/src/utils/code-gen.ts index b571bad..ae48f1d 100644 --- a/src/utils/code-gen.ts +++ b/src/utils/code-gen.ts @@ -7,48 +7,49 @@ import { sanitizeForTemplate } from './helpers.js'; /** * Generates the tool definition map code - * + * * @param tools List of tool definitions * @param securitySchemes Security schemes from OpenAPI spec * @returns Generated code for the tool definition map */ export function generateToolDefinitionMap( - tools: McpToolDefinition[], - securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes'] + tools: McpToolDefinition[], + securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes'] ): string { - if (tools.length === 0) return ""; - - return tools.map(tool => { - // Safely stringify complex objects - let schemaString; - try { - schemaString = JSON.stringify(tool.inputSchema); - } catch (e) { - schemaString = '{}'; - console.warn(`Failed to stringify schema for tool ${tool.name}: ${e}`); - } - - let execParamsString; - try { - execParamsString = JSON.stringify(tool.executionParameters); - } catch (e) { - execParamsString = "[]"; - console.warn(`Failed to stringify execution parameters for tool ${tool.name}: ${e}`); - } - - let securityReqsString; - try { - securityReqsString = JSON.stringify(tool.securityRequirements); - } catch (e) { - securityReqsString = "[]"; - console.warn(`Failed to stringify security requirements for tool ${tool.name}: ${e}`); - } - - // Sanitize description for template literal - const escapedDescription = sanitizeForTemplate(tool.description); - - // Build the tool definition entry - return ` + if (tools.length === 0) return ''; + + return tools + .map((tool) => { + // Safely stringify complex objects + let schemaString; + try { + schemaString = JSON.stringify(tool.inputSchema); + } catch (e) { + schemaString = '{}'; + console.warn(`Failed to stringify schema for tool ${tool.name}: ${e}`); + } + + let execParamsString; + try { + execParamsString = JSON.stringify(tool.executionParameters); + } catch (e) { + execParamsString = '[]'; + console.warn(`Failed to stringify execution parameters for tool ${tool.name}: ${e}`); + } + + let securityReqsString; + try { + securityReqsString = JSON.stringify(tool.securityRequirements); + } catch (e) { + securityReqsString = '[]'; + console.warn(`Failed to stringify security requirements for tool ${tool.name}: ${e}`); + } + + // Sanitize description for template literal + const escapedDescription = sanitizeForTemplate(tool.description); + + // Build the tool definition entry + return ` ["${tool.name}", { name: "${tool.name}", description: \`${escapedDescription}\`, @@ -59,16 +60,17 @@ export function generateToolDefinitionMap( requestBodyContentType: ${tool.requestBodyContentType ? `"${tool.requestBodyContentType}"` : 'undefined'}, securityRequirements: ${securityReqsString} }],`; - }).join(''); + }) + .join(''); } /** * Generates the list tools handler code - * + * * @returns Generated code for the list tools handler */ export function generateListToolsHandler(): string { - return ` + return ` server.setRequestHandler(ListToolsRequestSchema, async () => { const toolsForClient: Tool[] = Array.from(toolDefinitionMap.values()).map(def => ({ name: def.name, @@ -82,11 +84,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { /** * Generates the call tool handler code - * + * * @returns Generated code for the call tool handler */ export function generateCallToolHandler(): string { - return ` + return ` server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise => { const { name: toolName, arguments: toolArgs } = request.params; const toolDefinition = toolDefinitionMap.get(toolName); @@ -101,54 +103,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) /** * Convert a string to title case - * + * * @param str String to convert * @returns Title case string */ export function titleCase(str: string): string { - // Converts snake_case, kebab-case, or path/parts to TitleCase - return str - .toLowerCase() - .replace(/[-_\/](.)/g, (_, char) => char.toUpperCase()) // Handle separators - .replace(/^{/, '') // Remove leading { from path params - .replace(/}$/, '') // Remove trailing } from path params - .replace(/^./, (char) => char.toUpperCase()); // Capitalize first letter + // Converts snake_case, kebab-case, or path/parts to TitleCase + return str + .toLowerCase() + .replace(/[-_\/](.)/g, (_, char) => char.toUpperCase()) // Handle separators + .replace(/^{/, '') // Remove leading { from path params + .replace(/}$/, '') // Remove trailing } from path params + .replace(/^./, (char) => char.toUpperCase()); // Capitalize first letter } /** * Generates an operation ID from method and path - * + * * @param method HTTP method * @param path API path * @returns Generated operation ID */ export function generateOperationId(method: string, path: string): string { - // Generator: get /users/{userId}/posts -> GetUsersPostsByUserId - const parts = path.split('/').filter(p => p); // Split and remove empty parts + // Generator: get /users/{userId}/posts -> GetUsersPostsByUserId + const parts = path.split('/').filter((p) => p); // Split and remove empty parts - let name = method.toLowerCase(); // Start with method name + let name = method.toLowerCase(); // Start with method name - parts.forEach((part, index) => { - if (part.startsWith('{') && part.endsWith('}')) { - // Append 'By' + ParamName only for the *last* path parameter segment - if (index === parts.length - 1) { - name += 'By' + titleCase(part); - } - // Potentially include non-terminal params differently if needed, e.g.: - // else { name += 'With' + titleCase(part); } - } else { - // Append the static path part in TitleCase - name += titleCase(part); - } - }); - - // Simple fallback if name is just the method (e.g., GET /) - if (name === method.toLowerCase()) { - name += 'Root'; + parts.forEach((part, index) => { + if (part.startsWith('{') && part.endsWith('}')) { + // Append 'By' + ParamName only for the *last* path parameter segment + if (index === parts.length - 1) { + name += 'By' + titleCase(part); + } + // Potentially include non-terminal params differently if needed, e.g.: + // else { name += 'With' + titleCase(part); } + } else { + // Append the static path part in TitleCase + name += titleCase(part); } + }); - // Ensure first letter is uppercase after potential lowercase method start - name = name.charAt(0).toUpperCase() + name.slice(1); + // Simple fallback if name is just the method (e.g., GET /) + if (name === method.toLowerCase()) { + name += 'Root'; + } - return name; -} \ No newline at end of file + // Ensure first letter is uppercase after potential lowercase method start + name = name.charAt(0).toUpperCase() + name.slice(1); + + return name; +} diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 71d672b..2d11d5e 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -4,109 +4,109 @@ /** * Safely stringify a JSON object with proper error handling - * + * * @param obj Object to stringify * @param defaultValue Default value to return if stringify fails * @returns JSON string or default value */ export function safeJsonStringify(obj: any, defaultValue: string = '{}'): string { - try { - return JSON.stringify(obj); - } catch (e) { - console.warn(`Failed to stringify object: ${e}`); - return defaultValue; - } + try { + return JSON.stringify(obj); + } catch (e) { + console.warn(`Failed to stringify object: ${e}`); + return defaultValue; + } } /** * Sanitizes a string for use in template strings - * + * * @param str String to sanitize * @returns Sanitized string safe for use in template literals */ export function sanitizeForTemplate(str: string): string { - return (str || '').replace(/\\/g, '\\\\').replace(/`/g, '\\`'); + return (str || '').replace(/\\/g, '\\\\').replace(/`/g, '\\`'); } /** * Converts a string to camelCase - * + * * @param str String to convert * @returns camelCase string */ export function toCamelCase(str: string): string { - return str - .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => - index === 0 ? word.toLowerCase() : word.toUpperCase() - ) - .replace(/\s+/g, '') - .replace(/[^a-zA-Z0-9]/g, ''); + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => + index === 0 ? word.toLowerCase() : word.toUpperCase() + ) + .replace(/\s+/g, '') + .replace(/[^a-zA-Z0-9]/g, ''); } /** * Converts a string to PascalCase - * + * * @param str String to convert * @returns PascalCase string */ export function toPascalCase(str: string): string { - return str - .replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase()) - .replace(/\s+/g, '') - .replace(/[^a-zA-Z0-9]/g, ''); + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase()) + .replace(/\s+/g, '') + .replace(/[^a-zA-Z0-9]/g, ''); } /** * Creates a valid variable name from a string - * + * * @param str Input string * @returns Valid JavaScript variable name */ export function toValidVariableName(str: string): string { - // Replace non-alphanumeric characters with underscores - const sanitized = str.replace(/[^a-zA-Z0-9_$]/g, '_'); - - // Ensure the variable name doesn't start with a number - return sanitized.match(/^[0-9]/) ? '_' + sanitized : sanitized; + // Replace non-alphanumeric characters with underscores + const sanitized = str.replace(/[^a-zA-Z0-9_$]/g, '_'); + + // Ensure the variable name doesn't start with a number + return sanitized.match(/^[0-9]/) ? '_' + sanitized : sanitized; } /** * Checks if a string is a valid JavaScript identifier - * + * * @param str String to check * @returns True if valid identifier, false otherwise */ export function isValidIdentifier(str: string): boolean { - // Check if the string is a valid JavaScript identifier - return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str); + // Check if the string is a valid JavaScript identifier + return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str); } /** * Formats a string for use in code comments - * + * * @param str String to format * @param maxLineLength Maximum line length * @returns Formatted comment string */ export function formatComment(str: string, maxLineLength: number = 80): string { - if (!str) return ''; - - const words = str.trim().split(/\s+/); - const lines: string[] = []; - let currentLine = ''; - - words.forEach(word => { - if ((currentLine + ' ' + word).length <= maxLineLength) { - currentLine += (currentLine ? ' ' : '') + word; - } else { - lines.push(currentLine); - currentLine = word; - } - }); - - if (currentLine) { - lines.push(currentLine); + if (!str) return ''; + + const words = str.trim().split(/\s+/); + const lines: string[] = []; + let currentLine = ''; + + words.forEach((word) => { + if ((currentLine + ' ' + word).length <= maxLineLength) { + currentLine += (currentLine ? ' ' : '') + word; + } else { + lines.push(currentLine); + currentLine = word; } - - return lines.join('\n * '); -} \ No newline at end of file + }); + + if (currentLine) { + lines.push(currentLine); + } + + return lines.join('\n * '); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index b3f9f3c..f554458 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,4 +4,4 @@ export * from './code-gen.js'; export * from './security.js'; export * from './helpers.js'; -export { determineBaseUrl } from './url.js'; \ No newline at end of file +export { determineBaseUrl } from './url.js'; diff --git a/src/utils/security.ts b/src/utils/security.ts index 2f2f1d1..c6ecbc9 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -5,28 +5,37 @@ import { OpenAPIV3 } from 'openapi-types'; /** * Get environment variable name for a security scheme - * + * * @param schemeName Security scheme name * @param type Type of security credentials * @returns Environment variable name */ export function getEnvVarName( - schemeName: string, - type: 'API_KEY' | 'BEARER_TOKEN' | 'BASIC_USERNAME' | 'BASIC_PASSWORD' | 'OAUTH_CLIENT_ID' | 'OAUTH_CLIENT_SECRET' | 'OAUTH_TOKEN' | 'OAUTH_SCOPES' | 'OPENID_TOKEN' + schemeName: string, + type: + | 'API_KEY' + | 'BEARER_TOKEN' + | 'BASIC_USERNAME' + | 'BASIC_PASSWORD' + | 'OAUTH_CLIENT_ID' + | 'OAUTH_CLIENT_SECRET' + | 'OAUTH_TOKEN' + | 'OAUTH_SCOPES' + | 'OPENID_TOKEN' ): string { - const sanitizedName = schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase(); - return `${type}_${sanitizedName}`; + const sanitizedName = schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase(); + return `${type}_${sanitizedName}`; } /** * Generates code for handling API key security - * + * * @param scheme API key security scheme * @returns Generated code */ export function generateApiKeySecurityCode(scheme: OpenAPIV3.ApiKeySecurityScheme): string { - const schemeName = 'schemeName'; // Placeholder, will be replaced in template - return ` + const schemeName = 'schemeName'; // Placeholder, will be replaced in template + return ` if (scheme?.type === 'apiKey') { const apiKey = process.env[\`${getEnvVarName(schemeName, 'API_KEY')}\`]; if (apiKey) { @@ -45,12 +54,12 @@ export function generateApiKeySecurityCode(scheme: OpenAPIV3.ApiKeySecuritySchem /** * Generates code for handling HTTP security (Bearer/Basic) - * + * * @returns Generated code */ export function generateHttpSecurityCode(): string { - const schemeName = 'schemeName'; // Placeholder, will be replaced in template - return ` + const schemeName = 'schemeName'; // Placeholder, will be replaced in template + return ` else if (scheme?.type === 'http') { if (scheme.scheme?.toLowerCase() === 'bearer') { const token = process.env[\`${getEnvVarName(schemeName, 'BEARER_TOKEN')}\`]; @@ -70,11 +79,11 @@ export function generateHttpSecurityCode(): string { /** * Generates code for OAuth2 token acquisition - * + * * @returns Generated code for OAuth2 token acquisition */ export function generateOAuth2TokenAcquisitionCode(): string { - return ` + return ` /** * Type definition for cached OAuth tokens */ @@ -182,23 +191,23 @@ async function acquireOAuth2Token(schemeName: string, scheme: any): Promise { @@ -353,8 +362,8 @@ export function generateExecuteApiToolFunction( } `; - // Generate complete execute API tool function - return ` + // Generate complete execute API tool function + return ` ${oauth2TokenAcquisitionCode} /** @@ -506,79 +515,80 @@ ${securityCode} /** * Gets security scheme documentation for README - * + * * @param securitySchemes Security schemes from OpenAPI spec * @returns Documentation for security schemes */ export function getSecuritySchemesDocs( - securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes'] + securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes'] ): string { - if (!securitySchemes) return 'No security schemes defined in the OpenAPI spec.'; - - let docs = ''; - - for (const [name, schemeOrRef] of Object.entries(securitySchemes)) { - if ('$ref' in schemeOrRef) { - docs += `- \`${name}\`: Referenced security scheme (reference not resolved)\n`; - continue; - } - - const scheme = schemeOrRef; - - if (scheme.type === 'apiKey') { - const envVar = getEnvVarName(name, 'API_KEY'); - docs += `- \`${envVar}\`: API key for ${scheme.name} (in ${scheme.in})\n`; - } - else if (scheme.type === 'http') { - if (scheme.scheme?.toLowerCase() === 'bearer') { - const envVar = getEnvVarName(name, 'BEARER_TOKEN'); - docs += `- \`${envVar}\`: Bearer token for authentication\n`; - } - else if (scheme.scheme?.toLowerCase() === 'basic') { - const usernameEnvVar = getEnvVarName(name, 'BASIC_USERNAME'); - const passwordEnvVar = getEnvVarName(name, 'BASIC_PASSWORD'); - docs += `- \`${usernameEnvVar}\`: Username for Basic authentication\n`; - docs += `- \`${passwordEnvVar}\`: Password for Basic authentication\n`; - } - } - else if (scheme.type === 'oauth2') { - const flowTypes = scheme.flows ? Object.keys(scheme.flows) : ['unknown']; - - // Add client credentials for OAuth2 - const clientIdVar = getEnvVarName(name, 'OAUTH_CLIENT_ID'); - const clientSecretVar = getEnvVarName(name, 'OAUTH_CLIENT_SECRET'); - docs += `- \`${clientIdVar}\`: Client ID for OAuth2 authentication (${flowTypes.join(', ')} flow)\n`; - docs += `- \`${clientSecretVar}\`: Client secret for OAuth2 authentication\n`; - - // Add OAuth token for manual setting - const tokenVar = getEnvVarName(name, 'OAUTH_TOKEN'); - docs += `- \`${tokenVar}\`: OAuth2 token (if not using automatic token acquisition)\n`; - - // Add scopes env var - const scopesVar = getEnvVarName(name, 'OAUTH_SCOPES'); - docs += `- \`${scopesVar}\`: Space-separated list of OAuth2 scopes to request\n`; - - // If available, list flow-specific details - if (scheme.flows?.clientCredentials) { - docs += ` Client Credentials Flow Token URL: ${scheme.flows.clientCredentials.tokenUrl}\n`; - - // List available scopes if defined - if (scheme.flows.clientCredentials.scopes && Object.keys(scheme.flows.clientCredentials.scopes).length > 0) { - docs += ` Available scopes:\n`; - for (const [scope, description] of Object.entries(scheme.flows.clientCredentials.scopes)) { - docs += ` - \`${scope}\`: ${description}\n`; - } - } - } - } - else if (scheme.type === 'openIdConnect') { - const tokenVar = getEnvVarName(name, 'OPENID_TOKEN'); - docs += `- \`${tokenVar}\`: OpenID Connect token\n`; - if (scheme.openIdConnectUrl) { - docs += ` OpenID Connect Discovery URL: ${scheme.openIdConnectUrl}\n`; - } - } + if (!securitySchemes) return 'No security schemes defined in the OpenAPI spec.'; + + let docs = ''; + + for (const [name, schemeOrRef] of Object.entries(securitySchemes)) { + if ('$ref' in schemeOrRef) { + docs += `- \`${name}\`: Referenced security scheme (reference not resolved)\n`; + continue; } - - return docs; -} \ No newline at end of file + + const scheme = schemeOrRef; + + if (scheme.type === 'apiKey') { + const envVar = getEnvVarName(name, 'API_KEY'); + docs += `- \`${envVar}\`: API key for ${scheme.name} (in ${scheme.in})\n`; + } else if (scheme.type === 'http') { + if (scheme.scheme?.toLowerCase() === 'bearer') { + const envVar = getEnvVarName(name, 'BEARER_TOKEN'); + docs += `- \`${envVar}\`: Bearer token for authentication\n`; + } else if (scheme.scheme?.toLowerCase() === 'basic') { + const usernameEnvVar = getEnvVarName(name, 'BASIC_USERNAME'); + const passwordEnvVar = getEnvVarName(name, 'BASIC_PASSWORD'); + docs += `- \`${usernameEnvVar}\`: Username for Basic authentication\n`; + docs += `- \`${passwordEnvVar}\`: Password for Basic authentication\n`; + } + } else if (scheme.type === 'oauth2') { + const flowTypes = scheme.flows ? Object.keys(scheme.flows) : ['unknown']; + + // Add client credentials for OAuth2 + const clientIdVar = getEnvVarName(name, 'OAUTH_CLIENT_ID'); + const clientSecretVar = getEnvVarName(name, 'OAUTH_CLIENT_SECRET'); + docs += `- \`${clientIdVar}\`: Client ID for OAuth2 authentication (${flowTypes.join(', ')} flow)\n`; + docs += `- \`${clientSecretVar}\`: Client secret for OAuth2 authentication\n`; + + // Add OAuth token for manual setting + const tokenVar = getEnvVarName(name, 'OAUTH_TOKEN'); + docs += `- \`${tokenVar}\`: OAuth2 token (if not using automatic token acquisition)\n`; + + // Add scopes env var + const scopesVar = getEnvVarName(name, 'OAUTH_SCOPES'); + docs += `- \`${scopesVar}\`: Space-separated list of OAuth2 scopes to request\n`; + + // If available, list flow-specific details + if (scheme.flows?.clientCredentials) { + docs += ` Client Credentials Flow Token URL: ${scheme.flows.clientCredentials.tokenUrl}\n`; + + // List available scopes if defined + if ( + scheme.flows.clientCredentials.scopes && + Object.keys(scheme.flows.clientCredentials.scopes).length > 0 + ) { + docs += ` Available scopes:\n`; + for (const [scope, description] of Object.entries( + scheme.flows.clientCredentials.scopes + )) { + docs += ` - \`${scope}\`: ${description}\n`; + } + } + } + } else if (scheme.type === 'openIdConnect') { + const tokenVar = getEnvVarName(name, 'OPENID_TOKEN'); + docs += `- \`${tokenVar}\`: OpenID Connect token\n`; + if (scheme.openIdConnectUrl) { + docs += ` OpenID Connect Discovery URL: ${scheme.openIdConnectUrl}\n`; + } + } + } + + return docs; +} diff --git a/src/utils/url.ts b/src/utils/url.ts index 16dfd4a..8873303 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -5,97 +5,103 @@ import { OpenAPIV3 } from 'openapi-types'; /** * Determines the base URL from the OpenAPI document or CLI options - * + * * @param api OpenAPI document * @param cmdLineBaseUrl Optional base URL from command line options * @returns The determined base URL or null if none is available */ export function determineBaseUrl(api: OpenAPIV3.Document, cmdLineBaseUrl?: string): string | null { - // Command line option takes precedence - if (cmdLineBaseUrl) { - return normalizeUrl(cmdLineBaseUrl); - } - - // Single server in OpenAPI spec - if (api.servers && api.servers.length === 1 && api.servers[0].url) { - return normalizeUrl(api.servers[0].url); - } - - // Multiple servers - use first one with warning - if (api.servers && api.servers.length > 1) { - console.warn(`Multiple servers found. Using first: "${api.servers[0].url}". Use --base-url to override.`); - return normalizeUrl(api.servers[0].url); - } - - // No server information available - return null; + // Command line option takes precedence + if (cmdLineBaseUrl) { + return normalizeUrl(cmdLineBaseUrl); + } + + // Single server in OpenAPI spec + if (api.servers && api.servers.length === 1 && api.servers[0].url) { + return normalizeUrl(api.servers[0].url); + } + + // Multiple servers - use first one with warning + if (api.servers && api.servers.length > 1) { + console.warn( + `Multiple servers found. Using first: "${api.servers[0].url}". Use --base-url to override.` + ); + return normalizeUrl(api.servers[0].url); + } + + // No server information available + return null; } /** * Normalizes a URL by removing trailing slashes - * + * * @param url URL to normalize * @returns Normalized URL */ export function normalizeUrl(url: string): string { - return url.replace(/\/$/, ''); + return url.replace(/\/$/, ''); } /** * Joins URL segments handling slashes correctly - * + * * @param baseUrl Base URL * @param path Path to append * @returns Joined URL */ export function joinUrl(baseUrl: string, path: string): string { - if (!baseUrl) return path; - if (!path) return baseUrl; - - const normalizedBase = normalizeUrl(baseUrl); - const normalizedPath = path.startsWith('/') ? path : `/${path}`; - - return `${normalizedBase}${normalizedPath}`; + if (!baseUrl) return path; + if (!path) return baseUrl; + + const normalizedBase = normalizeUrl(baseUrl); + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + + return `${normalizedBase}${normalizedPath}`; } /** * Builds a URL with query parameters - * + * * @param baseUrl Base URL * @param queryParams Query parameters * @returns URL with query parameters */ export function buildUrlWithQuery(baseUrl: string, queryParams: Record): string { - if (!Object.keys(queryParams).length) return baseUrl; - - const url = new URL(baseUrl.startsWith('http') ? baseUrl : `http://localhost${baseUrl.startsWith('/') ? '' : '/'}${baseUrl}`); - - for (const [key, value] of Object.entries(queryParams)) { - if (Array.isArray(value)) { - value.forEach(item => url.searchParams.append(key, String(item))); - } else { - url.searchParams.append(key, String(value)); - } + if (!Object.keys(queryParams).length) return baseUrl; + + const url = new URL( + baseUrl.startsWith('http') + ? baseUrl + : `http://localhost${baseUrl.startsWith('/') ? '' : '/'}${baseUrl}` + ); + + for (const [key, value] of Object.entries(queryParams)) { + if (Array.isArray(value)) { + value.forEach((item) => url.searchParams.append(key, String(item))); + } else { + url.searchParams.append(key, String(value)); } - - // Remove http://localhost if we added it - return baseUrl.startsWith('http') ? url.toString() : url.pathname + url.search; + } + + // Remove http://localhost if we added it + return baseUrl.startsWith('http') ? url.toString() : url.pathname + url.search; } /** * Extracts path parameters from a URL template - * + * * @param urlTemplate URL template with {param} placeholders * @returns Array of parameter names */ export function extractPathParams(urlTemplate: string): string[] { - const paramRegex = /{([^}]+)}/g; - const params: string[] = []; - let match; - - while ((match = paramRegex.exec(urlTemplate)) !== null) { - params.push(match[1]); - } - - return params; -} \ No newline at end of file + const paramRegex = /{([^}]+)}/g; + const params: string[] = []; + let match; + + while ((match = paramRegex.exec(urlTemplate)) !== null) { + params.push(match[1]); + } + + return params; +}