@ivotoby/openapi-mcp-server 1.2.1 → 1.2.2

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 +82 -0
  2. package/dist/bundle.js +235 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -125,6 +125,7 @@ The server can be configured through environment variables or command line argum
125
125
  - `HTTP_PORT` - Port for HTTP transport (default: 3000)
126
126
  - `HTTP_HOST` - Host for HTTP transport (default: "127.0.0.1")
127
127
  - `ENDPOINT_PATH` - Endpoint path for HTTP transport (default: "/mcp")
128
+ - `TOOLS_MODE` - Tools loading mode: "all" (load all endpoint-based tools) or "dynamic" (load only meta-tools) (default: "all")
128
129
 
129
130
  ### Command Line Arguments
130
131
 
@@ -141,6 +142,61 @@ npx @ivotoby/openapi-mcp-server \
141
142
  --path /mcp
142
143
  ```
143
144
 
145
+ ### OpenAPI Schema Processing
146
+
147
+ #### Reference Resolution
148
+
149
+ This MCP server implements robust OpenAPI reference (`$ref`) resolution to ensure accurate representation of API schemas:
150
+
151
+ - **Parameter References**: Fully resolves `$ref` pointers to parameter components in the OpenAPI spec
152
+ - **Schema References**: Handles nested schema references within parameters and request bodies
153
+ - **Recursive References**: Prevents infinite loops by detecting and handling circular references
154
+ - **Nested Properties**: Preserves complex nested object and array structures with all their attributes
155
+
156
+ ### Input Schema Composition
157
+
158
+ The server intelligently merges parameters and request bodies into a unified input schema for each tool:
159
+
160
+ - **Parameters + Request Body Merging**: Combines path, query, and body parameters into a single schema
161
+ - **Collision Handling**: Resolves naming conflicts by prefixing body properties that conflict with parameter names
162
+ - **Type Preservation**: Maintains the original type information for all schema elements
163
+ - **Metadata Retention**: Preserves descriptions, formats, defaults, enums, and other schema attributes
164
+
165
+ ### Complex Schema Support
166
+
167
+ The MCP server handles various OpenAPI schema complexities:
168
+
169
+ - **Primitive Type Bodies**: Wraps non-object request bodies in a "body" property
170
+ - **Object Bodies**: Flattens object properties into the tool's input schema
171
+ - **Array Bodies**: Properly handles array schemas with their nested item definitions
172
+ - **Required Properties**: Tracks and preserves which parameters and properties are required
173
+
174
+ ## Tool Loading & Filtering Options
175
+
176
+ Based on the Stainless article "What We Learned Converting Complex OpenAPI Specs to MCP Servers" (https://www.stainless.com/blog/what-we-learned-converting-complex-openapi-specs-to-mcp-servers), the following flags were added to control which API endpoints (tools) are loaded:
177
+
178
+ - `--tools <all|dynamic>`: Choose to load all tools (default) or only dynamic meta-tools (`list-api-endpoints`, `get-api-endpoint-schema`, `invoke-api-endpoint`).
179
+ - `--tool <toolId>`: Import only specified tool IDs or names. Can be used multiple times.
180
+ - `--tag <tag>`: Import only tools with the specified OpenAPI tag. Can be used multiple times.
181
+ - `--resource <resource>`: Import only tools under the specified resource path prefixes. Can be used multiple times.
182
+ - `--operation <method>`: Import only tools for the specified HTTP methods (get, post, etc). Can be used multiple times.
183
+
184
+ **Examples:**
185
+
186
+ ```bash
187
+ # Load only dynamic meta-tools
188
+ npx @ivotoby/openapi-mcp-server --api-base-url https://api.example.com --openapi-spec https://api.example.com/openapi.json --tools dynamic
189
+
190
+ # Load only the GET /users endpoint tool
191
+ npx @ivotoby/openapi-mcp-server --api-base-url https://api.example.com --openapi-spec https://api.example.com/openapi.json --tool GET-users
192
+
193
+ # Load tools tagged with "user" under the "/users" resource
194
+ npx @ivotoby/openapi-mcp-server --api-base-url https://api.example.com --openapi-spec https://api.example.com/openapi.json --tag user --resource users
195
+
196
+ # Load only POST operations
197
+ npx @ivotoby/openapi-mcp-server --api-base-url https://api.example.com --openapi-spec https://api.example.com/openapi.json --operation post
198
+ ```
199
+
144
200
  ## Security Considerations
145
201
 
146
202
  - The HTTP transport validates Origin headers to prevent DNS rebinding attacks
@@ -187,6 +243,32 @@ To see debug logs:
187
243
  4. Run tests and linting: `npm run typecheck && npm run lint`
188
244
  5. Submit a pull request
189
245
 
246
+ ## FAQ
247
+
248
+ **Q: What is a "tool"?**
249
+ A: A tool corresponds to a single API endpoint derived from your OpenAPI specification, exposed as an MCP resource.
250
+
251
+ **Q: How do I filter which tools are loaded?**
252
+ A: Use the `--tool`, `--tag`, `--resource`, and `--operation` flags, or set `TOOLS_MODE=dynamic` for meta-tools only.
253
+
254
+ **Q: When should I use dynamic mode?**
255
+ A: Dynamic mode provides meta-tools (`list-api-endpoints`, `get-api-endpoint-schema`, `invoke-api-endpoint`) to inspect and interact with endpoints without preloading all operations, which is useful for large or changing APIs.
256
+
257
+ **Q: How do I specify custom headers for API requests?**
258
+ A: Use the `--headers` flag or `API_HEADERS` environment variable with `key:value` pairs separated by commas.
259
+
260
+ **Q: Which transport methods are supported?**
261
+ A: The server supports stdio transport (default) for integration with AI systems and HTTP transport (with streaming via SSE) for web clients.
262
+
263
+ **Q: How does the server handle complex OpenAPI schemas with references?**
264
+ A: The server fully resolves `$ref` references in parameters and schemas, preserving nested structures, default values, and other attributes. See the "OpenAPI Schema Processing" section for details on reference resolution and schema composition.
265
+
266
+ **Q: What happens when parameter names conflict with request body properties?**
267
+ A: The server detects naming conflicts and automatically prefixes body property names with `body_` to avoid collisions, ensuring all properties are accessible.
268
+
269
+ **Q: Where can I find development and contribution guidelines?**
270
+ A: See the "For Developers" section above for commands (`npm run build`, `npm run dev`, etc) and pull request workflow.
271
+
190
272
  ## License
191
273
 
192
274
  MIT
package/dist/bundle.js CHANGED
@@ -14399,6 +14399,50 @@ var OpenAPISpecLoader = class {
14399
14399
  }
14400
14400
  }
14401
14401
  }
14402
+ /**
14403
+ * Inline `$ref` schemas from components and drop recursive cycles
14404
+ */
14405
+ inlineSchema(schema2, components, visited) {
14406
+ if ("$ref" in schema2 && typeof schema2.$ref === "string") {
14407
+ const ref = schema2.$ref;
14408
+ const match = ref.match(/^#\/components\/schemas\/(.+)$/);
14409
+ if (match && components) {
14410
+ const name = match[1];
14411
+ if (visited.has(name)) {
14412
+ return {};
14413
+ }
14414
+ const comp = components[name];
14415
+ if (!comp) {
14416
+ return {};
14417
+ }
14418
+ visited.add(name);
14419
+ return this.inlineSchema(comp, components, visited);
14420
+ }
14421
+ }
14422
+ const schemaObj = schema2;
14423
+ if (schemaObj.type === "object" && schemaObj.properties) {
14424
+ const newProps = {};
14425
+ for (const [propName, propSchema] of Object.entries(schemaObj.properties)) {
14426
+ newProps[propName] = this.inlineSchema(
14427
+ propSchema,
14428
+ components,
14429
+ new Set(visited)
14430
+ );
14431
+ }
14432
+ return { ...schemaObj, properties: newProps };
14433
+ }
14434
+ if (schemaObj.type === "array" && schemaObj.items) {
14435
+ return {
14436
+ ...schemaObj,
14437
+ items: this.inlineSchema(
14438
+ schemaObj.items,
14439
+ components,
14440
+ new Set(visited)
14441
+ )
14442
+ };
14443
+ }
14444
+ return schemaObj;
14445
+ }
14402
14446
  /**
14403
14447
  * Parse an OpenAPI specification into a map of tools
14404
14448
  */
@@ -14408,8 +14452,7 @@ var OpenAPISpecLoader = class {
14408
14452
  if (!pathItem) continue;
14409
14453
  for (const [method, operation] of Object.entries(pathItem)) {
14410
14454
  if (method === "parameters" || !operation) continue;
14411
- if (!["get", "post", "put", "patch", "delete", "options", "head"].includes(method.toLowerCase())) {
14412
- console.log(`Skipping non-HTTP method "${method}" for path ${path}`);
14455
+ if (!["get", "put", "post", "delete", "options", "head", "patch", "trace"].includes(method)) {
14413
14456
  continue;
14414
14457
  }
14415
14458
  const op = operation;
@@ -14425,26 +14468,84 @@ var OpenAPISpecLoader = class {
14425
14468
  properties: {}
14426
14469
  }
14427
14470
  };
14471
+ const requiredParams = [];
14428
14472
  if (op.parameters) {
14429
- const requiredParams = [];
14430
14473
  for (const param of op.parameters) {
14431
- if ("name" in param && "in" in param) {
14432
- const paramSchema = param.schema;
14433
- if (tool.inputSchema && tool.inputSchema.properties) {
14434
- tool.inputSchema.properties[param.name] = {
14435
- type: paramSchema.type || "string",
14436
- description: param.description || `${param.name} parameter`
14437
- };
14474
+ let paramObj;
14475
+ if (!("name" in param)) {
14476
+ if ("$ref" in param && typeof param.$ref === "string") {
14477
+ const refMatch = param.$ref.match(/^#\/components\/parameters\/(.+)$/);
14478
+ if (refMatch && spec.components?.parameters) {
14479
+ const paramName = refMatch[1];
14480
+ const resolvedParam = spec.components.parameters[paramName];
14481
+ if (!resolvedParam || !("name" in resolvedParam)) continue;
14482
+ paramObj = resolvedParam;
14483
+ } else {
14484
+ continue;
14485
+ }
14486
+ } else {
14487
+ continue;
14488
+ }
14489
+ } else {
14490
+ paramObj = param;
14491
+ }
14492
+ if (paramObj.schema) {
14493
+ const paramSchema = this.inlineSchema(
14494
+ paramObj.schema,
14495
+ spec.components?.schemas,
14496
+ /* @__PURE__ */ new Set()
14497
+ );
14498
+ const paramDef = {
14499
+ description: paramObj.description || `${paramObj.name} parameter`
14500
+ };
14501
+ if (paramSchema.type) {
14502
+ paramDef.type = paramSchema.type;
14503
+ } else {
14504
+ paramDef.type = "string";
14438
14505
  }
14439
- if (param.required === true) {
14440
- requiredParams.push(param.name);
14506
+ for (const [key, value] of Object.entries(paramSchema)) {
14507
+ if (key === "type" || key === "description") continue;
14508
+ paramDef[key] = value;
14509
+ }
14510
+ tool.inputSchema.properties[paramObj.name] = paramDef;
14511
+ if (paramObj.required === true) {
14512
+ requiredParams.push(paramObj.name);
14441
14513
  }
14442
14514
  }
14443
14515
  }
14444
- if (requiredParams.length > 0 && tool.inputSchema) {
14445
- tool.inputSchema.required = requiredParams;
14516
+ }
14517
+ if (op.requestBody && "content" in op.requestBody) {
14518
+ const requestBodyObj = op.requestBody;
14519
+ let mediaTypeObj;
14520
+ if (requestBodyObj.content["application/json"]) {
14521
+ mediaTypeObj = requestBodyObj.content["application/json"];
14522
+ } else if (Object.keys(requestBodyObj.content).length > 0) {
14523
+ const firstContentType = Object.keys(requestBodyObj.content)[0];
14524
+ mediaTypeObj = requestBodyObj.content[firstContentType];
14525
+ }
14526
+ if (mediaTypeObj?.schema) {
14527
+ const inlinedSchema = this.inlineSchema(
14528
+ mediaTypeObj.schema,
14529
+ spec.components?.schemas,
14530
+ /* @__PURE__ */ new Set()
14531
+ );
14532
+ if (inlinedSchema.type === "object" && inlinedSchema.properties) {
14533
+ for (const [propName, propSchema] of Object.entries(inlinedSchema.properties)) {
14534
+ const paramName = tool.inputSchema.properties[propName] ? `body_${propName}` : propName;
14535
+ tool.inputSchema.properties[paramName] = propSchema;
14536
+ if (inlinedSchema.required && inlinedSchema.required.includes(propName)) {
14537
+ requiredParams.push(paramName);
14538
+ }
14539
+ }
14540
+ } else {
14541
+ tool.inputSchema.properties["body"] = inlinedSchema;
14542
+ requiredParams.push("body");
14543
+ }
14446
14544
  }
14447
14545
  }
14546
+ if (requiredParams.length > 0) {
14547
+ tool.inputSchema.required = requiredParams;
14548
+ }
14448
14549
  tools.set(toolId, tool);
14449
14550
  }
14450
14551
  }
@@ -14571,16 +14672,103 @@ var OpenAPISpecLoader = class {
14571
14672
  var ToolsManager = class {
14572
14673
  constructor(config) {
14573
14674
  this.config = config;
14675
+ this.config.toolsMode = this.config.toolsMode || "all";
14574
14676
  this.specLoader = new OpenAPISpecLoader();
14575
14677
  }
14576
14678
  tools = /* @__PURE__ */ new Map();
14577
14679
  specLoader;
14680
+ /**
14681
+ * Create dynamic discovery meta-tools
14682
+ */
14683
+ createDynamicTools() {
14684
+ const dynamicTools = /* @__PURE__ */ new Map();
14685
+ dynamicTools.set("LIST-API-ENDPOINTS", {
14686
+ name: "list-api-endpoints",
14687
+ description: "List all available API endpoints",
14688
+ inputSchema: { type: "object", properties: {} }
14689
+ });
14690
+ dynamicTools.set("GET-API-ENDPOINT-SCHEMA", {
14691
+ name: "get-api-endpoint-schema",
14692
+ description: "Get the JSON schema for a specified API endpoint",
14693
+ inputSchema: {
14694
+ type: "object",
14695
+ properties: {
14696
+ endpoint: { type: "string", description: "Endpoint path (e.g. /users/{id})" }
14697
+ },
14698
+ required: ["endpoint"]
14699
+ }
14700
+ });
14701
+ dynamicTools.set("INVOKE-API-ENDPOINT", {
14702
+ name: "invoke-api-endpoint",
14703
+ description: "Invoke an API endpoint with provided parameters",
14704
+ inputSchema: {
14705
+ type: "object",
14706
+ properties: {
14707
+ endpoint: { type: "string", description: "Endpoint path to invoke" },
14708
+ params: {
14709
+ type: "object",
14710
+ description: "Parameters for the API call",
14711
+ properties: {}
14712
+ }
14713
+ },
14714
+ required: ["endpoint"]
14715
+ }
14716
+ });
14717
+ return dynamicTools;
14718
+ }
14578
14719
  /**
14579
14720
  * Initialize tools from the OpenAPI specification
14580
14721
  */
14581
14722
  async initialize() {
14582
14723
  const spec = await this.specLoader.loadOpenAPISpec(this.config.openApiSpec);
14583
- this.tools = this.specLoader.parseOpenAPISpec(spec);
14724
+ if (this.config.toolsMode === "dynamic") {
14725
+ this.tools = this.createDynamicTools();
14726
+ return;
14727
+ }
14728
+ const rawTools = this.specLoader.parseOpenAPISpec(spec);
14729
+ const filtered = /* @__PURE__ */ new Map();
14730
+ const includeToolsLower = this.config.includeTools?.map((t) => t.toLowerCase()) || [];
14731
+ const includeOperationsLower = this.config.includeOperations?.map((op) => op.toLowerCase()) || [];
14732
+ const includeResourcesLower = this.config.includeResources || [];
14733
+ const includeTagsLower = this.config.includeTags?.map((tag) => tag.toLowerCase()) || [];
14734
+ const resourcePathsLower = includeResourcesLower.map((res) => ({
14735
+ exact: `/${res}`.toLowerCase(),
14736
+ prefix: `/${res}/`.toLowerCase()
14737
+ }));
14738
+ for (const [toolId, tool] of rawTools.entries()) {
14739
+ if (includeToolsLower.length > 0) {
14740
+ const toolIdLower = toolId.toLowerCase();
14741
+ const toolNameLower = tool.name.toLowerCase();
14742
+ if (!includeToolsLower.includes(toolIdLower) && !includeToolsLower.includes(toolNameLower)) {
14743
+ continue;
14744
+ }
14745
+ }
14746
+ if (includeOperationsLower.length > 0) {
14747
+ const { method } = this.parseToolId(toolId);
14748
+ if (!includeOperationsLower.includes(method.toLowerCase())) {
14749
+ continue;
14750
+ }
14751
+ }
14752
+ if (resourcePathsLower.length > 0) {
14753
+ const { path } = this.parseToolId(toolId);
14754
+ const pathLower = path.toLowerCase();
14755
+ const match = resourcePathsLower.some(
14756
+ (res) => pathLower === res.exact || pathLower.startsWith(res.prefix)
14757
+ );
14758
+ if (!match) continue;
14759
+ }
14760
+ if (includeTagsLower.length > 0) {
14761
+ const { method, path } = this.parseToolId(toolId);
14762
+ const methodLower = method.toLowerCase();
14763
+ const pathItem = spec.paths[path];
14764
+ if (!pathItem) continue;
14765
+ const opObj = pathItem[methodLower];
14766
+ const tags = Array.isArray(opObj?.tags) ? opObj.tags : [];
14767
+ if (!tags.some((tag) => includeTagsLower.includes(tag.toLowerCase()))) continue;
14768
+ }
14769
+ filtered.set(toolId, tool);
14770
+ }
14771
+ this.tools = filtered;
14584
14772
  for (const [toolId, tool] of this.tools.entries()) {
14585
14773
  console.error(`Registered tool: ${toolId} (${tool.name})`);
14586
14774
  }
@@ -14595,11 +14783,14 @@ var ToolsManager = class {
14595
14783
  * Find a tool by ID or name
14596
14784
  */
14597
14785
  findTool(idOrName) {
14598
- if (this.tools.has(idOrName)) {
14599
- return { toolId: idOrName, tool: this.tools.get(idOrName) };
14786
+ const lowerIdOrName = idOrName.toLowerCase();
14787
+ for (const [toolId, tool] of this.tools.entries()) {
14788
+ if (toolId.toLowerCase() === lowerIdOrName) {
14789
+ return { toolId, tool };
14790
+ }
14600
14791
  }
14601
14792
  for (const [toolId, tool] of this.tools.entries()) {
14602
- if (tool.name === idOrName) {
14793
+ if (tool.name.toLowerCase() === lowerIdOrName) {
14603
14794
  return { toolId, tool };
14604
14795
  }
14605
14796
  }
@@ -22980,6 +23171,26 @@ function loadConfig() {
22980
23171
  alias: "v",
22981
23172
  type: "string",
22982
23173
  description: "Server version"
23174
+ }).option("tools", {
23175
+ type: "string",
23176
+ choices: ["all", "dynamic"],
23177
+ description: "Which tools to load: all or dynamic meta-tools"
23178
+ }).option("tool", {
23179
+ type: "array",
23180
+ string: true,
23181
+ description: "Import only specified tool IDs or names"
23182
+ }).option("tag", {
23183
+ type: "array",
23184
+ string: true,
23185
+ description: "Import only tools with specified OpenAPI tags"
23186
+ }).option("resource", {
23187
+ type: "array",
23188
+ string: true,
23189
+ description: "Import only tools under specified resource path prefixes"
23190
+ }).option("operation", {
23191
+ type: "array",
23192
+ string: true,
23193
+ description: "Import only tools for specified HTTP methods (e.g., get, post)"
22983
23194
  }).help().parseSync();
22984
23195
  let transportType;
22985
23196
  if (argv.transport === "http" || process.env.TRANSPORT_TYPE === "http") {
@@ -23008,7 +23219,12 @@ function loadConfig() {
23008
23219
  transportType,
23009
23220
  httpPort,
23010
23221
  httpHost,
23011
- endpointPath
23222
+ endpointPath,
23223
+ includeTools: argv.tool,
23224
+ includeTags: argv.tag,
23225
+ includeResources: argv.resource,
23226
+ includeOperations: argv.operation,
23227
+ toolsMode: argv.tools || process.env.TOOLS_MODE || "all"
23012
23228
  };
23013
23229
  }
23014
23230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ivotoby/openapi-mcp-server",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "An MCP server that exposes OpenAPI endpoints as resources",
5
5
  "license": "MIT",
6
6
  "type": "module",