@ivotoby/openapi-mcp-server 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +116 -39
  2. package/dist/bundle.js +618 -25
  3. package/package.json +2 -7
package/README.md CHANGED
@@ -2,14 +2,24 @@
2
2
 
3
3
  A Model Context Protocol (MCP) server that exposes OpenAPI endpoints as MCP resources. This server allows Large Language Models to discover and interact with REST APIs defined by OpenAPI specifications through the MCP protocol.
4
4
 
5
- ## Quick Start
5
+ ## Overview
6
6
 
7
- You do not need to clone this repository to use this MCP server. You can simply configure it in Claude Desktop:
7
+ This MCP server supports two transport methods:
8
+
9
+ 1. **Stdio Transport** (default): For direct integration with AI systems like Claude Desktop that manage MCP connections through standard input/output.
10
+ 2. **Streamable HTTP Transport**: For connecting to the server over HTTP, allowing web clients and other HTTP-capable systems to use the MCP protocol.
11
+
12
+ ## Quick Start for Users
13
+
14
+ ### Option 1: Using with Claude Desktop (Stdio Transport)
15
+
16
+ No need to clone this repository. Simply configure Claude Desktop to use this MCP server:
8
17
 
9
18
  1. Locate or create your Claude Desktop configuration file:
19
+
10
20
  - On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
11
21
 
12
- 2. Add the following configuration to enable the OpenAPI MCP server:
22
+ 2. Add the following configuration:
13
23
 
14
24
  ```json
15
25
  {
@@ -32,27 +42,75 @@ You do not need to clone this repository to use this MCP server. You can simply
32
42
  - `OPENAPI_SPEC_PATH`: URL or path to your OpenAPI specification
33
43
  - `API_HEADERS`: Comma-separated key:value pairs for API authentication headers
34
44
 
35
- ## Development Tools
45
+ ### Option 2: Using with HTTP Clients (HTTP Transport)
36
46
 
37
- This project includes several development tools to make your workflow easier:
47
+ To use the server with HTTP clients:
38
48
 
39
- ### Building
49
+ 1. No installation required! Use npx to run the package directly:
40
50
 
41
- - `npm run build` - Builds the TypeScript source
42
- - `npm run clean` - Removes build artifacts
43
- - `npm run typecheck` - Runs TypeScript type checking
51
+ ```bash
52
+ npx @ivotoby/openapi-mcp-server \
53
+ --api-base-url https://api.example.com \
54
+ --openapi-spec https://api.example.com/openapi.json \
55
+ --headers "Authorization:Bearer token123" \
56
+ --transport http \
57
+ --port 3000
58
+ ```
44
59
 
45
- ### Development Mode
60
+ 2. Interact with the server using HTTP requests:
46
61
 
47
- - `npm run dev` - Watches source files and rebuilds on changes
48
- - `npm run inspect-watch` - Runs the inspector with auto-reload on changes
62
+ ```bash
63
+ # Initialize a session (first request)
64
+ curl -X POST http://localhost:3000/mcp \
65
+ -H "Content-Type: application/json" \
66
+ -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"client":{"name":"curl-client","version":"1.0.0"},"protocol":{"name":"mcp","version":"2025-03-26"}}}'
67
+
68
+ # The response includes a Mcp-Session-Id header that you must use for subsequent requests
69
+ # and the InitializeResult directly in the POST response body.
70
+
71
+ # Send a request to list tools
72
+ # This also receives its response directly on this POST request.
73
+ curl -X POST http://localhost:3000/mcp \
74
+ -H "Content-Type: application/json" \
75
+ -H "Mcp-Session-Id: your-session-id" \
76
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
77
+
78
+ # Open a streaming connection for other server responses (e.g., tool execution results)
79
+ # This uses Server-Sent Events (SSE).
80
+ curl -N http://localhost:3000/mcp -H "Mcp-Session-Id: your-session-id"
81
+
82
+ # Example: Execute a tool (response will arrive on the GET stream)
83
+ # curl -X POST http://localhost:3000/mcp \
84
+ # -H "Content-Type: application/json" \
85
+ # -H "Mcp-Session-Id: your-session-id" \
86
+ # -d '{"jsonrpc":"2.0","id":2,"method":"tools/execute","params":{"name":"yourToolName", "arguments": {}}}'
87
+
88
+ # Terminate the session when done
89
+ curl -X DELETE http://localhost:3000/mcp -H "Mcp-Session-Id: your-session-id"
90
+ ```
49
91
 
50
- ### Code Quality
92
+ ## Transport Types
51
93
 
52
- - `npm run lint` - Runs ESLint
53
- - `npm run typecheck` - Verifies TypeScript types
94
+ ### Stdio Transport (Default)
95
+
96
+ The stdio transport is designed for direct integration with AI systems like Claude Desktop that manage MCP connections through standard input/output. This is the simplest setup and requires no network configuration.
97
+
98
+ **When to use**: When integrating with Claude Desktop or other systems that support stdio-based MCP communication.
99
+
100
+ ### Streamable HTTP Transport
101
+
102
+ The HTTP transport allows the MCP server to be accessed over HTTP, enabling web applications and other HTTP-capable clients to interact with the MCP protocol. It supports session management, streaming responses, and standard HTTP methods.
54
103
 
55
- ## Configuration
104
+ **Key features**:
105
+
106
+ - Session management with Mcp-Session-Id header
107
+ - HTTP responses for `initialize` and `tools/list` requests are sent synchronously on the POST.
108
+ - Other server-to-client messages (e.g., `tools/execute` results, notifications) are streamed over a GET connection using Server-Sent Events (SSE).
109
+ - Support for POST/GET/DELETE methods
110
+
111
+ **When to use**: When you need to expose the MCP server to web clients or systems that communicate over HTTP rather than stdio.
112
+
113
+ ## Configuration Options
56
114
 
57
115
  The server can be configured through environment variables or command line arguments:
58
116
 
@@ -63,51 +121,70 @@ The server can be configured through environment variables or command line argum
63
121
  - `API_HEADERS` - Comma-separated key:value pairs for API headers
64
122
  - `SERVER_NAME` - Name for the MCP server (default: "mcp-openapi-server")
65
123
  - `SERVER_VERSION` - Version of the server (default: "1.0.0")
124
+ - `TRANSPORT_TYPE` - Transport type to use: "stdio" or "http" (default: "stdio")
125
+ - `HTTP_PORT` - Port for HTTP transport (default: 3000)
126
+ - `HTTP_HOST` - Host for HTTP transport (default: "127.0.0.1")
127
+ - `ENDPOINT_PATH` - Endpoint path for HTTP transport (default: "/mcp")
66
128
 
67
129
  ### Command Line Arguments
68
130
 
69
131
  ```bash
70
- npm run inspect -- \
132
+ npx @ivotoby/openapi-mcp-server \
71
133
  --api-base-url https://api.example.com \
72
134
  --openapi-spec https://api.example.com/openapi.json \
73
135
  --headers "Authorization:Bearer token123,X-API-Key:your-api-key" \
74
136
  --name "my-mcp-server" \
75
- --version "1.0.0"
137
+ --version "1.0.0" \
138
+ --transport http \
139
+ --port 3000 \
140
+ --host 127.0.0.1 \
141
+ --path /mcp
76
142
  ```
77
143
 
78
- ## Development Workflow
79
-
80
- 1. Start the development environment:
81
- ```bash
82
- npm run inspect-watch
83
- ```
144
+ ## Security Considerations
84
145
 
85
- 2. Make changes to the TypeScript files in `src/`
86
- 3. The server will automatically rebuild and restart
87
- 4. Use the MCP Inspector UI to test your changes
146
+ - The HTTP transport validates Origin headers to prevent DNS rebinding attacks
147
+ - By default, HTTP transport only binds to localhost (127.0.0.1)
148
+ - If exposing to other hosts, consider implementing additional authentication
88
149
 
89
150
  ## Debugging
90
151
 
91
- The server outputs debug logs to stderr. To see these logs:
152
+ To see debug logs:
92
153
 
93
- 1. In development mode:
94
- - Logs appear in the terminal running `inspect-watch`
95
-
96
- 2. When running directly:
154
+ 1. When using stdio transport with Claude Desktop:
155
+
156
+ - Logs appear in the Claude Desktop logs
157
+
158
+ 2. When using HTTP transport:
97
159
  ```bash
98
- npm run inspect 2>debug.log
160
+ npx @ivotoby/openapi-mcp-server --transport http 2>debug.log
99
161
  ```
100
162
 
101
- ## Contributing
163
+ ## For Developers
164
+
165
+ ### Development Tools
166
+
167
+ - `npm run build` - Builds the TypeScript source
168
+ - `npm run clean` - Removes build artifacts
169
+ - `npm run typecheck` - Runs TypeScript type checking
170
+ - `npm run lint` - Runs ESLint
171
+ - `npm run dev` - Watches source files and rebuilds on changes
172
+ - `npm run inspect-watch` - Runs the inspector with auto-reload on changes
173
+
174
+ ### Development Workflow
175
+
176
+ 1. Clone the repository
177
+ 2. Install dependencies: `npm install`
178
+ 3. Start the development environment: `npm run inspect-watch`
179
+ 4. Make changes to the TypeScript files in `src/`
180
+ 5. The server will automatically rebuild and restart
181
+
182
+ ### Contributing
102
183
 
103
184
  1. Fork the repository
104
185
  2. Create a feature branch
105
186
  3. Make your changes
106
- 4. Run tests and linting:
107
- ```bash
108
- npm run typecheck
109
- npm run lint
110
- ```
187
+ 4. Run tests and linting: `npm run typecheck && npm run lint`
111
188
  5. Submit a pull request
112
189
 
113
190
  ## License
package/dist/bundle.js CHANGED
@@ -9991,7 +9991,7 @@ var require_form_data = __commonJS({
9991
9991
  var CombinedStream = require_combined_stream();
9992
9992
  var util3 = __require("util");
9993
9993
  var path = __require("path");
9994
- var http2 = __require("http");
9994
+ var http3 = __require("http");
9995
9995
  var https2 = __require("https");
9996
9996
  var parseUrl = __require("url").parse;
9997
9997
  var fs = __require("fs");
@@ -10263,7 +10263,7 @@ var require_form_data = __commonJS({
10263
10263
  if (options.protocol == "https:") {
10264
10264
  request = https2.request(options);
10265
10265
  } else {
10266
- request = http2.request(options);
10266
+ request = http3.request(options);
10267
10267
  }
10268
10268
  this.getLength(function(err, length) {
10269
10269
  if (err && err !== "Unknown stream") {
@@ -11160,7 +11160,7 @@ var require_follow_redirects = __commonJS({
11160
11160
  "node_modules/follow-redirects/index.js"(exports, module) {
11161
11161
  var url2 = __require("url");
11162
11162
  var URL2 = url2.URL;
11163
- var http2 = __require("http");
11163
+ var http3 = __require("http");
11164
11164
  var https2 = __require("https");
11165
11165
  var Writable = __require("stream").Writable;
11166
11166
  var assert = __require("assert");
@@ -11646,7 +11646,7 @@ var require_follow_redirects = __commonJS({
11646
11646
  function isURL(value) {
11647
11647
  return URL2 && value instanceof URL2;
11648
11648
  }
11649
- module.exports = wrap2({ http: http2, https: https2 });
11649
+ module.exports = wrap2({ http: http3, https: https2 });
11650
11650
  module.exports.wrap = wrap2;
11651
11651
  }
11652
11652
  });
@@ -15007,21 +15007,24 @@ var OpenAPISpecLoader = class {
15007
15007
  }
15008
15008
  };
15009
15009
  if (op.parameters) {
15010
+ const requiredParams = [];
15010
15011
  for (const param of op.parameters) {
15011
15012
  if ("name" in param && "in" in param) {
15012
15013
  const paramSchema = param.schema;
15013
- tool.inputSchema.properties[param.name] = {
15014
- type: paramSchema.type || "string",
15015
- description: param.description || `${param.name} parameter`
15016
- };
15014
+ if (tool.inputSchema && tool.inputSchema.properties) {
15015
+ tool.inputSchema.properties[param.name] = {
15016
+ type: paramSchema.type || "string",
15017
+ description: param.description || `${param.name} parameter`
15018
+ };
15019
+ }
15017
15020
  if (param.required === true) {
15018
- if (!tool.inputSchema.required) {
15019
- tool.inputSchema.required = [];
15020
- }
15021
- tool.inputSchema.required.push(param.name);
15021
+ requiredParams.push(param.name);
15022
15022
  }
15023
15023
  }
15024
15024
  }
15025
+ if (requiredParams.length > 0 && tool.inputSchema) {
15026
+ tool.inputSchema.required = requiredParams;
15027
+ }
15025
15028
  }
15026
15029
  tools.set(toolId, tool);
15027
15030
  }
@@ -15087,7 +15090,6 @@ var ApiClient = class {
15087
15090
  * @param headers - Optional headers to include with every request
15088
15091
  */
15089
15092
  constructor(baseUrl, headers = {}) {
15090
- this.baseUrl = baseUrl;
15091
15093
  this.headers = headers;
15092
15094
  this.axiosInstance = axios_default.create({
15093
15095
  baseURL: baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`
@@ -15159,13 +15161,18 @@ var ApiClient = class {
15159
15161
 
15160
15162
  // src/server.ts
15161
15163
  var OpenAPIServer = class {
15164
+ server;
15165
+ toolsManager;
15166
+ apiClient;
15162
15167
  constructor(config) {
15163
- this.config = config;
15164
15168
  this.server = new Server(
15165
15169
  { name: config.name, version: config.version },
15166
15170
  {
15167
15171
  capabilities: {
15168
- tools: {}
15172
+ tools: {
15173
+ list: true,
15174
+ execute: true
15175
+ }
15169
15176
  }
15170
15177
  }
15171
15178
  );
@@ -15173,9 +15180,6 @@ var OpenAPIServer = class {
15173
15180
  this.apiClient = new ApiClient(config.apiBaseUrl, config.headers);
15174
15181
  this.initializeHandlers();
15175
15182
  }
15176
- server;
15177
- toolsManager;
15178
- apiClient;
15179
15183
  /**
15180
15184
  * Initialize request handlers
15181
15185
  */
@@ -15189,7 +15193,7 @@ var OpenAPIServer = class {
15189
15193
  const { id, name, arguments: params } = request.params;
15190
15194
  console.error("Received request:", request.params);
15191
15195
  console.error("Using parameters from arguments:", params);
15192
- const idOrName = id || name;
15196
+ const idOrName = typeof id === "string" ? id : typeof name === "string" ? name : "";
15193
15197
  if (!idOrName) {
15194
15198
  throw new Error("Tool ID or name is required");
15195
15199
  }
@@ -20098,7 +20102,22 @@ function parseHeaders(headerStr) {
20098
20102
  return headers;
20099
20103
  }
20100
20104
  function loadConfig() {
20101
- const argv = yargs_default(hideBin(process.argv)).option("api-base-url", {
20105
+ const argv = yargs_default(hideBin(process.argv)).option("transport", {
20106
+ alias: "t",
20107
+ type: "string",
20108
+ choices: ["stdio", "http"],
20109
+ description: "Transport type to use (stdio or http)"
20110
+ }).option("port", {
20111
+ alias: "p",
20112
+ type: "number",
20113
+ description: "HTTP port for HTTP transport"
20114
+ }).option("host", {
20115
+ type: "string",
20116
+ description: "HTTP host for HTTP transport"
20117
+ }).option("path", {
20118
+ type: "string",
20119
+ description: "HTTP endpoint path for HTTP transport"
20120
+ }).option("api-base-url", {
20102
20121
  alias: "u",
20103
20122
  type: "string",
20104
20123
  description: "Base URL for the API"
@@ -20118,7 +20137,16 @@ function loadConfig() {
20118
20137
  alias: "v",
20119
20138
  type: "string",
20120
20139
  description: "Server version"
20121
- }).help().argv;
20140
+ }).help().parseSync();
20141
+ let transportType;
20142
+ if (argv.transport === "http" || process.env.TRANSPORT_TYPE === "http") {
20143
+ transportType = "http";
20144
+ } else {
20145
+ transportType = "stdio";
20146
+ }
20147
+ const httpPort = argv.port ?? (process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT, 10) : 3e3);
20148
+ const httpHost = argv.host || process.env.HTTP_HOST || "127.0.0.1";
20149
+ const endpointPath = argv.path || process.env.ENDPOINT_PATH || "/mcp";
20122
20150
  const apiBaseUrl = argv["api-base-url"] || process.env.API_BASE_URL;
20123
20151
  const openApiSpec = argv["openapi-spec"] || process.env.OPENAPI_SPEC_PATH;
20124
20152
  if (!apiBaseUrl) {
@@ -20133,18 +20161,582 @@ function loadConfig() {
20133
20161
  version: argv.version || process.env.SERVER_VERSION || "1.0.0",
20134
20162
  apiBaseUrl,
20135
20163
  openApiSpec,
20136
- headers
20164
+ headers,
20165
+ transportType,
20166
+ httpPort,
20167
+ httpHost,
20168
+ endpointPath
20137
20169
  };
20138
20170
  }
20139
20171
 
20172
+ // src/transport/StreamableHttpServerTransport.ts
20173
+ import {
20174
+ isInitializeRequest,
20175
+ isJSONRPCRequest,
20176
+ isJSONRPCResponse
20177
+ } from "@modelcontextprotocol/sdk/types.js";
20178
+ import * as http2 from "http";
20179
+ import { randomUUID } from "crypto";
20180
+ var StreamableHttpServerTransport = class {
20181
+ // Maps request IDs to session IDs
20182
+ /**
20183
+ * Initialize a new StreamableHttpServerTransport
20184
+ *
20185
+ * @param port HTTP port to listen on
20186
+ * @param host Host to bind to (default: 127.0.0.1)
20187
+ * @param endpointPath Endpoint path (default: /mcp)
20188
+ */
20189
+ constructor(port, host = "127.0.0.1", endpointPath = "/mcp") {
20190
+ this.port = port;
20191
+ this.host = host;
20192
+ this.endpointPath = endpointPath;
20193
+ this.server = http2.createServer(this.handleRequest.bind(this));
20194
+ }
20195
+ server;
20196
+ sessions = /* @__PURE__ */ new Map();
20197
+ started = false;
20198
+ maxBodySize = 4 * 1024 * 1024;
20199
+ // 4MB max request size
20200
+ requestSessionMap = /* @__PURE__ */ new Map();
20201
+ /**
20202
+ * Callback when message is received
20203
+ */
20204
+ onmessage;
20205
+ /**
20206
+ * Callback when error occurs
20207
+ */
20208
+ onerror;
20209
+ /**
20210
+ * Callback when transport closes
20211
+ */
20212
+ onclose;
20213
+ /**
20214
+ * Start the transport
20215
+ */
20216
+ async start() {
20217
+ if (this.started) {
20218
+ throw new Error("Transport already started");
20219
+ }
20220
+ return new Promise((resolve5, reject) => {
20221
+ this.server.listen(this.port, this.host, () => {
20222
+ this.started = true;
20223
+ console.error(
20224
+ `Streamable HTTP transport listening on http://${this.host}:${this.port}${this.endpointPath}`
20225
+ );
20226
+ resolve5();
20227
+ });
20228
+ this.server.on("error", (err) => {
20229
+ reject(err);
20230
+ if (this.onerror) {
20231
+ this.onerror(err);
20232
+ }
20233
+ });
20234
+ });
20235
+ }
20236
+ /**
20237
+ * Close the transport
20238
+ */
20239
+ async close() {
20240
+ for (const session of this.sessions.values()) {
20241
+ for (const response of session.activeResponses) {
20242
+ try {
20243
+ response.end();
20244
+ } catch (err) {
20245
+ }
20246
+ }
20247
+ }
20248
+ this.sessions.clear();
20249
+ return new Promise((resolve5, reject) => {
20250
+ this.server.close((err) => {
20251
+ if (err) {
20252
+ reject(err instanceof Error ? err : new Error(String(err)));
20253
+ } else {
20254
+ this.started = false;
20255
+ if (this.onclose) {
20256
+ this.onclose();
20257
+ }
20258
+ resolve5();
20259
+ }
20260
+ });
20261
+ });
20262
+ }
20263
+ /**
20264
+ * Send message to client(s)
20265
+ *
20266
+ * @param message JSON-RPC message
20267
+ */
20268
+ async send(message) {
20269
+ console.error(`StreamableHttpServerTransport: Sending message: ${JSON.stringify(message)}`);
20270
+ let targetSessionId;
20271
+ let messageIdForThisResponse = null;
20272
+ if (isJSONRPCResponse(message) && message.id !== null) {
20273
+ messageIdForThisResponse = message.id;
20274
+ targetSessionId = this.requestSessionMap.get(messageIdForThisResponse);
20275
+ console.error(
20276
+ `StreamableHttpServerTransport: Potential target session for response ID ${messageIdForThisResponse}: ${targetSessionId}`
20277
+ );
20278
+ if (targetSessionId && this.initResponseHandlers.has(targetSessionId)) {
20279
+ console.error(
20280
+ `StreamableHttpServerTransport: Session ${targetSessionId} has initResponseHandlers. Invoking them for message ID ${messageIdForThisResponse}.`
20281
+ );
20282
+ const handlers = this.initResponseHandlers.get(targetSessionId);
20283
+ [...handlers].forEach((handler) => handler(message));
20284
+ if (!this.requestSessionMap.has(messageIdForThisResponse)) {
20285
+ console.error(
20286
+ `StreamableHttpServerTransport: Response for ID ${messageIdForThisResponse} was handled by an initResponseHandler (e.g., synchronous POST response for initialize or tools/list).`
20287
+ );
20288
+ return;
20289
+ } else {
20290
+ console.error(
20291
+ `StreamableHttpServerTransport: Response for ID ${messageIdForThisResponse} was NOT exclusively handled by an initResponseHandler or handler did not remove from requestSessionMap. Proceeding to GET stream / broadcast if applicable.`
20292
+ );
20293
+ }
20294
+ }
20295
+ if (this.requestSessionMap.has(messageIdForThisResponse)) {
20296
+ console.error(
20297
+ `StreamableHttpServerTransport: Deleting request ID ${messageIdForThisResponse} from requestSessionMap as it's being processed for GET stream or broadcast.`
20298
+ );
20299
+ this.requestSessionMap.delete(messageIdForThisResponse);
20300
+ }
20301
+ }
20302
+ if (!targetSessionId) {
20303
+ const idForLog = messageIdForThisResponse !== null ? messageIdForThisResponse : isJSONRPCRequest(message) ? message.id : "N/A";
20304
+ console.warn(
20305
+ `StreamableHttpServerTransport: No specific target session for message (ID: ${idForLog}). Broadcasting to all applicable sessions.`
20306
+ );
20307
+ for (const [sid, session2] of this.sessions.entries()) {
20308
+ if (session2.initialized && session2.activeResponses.size > 0) {
20309
+ this.sendMessageToSession(sid, session2, message);
20310
+ }
20311
+ }
20312
+ return;
20313
+ }
20314
+ const session = this.sessions.get(targetSessionId);
20315
+ if (session && session.activeResponses.size > 0) {
20316
+ console.error(
20317
+ `StreamableHttpServerTransport: Sending message (ID: ${messageIdForThisResponse}) to GET stream for session ${targetSessionId} (${session.activeResponses.size} active connections).`
20318
+ );
20319
+ this.sendMessageToSession(targetSessionId, session, message);
20320
+ } else if (targetSessionId) {
20321
+ console.error(
20322
+ `StreamableHttpServerTransport: No active GET connections for session ${targetSessionId} to send message (ID: ${messageIdForThisResponse}). Message might not be delivered if not handled by POST.`
20323
+ );
20324
+ }
20325
+ }
20326
+ /**
20327
+ * Helper method to send a message to a specific session
20328
+ */
20329
+ sendMessageToSession(sessionId, session, message) {
20330
+ const messageStr = `data: ${JSON.stringify(message)}
20331
+
20332
+ `;
20333
+ for (const response of session.activeResponses) {
20334
+ try {
20335
+ response.write(messageStr);
20336
+ } catch (err) {
20337
+ session.activeResponses.delete(response);
20338
+ if (this.onerror) {
20339
+ this.onerror(new Error(`Failed to write to response: ${err.message}`));
20340
+ }
20341
+ }
20342
+ }
20343
+ }
20344
+ /**
20345
+ * Handle HTTP request
20346
+ */
20347
+ handleRequest(req, res) {
20348
+ if (req.url !== this.endpointPath) {
20349
+ res.writeHead(404);
20350
+ res.end();
20351
+ return;
20352
+ }
20353
+ this.validateOrigin(req, res);
20354
+ switch (req.method) {
20355
+ case "POST":
20356
+ this.handlePostRequest(req, res);
20357
+ break;
20358
+ case "GET":
20359
+ this.handleGetRequest(req, res);
20360
+ break;
20361
+ case "DELETE":
20362
+ this.handleDeleteRequest(req, res);
20363
+ break;
20364
+ default:
20365
+ res.writeHead(405, { Allow: "POST, GET, DELETE" });
20366
+ res.end(
20367
+ JSON.stringify({
20368
+ jsonrpc: "2.0",
20369
+ error: {
20370
+ code: -32e3,
20371
+ message: "Method not allowed"
20372
+ },
20373
+ id: null
20374
+ })
20375
+ );
20376
+ }
20377
+ }
20378
+ /**
20379
+ * Validate origin header to prevent DNS rebinding attacks
20380
+ */
20381
+ validateOrigin(req, res) {
20382
+ const origin2 = req.headers.origin;
20383
+ if (!origin2) {
20384
+ return true;
20385
+ }
20386
+ try {
20387
+ const originUrl = new URL(origin2);
20388
+ const isLocalhost = originUrl.hostname === "localhost" || originUrl.hostname === "127.0.0.1";
20389
+ if (!isLocalhost) {
20390
+ res.writeHead(403);
20391
+ res.end(
20392
+ JSON.stringify({
20393
+ jsonrpc: "2.0",
20394
+ error: {
20395
+ code: -32e3,
20396
+ message: "Origin not allowed"
20397
+ },
20398
+ id: null
20399
+ })
20400
+ );
20401
+ return false;
20402
+ }
20403
+ return true;
20404
+ } catch (err) {
20405
+ res.writeHead(400);
20406
+ res.end(
20407
+ JSON.stringify({
20408
+ jsonrpc: "2.0",
20409
+ error: {
20410
+ code: -32e3,
20411
+ message: "Invalid origin"
20412
+ },
20413
+ id: null
20414
+ })
20415
+ );
20416
+ return false;
20417
+ }
20418
+ }
20419
+ /**
20420
+ * Handle POST request
20421
+ */
20422
+ handlePostRequest(req, res) {
20423
+ const contentType = req.headers["content-type"];
20424
+ if (!contentType || !contentType.includes("application/json")) {
20425
+ res.writeHead(415);
20426
+ res.end(
20427
+ JSON.stringify({
20428
+ jsonrpc: "2.0",
20429
+ error: {
20430
+ code: -32e3,
20431
+ message: "Unsupported Media Type: Content-Type must be application/json"
20432
+ },
20433
+ id: null
20434
+ })
20435
+ );
20436
+ return;
20437
+ }
20438
+ let body = "";
20439
+ let size = 0;
20440
+ req.on("data", (chunk) => {
20441
+ size += chunk.length;
20442
+ if (size > this.maxBodySize) {
20443
+ res.writeHead(413);
20444
+ res.end(
20445
+ JSON.stringify({
20446
+ jsonrpc: "2.0",
20447
+ error: {
20448
+ code: -32e3,
20449
+ message: "Request entity too large"
20450
+ },
20451
+ id: null
20452
+ })
20453
+ );
20454
+ req.destroy();
20455
+ return;
20456
+ }
20457
+ body += chunk.toString();
20458
+ });
20459
+ req.on("end", () => {
20460
+ try {
20461
+ const message = JSON.parse(body);
20462
+ if (isInitializeRequest(message)) {
20463
+ this.handleInitializeRequest(message, req, res);
20464
+ } else if (isJSONRPCRequest(message) && message.method === "tools/list") {
20465
+ const sessionId = req.headers["mcp-session-id"];
20466
+ if (!sessionId || !this.sessions.has(sessionId)) {
20467
+ res.writeHead(400);
20468
+ res.end(
20469
+ JSON.stringify({
20470
+ jsonrpc: "2.0",
20471
+ error: {
20472
+ code: -32e3,
20473
+ message: "Invalid session. A valid Mcp-Session-Id header is required."
20474
+ },
20475
+ id: "id" in message ? message.id : null
20476
+ })
20477
+ );
20478
+ return;
20479
+ }
20480
+ const session = this.sessions.get(sessionId);
20481
+ if (message.id !== void 0 && message.id !== null) {
20482
+ this.requestSessionMap.set(message.id, sessionId);
20483
+ if (!session.pendingRequests) {
20484
+ session.pendingRequests = /* @__PURE__ */ new Set();
20485
+ }
20486
+ session.pendingRequests.add(message.id);
20487
+ }
20488
+ const responseHandler = (responseMessage) => {
20489
+ if (isJSONRPCResponse(responseMessage) && responseMessage.id === message.id) {
20490
+ res.setHeader("Content-Type", "application/json");
20491
+ res.writeHead(200);
20492
+ res.end(JSON.stringify(responseMessage));
20493
+ this.removeInitResponseHandler(sessionId, responseHandler);
20494
+ if (message.id !== void 0 && message.id !== null) {
20495
+ this.requestSessionMap.delete(message.id);
20496
+ session.pendingRequests.delete(message.id);
20497
+ }
20498
+ }
20499
+ };
20500
+ this.addInitResponseHandler(sessionId, responseHandler);
20501
+ if (session.messageHandler) {
20502
+ session.messageHandler(message);
20503
+ } else {
20504
+ this.removeInitResponseHandler(sessionId, responseHandler);
20505
+ res.writeHead(500);
20506
+ res.end(
20507
+ JSON.stringify({
20508
+ jsonrpc: "2.0",
20509
+ error: {
20510
+ code: -32603,
20511
+ message: "Internal error: No message handler available"
20512
+ },
20513
+ id: "id" in message ? message.id : null
20514
+ })
20515
+ );
20516
+ }
20517
+ } else {
20518
+ const sessionId = req.headers["mcp-session-id"];
20519
+ if (!sessionId || !this.sessions.has(sessionId)) {
20520
+ res.writeHead(400);
20521
+ res.end(
20522
+ JSON.stringify({
20523
+ jsonrpc: "2.0",
20524
+ error: {
20525
+ code: -32e3,
20526
+ message: "Invalid session. A valid Mcp-Session-Id header is required."
20527
+ },
20528
+ id: "id" in message ? message.id : null
20529
+ })
20530
+ );
20531
+ return;
20532
+ }
20533
+ const session = this.sessions.get(sessionId);
20534
+ if (isJSONRPCRequest(message)) {
20535
+ if (session.messageHandler) {
20536
+ if (message.id !== void 0 && message.id !== null) {
20537
+ this.requestSessionMap.set(message.id, sessionId);
20538
+ if (!session.pendingRequests) {
20539
+ session.pendingRequests = /* @__PURE__ */ new Set();
20540
+ }
20541
+ session.pendingRequests.add(message.id);
20542
+ }
20543
+ session.messageHandler(message);
20544
+ res.writeHead(202);
20545
+ res.end();
20546
+ }
20547
+ } else {
20548
+ if (session.messageHandler) {
20549
+ session.messageHandler(message);
20550
+ res.writeHead(202);
20551
+ res.end();
20552
+ }
20553
+ }
20554
+ }
20555
+ } catch (err) {
20556
+ res.writeHead(400);
20557
+ res.end(
20558
+ JSON.stringify({
20559
+ jsonrpc: "2.0",
20560
+ error: {
20561
+ code: -32700,
20562
+ message: "Parse error",
20563
+ data: String(err)
20564
+ },
20565
+ id: null
20566
+ })
20567
+ );
20568
+ if (this.onerror) {
20569
+ this.onerror(new Error(`Parse error: ${String(err)}`));
20570
+ }
20571
+ }
20572
+ });
20573
+ req.on("error", (err) => {
20574
+ if (this.onerror) {
20575
+ this.onerror(err instanceof Error ? err : new Error(String(err)));
20576
+ }
20577
+ });
20578
+ }
20579
+ /**
20580
+ * Handle initialization request
20581
+ */
20582
+ handleInitializeRequest(message, _req, res) {
20583
+ const sessionId = randomUUID();
20584
+ this.sessions.set(sessionId, {
20585
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
20586
+ messageHandler: this.onmessage || (() => {
20587
+ }),
20588
+ activeResponses: /* @__PURE__ */ new Set(),
20589
+ initialized: true,
20590
+ pendingRequests: /* @__PURE__ */ new Set()
20591
+ });
20592
+ if ("id" in message && message.id !== null && message.id !== void 0) {
20593
+ this.requestSessionMap.set(message.id, sessionId);
20594
+ }
20595
+ res.setHeader("Content-Type", "application/json");
20596
+ res.setHeader("Mcp-Session-Id", sessionId);
20597
+ const responseHandler = (responseMessage) => {
20598
+ if (isJSONRPCResponse(responseMessage) && "id" in message && responseMessage.id === message.id) {
20599
+ res.writeHead(200);
20600
+ res.end(JSON.stringify(responseMessage));
20601
+ this.removeInitResponseHandler(sessionId, responseHandler);
20602
+ this.requestSessionMap.delete(message.id);
20603
+ }
20604
+ };
20605
+ this.addInitResponseHandler(sessionId, responseHandler);
20606
+ if (this.onmessage) {
20607
+ this.onmessage(message);
20608
+ } else {
20609
+ this.removeInitResponseHandler(sessionId, responseHandler);
20610
+ res.writeHead(500);
20611
+ res.end(
20612
+ JSON.stringify({
20613
+ jsonrpc: "2.0",
20614
+ error: {
20615
+ code: -32603,
20616
+ message: "Internal error: No message handler available"
20617
+ },
20618
+ id: "id" in message ? message.id : null
20619
+ })
20620
+ );
20621
+ }
20622
+ }
20623
+ /**
20624
+ * Add initialize response handler
20625
+ */
20626
+ initResponseHandlers = /* @__PURE__ */ new Map();
20627
+ addInitResponseHandler(sessionId, handler) {
20628
+ if (!this.initResponseHandlers.has(sessionId)) {
20629
+ this.initResponseHandlers.set(sessionId, []);
20630
+ }
20631
+ this.initResponseHandlers.get(sessionId).push(handler);
20632
+ }
20633
+ removeInitResponseHandler(sessionId, handler) {
20634
+ if (this.initResponseHandlers.has(sessionId)) {
20635
+ const handlers = this.initResponseHandlers.get(sessionId);
20636
+ const index = handlers.indexOf(handler);
20637
+ if (index !== -1) {
20638
+ handlers.splice(index, 1);
20639
+ }
20640
+ if (handlers.length === 0) {
20641
+ this.initResponseHandlers.delete(sessionId);
20642
+ }
20643
+ }
20644
+ }
20645
+ /**
20646
+ * Handle GET request (streaming connection)
20647
+ */
20648
+ handleGetRequest(req, res) {
20649
+ const sessionId = req.headers["mcp-session-id"];
20650
+ if (!sessionId || !this.sessions.has(sessionId)) {
20651
+ res.writeHead(400);
20652
+ res.end(
20653
+ JSON.stringify({
20654
+ jsonrpc: "2.0",
20655
+ error: {
20656
+ code: -32e3,
20657
+ message: "Invalid session. A valid Mcp-Session-Id header is required."
20658
+ },
20659
+ id: null
20660
+ })
20661
+ );
20662
+ return;
20663
+ }
20664
+ const session = this.sessions.get(sessionId);
20665
+ res.setHeader("Content-Type", "text/event-stream");
20666
+ res.setHeader("Connection", "keep-alive");
20667
+ res.setHeader("Cache-Control", "no-cache, no-transform");
20668
+ res.setHeader("Transfer-Encoding", "chunked");
20669
+ res.setHeader("Mcp-Session-Id", sessionId);
20670
+ res.writeHead(200);
20671
+ session.activeResponses.add(res);
20672
+ req.on("close", () => {
20673
+ session.activeResponses.delete(res);
20674
+ });
20675
+ res.on("error", (err) => {
20676
+ session.activeResponses.delete(res);
20677
+ if (this.onerror) {
20678
+ this.onerror(err);
20679
+ }
20680
+ });
20681
+ }
20682
+ /**
20683
+ * Handle DELETE request (session termination)
20684
+ */
20685
+ handleDeleteRequest(req, res) {
20686
+ const sessionId = req.headers["mcp-session-id"];
20687
+ if (!sessionId || !this.sessions.has(sessionId)) {
20688
+ res.writeHead(400);
20689
+ res.end(
20690
+ JSON.stringify({
20691
+ jsonrpc: "2.0",
20692
+ error: {
20693
+ code: -32e3,
20694
+ message: "Invalid session. A valid Mcp-Session-Id header is required."
20695
+ },
20696
+ id: null
20697
+ })
20698
+ );
20699
+ return;
20700
+ }
20701
+ const session = this.sessions.get(sessionId);
20702
+ for (const response of session.activeResponses) {
20703
+ try {
20704
+ response.end();
20705
+ } catch (err) {
20706
+ }
20707
+ }
20708
+ if (session.pendingRequests) {
20709
+ for (const requestId of session.pendingRequests) {
20710
+ this.requestSessionMap.delete(requestId);
20711
+ }
20712
+ }
20713
+ this.sessions.delete(sessionId);
20714
+ res.writeHead(204);
20715
+ res.end();
20716
+ }
20717
+ };
20718
+
20140
20719
  // src/index.ts
20141
20720
  async function main() {
20142
20721
  try {
20143
20722
  const config = loadConfig();
20144
20723
  const server = new OpenAPIServer(config);
20145
- const transport = new StdioServerTransport();
20146
- await server.start(transport);
20147
- console.error("OpenAPI MCP Server running on stdio");
20724
+ let transport;
20725
+ if (config.transportType === "http") {
20726
+ transport = new StreamableHttpServerTransport(
20727
+ config.httpPort,
20728
+ config.httpHost,
20729
+ config.endpointPath
20730
+ );
20731
+ await server.start(transport);
20732
+ console.error(
20733
+ `OpenAPI MCP Server running on http://${config.httpHost}:${config.httpPort}${config.endpointPath}`
20734
+ );
20735
+ } else {
20736
+ transport = new StdioServerTransport();
20737
+ await server.start(transport);
20738
+ console.error("OpenAPI MCP Server running on stdio");
20739
+ }
20148
20740
  } catch (error) {
20149
20741
  console.error("Failed to start server:", error);
20150
20742
  process.exit(1);
@@ -20155,6 +20747,7 @@ export {
20155
20747
  ApiClient,
20156
20748
  OpenAPIServer,
20157
20749
  OpenAPISpecLoader,
20750
+ StreamableHttpServerTransport,
20158
20751
  ToolsManager,
20159
20752
  loadConfig,
20160
20753
  parseHeaders
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ivotoby/openapi-mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "An MCP server that exposes OpenAPI endpoints as resources",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,10 +44,8 @@
44
44
  "@semantic-release/github": "^9.2.6",
45
45
  "@semantic-release/npm": "^11.0.3",
46
46
  "@semantic-release/release-notes-generator": "^12.1.0",
47
- "@types/chai": "^4.3.11",
48
- "@types/mocha": "^10.0.6",
49
47
  "@types/node": "^22.13.11",
50
- "@types/sinon": "^17.0.3",
48
+ "@types/yargs": "^17.0.33",
51
49
  "@typescript-eslint/eslint-plugin": "^6.12.0",
52
50
  "@typescript-eslint/parser": "^6.12.0",
53
51
  "dotenv": "^16.4.7",
@@ -56,13 +54,10 @@
56
54
  "eslint-config-prettier": "^10.1.5",
57
55
  "eslint-plugin-perfectionist": "^4.7.0",
58
56
  "eslint-plugin-prettier": "^5.4.0",
59
- "jest": "^29.7.0",
60
57
  "msw": "^2.7.0",
61
58
  "nodemon": "^3.1.7",
62
59
  "prettier": "^3.4.2",
63
60
  "semantic-release": "^22.0.12",
64
- "sinon": "^17.0.1",
65
- "ts-jest": "^29.1.1",
66
61
  "typescript": "^5.3.2",
67
62
  "typescript-eslint": "^8.22.0",
68
63
  "vitest": "^3.1.3"