@ivotoby/openapi-mcp-server 1.5.0 → 1.6.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.
package/README.md CHANGED
@@ -1,14 +1,191 @@
1
1
  # OpenAPI MCP Server
2
2
 
3
+ [![smithery badge](https://smithery.ai/badge/@ivo-toby/mcp-openapi-server)](https://smithery.ai/server/@ivo-toby/mcp-openapi-server)
4
+
3
5
  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
6
 
5
7
  ## Overview
6
8
 
7
- 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:
8
15
 
9
16
  1. **Stdio Transport** (default): For direct integration with AI systems like Claude Desktop that manage MCP connections through standard input/output.
10
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.
11
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
+
12
189
  ## Quick Start for Users
13
190
 
14
191
  ### Option 1: Using with Claude Desktop (Stdio Transport)
@@ -63,7 +240,7 @@ npx @ivotoby/openapi-mcp-server \
63
240
  # Initialize a session (first request)
64
241
  curl -X POST http://localhost:3000/mcp \
65
242
  -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"}}}'
243
+ -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl-client","version":"1.0.0"}}}'
67
244
 
68
245
  # The response includes a Mcp-Session-Id header that you must use for subsequent requests
69
246
  # and the InitializeResult directly in the POST response body.
@@ -351,6 +528,18 @@ To see debug logs:
351
528
  **Q: What is a "tool"?**
352
529
  A: A tool corresponds to a single API endpoint derived from your OpenAPI specification, exposed as an MCP resource.
353
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
+
354
543
  **Q: How do I filter which tools are loaded?**
355
544
  A: Use the `--tool`, `--tag`, `--resource`, and `--operation` flags, or set `TOOLS_MODE=dynamic` for meta-tools only.
356
545
 
@@ -358,7 +547,7 @@ A: Use the `--tool`, `--tag`, `--resource`, and `--operation` flags, or set `TOO
358
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.
359
548
 
360
549
  **Q: How do I specify custom headers for API requests?**
361
- 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.
362
551
 
363
552
  **Q: Which transport methods are supported?**
364
553
  A: The server supports stdio transport (default) for integration with AI systems and HTTP transport (with streaming via SSE) for web clients.
@@ -369,6 +558,9 @@ A: The server fully resolves `$ref` references in parameters and schemas, preser
369
558
  **Q: What happens when parameter names conflict with request body properties?**
370
559
  A: The server detects naming conflicts and automatically prefixes body property names with `body_` to avoid collisions, ensuring all properties are accessible.
371
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
+
372
564
  **Q: Where can I find development and contribution guidelines?**
373
565
  A: See the "For Developers" section above for commands (`npm run build`, `npm run dev`, etc) and pull request workflow.
374
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
@@ -18257,22 +18257,46 @@ var {
18257
18257
  mergeConfig: mergeConfig2
18258
18258
  } = axios_default;
18259
18259
 
18260
+ // src/auth-provider.ts
18261
+ function isAuthError(error) {
18262
+ return error.response?.status === 401 || error.response?.status === 403;
18263
+ }
18264
+ var StaticAuthProvider = class {
18265
+ constructor(headers = {}) {
18266
+ this.headers = headers;
18267
+ }
18268
+ async getAuthHeaders() {
18269
+ return { ...this.headers };
18270
+ }
18271
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
18272
+ async handleAuthError(_error) {
18273
+ return false;
18274
+ }
18275
+ };
18276
+
18260
18277
  // src/api-client.ts
18261
18278
  var ApiClient = class {
18279
+ axiosInstance;
18280
+ toolsMap = /* @__PURE__ */ new Map();
18281
+ authProvider;
18262
18282
  /**
18263
18283
  * Create a new API client
18264
18284
  *
18265
18285
  * @param baseUrl - Base URL for the API
18266
- * @param headers - Optional headers to include with every request
18286
+ * @param authProviderOrHeaders - AuthProvider instance or static headers for backward compatibility
18267
18287
  */
18268
- constructor(baseUrl, headers = {}) {
18269
- this.headers = headers;
18288
+ constructor(baseUrl, authProviderOrHeaders) {
18270
18289
  this.axiosInstance = axios_default.create({
18271
18290
  baseURL: baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`
18272
18291
  });
18292
+ if (!authProviderOrHeaders) {
18293
+ this.authProvider = new StaticAuthProvider();
18294
+ } else if (typeof authProviderOrHeaders === "object" && !("getAuthHeaders" in authProviderOrHeaders)) {
18295
+ this.authProvider = new StaticAuthProvider(authProviderOrHeaders);
18296
+ } else {
18297
+ this.authProvider = authProviderOrHeaders;
18298
+ }
18273
18299
  }
18274
- axiosInstance;
18275
- toolsMap = /* @__PURE__ */ new Map();
18276
18300
  /**
18277
18301
  * Set the available tools for the client
18278
18302
  *
@@ -18298,6 +18322,17 @@ var ApiClient = class {
18298
18322
  * @returns The API response data
18299
18323
  */
18300
18324
  async executeApiCall(toolId, params) {
18325
+ return this.executeApiCallWithRetry(toolId, params, false);
18326
+ }
18327
+ /**
18328
+ * Execute an API call with optional retry on auth error
18329
+ *
18330
+ * @param toolId - The tool ID in format METHOD-path-parts
18331
+ * @param params - Parameters for the API call
18332
+ * @param isRetry - Whether this is a retry attempt
18333
+ * @returns The API response data
18334
+ */
18335
+ async executeApiCallWithRetry(toolId, params, isRetry) {
18301
18336
  try {
18302
18337
  const { method, path } = this.parseToolId(toolId);
18303
18338
  const toolDef = this.getToolDefinition(toolId);
@@ -18342,10 +18377,11 @@ var ApiClient = class {
18342
18377
  }
18343
18378
  }
18344
18379
  }
18380
+ const authHeaders = await this.authProvider.getAuthHeaders();
18345
18381
  const config = {
18346
18382
  method: method.toLowerCase(),
18347
18383
  url: resolvedPath,
18348
- headers: this.headers
18384
+ headers: authHeaders
18349
18385
  };
18350
18386
  if (["get", "delete", "head", "options"].includes(method.toLowerCase())) {
18351
18387
  config.params = this.processQueryParams(paramsCopy);
@@ -18357,6 +18393,16 @@ var ApiClient = class {
18357
18393
  } catch (error) {
18358
18394
  if (axios_default.isAxiosError(error)) {
18359
18395
  const axiosError = error;
18396
+ if (!isRetry && isAuthError(axiosError)) {
18397
+ try {
18398
+ const shouldRetry = await this.authProvider.handleAuthError(axiosError);
18399
+ if (shouldRetry) {
18400
+ return this.executeApiCallWithRetry(toolId, params, true);
18401
+ }
18402
+ } catch (authHandlerError) {
18403
+ throw authHandlerError;
18404
+ }
18405
+ }
18360
18406
  throw new Error(
18361
18407
  `API request failed: ${axiosError.message}${axiosError.response ? ` (${axiosError.response.status}: ${typeof axiosError.response.data === "object" ? JSON.stringify(axiosError.response.data) : axiosError.response.data})` : ""}`
18362
18408
  );
@@ -18413,7 +18459,8 @@ var OpenAPIServer = class {
18413
18459
  }
18414
18460
  );
18415
18461
  this.toolsManager = new ToolsManager(config);
18416
- this.apiClient = new ApiClient(config.apiBaseUrl, config.headers);
18462
+ const authProviderOrHeaders = config.authProvider || new StaticAuthProvider(config.headers);
18463
+ this.apiClient = new ApiClient(config.apiBaseUrl, authProviderOrHeaders);
18417
18464
  this.initializeHandlers();
18418
18465
  }
18419
18466
  /**
@@ -24049,14 +24096,19 @@ async function main() {
24049
24096
  process.exit(1);
24050
24097
  }
24051
24098
  }
24052
- main();
24099
+ if (import.meta.url === `file://${process.argv[1]}`) {
24100
+ main();
24101
+ }
24053
24102
  export {
24054
24103
  ApiClient,
24055
24104
  OpenAPIServer,
24056
24105
  OpenAPISpecLoader,
24106
+ StaticAuthProvider,
24057
24107
  StreamableHttpServerTransport,
24058
24108
  ToolsManager,
24109
+ isAuthError,
24059
24110
  loadConfig,
24111
+ main,
24060
24112
  parseHeaders
24061
24113
  };
24062
24114
  /*! Bundled license information: