@ivotoby/openapi-mcp-server 1.5.1 → 1.6.1

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.
package/README.md CHANGED
@@ -6,11 +6,186 @@ A Model Context Protocol (MCP) server that exposes OpenAPI endpoints as MCP reso
6
6
 
7
7
  ## Overview
8
8
 
9
- This MCP server supports two transport methods:
9
+ This MCP server can be used in two ways:
10
+
11
+ 1. **CLI Tool**: Use `npx @ivotoby/openapi-mcp-server` directly with command-line arguments for quick setup
12
+ 2. **Library**: Import and use the `OpenAPIServer` class in your own Node.js applications for custom implementations
13
+
14
+ The server supports two transport methods:
10
15
 
11
16
  1. **Stdio Transport** (default): For direct integration with AI systems like Claude Desktop that manage MCP connections through standard input/output.
12
17
  2. **Streamable HTTP Transport**: For connecting to the server over HTTP, allowing web clients and other HTTP-capable systems to use the MCP protocol.
13
18
 
19
+ ## 🚀 Using as a Library
20
+
21
+ Create dedicated MCP servers for specific APIs by importing and configuring the `OpenAPIServer` class. This approach is ideal for:
22
+
23
+ - **Custom Authentication**: Implement complex authentication patterns with the `AuthProvider` interface
24
+ - **API-Specific Optimizations**: Filter endpoints, customize error handling, and optimize for specific use cases
25
+ - **Distribution**: Package your server as a standalone npm module for easy sharing
26
+ - **Integration**: Embed the server in larger applications or add custom middleware
27
+
28
+ ### Basic Library Usage
29
+
30
+ ```typescript
31
+ import { OpenAPIServer } from "@ivotoby/openapi-mcp-server"
32
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
33
+
34
+ const config = {
35
+ name: "my-api-server",
36
+ version: "1.0.0",
37
+ apiBaseUrl: "https://api.example.com",
38
+ openApiSpec: "https://api.example.com/openapi.json",
39
+ specInputMethod: "url" as const,
40
+ headers: {
41
+ Authorization: "Bearer your-token",
42
+ "X-API-Key": "your-api-key",
43
+ },
44
+ transportType: "stdio" as const,
45
+ toolsMode: "all" as const,
46
+ }
47
+
48
+ const server = new OpenAPIServer(config)
49
+ const transport = new StdioServerTransport()
50
+ await server.start(transport)
51
+ ```
52
+
53
+ ### Advanced Authentication with AuthProvider
54
+
55
+ For APIs with token expiration, refresh requirements, or complex authentication:
56
+
57
+ ```typescript
58
+ import { OpenAPIServer, AuthProvider } from "@ivotoby/openapi-mcp-server"
59
+ import { AxiosError } from "axios"
60
+
61
+ class MyAuthProvider implements AuthProvider {
62
+ async getAuthHeaders(): Promise<Record<string, string>> {
63
+ // Called before each request - return fresh headers
64
+ if (this.isTokenExpired()) {
65
+ await this.refreshToken()
66
+ }
67
+ return { Authorization: `Bearer ${this.token}` }
68
+ }
69
+
70
+ async handleAuthError(error: AxiosError): Promise<boolean> {
71
+ // Called on 401/403 errors - return true to retry
72
+ if (error.response?.status === 401) {
73
+ await this.refreshToken()
74
+ return true // Retry the request
75
+ }
76
+ return false
77
+ }
78
+ }
79
+
80
+ const authProvider = new MyAuthProvider()
81
+ const config = {
82
+ // ... other config
83
+ authProvider: authProvider, // Use AuthProvider instead of static headers
84
+ }
85
+ ```
86
+
87
+ **📁 See the [examples/](./examples/) directory for complete, runnable examples including:**
88
+
89
+ - Basic library usage with static authentication
90
+ - AuthProvider implementations for different scenarios
91
+ - Real-world Beatport API integration
92
+ - Production-ready packaging patterns
93
+
94
+ ## 🔐 Dynamic Authentication with AuthProvider
95
+
96
+ The `AuthProvider` interface enables sophisticated authentication scenarios that static headers cannot handle:
97
+
98
+ ### Key Features
99
+
100
+ - **Dynamic Headers**: Fresh authentication headers for each request
101
+ - **Token Expiration Handling**: Automatic detection and handling of expired tokens
102
+ - **Authentication Error Recovery**: Retry logic for recoverable authentication failures
103
+ - **Custom Error Messages**: Provide clear, actionable guidance to users
104
+
105
+ ### AuthProvider Interface
106
+
107
+ ```typescript
108
+ interface AuthProvider {
109
+ /**
110
+ * Get authentication headers for the current request
111
+ * Called before each API request to get fresh headers
112
+ */
113
+ getAuthHeaders(): Promise<Record<string, string>>
114
+
115
+ /**
116
+ * Handle authentication errors from API responses
117
+ * Called when the API returns 401 or 403 errors
118
+ * Return true to retry the request, false otherwise
119
+ */
120
+ handleAuthError(error: AxiosError): Promise<boolean>
121
+ }
122
+ ```
123
+
124
+ ### Common Patterns
125
+
126
+ #### Automatic Token Refresh
127
+
128
+ ```typescript
129
+ class RefreshableAuthProvider implements AuthProvider {
130
+ async getAuthHeaders(): Promise<Record<string, string>> {
131
+ if (this.isTokenExpired()) {
132
+ await this.refreshToken()
133
+ }
134
+ return { Authorization: `Bearer ${this.accessToken}` }
135
+ }
136
+
137
+ async handleAuthError(error: AxiosError): Promise<boolean> {
138
+ if (error.response?.status === 401) {
139
+ await this.refreshToken()
140
+ return true // Retry with fresh token
141
+ }
142
+ return false
143
+ }
144
+ }
145
+ ```
146
+
147
+ #### Manual Token Management (e.g., Beatport)
148
+
149
+ ```typescript
150
+ class ManualTokenAuthProvider implements AuthProvider {
151
+ async getAuthHeaders(): Promise<Record<string, string>> {
152
+ if (!this.token || this.isTokenExpired()) {
153
+ throw new Error(
154
+ "Token expired. Please get a new token from your browser:\n" +
155
+ "1. Go to the API website and log in\n" +
156
+ "2. Open browser dev tools (F12)\n" +
157
+ "3. Copy the Authorization header from any API request\n" +
158
+ "4. Update your token using updateToken()",
159
+ )
160
+ }
161
+ return { Authorization: `Bearer ${this.token}` }
162
+ }
163
+
164
+ updateToken(token: string): void {
165
+ this.token = token
166
+ this.tokenExpiry = new Date(Date.now() + 3600000) // 1 hour
167
+ }
168
+ }
169
+ ```
170
+
171
+ #### API Key Authentication
172
+
173
+ ```typescript
174
+ class ApiKeyAuthProvider implements AuthProvider {
175
+ constructor(private apiKey: string) {}
176
+
177
+ async getAuthHeaders(): Promise<Record<string, string>> {
178
+ return { "X-API-Key": this.apiKey }
179
+ }
180
+
181
+ async handleAuthError(error: AxiosError): Promise<boolean> {
182
+ throw new Error("API key authentication failed. Please check your key.")
183
+ }
184
+ }
185
+ ```
186
+
187
+ **📖 For detailed AuthProvider documentation and examples, see [docs/auth-provider-guide.md](./docs/auth-provider-guide.md)**
188
+
14
189
  ## Quick Start for Users
15
190
 
16
191
  ### Option 1: Using with Claude Desktop (Stdio Transport)
@@ -353,6 +528,18 @@ To see debug logs:
353
528
  **Q: What is a "tool"?**
354
529
  A: A tool corresponds to a single API endpoint derived from your OpenAPI specification, exposed as an MCP resource.
355
530
 
531
+ **Q: How can I use this package in my own project?**
532
+ A: You can import the `OpenAPIServer` class and use it as a library in your Node.js application. This allows you to create dedicated MCP servers for specific APIs with custom authentication, filtering, and error handling. See the [examples/](./examples/) directory for complete implementations.
533
+
534
+ **Q: What's the difference between using the CLI and using it as a library?**
535
+ A: The CLI is great for quick setup and testing, while the library approach allows you to create dedicated packages for specific APIs, implement custom authentication with `AuthProvider`, add custom logic, and distribute your server as a standalone npm module.
536
+
537
+ **Q: How do I handle APIs with expiring tokens?**
538
+ A: Use the `AuthProvider` interface instead of static headers. AuthProvider allows you to implement dynamic authentication with token refresh, expiration handling, and custom error recovery. See the AuthProvider examples for different patterns.
539
+
540
+ **Q: What is AuthProvider and when should I use it?**
541
+ A: `AuthProvider` is an interface for dynamic authentication that gets fresh headers before each request and handles authentication errors. Use it when your API has expiring tokens, requires token refresh, or needs complex authentication logic that static headers can't handle.
542
+
356
543
  **Q: How do I filter which tools are loaded?**
357
544
  A: Use the `--tool`, `--tag`, `--resource`, and `--operation` flags, or set `TOOLS_MODE=dynamic` for meta-tools only.
358
545
 
@@ -360,7 +547,7 @@ A: Use the `--tool`, `--tag`, `--resource`, and `--operation` flags, or set `TOO
360
547
  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.
361
548
 
362
549
  **Q: How do I specify custom headers for API requests?**
363
- A: Use the `--headers` flag or `API_HEADERS` environment variable with `key:value` pairs separated by commas.
550
+ A: Use the `--headers` flag or `API_HEADERS` environment variable with `key:value` pairs separated by commas for CLI usage. For library usage, use the `headers` config option or implement an `AuthProvider` for dynamic headers.
364
551
 
365
552
  **Q: Which transport methods are supported?**
366
553
  A: The server supports stdio transport (default) for integration with AI systems and HTTP transport (with streaming via SSE) for web clients.
@@ -371,6 +558,9 @@ A: The server fully resolves `$ref` references in parameters and schemas, preser
371
558
  **Q: What happens when parameter names conflict with request body properties?**
372
559
  A: The server detects naming conflicts and automatically prefixes body property names with `body_` to avoid collisions, ensuring all properties are accessible.
373
560
 
561
+ **Q: Can I package my MCP server for distribution?**
562
+ A: Yes! When using the library approach, you can create a dedicated npm package for your API. See the Beatport example for a complete implementation that can be packaged and distributed as `npx your-api-mcp-server`.
563
+
374
564
  **Q: Where can I find development and contribution guidelines?**
375
565
  A: See the "For Developers" section above for commands (`npm run build`, `npm run dev`, etc) and pull request workflow.
376
566
 
package/bin/mcp-server.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import "../dist/bundle.js";
2
+ import "../dist/cli.js"
package/dist/bundle.js CHANGED
@@ -14286,7 +14286,7 @@ var js_yaml_default = jsYaml;
14286
14286
  // src/openapi-loader.ts
14287
14287
  import crypto from "crypto";
14288
14288
 
14289
- // src/abbreviations.ts
14289
+ // src/utils/abbreviations.ts
14290
14290
  var REVISED_COMMON_WORDS_TO_REMOVE = [
14291
14291
  "controller",
14292
14292
  "api",
@@ -14371,6 +14371,21 @@ var WORD_ABBREVIATIONS = {
14371
14371
  query: "Qry"
14372
14372
  };
14373
14373
 
14374
+ // src/utils/tool-id.ts
14375
+ function parseToolId(toolId) {
14376
+ const [method, pathPart] = toolId.split("::", 2);
14377
+ const path = pathPart ? "/" + pathPart.replace(/-/g, "/") : "";
14378
+ return { method, path };
14379
+ }
14380
+ function sanitizeForToolId(input) {
14381
+ return input.replace(/[^A-Za-z0-9_-]/g, "").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
14382
+ }
14383
+ function generateToolId(method, path) {
14384
+ const cleanPath = path.replace(/^\//, "").replace(/\{([^}]+)\}/g, "$1").replace(/\//g, "-");
14385
+ const sanitizedPath = sanitizeForToolId(cleanPath);
14386
+ return `${method.toUpperCase()}::${sanitizedPath}`;
14387
+ }
14388
+
14374
14389
  // src/openapi-loader.ts
14375
14390
  var OpenAPISpecLoader = class {
14376
14391
  /**
@@ -14562,9 +14577,8 @@ var OpenAPISpecLoader = class {
14562
14577
  continue;
14563
14578
  }
14564
14579
  const op = operation;
14565
- const cleanPath = path.replace(/^\//, "").replace(/\{([^}]+)\}/g, "$1");
14566
- const toolId = `${method.toUpperCase()}-${cleanPath}`.replace(/[^a-zA-Z0-9-]/g, "-");
14567
- let nameSource = op.operationId || op.summary || `${method.toUpperCase()} ${path}`;
14580
+ const toolId = generateToolId(method, path);
14581
+ const nameSource = op.operationId || op.summary || `${method.toUpperCase()} ${path}`;
14568
14582
  const name = this.abbreviateOperationId(nameSource);
14569
14583
  const tool = {
14570
14584
  name,
@@ -14574,6 +14588,7 @@ var OpenAPISpecLoader = class {
14574
14588
  properties: {}
14575
14589
  }
14576
14590
  };
14591
+ tool["x-original-path"] = path;
14577
14592
  const requiredParams = [];
14578
14593
  if (op.parameters) {
14579
14594
  for (const param of op.parameters) {
@@ -14940,11 +14955,12 @@ var ToolsManager = class {
14940
14955
  }
14941
14956
  /**
14942
14957
  * Get the path and method from a tool ID
14958
+ *
14959
+ * Note: This converts hyphens back to slashes to reconstruct the original API path.
14960
+ * This is consistent with ApiClient.parseToolId() which needs the actual path for HTTP requests.
14943
14961
  */
14944
14962
  parseToolId(toolId) {
14945
- const [method, ...pathParts] = toolId.split("-");
14946
- const path = "/" + pathParts.join("/").replace(/-/g, "/");
14947
- return { method, path };
14963
+ return parseToolId(toolId);
14948
14964
  }
14949
14965
  };
14950
14966
 
@@ -18257,22 +18273,46 @@ var {
18257
18273
  mergeConfig: mergeConfig2
18258
18274
  } = axios_default;
18259
18275
 
18276
+ // src/auth-provider.ts
18277
+ function isAuthError(error) {
18278
+ return error.response?.status === 401 || error.response?.status === 403;
18279
+ }
18280
+ var StaticAuthProvider = class {
18281
+ constructor(headers = {}) {
18282
+ this.headers = headers;
18283
+ }
18284
+ async getAuthHeaders() {
18285
+ return { ...this.headers };
18286
+ }
18287
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
18288
+ async handleAuthError(_error) {
18289
+ return false;
18290
+ }
18291
+ };
18292
+
18260
18293
  // src/api-client.ts
18261
18294
  var ApiClient = class {
18295
+ axiosInstance;
18296
+ toolsMap = /* @__PURE__ */ new Map();
18297
+ authProvider;
18262
18298
  /**
18263
18299
  * Create a new API client
18264
18300
  *
18265
18301
  * @param baseUrl - Base URL for the API
18266
- * @param headers - Optional headers to include with every request
18302
+ * @param authProviderOrHeaders - AuthProvider instance or static headers for backward compatibility
18267
18303
  */
18268
- constructor(baseUrl, headers = {}) {
18269
- this.headers = headers;
18304
+ constructor(baseUrl, authProviderOrHeaders) {
18270
18305
  this.axiosInstance = axios_default.create({
18271
18306
  baseURL: baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`
18272
18307
  });
18308
+ if (!authProviderOrHeaders) {
18309
+ this.authProvider = new StaticAuthProvider();
18310
+ } else if (typeof authProviderOrHeaders === "object" && !("getAuthHeaders" in authProviderOrHeaders)) {
18311
+ this.authProvider = new StaticAuthProvider(authProviderOrHeaders);
18312
+ } else {
18313
+ this.authProvider = authProviderOrHeaders;
18314
+ }
18273
18315
  }
18274
- axiosInstance;
18275
- toolsMap = /* @__PURE__ */ new Map();
18276
18316
  /**
18277
18317
  * Set the available tools for the client
18278
18318
  *
@@ -18298,6 +18338,17 @@ var ApiClient = class {
18298
18338
  * @returns The API response data
18299
18339
  */
18300
18340
  async executeApiCall(toolId, params) {
18341
+ return this.executeApiCallWithRetry(toolId, params, false);
18342
+ }
18343
+ /**
18344
+ * Execute an API call with optional retry on auth error
18345
+ *
18346
+ * @param toolId - The tool ID in format METHOD-path-parts
18347
+ * @param params - Parameters for the API call
18348
+ * @param isRetry - Whether this is a retry attempt
18349
+ * @returns The API response data
18350
+ */
18351
+ async executeApiCallWithRetry(toolId, params, isRetry) {
18301
18352
  try {
18302
18353
  const { method, path } = this.parseToolId(toolId);
18303
18354
  const toolDef = this.getToolDefinition(toolId);
@@ -18342,10 +18393,11 @@ var ApiClient = class {
18342
18393
  }
18343
18394
  }
18344
18395
  }
18396
+ const authHeaders = await this.authProvider.getAuthHeaders();
18345
18397
  const config = {
18346
18398
  method: method.toLowerCase(),
18347
18399
  url: resolvedPath,
18348
- headers: this.headers
18400
+ headers: authHeaders
18349
18401
  };
18350
18402
  if (["get", "delete", "head", "options"].includes(method.toLowerCase())) {
18351
18403
  config.params = this.processQueryParams(paramsCopy);
@@ -18357,6 +18409,12 @@ var ApiClient = class {
18357
18409
  } catch (error) {
18358
18410
  if (axios_default.isAxiosError(error)) {
18359
18411
  const axiosError = error;
18412
+ if (!isRetry && isAuthError(axiosError)) {
18413
+ const shouldRetry = await this.authProvider.handleAuthError(axiosError);
18414
+ if (shouldRetry) {
18415
+ return this.executeApiCallWithRetry(toolId, params, true);
18416
+ }
18417
+ }
18360
18418
  throw new Error(
18361
18419
  `API request failed: ${axiosError.message}${axiosError.response ? ` (${axiosError.response.status}: ${typeof axiosError.response.data === "object" ? JSON.stringify(axiosError.response.data) : axiosError.response.data})` : ""}`
18362
18420
  );
@@ -18367,13 +18425,11 @@ var ApiClient = class {
18367
18425
  /**
18368
18426
  * Parse a tool ID into HTTP method and path
18369
18427
  *
18370
- * @param toolId - Tool ID in format METHOD-path-parts
18428
+ * @param toolId - Tool ID in format METHOD::pathPart
18371
18429
  * @returns Object containing method and path
18372
18430
  */
18373
18431
  parseToolId(toolId) {
18374
- const [method, ...pathParts] = toolId.split("-");
18375
- const path = "/" + pathParts.join("/").replace(/-/g, "/");
18376
- return { method, path };
18432
+ return parseToolId(toolId);
18377
18433
  }
18378
18434
  /**
18379
18435
  * Process query parameters for GET requests
@@ -18413,7 +18469,8 @@ var OpenAPIServer = class {
18413
18469
  }
18414
18470
  );
18415
18471
  this.toolsManager = new ToolsManager(config);
18416
- this.apiClient = new ApiClient(config.apiBaseUrl, config.headers);
18472
+ const authProviderOrHeaders = config.authProvider || new StaticAuthProvider(config.headers);
18473
+ this.apiClient = new ApiClient(config.apiBaseUrl, authProviderOrHeaders);
18417
18474
  this.initializeHandlers();
18418
18475
  }
18419
18476
  /**
@@ -24049,14 +24106,19 @@ async function main() {
24049
24106
  process.exit(1);
24050
24107
  }
24051
24108
  }
24052
- main();
24109
+ if (import.meta.url === `file://${process.argv[1]}`) {
24110
+ main();
24111
+ }
24053
24112
  export {
24054
24113
  ApiClient,
24055
24114
  OpenAPIServer,
24056
24115
  OpenAPISpecLoader,
24116
+ StaticAuthProvider,
24057
24117
  StreamableHttpServerTransport,
24058
24118
  ToolsManager,
24119
+ isAuthError,
24059
24120
  loadConfig,
24121
+ main,
24060
24122
  parseHeaders
24061
24123
  };
24062
24124
  /*! Bundled license information: