anyapi-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
@@ -8,239 +8,146 @@ Traditional MCP servers hand-pick a handful of endpoints and call it a day — l
8
8
 
9
9
  `anyapi-mcp-server` is a universal [MCP](https://modelcontextprotocol.io) server that connects **any REST API** to AI assistants like Claude, Cursor, and other LLM-powered tools — just point it at an OpenAPI spec or Postman collection. Every endpoint the API provides becomes available instantly, with **GraphQL-style field selection** and automatic schema inference. No custom server code, no artificial limits.
10
10
 
11
- Works with services like **Datadog**, **PostHog**, **Metabase**, **Cloudflare**, **Stripe**, **GitHub**, **Slack**, **Twilio**, **Shopify**, **HubSpot**, and anything else with a REST API — if it has an API, it just works.
12
-
13
- ## Features
14
-
15
- - **Works with any REST API** — provide an OpenAPI (JSON/YAML) or Postman Collection v2.x spec as a local file or HTTPS URL
16
- - **Remote spec caching** — HTTPS spec URLs are fetched once and cached locally in ``~/.cache/anyapi-mcp/` (Linux/macOS) or `%LOCALAPPDATA%\anyapi-mcp\` (Windows)`
17
- - **GraphQL-style queries** — select only the fields you need from API responses
18
- - **Automatic schema inference** — calls an endpoint once, infers the response schema, then lets you query specific fields
19
- - **Multi-sample merging** — samples up to 10 array elements to build richer schemas that capture fields missing from individual items
20
- - **Mutation support** — POST/PUT/DELETE/PATCH endpoints with OpenAPI request body schemas get GraphQL mutation types with typed inputs
21
- - **Smart query suggestions** — `call_api` returns ready-to-use GraphQL queries based on the inferred schema
22
- - **Shape-aware schema caching** — schemas are cached by response structure (not just endpoint), so the same path returning different shapes gets distinct schemas; `shapeHash` is returned for cache-aware workflows
23
- - **Response caching** — 30-second TTL cache prevents duplicate HTTP calls across consecutive `call_api` → `query_api` flows
24
- - **Retry with backoff** — automatic retries with exponential backoff and jitter for 429/5xx errors, honoring `Retry-After` headers
25
- - **Multi-format responses** — parses JSON, XML, CSV, and plain text responses automatically
26
- - **Built-in pagination** — API-level pagination via `params`; client-side slicing with top-level `limit`/`offset`
27
- - **Spec documentation lookup** — `explain_api` returns rich endpoint docs (parameters, response codes, deprecation, request body schema) without making HTTP requests
28
- - **Concurrent batch queries** — `batch_query` fetches data from up to 10 endpoints in parallel, returning all results in one tool call
29
- - **Per-request headers** — override default headers on individual `call_api`/`query_api`/`batch_query` calls
30
- - **Environment variable interpolation** — use `${ENV_VAR}` in base URLs and headers
31
- - **Request logging** — optional NDJSON request/response log with sensitive header masking
32
-
33
11
  [![npm](https://img.shields.io/npm/v/anyapi-mcp-server)](https://www.npmjs.com/package/anyapi-mcp-server)
34
12
 
35
- ## Installation
13
+ ## Quick start
14
+
15
+ **1. Install**
36
16
 
37
17
  ```bash
38
18
  npm install -g anyapi-mcp-server
39
19
  ```
40
20
 
41
- ### Required arguments
42
-
43
- | Flag | Description |
44
- |------|-------------|
45
- | `--name` | Server name (e.g. `petstore`) |
46
- | `--spec` | Path or HTTPS URL to OpenAPI spec (JSON or YAML) or Postman Collection. HTTPS URLs are cached locally in ``~/.cache/anyapi-mcp/` (Linux/macOS) or `%LOCALAPPDATA%\anyapi-mcp\` (Windows)`. Supports `${ENV_VAR}` interpolation. |
47
- | `--base-url` | API base URL (e.g. `https://api.example.com`). Supports `${ENV_VAR}` interpolation. |
48
-
49
- ### Optional arguments
50
-
51
- | Flag | Description |
52
- |------|-------------|
53
- | `--header` | HTTP header as `"Key: Value"` (repeatable). Supports `${ENV_VAR}` interpolation in values. |
54
- | `--log` | Path to request/response log file (NDJSON format). Sensitive headers are masked automatically. |
55
-
56
- ### Example: Cursor / Claude Desktop configuration
57
-
58
- Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop config):
59
-
60
- #### Cloudflare API
21
+ **2. Add to your MCP client** (Cursor, Claude Desktop, etc.)
61
22
 
62
23
  ```json
63
24
  {
64
25
  "mcpServers": {
65
- "cloudflare": {
26
+ "your-api": {
66
27
  "command": "npx",
67
28
  "args": [
68
29
  "-y",
69
30
  "anyapi-mcp-server",
70
- "--name", "cloudflare",
71
- "--spec", "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json",
72
- "--base-url", "https://api.cloudflare.com/client/v4",
73
- "--header", "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}"
31
+ "--name", "your-api",
32
+ "--spec", "path/to/openapi.json",
33
+ "--base-url", "https://api.example.com",
34
+ "--header", "Authorization: Bearer ${API_KEY}"
74
35
  ],
75
36
  "env": {
76
- "CLOUDFLARE_API_TOKEN": "your-cloudflare-api-token"
37
+ "API_KEY": "your-api-key"
77
38
  }
78
39
  }
79
40
  }
80
41
  }
81
42
  ```
82
43
 
83
- #### Datadog API
44
+ **3. Use the tools** — discover endpoints with `list_api`, inspect schemas with `call_api`, fetch data with `query_api`.
84
45
 
85
- ```json
86
- {
87
- "mcpServers": {
88
- "datadog": {
89
- "command": "npx",
90
- "args": [
91
- "-y",
92
- "anyapi-mcp-server",
93
- "--name", "datadog",
94
- "--spec", "https://raw.githubusercontent.com/DataDog/datadog-api-client-typescript/master/.generator/schemas/v1/openapi.yaml",
95
- "--base-url", "https://api.datadoghq.com",
96
- "--header", "DD-API-KEY: ${DD_API_KEY}",
97
- "--header", "DD-APPLICATION-KEY: ${DD_APP_KEY}"
98
- ],
99
- "env": {
100
- "DD_API_KEY": "your-datadog-api-key",
101
- "DD_APP_KEY": "your-datadog-app-key"
102
- }
103
- }
104
- }
105
- }
106
- ```
46
+ ## Provider examples
107
47
 
108
- #### Metabase API
48
+ Ready-to-use configurations for popular APIs:
109
49
 
110
- ```json
111
- {
112
- "mcpServers": {
113
- "metabase": {
114
- "command": "npx",
115
- "args": [
116
- "-y",
117
- "anyapi-mcp-server",
118
- "--name", "metabase",
119
- "--base-url", "https://your-metabase-instance.com/api",
120
- "--header", "x-api-key: ${METABASE_API_KEY}"
121
- ],
122
- "env": {
123
- "METABASE_API_KEY": "your-metabase-api-key"
124
- }
125
- }
126
- }
127
- }
128
- ```
50
+ | Provider | Auth |
51
+ |----------|------|
52
+ | [Cloudflare](docs/cloudflare.md) | API Token or Key + Email |
53
+ | [Datadog](docs/datadog.md) | API Key + App Key |
54
+ | [GitHub](docs/github.md) | Personal Access Token |
55
+ | [Google Workspace](docs/google-workspace.md) | OAuth 2.0 |
56
+ | [Metabase](docs/metabase.md) | API Key |
57
+ | [PostHog](docs/posthog.md) | Personal API Key |
58
+ | [Slack](docs/slack.md) | Bot/User Token |
59
+
60
+ These work with any API that has an OpenAPI or Postman spec — the above are just examples. Stripe, Twilio, Shopify, HubSpot, and anything else with a REST API will work the same way.
61
+
62
+ ## CLI reference
63
+
64
+ ### Required flags
65
+
66
+ | Flag | Description |
67
+ |------|-------------|
68
+ | `--name` | Server name (e.g. `cloudflare`) |
69
+ | `--spec` | Path or HTTPS URL to an OpenAPI spec (JSON/YAML) or Postman Collection. Remote URLs are cached locally. Supports `${ENV_VAR}`. |
70
+ | `--base-url` | API base URL (e.g. `https://api.example.com`). Supports `${ENV_VAR}`. |
71
+
72
+ ### Optional flags
73
+
74
+ | Flag | Description |
75
+ |------|-------------|
76
+ | `--header` | HTTP header as `"Key: Value"` (repeatable). Supports `${ENV_VAR}` in values. |
77
+ | `--log` | Path to NDJSON request/response log. Sensitive headers are masked automatically. |
78
+
79
+ ### OAuth flags
80
+
81
+ For APIs that use OAuth 2.0 instead of static tokens. If any of the three required flags is provided, all three are required. All flags support `${ENV_VAR}`.
82
+
83
+ | Flag | Required | Description |
84
+ |------|----------|-------------|
85
+ | `--oauth-client-id` | Yes* | OAuth client ID |
86
+ | `--oauth-client-secret` | Yes* | OAuth client secret |
87
+ | `--oauth-token-url` | Yes* | Token endpoint URL |
88
+ | `--oauth-auth-url` | No | Authorization endpoint (auto-detected from spec if available) |
89
+ | `--oauth-scopes` | No | Comma-separated scopes |
90
+ | `--oauth-flow` | No | `authorization_code` (default) or `client_credentials` |
91
+ | `--oauth-param` | No | Extra token parameter as `key=value` (repeatable) |
92
+
93
+ See the [Google Workspace guide](docs/google-workspace.md) for a complete OAuth example.
129
94
 
130
95
  ## Tools
131
96
 
132
- The server exposes five MCP tools:
97
+ The server exposes five tools (plus `auth` when OAuth is configured):
133
98
 
134
- ### `list_api`
99
+ ### `list_api` — Browse endpoints
135
100
 
136
- Browse and search available API endpoints from the spec.
101
+ Discover what the API offers. Call with no arguments to see all categories, provide `category` to list endpoints in a tag, or `search` to find endpoints by keyword.
137
102
 
138
- - Call with no arguments to see all categories/tags
139
- - Provide `category` to list endpoints in a tag
140
- - Provide `search` to search across paths and descriptions
141
- - Results are paginated with `limit` (default 20) and `offset`
142
- - GraphQL selection for categories:
143
- ```graphql
144
- {
145
- items {
146
- tag
147
- endpointCount
148
- }
149
- _count
150
- }
151
- ```
152
- - GraphQL selection for endpoints:
153
- ```graphql
154
- {
155
- items {
156
- method
157
- path
158
- summary
159
- }
160
- _count
161
- }
162
- ```
103
+ ### `call_api` Inspect an endpoint
163
104
 
164
- ### `call_api`
105
+ Makes a real HTTP request and returns the **inferred GraphQL schema** (SDL) — not the data itself. Use this to discover the response shape and get `suggestedQueries` you can copy into `query_api`.
165
106
 
166
- Inspect an API endpoint by making a real request and returning the inferred GraphQL schema (SDL). No response data is returned — use `query_api` to fetch actual data.
107
+ ### `query_api` Fetch data
167
108
 
168
- - Returns the full schema SDL showing all available fields and types
169
- - Returns accepted parameters (name, location, required) from the API spec
170
- - Returns `suggestedQueries` — ready-to-use GraphQL queries generated from the schema
171
- - Returns `shapeHash` — a structural fingerprint of the response for cache-aware workflows
172
- - Returns `bodyHash` for write operations when a request body is provided
173
- - Accepts optional `headers` to override defaults for this request
174
- - For write operations (POST/PUT/DELETE/PATCH) with request body schemas, the schema includes a Mutation type
109
+ Fetches data and returns **only the fields you select** via a GraphQL query. Supports both reads and writes (mutations for POST/PUT/DELETE/PATCH).
175
110
 
176
- ### `query_api`
111
+ ```graphql
112
+ # Read
113
+ { items { id name status } _count }
177
114
 
178
- Fetch data from an API endpoint, returning only the fields you select via GraphQL.
115
+ # Write
116
+ mutation { post_endpoint(input: { name: "example" }) { id } }
117
+ ```
179
118
 
180
- - Object responses:
181
- ```graphql
182
- {
183
- id
184
- name
185
- collection {
186
- id
187
- name
188
- }
189
- }
190
- ```
191
- - Array responses:
192
- ```graphql
193
- {
194
- items {
195
- id
196
- name
197
- }
198
- _count
199
- }
200
- ```
201
- - Mutation syntax for writes:
202
- ```graphql
203
- mutation {
204
- post_endpoint(input: { ... }) {
205
- id
206
- name
207
- }
208
- }
209
- ```
210
- - Supports `limit` and `offset` for client-side slicing of already-fetched data
211
- - For API-level pagination, pass limit/offset inside `params` instead
212
- - Accepts optional `headers` to override defaults for this request
213
- - Response includes `_shapeHash` (and `_bodyHash` for writes) for tracking schema identity
119
+ ### `explain_api` — Read the docs
214
120
 
215
- ### `explain_api`
121
+ Returns spec documentation for an endpoint (parameters, request body schema, response codes) **without making an HTTP request**.
216
122
 
217
- Get detailed documentation for an endpoint directly from the spec no HTTP request is made.
123
+ ### `batch_query`Parallel requests
218
124
 
219
- - Returns summary, description, operationId, deprecation status, tag
220
- - Lists all parameters with name, location (`path`/`query`/`header`), required flag, and description
221
- - Shows request body schema with property types, required fields, and descriptions
222
- - Lists response status codes with descriptions (e.g. `200 OK`, `404 Not Found`)
223
- - Includes external docs link when available
125
+ Fetches data from up to 10 endpoints concurrently in a single tool call. Each request follows the `query_api` flow.
224
126
 
225
- ### `batch_query`
127
+ ### `auth` — OAuth authentication
226
128
 
227
- Fetch data from multiple endpoints concurrently in a single tool call.
129
+ Only available when `--oauth-*` flags are configured. Manages the OAuth flow:
130
+ - `action: "start"` — returns an authorization URL (or exchanges credentials for `client_credentials`)
131
+ - `action: "exchange"` — completes the authorization code flow (callback is captured automatically)
132
+ - `action: "status"` — shows current token status
228
133
 
229
- - Accepts an array of 1–10 requests, each with `method`, `path`, `params`, `body`, `query`, and optional `headers`
230
- - All requests execute in parallel via `Promise.allSettled` — one failure does not affect the others
231
- - Each request follows the `query_api` flow: HTTP fetch → schema inference → GraphQL field selection
232
- - Returns an array of results: `{ method, path, data, shapeHash }` on success or `{ method, path, error }` on failure
233
- - Run `call_api` first on each endpoint to discover the schema field names
134
+ Tokens are persisted and refreshed automatically.
234
135
 
235
- ## Workflow
136
+ ## Typical workflow
236
137
 
237
- 1. **Discover** endpoints with `list_api`
238
- 2. **Understand** an endpoint with `explain_api` to see its parameters, request body, and response codes
239
- 3. **Inspect** a specific endpoint with `call_api` to see the inferred response schema and suggested queries
240
- 4. **Query** the endpoint with `query_api` to fetch exactly the fields you need
241
- 5. **Batch** multiple queries with `batch_query` when you need data from several endpoints at once
138
+ ```
139
+ list_api → discover what's available
140
+
141
+ explain_api → read the docs for an endpoint
142
+
143
+ call_api → inspect the response schema
144
+
145
+ query_api → fetch exactly the fields you need
146
+
147
+ batch_query → fetch from multiple endpoints at once
148
+ ```
242
149
 
243
- ## How It Works
150
+ ## How it works
244
151
 
245
152
  ```
246
153
  OpenAPI/Postman spec
@@ -263,19 +170,30 @@ OpenAPI/Postman spec
263
170
  response data
264
171
  ```
265
172
 
266
- 1. The spec is loaded at startup (from a local file or fetched from an HTTPS URL with filesystem caching) and parsed into an endpoint index with tags, paths, parameters, and request body schemas
267
- 2. `call_api` makes a real HTTP request, infers a GraphQL schema from the JSON response, and caches both the response (30s TTL) and the schema. Schemas are keyed by endpoint + response shape, so the same path returning different structures gets distinct schemas
268
- 3. `query_api` re-uses the cached response if called within 30s, executes your GraphQL field selection against the data, and returns only the fields you asked for. Includes `_shapeHash` in the response for tracking schema identity
269
- 4. Write operations (POST/PUT/DELETE/PATCH) with OpenAPI request body schemas get a Mutation type with typed `GraphQLInputObjectType` inputs
173
+ ## Features
270
174
 
271
- ## Supported Spec Formats
175
+ - **Any REST API** — provide an OpenAPI (JSON/YAML) or Postman Collection v2.x spec as a file or URL
176
+ - **Remote spec caching** — HTTPS specs are fetched once and cached to `~/.cache/anyapi-mcp/`
177
+ - **GraphQL field selection** — query only the fields you need from any response
178
+ - **Schema inference** — automatically builds GraphQL schemas from live API responses
179
+ - **Multi-sample merging** — samples up to 10 array elements for richer schemas
180
+ - **Mutation support** — write operations get typed GraphQL mutations from OpenAPI body schemas
181
+ - **Smart suggestions** — `call_api` returns ready-to-use queries based on the inferred schema
182
+ - **Response caching** — 30s TTL prevents duplicate calls across `call_api` → `query_api`
183
+ - **Retry with backoff** — automatic retries for 429/5xx with exponential backoff and `Retry-After` support
184
+ - **Multi-format** — parses JSON, XML, CSV, and plain text responses
185
+ - **Pagination** — API-level via `params`, client-side slicing via `limit`/`offset`
186
+ - **Rich errors** — structured error messages with status-specific suggestions and spec context for self-correction
187
+ - **OAuth 2.0** — Authorization Code (with PKCE) and Client Credentials flows with automatic token refresh
188
+ - **Env var interpolation** — `${ENV_VAR}` in base URLs, headers, and spec paths
189
+ - **Request logging** — optional NDJSON log with sensitive header masking
190
+
191
+ ## Supported spec formats
272
192
 
273
193
  - **OpenAPI 3.x** (JSON or YAML)
274
194
  - **OpenAPI 2.0 / Swagger** (JSON or YAML)
275
195
  - **Postman Collection v2.x** (JSON)
276
196
 
277
- `$ref` resolution is supported for OpenAPI request body schemas. Postman `:param` path variables are converted to OpenAPI-style `{param}` automatically.
278
-
279
197
  ## License
280
198
 
281
199
  Proprietary Non-Commercial. Free for personal and educational use. Commercial use requires written permission. See [LICENSE](LICENSE) for details.
@@ -2,6 +2,8 @@ import { withRetry, RetryableError, isRetryableStatus } from "./retry.js";
2
2
  import { buildCacheKey, consumeCached, setCache } from "./response-cache.js";
3
3
  import { logEntry, isLoggingEnabled } from "./logger.js";
4
4
  import { parseResponse } from "./response-parser.js";
5
+ import { ApiError } from "./error-context.js";
6
+ import { getValidAccessToken, refreshTokens } from "./oauth.js";
5
7
  const TIMEOUT_MS = 30_000;
6
8
  function interpolatePath(pathTemplate, params) {
7
9
  const remaining = { ...params };
@@ -43,72 +45,109 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
43
45
  }
44
46
  fullUrl += `?${qs.toString()}`;
45
47
  }
46
- const mergedHeaders = {
47
- "Content-Type": "application/json",
48
+ // Check if an explicit Authorization header was provided by the caller
49
+ const hasExplicitAuth = Object.keys({
48
50
  ...config.headers,
49
51
  ...extraHeaders,
50
- };
51
- // --- Retry-wrapped fetch ---
52
- const result = await withRetry(async () => {
53
- const controller = new AbortController();
54
- const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
55
- const startTime = Date.now();
56
- try {
57
- const fetchOptions = {
58
- method,
59
- headers: mergedHeaders,
60
- signal: controller.signal,
61
- };
62
- if (body && method !== "GET") {
63
- fetchOptions.body = JSON.stringify(body);
52
+ }).some((k) => k.toLowerCase() === "authorization");
53
+ const doRequest = async () => {
54
+ const mergedHeaders = {
55
+ "Content-Type": "application/json",
56
+ ...config.headers,
57
+ ...extraHeaders,
58
+ };
59
+ // Inject OAuth bearer token if no explicit Authorization header
60
+ if (!hasExplicitAuth) {
61
+ const accessToken = await getValidAccessToken(config.oauth);
62
+ if (accessToken) {
63
+ mergedHeaders["Authorization"] = `Bearer ${accessToken}`;
64
64
  }
65
- const response = await fetch(fullUrl, fetchOptions);
66
- const durationMs = Date.now() - startTime;
67
- const bodyText = await response.text();
68
- // Log request/response
69
- if (isLoggingEnabled()) {
65
+ }
66
+ return withRetry(async () => {
67
+ const controller = new AbortController();
68
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
69
+ const startTime = Date.now();
70
+ try {
71
+ const fetchOptions = {
72
+ method,
73
+ headers: mergedHeaders,
74
+ signal: controller.signal,
75
+ };
76
+ if (body && method !== "GET") {
77
+ fetchOptions.body = JSON.stringify(body);
78
+ }
79
+ const response = await fetch(fullUrl, fetchOptions);
80
+ const durationMs = Date.now() - startTime;
81
+ const bodyText = await response.text();
70
82
  const responseHeaders = {};
71
83
  response.headers.forEach((v, k) => {
72
84
  responseHeaders[k] = v;
73
85
  });
74
- await logEntry({
75
- timestamp: new Date().toISOString(),
76
- method,
77
- url: fullUrl,
78
- requestHeaders: mergedHeaders,
79
- requestBody: body,
80
- responseStatus: response.status,
81
- responseHeaders,
82
- responseBody: bodyText,
83
- durationMs,
84
- });
85
- }
86
- if (!response.ok) {
87
- const msg = `API error ${response.status} ${response.statusText}: ${bodyText}`;
88
- if (isRetryableStatus(response.status)) {
89
- let retryAfterMs;
90
- const retryAfter = response.headers.get("retry-after");
91
- if (retryAfter) {
92
- const seconds = parseInt(retryAfter, 10);
93
- if (!isNaN(seconds))
94
- retryAfterMs = seconds * 1000;
86
+ // Log request/response
87
+ if (isLoggingEnabled()) {
88
+ await logEntry({
89
+ timestamp: new Date().toISOString(),
90
+ method,
91
+ url: fullUrl,
92
+ requestHeaders: mergedHeaders,
93
+ requestBody: body,
94
+ responseStatus: response.status,
95
+ responseHeaders,
96
+ responseBody: bodyText,
97
+ durationMs,
98
+ });
99
+ }
100
+ if (!response.ok) {
101
+ if (isRetryableStatus(response.status)) {
102
+ const msg = `API error ${response.status} ${response.statusText}: ${bodyText}`;
103
+ let retryAfterMs;
104
+ const retryAfter = response.headers.get("retry-after");
105
+ if (retryAfter) {
106
+ const seconds = parseInt(retryAfter, 10);
107
+ if (!isNaN(seconds))
108
+ retryAfterMs = seconds * 1000;
109
+ }
110
+ throw new RetryableError(msg, response.status, retryAfterMs);
95
111
  }
96
- throw new RetryableError(msg, response.status, retryAfterMs);
112
+ throw new ApiError(`API error ${response.status} ${response.statusText}`, response.status, response.statusText, bodyText, responseHeaders);
97
113
  }
98
- throw new Error(msg);
114
+ const parsedData = parseResponse(response.headers.get("content-type"), bodyText);
115
+ return { data: parsedData, responseHeaders };
99
116
  }
100
- return parseResponse(response.headers.get("content-type"), bodyText);
101
- }
102
- catch (error) {
103
- if (error instanceof DOMException && error.name === "AbortError") {
104
- throw new Error(`Request to ${method} ${pathTemplate} timed out after ${TIMEOUT_MS / 1000}s`);
117
+ catch (error) {
118
+ if (error instanceof DOMException && error.name === "AbortError") {
119
+ throw new Error(`Request to ${method} ${pathTemplate} timed out after ${TIMEOUT_MS / 1000}s`);
120
+ }
121
+ throw error;
122
+ }
123
+ finally {
124
+ clearTimeout(timeout);
125
+ }
126
+ });
127
+ };
128
+ // --- Execute with 401 refresh-and-retry ---
129
+ let result;
130
+ try {
131
+ result = await doRequest();
132
+ }
133
+ catch (error) {
134
+ if (error instanceof ApiError &&
135
+ error.status === 401 &&
136
+ config.oauth &&
137
+ !hasExplicitAuth) {
138
+ // Refresh token and retry once
139
+ try {
140
+ await refreshTokens(config.oauth);
141
+ result = await doRequest();
142
+ }
143
+ catch {
144
+ throw error; // Throw the original 401
105
145
  }
106
- throw error;
107
146
  }
108
- finally {
109
- clearTimeout(timeout);
147
+ else {
148
+ throw error;
110
149
  }
111
- });
150
+ }
112
151
  // --- Cache store (populate mode only) ---
113
152
  if (cacheMode === "populate") {
114
153
  setCache(cacheKey, result);
@@ -118,6 +118,7 @@ function postmanUrlToPath(url) {
118
118
  export class ApiIndex {
119
119
  byTag = new Map();
120
120
  allEndpoints = [];
121
+ oauthSchemes = [];
121
122
  constructor(specContents) {
122
123
  for (const specContent of specContents) {
123
124
  let parsed;
@@ -185,6 +186,46 @@ export class ApiIndex {
185
186
  this.addEndpoint(endpoint);
186
187
  }
187
188
  }
189
+ this.extractSecuritySchemes(rawSpec);
190
+ }
191
+ extractSecuritySchemes(spec) {
192
+ // OpenAPI 3.x: components.securitySchemes
193
+ const components = spec.components;
194
+ const securitySchemes = components?.securitySchemes;
195
+ // OpenAPI 2.x (Swagger): securityDefinitions
196
+ const securityDefs = spec.securityDefinitions;
197
+ const schemes = securitySchemes ?? securityDefs ?? {};
198
+ for (const schemeDef of Object.values(schemes)) {
199
+ if (schemeDef.type !== "oauth2")
200
+ continue;
201
+ // OpenAPI 3.x: flows.authorizationCode, flows.clientCredentials, etc.
202
+ const flows = schemeDef.flows;
203
+ if (flows) {
204
+ for (const flow of Object.values(flows)) {
205
+ const scopes = flow.scopes
206
+ ? Object.keys(flow.scopes)
207
+ : [];
208
+ this.oauthSchemes.push({
209
+ authorizationUrl: flow.authorizationUrl,
210
+ tokenUrl: flow.tokenUrl,
211
+ scopes,
212
+ });
213
+ }
214
+ continue;
215
+ }
216
+ // OpenAPI 2.x (Swagger): direct fields on the scheme
217
+ const scopes = schemeDef.scopes
218
+ ? Object.keys(schemeDef.scopes)
219
+ : [];
220
+ this.oauthSchemes.push({
221
+ authorizationUrl: schemeDef.authorizationUrl,
222
+ tokenUrl: schemeDef.tokenUrl,
223
+ scopes,
224
+ });
225
+ }
226
+ }
227
+ getOAuthSchemes() {
228
+ return this.oauthSchemes;
188
229
  }
189
230
  parsePostman(collection) {
190
231
  this.walkPostmanItems(collection.item ?? [], []);