@ivotoby/openapi-mcp-server 1.2.1 → 1.3.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 +85 -1
  2. package/dist/bundle.js +258 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -125,6 +125,8 @@ 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")
129
+ - `DISABLE_ABBREVIATION` - Disable name optimization (this could throw errors when name is > 64 chars)
128
130
 
129
131
  ### Command Line Arguments
130
132
 
@@ -138,7 +140,63 @@ npx @ivotoby/openapi-mcp-server \
138
140
  --transport http \
139
141
  --port 3000 \
140
142
  --host 127.0.0.1 \
141
- --path /mcp
143
+ --path /mcp \
144
+ --disable-abbreviation true
145
+ ```
146
+
147
+ ### OpenAPI Schema Processing
148
+
149
+ #### Reference Resolution
150
+
151
+ This MCP server implements robust OpenAPI reference (`$ref`) resolution to ensure accurate representation of API schemas:
152
+
153
+ - **Parameter References**: Fully resolves `$ref` pointers to parameter components in the OpenAPI spec
154
+ - **Schema References**: Handles nested schema references within parameters and request bodies
155
+ - **Recursive References**: Prevents infinite loops by detecting and handling circular references
156
+ - **Nested Properties**: Preserves complex nested object and array structures with all their attributes
157
+
158
+ ### Input Schema Composition
159
+
160
+ The server intelligently merges parameters and request bodies into a unified input schema for each tool:
161
+
162
+ - **Parameters + Request Body Merging**: Combines path, query, and body parameters into a single schema
163
+ - **Collision Handling**: Resolves naming conflicts by prefixing body properties that conflict with parameter names
164
+ - **Type Preservation**: Maintains the original type information for all schema elements
165
+ - **Metadata Retention**: Preserves descriptions, formats, defaults, enums, and other schema attributes
166
+
167
+ ### Complex Schema Support
168
+
169
+ The MCP server handles various OpenAPI schema complexities:
170
+
171
+ - **Primitive Type Bodies**: Wraps non-object request bodies in a "body" property
172
+ - **Object Bodies**: Flattens object properties into the tool's input schema
173
+ - **Array Bodies**: Properly handles array schemas with their nested item definitions
174
+ - **Required Properties**: Tracks and preserves which parameters and properties are required
175
+
176
+ ## Tool Loading & Filtering Options
177
+
178
+ 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:
179
+
180
+ - `--tools <all|dynamic>`: Choose to load all tools (default) or only dynamic meta-tools (`list-api-endpoints`, `get-api-endpoint-schema`, `invoke-api-endpoint`).
181
+ - `--tool <toolId>`: Import only specified tool IDs or names. Can be used multiple times.
182
+ - `--tag <tag>`: Import only tools with the specified OpenAPI tag. Can be used multiple times.
183
+ - `--resource <resource>`: Import only tools under the specified resource path prefixes. Can be used multiple times.
184
+ - `--operation <method>`: Import only tools for the specified HTTP methods (get, post, etc). Can be used multiple times.
185
+
186
+ **Examples:**
187
+
188
+ ```bash
189
+ # Load only dynamic meta-tools
190
+ npx @ivotoby/openapi-mcp-server --api-base-url https://api.example.com --openapi-spec https://api.example.com/openapi.json --tools dynamic
191
+
192
+ # Load only the GET /users endpoint tool
193
+ npx @ivotoby/openapi-mcp-server --api-base-url https://api.example.com --openapi-spec https://api.example.com/openapi.json --tool GET-users
194
+
195
+ # Load tools tagged with "user" under the "/users" resource
196
+ npx @ivotoby/openapi-mcp-server --api-base-url https://api.example.com --openapi-spec https://api.example.com/openapi.json --tag user --resource users
197
+
198
+ # Load only POST operations
199
+ npx @ivotoby/openapi-mcp-server --api-base-url https://api.example.com --openapi-spec https://api.example.com/openapi.json --operation post
142
200
  ```
143
201
 
144
202
  ## Security Considerations
@@ -187,6 +245,32 @@ To see debug logs:
187
245
  4. Run tests and linting: `npm run typecheck && npm run lint`
188
246
  5. Submit a pull request
189
247
 
248
+ ## FAQ
249
+
250
+ **Q: What is a "tool"?**
251
+ A: A tool corresponds to a single API endpoint derived from your OpenAPI specification, exposed as an MCP resource.
252
+
253
+ **Q: How do I filter which tools are loaded?**
254
+ A: Use the `--tool`, `--tag`, `--resource`, and `--operation` flags, or set `TOOLS_MODE=dynamic` for meta-tools only.
255
+
256
+ **Q: When should I use dynamic mode?**
257
+ 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.
258
+
259
+ **Q: How do I specify custom headers for API requests?**
260
+ A: Use the `--headers` flag or `API_HEADERS` environment variable with `key:value` pairs separated by commas.
261
+
262
+ **Q: Which transport methods are supported?**
263
+ A: The server supports stdio transport (default) for integration with AI systems and HTTP transport (with streaming via SSE) for web clients.
264
+
265
+ **Q: How does the server handle complex OpenAPI schemas with references?**
266
+ 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.
267
+
268
+ **Q: What happens when parameter names conflict with request body properties?**
269
+ A: The server detects naming conflicts and automatically prefixes body property names with `body_` to avoid collisions, ensuring all properties are accessible.
270
+
271
+ **Q: Where can I find development and contribution guidelines?**
272
+ A: See the "For Developers" section above for commands (`npm run build`, `npm run dev`, etc) and pull request workflow.
273
+
190
274
  ## License
191
275
 
192
276
  MIT
package/dist/bundle.js CHANGED
@@ -14373,6 +14373,13 @@ var WORD_ABBREVIATIONS = {
14373
14373
 
14374
14374
  // src/openapi-loader.ts
14375
14375
  var OpenAPISpecLoader = class {
14376
+ /**
14377
+ * Disable name optimization
14378
+ */
14379
+ disableAbbreviation;
14380
+ constructor(config) {
14381
+ this.disableAbbreviation = config?.disableAbbreviation ?? false;
14382
+ }
14376
14383
  /**
14377
14384
  * Load an OpenAPI specification from a file path or URL
14378
14385
  */
@@ -14399,6 +14406,50 @@ var OpenAPISpecLoader = class {
14399
14406
  }
14400
14407
  }
14401
14408
  }
14409
+ /**
14410
+ * Inline `$ref` schemas from components and drop recursive cycles
14411
+ */
14412
+ inlineSchema(schema2, components, visited) {
14413
+ if ("$ref" in schema2 && typeof schema2.$ref === "string") {
14414
+ const ref = schema2.$ref;
14415
+ const match = ref.match(/^#\/components\/schemas\/(.+)$/);
14416
+ if (match && components) {
14417
+ const name = match[1];
14418
+ if (visited.has(name)) {
14419
+ return {};
14420
+ }
14421
+ const comp = components[name];
14422
+ if (!comp) {
14423
+ return {};
14424
+ }
14425
+ visited.add(name);
14426
+ return this.inlineSchema(comp, components, visited);
14427
+ }
14428
+ }
14429
+ const schemaObj = schema2;
14430
+ if (schemaObj.type === "object" && schemaObj.properties) {
14431
+ const newProps = {};
14432
+ for (const [propName, propSchema] of Object.entries(schemaObj.properties)) {
14433
+ newProps[propName] = this.inlineSchema(
14434
+ propSchema,
14435
+ components,
14436
+ new Set(visited)
14437
+ );
14438
+ }
14439
+ return { ...schemaObj, properties: newProps };
14440
+ }
14441
+ if (schemaObj.type === "array" && schemaObj.items) {
14442
+ return {
14443
+ ...schemaObj,
14444
+ items: this.inlineSchema(
14445
+ schemaObj.items,
14446
+ components,
14447
+ new Set(visited)
14448
+ )
14449
+ };
14450
+ }
14451
+ return schemaObj;
14452
+ }
14402
14453
  /**
14403
14454
  * Parse an OpenAPI specification into a map of tools
14404
14455
  */
@@ -14408,8 +14459,7 @@ var OpenAPISpecLoader = class {
14408
14459
  if (!pathItem) continue;
14409
14460
  for (const [method, operation] of Object.entries(pathItem)) {
14410
14461
  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}`);
14462
+ if (!["get", "put", "post", "delete", "options", "head", "patch", "trace"].includes(method)) {
14413
14463
  continue;
14414
14464
  }
14415
14465
  const op = operation;
@@ -14425,26 +14475,84 @@ var OpenAPISpecLoader = class {
14425
14475
  properties: {}
14426
14476
  }
14427
14477
  };
14478
+ const requiredParams = [];
14428
14479
  if (op.parameters) {
14429
- const requiredParams = [];
14430
14480
  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
- };
14481
+ let paramObj;
14482
+ if (!("name" in param)) {
14483
+ if ("$ref" in param && typeof param.$ref === "string") {
14484
+ const refMatch = param.$ref.match(/^#\/components\/parameters\/(.+)$/);
14485
+ if (refMatch && spec.components?.parameters) {
14486
+ const paramName = refMatch[1];
14487
+ const resolvedParam = spec.components.parameters[paramName];
14488
+ if (!resolvedParam || !("name" in resolvedParam)) continue;
14489
+ paramObj = resolvedParam;
14490
+ } else {
14491
+ continue;
14492
+ }
14493
+ } else {
14494
+ continue;
14495
+ }
14496
+ } else {
14497
+ paramObj = param;
14498
+ }
14499
+ if (paramObj.schema) {
14500
+ const paramSchema = this.inlineSchema(
14501
+ paramObj.schema,
14502
+ spec.components?.schemas,
14503
+ /* @__PURE__ */ new Set()
14504
+ );
14505
+ const paramDef = {
14506
+ description: paramObj.description || `${paramObj.name} parameter`
14507
+ };
14508
+ if (paramSchema.type) {
14509
+ paramDef.type = paramSchema.type;
14510
+ } else {
14511
+ paramDef.type = "string";
14438
14512
  }
14439
- if (param.required === true) {
14440
- requiredParams.push(param.name);
14513
+ for (const [key, value] of Object.entries(paramSchema)) {
14514
+ if (key === "type" || key === "description") continue;
14515
+ paramDef[key] = value;
14516
+ }
14517
+ tool.inputSchema.properties[paramObj.name] = paramDef;
14518
+ if (paramObj.required === true) {
14519
+ requiredParams.push(paramObj.name);
14441
14520
  }
14442
14521
  }
14443
14522
  }
14444
- if (requiredParams.length > 0 && tool.inputSchema) {
14445
- tool.inputSchema.required = requiredParams;
14523
+ }
14524
+ if (op.requestBody && "content" in op.requestBody) {
14525
+ const requestBodyObj = op.requestBody;
14526
+ let mediaTypeObj;
14527
+ if (requestBodyObj.content["application/json"]) {
14528
+ mediaTypeObj = requestBodyObj.content["application/json"];
14529
+ } else if (Object.keys(requestBodyObj.content).length > 0) {
14530
+ const firstContentType = Object.keys(requestBodyObj.content)[0];
14531
+ mediaTypeObj = requestBodyObj.content[firstContentType];
14532
+ }
14533
+ if (mediaTypeObj?.schema) {
14534
+ const inlinedSchema = this.inlineSchema(
14535
+ mediaTypeObj.schema,
14536
+ spec.components?.schemas,
14537
+ /* @__PURE__ */ new Set()
14538
+ );
14539
+ if (inlinedSchema.type === "object" && inlinedSchema.properties) {
14540
+ for (const [propName, propSchema] of Object.entries(inlinedSchema.properties)) {
14541
+ const paramName = tool.inputSchema.properties[propName] ? `body_${propName}` : propName;
14542
+ tool.inputSchema.properties[paramName] = propSchema;
14543
+ if (inlinedSchema.required && inlinedSchema.required.includes(propName)) {
14544
+ requiredParams.push(paramName);
14545
+ }
14546
+ }
14547
+ } else {
14548
+ tool.inputSchema.properties["body"] = inlinedSchema;
14549
+ requiredParams.push("body");
14550
+ }
14446
14551
  }
14447
14552
  }
14553
+ if (requiredParams.length > 0) {
14554
+ tool.inputSchema.required = requiredParams;
14555
+ }
14448
14556
  tools.set(toolId, tool);
14449
14557
  }
14450
14558
  }
@@ -14548,14 +14656,20 @@ var OpenAPISpecLoader = class {
14548
14656
  return finalName;
14549
14657
  }
14550
14658
  abbreviateOperationId(originalId, maxLength = 64) {
14659
+ maxLength = this.disableAbbreviation ? Number.MAX_SAFE_INTEGER : maxLength;
14551
14660
  const {
14552
14661
  currentName: sanitizedName,
14553
14662
  originalWasLong,
14554
14663
  errorName
14555
14664
  } = this._initialSanitizeAndValidate(originalId, maxLength);
14556
14665
  if (errorName) return errorName;
14557
- let processedName = this._performSemanticAbbreviation(sanitizedName);
14558
- processedName = this._applyVowelRemovalIfOverLength(processedName, maxLength);
14666
+ let processedName;
14667
+ if (this.disableAbbreviation) {
14668
+ processedName = this.splitCombined(sanitizedName).join("-");
14669
+ } else {
14670
+ processedName = this._performSemanticAbbreviation(sanitizedName);
14671
+ processedName = this._applyVowelRemovalIfOverLength(processedName, maxLength);
14672
+ }
14559
14673
  processedName = this._truncateAndApplyHashIfNeeded(
14560
14674
  processedName,
14561
14675
  originalId,
@@ -14571,16 +14685,105 @@ var OpenAPISpecLoader = class {
14571
14685
  var ToolsManager = class {
14572
14686
  constructor(config) {
14573
14687
  this.config = config;
14574
- this.specLoader = new OpenAPISpecLoader();
14688
+ this.config.toolsMode = this.config.toolsMode || "all";
14689
+ this.specLoader = new OpenAPISpecLoader({
14690
+ disableAbbreviation: this.config.disableAbbreviation
14691
+ });
14575
14692
  }
14576
14693
  tools = /* @__PURE__ */ new Map();
14577
14694
  specLoader;
14695
+ /**
14696
+ * Create dynamic discovery meta-tools
14697
+ */
14698
+ createDynamicTools() {
14699
+ const dynamicTools = /* @__PURE__ */ new Map();
14700
+ dynamicTools.set("LIST-API-ENDPOINTS", {
14701
+ name: "list-api-endpoints",
14702
+ description: "List all available API endpoints",
14703
+ inputSchema: { type: "object", properties: {} }
14704
+ });
14705
+ dynamicTools.set("GET-API-ENDPOINT-SCHEMA", {
14706
+ name: "get-api-endpoint-schema",
14707
+ description: "Get the JSON schema for a specified API endpoint",
14708
+ inputSchema: {
14709
+ type: "object",
14710
+ properties: {
14711
+ endpoint: { type: "string", description: "Endpoint path (e.g. /users/{id})" }
14712
+ },
14713
+ required: ["endpoint"]
14714
+ }
14715
+ });
14716
+ dynamicTools.set("INVOKE-API-ENDPOINT", {
14717
+ name: "invoke-api-endpoint",
14718
+ description: "Invoke an API endpoint with provided parameters",
14719
+ inputSchema: {
14720
+ type: "object",
14721
+ properties: {
14722
+ endpoint: { type: "string", description: "Endpoint path to invoke" },
14723
+ params: {
14724
+ type: "object",
14725
+ description: "Parameters for the API call",
14726
+ properties: {}
14727
+ }
14728
+ },
14729
+ required: ["endpoint"]
14730
+ }
14731
+ });
14732
+ return dynamicTools;
14733
+ }
14578
14734
  /**
14579
14735
  * Initialize tools from the OpenAPI specification
14580
14736
  */
14581
14737
  async initialize() {
14582
14738
  const spec = await this.specLoader.loadOpenAPISpec(this.config.openApiSpec);
14583
- this.tools = this.specLoader.parseOpenAPISpec(spec);
14739
+ if (this.config.toolsMode === "dynamic") {
14740
+ this.tools = this.createDynamicTools();
14741
+ return;
14742
+ }
14743
+ const rawTools = this.specLoader.parseOpenAPISpec(spec);
14744
+ const filtered = /* @__PURE__ */ new Map();
14745
+ const includeToolsLower = this.config.includeTools?.map((t) => t.toLowerCase()) || [];
14746
+ const includeOperationsLower = this.config.includeOperations?.map((op) => op.toLowerCase()) || [];
14747
+ const includeResourcesLower = this.config.includeResources || [];
14748
+ const includeTagsLower = this.config.includeTags?.map((tag) => tag.toLowerCase()) || [];
14749
+ const resourcePathsLower = includeResourcesLower.map((res) => ({
14750
+ exact: `/${res}`.toLowerCase(),
14751
+ prefix: `/${res}/`.toLowerCase()
14752
+ }));
14753
+ for (const [toolId, tool] of rawTools.entries()) {
14754
+ if (includeToolsLower.length > 0) {
14755
+ const toolIdLower = toolId.toLowerCase();
14756
+ const toolNameLower = tool.name.toLowerCase();
14757
+ if (!includeToolsLower.includes(toolIdLower) && !includeToolsLower.includes(toolNameLower)) {
14758
+ continue;
14759
+ }
14760
+ }
14761
+ if (includeOperationsLower.length > 0) {
14762
+ const { method } = this.parseToolId(toolId);
14763
+ if (!includeOperationsLower.includes(method.toLowerCase())) {
14764
+ continue;
14765
+ }
14766
+ }
14767
+ if (resourcePathsLower.length > 0) {
14768
+ const { path } = this.parseToolId(toolId);
14769
+ const pathLower = path.toLowerCase();
14770
+ const match = resourcePathsLower.some(
14771
+ (res) => pathLower === res.exact || pathLower.startsWith(res.prefix)
14772
+ );
14773
+ if (!match) continue;
14774
+ }
14775
+ if (includeTagsLower.length > 0) {
14776
+ const { method, path } = this.parseToolId(toolId);
14777
+ const methodLower = method.toLowerCase();
14778
+ const pathItem = spec.paths[path];
14779
+ if (!pathItem) continue;
14780
+ const opObj = pathItem[methodLower];
14781
+ const tags = Array.isArray(opObj?.tags) ? opObj.tags : [];
14782
+ if (!tags.some((tag) => includeTagsLower.includes(tag.toLowerCase()))) continue;
14783
+ }
14784
+ filtered.set(toolId, tool);
14785
+ }
14786
+ this.tools = filtered;
14584
14787
  for (const [toolId, tool] of this.tools.entries()) {
14585
14788
  console.error(`Registered tool: ${toolId} (${tool.name})`);
14586
14789
  }
@@ -14595,11 +14798,14 @@ var ToolsManager = class {
14595
14798
  * Find a tool by ID or name
14596
14799
  */
14597
14800
  findTool(idOrName) {
14598
- if (this.tools.has(idOrName)) {
14599
- return { toolId: idOrName, tool: this.tools.get(idOrName) };
14801
+ const lowerIdOrName = idOrName.toLowerCase();
14802
+ for (const [toolId, tool] of this.tools.entries()) {
14803
+ if (toolId.toLowerCase() === lowerIdOrName) {
14804
+ return { toolId, tool };
14805
+ }
14600
14806
  }
14601
14807
  for (const [toolId, tool] of this.tools.entries()) {
14602
- if (tool.name === idOrName) {
14808
+ if (tool.name.toLowerCase() === lowerIdOrName) {
14603
14809
  return { toolId, tool };
14604
14810
  }
14605
14811
  }
@@ -22980,6 +23186,29 @@ function loadConfig() {
22980
23186
  alias: "v",
22981
23187
  type: "string",
22982
23188
  description: "Server version"
23189
+ }).option("tools", {
23190
+ type: "string",
23191
+ choices: ["all", "dynamic"],
23192
+ description: "Which tools to load: all or dynamic meta-tools"
23193
+ }).option("tool", {
23194
+ type: "array",
23195
+ string: true,
23196
+ description: "Import only specified tool IDs or names"
23197
+ }).option("tag", {
23198
+ type: "array",
23199
+ string: true,
23200
+ description: "Import only tools with specified OpenAPI tags"
23201
+ }).option("resource", {
23202
+ type: "array",
23203
+ string: true,
23204
+ description: "Import only tools under specified resource path prefixes"
23205
+ }).option("operation", {
23206
+ type: "array",
23207
+ string: true,
23208
+ description: "Import only tools for specified HTTP methods (e.g., get, post)"
23209
+ }).option("disable-abbreviation", {
23210
+ type: "boolean",
23211
+ description: "Disable name optimization"
22983
23212
  }).help().parseSync();
22984
23213
  let transportType;
22985
23214
  if (argv.transport === "http" || process.env.TRANSPORT_TYPE === "http") {
@@ -22992,6 +23221,7 @@ function loadConfig() {
22992
23221
  const endpointPath = argv.path || process.env.ENDPOINT_PATH || "/mcp";
22993
23222
  const apiBaseUrl = argv["api-base-url"] || process.env.API_BASE_URL;
22994
23223
  const openApiSpec = argv["openapi-spec"] || process.env.OPENAPI_SPEC_PATH;
23224
+ const disableAbbreviation = argv["disable-abbreviation"] || (process.env.DISABLE_ABBREVIATION ? process.env.DISABLE_ABBREVIATION === "true" : false);
22995
23225
  if (!apiBaseUrl) {
22996
23226
  throw new Error("API base URL is required (--api-base-url or API_BASE_URL)");
22997
23227
  }
@@ -23008,7 +23238,13 @@ function loadConfig() {
23008
23238
  transportType,
23009
23239
  httpPort,
23010
23240
  httpHost,
23011
- endpointPath
23241
+ endpointPath,
23242
+ includeTools: argv.tool,
23243
+ includeTags: argv.tag,
23244
+ includeResources: argv.resource,
23245
+ includeOperations: argv.operation,
23246
+ toolsMode: argv.tools || process.env.TOOLS_MODE || "all",
23247
+ disableAbbreviation: disableAbbreviation ? true : void 0
23012
23248
  };
23013
23249
  }
23014
23250
 
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.3.0",
4
4
  "description": "An MCP server that exposes OpenAPI endpoints as resources",
5
5
  "license": "MIT",
6
6
  "type": "module",