anyapi-mcp-server 1.6.0 → 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 +111 -195
- package/build/api-client.js +93 -55
- package/build/api-index.js +41 -0
- package/build/config.js +55 -1
- package/build/error-context.js +1 -1
- package/build/index.js +172 -4
- package/build/oauth.js +340 -0
- package/build/response-parser.js +47 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,240 +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
|
-
- **Rich error context** — API errors return structured messages (parses RFC 7807, `{ error: { message, code } }`, `{ errors: [...] }`, and more), status-specific suggestions (e.g. "Authentication required" for 401), and relevant spec info (required parameters, request body schema) for 400/422 errors so the LLM can self-correct
|
|
32
|
-
- **Request logging** — optional NDJSON request/response log with sensitive header masking
|
|
33
|
-
|
|
34
11
|
[](https://www.npmjs.com/package/anyapi-mcp-server)
|
|
35
12
|
|
|
36
|
-
##
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
**1. Install**
|
|
37
16
|
|
|
38
17
|
```bash
|
|
39
18
|
npm install -g anyapi-mcp-server
|
|
40
19
|
```
|
|
41
20
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
| Flag | Description |
|
|
45
|
-
|------|-------------|
|
|
46
|
-
| `--name` | Server name (e.g. `petstore`) |
|
|
47
|
-
| `--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. |
|
|
48
|
-
| `--base-url` | API base URL (e.g. `https://api.example.com`). Supports `${ENV_VAR}` interpolation. |
|
|
49
|
-
|
|
50
|
-
### Optional arguments
|
|
51
|
-
|
|
52
|
-
| Flag | Description |
|
|
53
|
-
|------|-------------|
|
|
54
|
-
| `--header` | HTTP header as `"Key: Value"` (repeatable). Supports `${ENV_VAR}` interpolation in values. |
|
|
55
|
-
| `--log` | Path to request/response log file (NDJSON format). Sensitive headers are masked automatically. |
|
|
56
|
-
|
|
57
|
-
### Example: Cursor / Claude Desktop configuration
|
|
58
|
-
|
|
59
|
-
Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop config):
|
|
60
|
-
|
|
61
|
-
#### Cloudflare API
|
|
21
|
+
**2. Add to your MCP client** (Cursor, Claude Desktop, etc.)
|
|
62
22
|
|
|
63
23
|
```json
|
|
64
24
|
{
|
|
65
25
|
"mcpServers": {
|
|
66
|
-
"
|
|
26
|
+
"your-api": {
|
|
67
27
|
"command": "npx",
|
|
68
28
|
"args": [
|
|
69
29
|
"-y",
|
|
70
30
|
"anyapi-mcp-server",
|
|
71
|
-
"--name", "
|
|
72
|
-
"--spec", "
|
|
73
|
-
"--base-url", "https://api.
|
|
74
|
-
"--header", "Authorization: Bearer ${
|
|
31
|
+
"--name", "your-api",
|
|
32
|
+
"--spec", "path/to/openapi.json",
|
|
33
|
+
"--base-url", "https://api.example.com",
|
|
34
|
+
"--header", "Authorization: Bearer ${API_KEY}"
|
|
75
35
|
],
|
|
76
36
|
"env": {
|
|
77
|
-
"
|
|
37
|
+
"API_KEY": "your-api-key"
|
|
78
38
|
}
|
|
79
39
|
}
|
|
80
40
|
}
|
|
81
41
|
}
|
|
82
42
|
```
|
|
83
43
|
|
|
84
|
-
|
|
44
|
+
**3. Use the tools** — discover endpoints with `list_api`, inspect schemas with `call_api`, fetch data with `query_api`.
|
|
85
45
|
|
|
86
|
-
|
|
87
|
-
{
|
|
88
|
-
"mcpServers": {
|
|
89
|
-
"datadog": {
|
|
90
|
-
"command": "npx",
|
|
91
|
-
"args": [
|
|
92
|
-
"-y",
|
|
93
|
-
"anyapi-mcp-server",
|
|
94
|
-
"--name", "datadog",
|
|
95
|
-
"--spec", "https://raw.githubusercontent.com/DataDog/datadog-api-client-typescript/master/.generator/schemas/v1/openapi.yaml",
|
|
96
|
-
"--base-url", "https://api.datadoghq.com",
|
|
97
|
-
"--header", "DD-API-KEY: ${DD_API_KEY}",
|
|
98
|
-
"--header", "DD-APPLICATION-KEY: ${DD_APP_KEY}"
|
|
99
|
-
],
|
|
100
|
-
"env": {
|
|
101
|
-
"DD_API_KEY": "your-datadog-api-key",
|
|
102
|
-
"DD_APP_KEY": "your-datadog-app-key"
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
```
|
|
46
|
+
## Provider examples
|
|
108
47
|
|
|
109
|
-
|
|
48
|
+
Ready-to-use configurations for popular APIs:
|
|
110
49
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
130
94
|
|
|
131
95
|
## Tools
|
|
132
96
|
|
|
133
|
-
The server exposes five
|
|
97
|
+
The server exposes five tools (plus `auth` when OAuth is configured):
|
|
134
98
|
|
|
135
|
-
### `list_api`
|
|
99
|
+
### `list_api` — Browse endpoints
|
|
136
100
|
|
|
137
|
-
|
|
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.
|
|
138
102
|
|
|
139
|
-
|
|
140
|
-
- Provide `category` to list endpoints in a tag
|
|
141
|
-
- Provide `search` to search across paths and descriptions
|
|
142
|
-
- Results are paginated with `limit` (default 20) and `offset`
|
|
143
|
-
- GraphQL selection for categories:
|
|
144
|
-
```graphql
|
|
145
|
-
{
|
|
146
|
-
items {
|
|
147
|
-
tag
|
|
148
|
-
endpointCount
|
|
149
|
-
}
|
|
150
|
-
_count
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
- GraphQL selection for endpoints:
|
|
154
|
-
```graphql
|
|
155
|
-
{
|
|
156
|
-
items {
|
|
157
|
-
method
|
|
158
|
-
path
|
|
159
|
-
summary
|
|
160
|
-
}
|
|
161
|
-
_count
|
|
162
|
-
}
|
|
163
|
-
```
|
|
103
|
+
### `call_api` — Inspect an endpoint
|
|
164
104
|
|
|
165
|
-
|
|
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`.
|
|
166
106
|
|
|
167
|
-
|
|
107
|
+
### `query_api` — Fetch data
|
|
168
108
|
|
|
169
|
-
|
|
170
|
-
- Returns accepted parameters (name, location, required) from the API spec
|
|
171
|
-
- Returns `suggestedQueries` — ready-to-use GraphQL queries generated from the schema
|
|
172
|
-
- Returns `shapeHash` — a structural fingerprint of the response for cache-aware workflows
|
|
173
|
-
- Returns `bodyHash` for write operations when a request body is provided
|
|
174
|
-
- Accepts optional `headers` to override defaults for this request
|
|
175
|
-
- 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).
|
|
176
110
|
|
|
177
|
-
|
|
111
|
+
```graphql
|
|
112
|
+
# Read
|
|
113
|
+
{ items { id name status } _count }
|
|
178
114
|
|
|
179
|
-
|
|
115
|
+
# Write
|
|
116
|
+
mutation { post_endpoint(input: { name: "example" }) { id } }
|
|
117
|
+
```
|
|
180
118
|
|
|
181
|
-
|
|
182
|
-
```graphql
|
|
183
|
-
{
|
|
184
|
-
id
|
|
185
|
-
name
|
|
186
|
-
collection {
|
|
187
|
-
id
|
|
188
|
-
name
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
- Array responses:
|
|
193
|
-
```graphql
|
|
194
|
-
{
|
|
195
|
-
items {
|
|
196
|
-
id
|
|
197
|
-
name
|
|
198
|
-
}
|
|
199
|
-
_count
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
- Mutation syntax for writes:
|
|
203
|
-
```graphql
|
|
204
|
-
mutation {
|
|
205
|
-
post_endpoint(input: { ... }) {
|
|
206
|
-
id
|
|
207
|
-
name
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
- Supports `limit` and `offset` for client-side slicing of already-fetched data
|
|
212
|
-
- For API-level pagination, pass limit/offset inside `params` instead
|
|
213
|
-
- Accepts optional `headers` to override defaults for this request
|
|
214
|
-
- Response includes `_shapeHash` (and `_bodyHash` for writes) for tracking schema identity
|
|
119
|
+
### `explain_api` — Read the docs
|
|
215
120
|
|
|
216
|
-
|
|
121
|
+
Returns spec documentation for an endpoint (parameters, request body schema, response codes) **without making an HTTP request**.
|
|
217
122
|
|
|
218
|
-
|
|
123
|
+
### `batch_query` — Parallel requests
|
|
219
124
|
|
|
220
|
-
|
|
221
|
-
- Lists all parameters with name, location (`path`/`query`/`header`), required flag, and description
|
|
222
|
-
- Shows request body schema with property types, required fields, and descriptions
|
|
223
|
-
- Lists response status codes with descriptions (e.g. `200 OK`, `404 Not Found`)
|
|
224
|
-
- 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.
|
|
225
126
|
|
|
226
|
-
### `
|
|
127
|
+
### `auth` — OAuth authentication
|
|
227
128
|
|
|
228
|
-
|
|
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
|
|
229
133
|
|
|
230
|
-
|
|
231
|
-
- All requests execute in parallel via `Promise.allSettled` — one failure does not affect the others
|
|
232
|
-
- Each request follows the `query_api` flow: HTTP fetch → schema inference → GraphQL field selection
|
|
233
|
-
- Returns an array of results: `{ method, path, data, shapeHash }` on success or a structured error with status, suggestion, and spec context on failure
|
|
234
|
-
- Run `call_api` first on each endpoint to discover the schema field names
|
|
134
|
+
Tokens are persisted and refreshed automatically.
|
|
235
135
|
|
|
236
|
-
##
|
|
136
|
+
## Typical workflow
|
|
237
137
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
+
```
|
|
243
149
|
|
|
244
|
-
## How
|
|
150
|
+
## How it works
|
|
245
151
|
|
|
246
152
|
```
|
|
247
153
|
OpenAPI/Postman spec
|
|
@@ -264,20 +170,30 @@ OpenAPI/Postman spec
|
|
|
264
170
|
response data
|
|
265
171
|
```
|
|
266
172
|
|
|
267
|
-
|
|
268
|
-
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
|
|
269
|
-
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
|
|
270
|
-
4. Write operations (POST/PUT/DELETE/PATCH) with OpenAPI request body schemas get a Mutation type with typed `GraphQLInputObjectType` inputs
|
|
271
|
-
5. When an API call fails, the error response includes a parsed error message (extracted from common formats like RFC 7807 Problem Details, `{ error: { message } }`, GraphQL-style `{ errors: [...] }`), the HTTP status code, a status-specific suggestion for what to try next, and — for validation errors (400/422) — the full parameter list and request body schema from the spec
|
|
173
|
+
## Features
|
|
272
174
|
|
|
273
|
-
|
|
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
|
|
274
192
|
|
|
275
193
|
- **OpenAPI 3.x** (JSON or YAML)
|
|
276
194
|
- **OpenAPI 2.0 / Swagger** (JSON or YAML)
|
|
277
195
|
- **Postman Collection v2.x** (JSON)
|
|
278
196
|
|
|
279
|
-
`$ref` resolution is supported for OpenAPI request body schemas. Postman `:param` path variables are converted to OpenAPI-style `{param}` automatically.
|
|
280
|
-
|
|
281
197
|
## License
|
|
282
198
|
|
|
283
199
|
Proprietary Non-Commercial. Free for personal and educational use. Commercial use requires written permission. See [LICENSE](LICENSE) for details.
|
package/build/api-client.js
CHANGED
|
@@ -3,6 +3,7 @@ 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
5
|
import { ApiError } from "./error-context.js";
|
|
6
|
+
import { getValidAccessToken, refreshTokens } from "./oauth.js";
|
|
6
7
|
const TIMEOUT_MS = 30_000;
|
|
7
8
|
function interpolatePath(pathTemplate, params) {
|
|
8
9
|
const remaining = { ...params };
|
|
@@ -44,72 +45,109 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
44
45
|
}
|
|
45
46
|
fullUrl += `?${qs.toString()}`;
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Check if an explicit Authorization header was provided by the caller
|
|
49
|
+
const hasExplicitAuth = Object.keys({
|
|
49
50
|
...config.headers,
|
|
50
51
|
...extraHeaders,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (body && method !== "GET") {
|
|
64
|
-
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}`;
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Log request/response
|
|
74
|
-
if (isLoggingEnabled()) {
|
|
75
|
-
await logEntry({
|
|
76
|
-
timestamp: new Date().toISOString(),
|
|
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 = {
|
|
77
72
|
method,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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();
|
|
82
|
+
const responseHeaders = {};
|
|
83
|
+
response.headers.forEach((v, k) => {
|
|
84
|
+
responseHeaders[k] = v;
|
|
85
85
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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);
|
|
96
111
|
}
|
|
97
|
-
throw new
|
|
112
|
+
throw new ApiError(`API error ${response.status} ${response.statusText}`, response.status, response.statusText, bodyText, responseHeaders);
|
|
98
113
|
}
|
|
99
|
-
|
|
114
|
+
const parsedData = parseResponse(response.headers.get("content-type"), bodyText);
|
|
115
|
+
return { data: parsedData, responseHeaders };
|
|
100
116
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
throw
|
|
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
|
|
106
145
|
}
|
|
107
|
-
throw error;
|
|
108
146
|
}
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
else {
|
|
148
|
+
throw error;
|
|
111
149
|
}
|
|
112
|
-
}
|
|
150
|
+
}
|
|
113
151
|
// --- Cache store (populate mode only) ---
|
|
114
152
|
if (cacheMode === "populate") {
|
|
115
153
|
setCache(cacheKey, result);
|
package/build/api-index.js
CHANGED
|
@@ -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 ?? [], []);
|