swagger-digest 0.3.0 → 0.4.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,101 +1,143 @@
1
+ <img width="1376" height="768" alt="Image" src="https://github.com/user-attachments/assets/6a9e4eb3-fea8-47b3-9296-e57b4a702c7c" />
2
+
1
3
  # swagger-digest
2
4
 
3
- `swagger-digest` is an interactive Node.js CLI that converts a Swagger UI page or raw OpenAPI/Swagger spec into a compact Markdown reference designed for AI coding agents and LLM context windows.
5
+ [![npm version](https://img.shields.io/npm/v/swagger-digest)](https://www.npmjs.com/package/swagger-digest)
6
+ [![license](https://img.shields.io/npm/l/swagger-digest)](https://github.com/VladLegkowski/swagger-digest)
7
+ [![node](https://img.shields.io/badge/node-%3E%3D20-blue)](https://www.npmjs.com/package/swagger-digest)
8
+ [![MCP server](https://img.shields.io/badge/MCP-server-8a5cf5)](https://modelcontextprotocol.io)
4
9
 
5
- ## Quick start
10
+ **An API context engine for AI coding agents.** Turn large Swagger/OpenAPI specifications into compact, structured, searchable API context — as Markdown digests or through an on-demand MCP server.
6
11
 
7
- ```bash
8
- npx swagger-digest
9
- ```
12
+ A 600-operation spec like Stripe's is ~2M tokens of raw JSON — several times most models' context windows, and mostly noise even when it fits. swagger-digest reduces it to ~560k tokens of structured Markdown (benchmark below), and its MCP server lets your agent pull in only the endpoints and schemas it needs, when it needs them.
10
13
 
11
- The CLI asks for a URL, a destination directory (Downloads by default), and an output filename such as `payments-api-digest.md`.
14
+ ## The problem
12
15
 
13
- You can also provide arguments:
16
+ - Real specifications contain hundreds of endpoints and thousands of lines of JSON/YAML.
17
+ - Pasting a raw spec into an AI coding agent burns context on boilerplate — descriptions repeated per operation, example payloads, schema trees.
18
+ - Finding the endpoints relevant to your task — and understanding their parameters and response shapes — means manually wading through the contract.
19
+ - Agents generating frontend code need accurate API contracts. Guessing field names from partial context produces broken integrations.
14
20
 
15
- ```bash
16
- npx swagger-digest \
17
- --url https://petstore.swagger.io/ \
18
- --out-dir ./docs \
19
- --filename petstore-digest.md
20
- ```
21
+ swagger-digest solves this at two levels: a **digest** (compact, complete, token-budgeted Markdown) and an **MCP server** (on-demand API context for agents).
22
+
23
+ ## Two ways to use it
24
+
25
+ ### Option 1: MCP server — recommended for AI coding agents
21
26
 
22
- For a direct spec URL:
27
+ Run it as an [MCP](https://modelcontextprotocol.io) server and your agent explores APIs on its own: it calls the `swagger_digest` tool with a Swagger UI page or spec URL and receives a compact digest directly in its context — discovery, filtering and token budgets handled for it. Optionally, it can also execute requests against a trusted API with the `api_call` tool (see [Live API execution](#live-api-execution-api_call)).
28
+
29
+ - **On-demand:** the agent fetches API context only when the task needs it.
30
+ - **Scoped:** filter to specific tags or paths so only relevant operations enter context.
31
+ - **Discovery built in:** point it at a Swagger UI page — it finds the spec automatically (initializer scripts, multi-spec dropdowns, common spec paths).
32
+ - **Ideal for ongoing development work**, where the agent revisits the same API repeatedly without you re-pasting documentation.
33
+
34
+ Install in your MCP client (stdio transport; the client spawns the process):
23
35
 
24
36
  ```bash
25
- npx swagger-digest --url https://example.com/openapi.yaml --out-dir ./docs
37
+ # OpenCode
38
+ opencode mcp add swagger-digest -- npx -y swagger-digest-mcp
26
39
  ```
27
40
 
28
- If the Swagger UI initializer lists several spec definitions (`urls: [{ url, name }, …]`), the tool probes them in the order they appear and uses the first one that parses, printing a notice to stderr naming the pick and the alternatives. To choose a specific spec yourself, pass it explicitly:
41
+ ```json
42
+ // JSON-configured clients (Claude Desktop, Cursor, …)
43
+ {
44
+ "mcpServers": {
45
+ "swagger-digest": {
46
+ "command": ["npx", "-y", "swagger-digest-mcp"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ Tool arguments and environment variables are documented in [MCP configuration](#mcp-configuration).
53
+
54
+ ### Option 2: Markdown digest
55
+
56
+ Generate a compact Markdown reference from any Swagger UI page or OpenAPI/Swagger spec — for documentation, sharing, or feeding API context into agents that don't use MCP.
29
57
 
30
58
  ```bash
31
- npx swagger-digest --url https://api.example.com/swagger-ui \
32
- --spec https://api.example.com/v2/tenant-a.json --out-dir ./docs
59
+ # Interactive
60
+ npx swagger-digest
61
+
62
+ # Direct: fetch, digest, save
63
+ npx swagger-digest --url https://petstore.swagger.io/ --out-dir ./docs
64
+
65
+ # To stdout (pipes stay clean; progress goes to stderr)
66
+ npx swagger-digest --url https://petstore.swagger.io/ --stdout | pbcopy
67
+
68
+ # Keep only the operations you need
69
+ npx swagger-digest --url https://api.example.com/openapi.json \
70
+ --include-tag Subscriptions --path-prefix /v1/ --out-dir ./docs
33
71
  ```
34
72
 
35
- `--spec` overrides all discovery: the given URL is fetched directly (with the same header scoping and size limits) and the Swagger UI page is never consulted when it parses successfully.
73
+ Output is a structured Markdown document: endpoint index, per-tag operation details (auth, parameters, request/response bodies, examples), and TypeScript-like data type declarations. Save it next to your code, paste it into a PR, or drop it into your agent's context manually.
36
74
 
37
- ### Writing to stdout
75
+ ## Quick start
38
76
 
39
77
  ```bash
40
- npx swagger-digest --url https://petstore.swagger.io/ --stdout > petstore.md
41
- npx swagger-digest --url https://api.example.com/openapi.json --stdout | pbcopy
78
+ # 1. Generate a digest from a public API (interactive prompts, saved to ~/Downloads)
79
+ npx swagger-digest --url https://petstore.swagger.io/
80
+
81
+ # 2. Or generate to stdout
82
+ npx swagger-digest --url https://petstore.swagger.io/ --stdout > petstore-digest.md
83
+
84
+ # 3. Register the MCP server (OpenCode example)
85
+ opencode mcp add swagger-digest -- npx -y swagger-digest-mcp
42
86
  ```
43
87
 
44
- `--stdout` prints the digest to standard output instead of saving a file — handy for piping into other tools or copying to the clipboard. Progress and warnings go to stderr, so the pipe receives only the Markdown. `--stdout` cannot be combined with `--out-dir` or `--filename`; the CLI exits with a clear error if you try.
88
+ Requires Node.js 20+. Run `npx swagger-digest --help` for all options.
45
89
 
46
- Run `npx swagger-digest --help` for all CLI options. The package requires Node.js 20 or newer.
90
+ ## Real-world example: building a subscriptions page
47
91
 
48
- ## MCP server
92
+ A React/TypeScript developer asks their AI coding agent:
49
93
 
50
- The package ships an [MCP](https://modelcontextprotocol.io) server exposing the same pipeline as a tool for AI agents, so they can fetch and digest API docs on their own instead of being handed a Markdown file.
94
+ > "Implement a subscription management page against our billing API — docs are at https://api.example.com/swagger-ui"
51
95
 
52
- ```bash
53
- npx swagger-digest-mcp
54
- ```
96
+ **Without swagger-digest**, the agent either guesses the API contract or you paste thousands of lines of raw JSON into the conversation.
55
97
 
56
- It speaks the stdio transport: the MCP client spawns the process and communicates over stdin/stdout. Register it in your client's configuration:
98
+ **With the MCP server**, the agent calls the tool itself:
57
99
 
58
100
  ```json
59
101
  {
60
- "mcp": {
61
- "swagger-digest": {
62
- "command": ["npx", "-y", "swagger-digest-mcp"]
63
- }
64
- }
102
+ "url": "https://api.example.com/swagger-ui",
103
+ "include_tags": ["Subscriptions"]
65
104
  }
66
105
  ```
67
106
 
68
- ### The `swagger_digest` tool
107
+ and receives only the relevant slice — in this illustrative excerpt of digest output:
69
108
 
70
- | Argument | Type | Description |
71
- | --------------- | ---------------- | -------------------------------------------------------------------------- |
72
- | `url` | string, required | Swagger UI page URL or direct OpenAPI spec URL |
73
- | `spec_url` | string, optional | Explicit spec URL; overrides discovery from the page |
74
- | `headers` | object, optional | Request headers (e.g. `Authorization`); sent only to the origin of `url` |
75
- | `include_tags` | string[] | Keep only operations with these tags |
76
- | `exclude_tags` | string[] | Drop operations with these tags |
77
- | `path_prefix` | string | Keep only paths starting with this prefix |
78
- | `max_tokens` | number | Trim the digest to an approximate token budget |
79
- | `write_to_file` | object, optional | `{ directory, filename }` — save the digest instead of returning it inline |
109
+ ```markdown
110
+ #### `GET /v1/subscriptions/{id}`
80
111
 
81
- By default the digest is returned directly in the tool result (so it lands in the agent's context), with a metadata footer: retrieval method, operation/model counts, and a token estimate.
112
+ **Authentication:** Bearer
82
113
 
83
- ### Environment variables
114
+ **Parameters**
84
115
 
85
- - `SWAGGER_DIGEST_HEADER` — newline-separated `Name: value` pairs, merged with (and overridden by) the tool call's `headers` argument. Keeps secrets out of the conversation.
86
- - `SWAGGER_DIGEST_ALLOW_PRIVATE=1` — opt out of the private-network guard (see below).
116
+ - `id` (`path`, required) · `string` — Subscription identifier.
117
+ - `expand` (`query`, optional) · `string[]` — Fields to expand.
87
118
 
88
- ### Security differences from the CLI
119
+ **Responses**
89
120
 
90
- - **SSRF guard on by default:** URLs resolving to loopback, private, or link-local (cloud metadata) addresses are rejected, because MCP clients supply URLs on the model's behalf. Opt out with `SWAGGER_DIGEST_ALLOW_PRIVATE=1` — for example to digest an internal corporate API — only if you trust the hosts involved.
91
- - **No headless-browser fallback:** the optional Playwright fallback is disabled in the MCP server.
92
- - Diagnostics go to stderr only; stdout carries protocol frames exclusively.
121
+ - **200 OK** — `Subscription` (application/json)
122
+
123
+ ## Data types
124
+
125
+ export interface Subscription {
126
+ id?: string;
127
+ status?: "active" | "canceled" | "past_due";
128
+ current_period_end?: number;
129
+ items?: SubscriptionItems;
130
+ [key: string]: unknown;
131
+ }
132
+ ```
133
+
134
+ From there the agent can: confirm the exact path, method and required parameters; read the `Subscription` schema and its referenced types; generate TypeScript types that match the contract; and implement the page without guessing. If the digest is too large, the agent re-calls with `path_prefix` or `max_tokens` to narrow it further.
93
135
 
94
136
  ## Benchmarks
95
137
 
96
138
  Ten public APIs, digest vs. raw spec JSON in an LLM context, ground truth from the parsed raw specs. Run in November 2026 against swagger-digest 0.2.0; token counts are heuristic estimates, not tokenizer-exact.
97
139
 
98
- **7/10 wins · 100% operation coverage on every digest produced · 1.6–3.7× lighter · 1 call vs 1–5**
140
+ **7/10 wins · 100% operation coverage on every digest produced · 1.6–3.7× lighter · 1 tool call vs 1–5 fetch/guess attempts**
99
141
 
100
142
  | Spec | Ops | Raw tokens | Digest tokens | Compression | Op coverage | Winner |
101
143
  | ------------------- | ----- | ---------------------- | ----------------------- | ----------- | ----------- | ------- |
@@ -112,6 +154,8 @@ Ten public APIs, digest vs. raw spec JSON in an LLM context, ground truth from t
112
154
 
113
155
  > For its target use case — understanding an API in an LLM context — the digest wins on the spec class that actually matters (up to ~600 operations): 100% endpoint coverage, never truncated, one call, and 1.6–3.7× lighter. The bigger the spec, the bigger the win.
114
156
 
157
+ **Why token efficiency matters:** an agent's context window is shared between your codebase, the conversation, and every tool result. A raw spec that consumes 500k tokens crowds out everything else; the same API as a 150k-token digest leaves room to work — and smaller digests mean faster, cheaper, more focused generations.
158
+
115
159
  \* Raw's three "wins" carry heavy caveats: the Petstore UI-page case needed brute-forced candidate URLs (the digest's discovery has since been fixed for that class of pages); GitHub's 3.55M-token raw spec would truncate in any model's context anyway; and the Kubernetes expansion is now prevented by the default token cap described below.
116
160
 
117
161
  ### Known failure modes (and their mitigations)
@@ -122,134 +166,225 @@ Ten public APIs, digest vs. raw spec JSON in an LLM context, ground truth from t
122
166
  | Mega-specs (Kubernetes-class, ≥1M tokens) | Digest can _expand_ past raw size — descriptions inlined per operation | **Default in 0.3.0**: automatic 250k-token cap (opt out with `--no-auto-trim` / `auto_trim: false`). Explicit `max_tokens` always wins |
123
167
  | Huge fetches (14 MB GitHub spec) | Fetch fails at the 10 MB response cap | Use a filtered direct spec URL |
124
168
 
125
- ### Filtering and token budget
169
+ Not every specification benefits: very large kitchen-sink specs are where compression can invert — handled by the default cap, but worth knowing.
126
170
 
127
- Large specs produce large digests. The CLI can remove whole operations before rendering and/or trim the remaining digest to an approximate token budget:
171
+ ## Feature overview
128
172
 
129
- ```bash
130
- # Keep only operations tagged "Payments", plus everything under /invoices
131
- npx swagger-digest --url https://api.example.com/openapi.json \
132
- --include-tag Payments --path-prefix /payments/ --out-dir ./docs
173
+ **Ingestion and discovery**
133
174
 
134
- # Drop a noisy tag
135
- npx swagger-digest --url https://api.example.com/openapi.json --exclude-tag Internal
175
+ - Fetches direct OpenAPI JSON/YAML URLs and Swagger UI pages (OpenAPI 2.0, 3.0, 3.1).
176
+ - Multi-strategy discovery: direct spec URL → page script configuration (`url:`, `urls:` dropdown, inline `spec:`, dynamically computed initializer URLs) → common spec paths (`/v3/api-docs`, `/swagger.json`, …) → optional headless-browser fallback.
177
+ - Multi-spec Swagger UI dropdowns: probes candidates in order, uses the first that parses, names the pick.
178
+ - External `$ref` bundling: references to other HTTP(S) documents are fetched and inlined under namespaced names (one level deep, at most 5 documents).
136
179
 
137
- # Fit the digest into a model's context window
138
- npx swagger-digest --url https://api.example.com/openapi.json --max-tokens 50000
139
- ```
180
+ **Markdown generation**
181
+
182
+ - Endpoint index plus per-tag operation details: auth requirements, path/query/header parameters, request bodies, success/error responses, examples.
183
+ - TypeScript-like type declarations for reachable schemas; `allOf`/`oneOf`/`anyOf`, enums, arrays, nullable fields, `additionalProperties` get compact representations.
184
+ - Unknown schema constructs render as `unknown` instead of failing the digest.
185
+ - Synthetic examples generated from schemas when none are declared, clearly marked as illustrative.
186
+
187
+ **MCP server**
140
188
 
141
- How `--max-tokens` trims (in this fixed order, re-checked against the estimate after each step):
189
+ - Single `swagger_digest` tool over stdio; digest returned inline in the tool result with metadata (retrieval method, operation/model counts, token estimate), or written to a file.
190
+ - Same discovery and filtering pipeline as the CLI; optional Playwright fallback disabled.
191
+ - Private-network (SSRF) guard on by default, because URLs come from the model.
142
192
 
143
- 1. Drop declared example payloads (synthetic examples are never emitted).
144
- 2. Shorten long descriptions.
145
- 3. Collapse parameter/response detail per tag — the **tag priority rule** is that the tag appearing **last in the document** is collapsed first (lowest priority), one tag per step.
146
- 4. Keep only the Quick index entries (no endpoint detail, no data types).
193
+ **Filtering and token management**
147
194
 
148
- A `## Trimmed` note at the end of the digest records exactly which steps were applied. After every step, schemas that became unreachable (referenced only by collapsed/removed operations) are dropped from the Data types section. The token estimator is a heuristic, **not** a real tokenizer — treat `--max-tokens` as a guide, not a guarantee. Unknown tag names passed to `--include-tag`/`--exclude-tag` produce a warning on stderr listing the tags that exist in the spec.
195
+ - Filter by tags (`include_tags`/`exclude_tags`) and path prefix, before rendering.
196
+ - `max_tokens` budget with deterministic trim order; `## Trimmed` note records exactly what was removed.
197
+ - Automatic 250,000-token default cap for unbounded digests.
149
198
 
150
- **Default token cap:** without an explicit `--max-tokens`, digests are automatically trimmed to an estimated 250,000 tokens (a warning names the untrimmed estimate). Mega-specs can otherwise expand _past_ their raw size once descriptions are inlined per operation. Opt out with `--no-auto-trim`; an explicit `--max-tokens` always takes precedence. The MCP server behaves the same via its `auto_trim` argument (default true).
199
+ **Authentication and remote access**
151
200
 
152
- The same options are available programmatically via the third argument of `transformOpenApiToMarkdown(spec, sourceUrl, { includeTags, excludeTags, pathPrefix, maxTokens, onWarning })`.
201
+ - Origin-scoped custom headers and cookies: sent only to the origin you supply, never logged, never written to the digest.
202
+ - `SWAGGER_DIGEST_HEADER` environment variable for secrets outside the command line.
203
+ - Hardened fetching: response size cap (10 MB), redirect limit (5), timeouts, optional private-network blocking.
153
204
 
154
- ## What it extracts
205
+ ## Configuration and advanced usage
155
206
 
156
- The retrieval strategy deliberately avoids scraping rendered operation accordions or simulating UI clicks:
207
+ ### CLI reference
157
208
 
158
- 1. Fetch and parse a direct `.json`, `.yaml`, or `.yml` URL.
159
- 2. Fetch the Swagger UI page and inspect script configuration for `url: "..."` or an inline `spec: { ... }` object, plus any spec-like URLs embedded in same-origin initializer scripts (covers initializers that compute the spec URL dynamically).
160
- 3. Try common spec endpoints on the same origin: `/v3/api-docs`, `/swagger/v1/swagger.json`, `/swagger.json`, `/openapi.json`, and `/api-docs`.
161
- 4. Optionally launch a headless Chromium browser and read the parsed specification from Swagger UI's in-memory state.
209
+ | Option | Description |
210
+ | ------------------------------ | --------------------------------------------------------- |
211
+ | `-u, --url <url>` | Swagger UI page URL or direct OpenAPI spec URL |
212
+ | `-d, --out-dir <directory>` | Destination directory for the digest file |
213
+ | `-f, --filename <filename>` | Output filename (defaults to `<api-title>-digest.md`) |
214
+ | `--stdout` | Print to stdout instead of writing a file |
215
+ | `--spec <url>` | Explicit spec URL; overrides all discovery |
216
+ | `--include-tag <tag>` | Keep only operations with this tag (repeatable) |
217
+ | `--exclude-tag <tag>` | Drop operations with this tag (repeatable) |
218
+ | `--path-prefix <prefix>` | Keep only operations whose path starts with this prefix |
219
+ | `--max-tokens <n>` | Trim the digest until the token estimate fits this budget |
220
+ | `--no-auto-trim` | Disable the automatic 250,000-token default cap |
221
+ | `--max-bytes <bytes>` | Maximum response size (default 10,000,000) |
222
+ | `-H, --header <"Name: value">` | Custom request header, repeatable; origin-scoped |
223
+ | `--cookie <string>` | Convenience for a Cookie header; origin-scoped |
224
+ | `--no-browser` | Disable the optional Playwright fallback |
162
225
 
163
- The first three paths use ordinary HTTP requests. The browser fallback is optional so the normal install stays lightweight. To enable it for a difficult JavaScript-initialized Swagger UI deployment:
226
+ ### MCP configuration
227
+
228
+ Register the server (stdio transport):
164
229
 
165
230
  ```bash
166
- npm install playwright
167
- npx playwright install chromium
231
+ # OpenCode
232
+ opencode mcp add swagger-digest -- npx -y swagger-digest-mcp
168
233
  ```
169
234
 
170
- Then run the CLI again. Use `--no-browser` to disable the fallback.
235
+ ```json
236
+ // JSON-configured clients
237
+ {
238
+ "mcpServers": {
239
+ "swagger-digest": {
240
+ "command": ["npx", "-y", "swagger-digest-mcp"]
241
+ }
242
+ }
243
+ }
244
+ ```
171
245
 
172
- ### Authentication headers
246
+ The `swagger_digest` tool:
173
247
 
174
- Some Swagger UI pages and spec URLs live behind authentication. Pass custom headers with `-H`/`--header` (repeatable) or `--cookie`:
248
+ | Argument | Type | Description |
249
+ | --------------- | ---------------- | ------------------------------------------------------------ |
250
+ | `url` | string, required | Swagger UI page URL or direct OpenAPI spec URL |
251
+ | `spec_url` | string | Explicit spec URL; overrides discovery |
252
+ | `headers` | object | Request headers; sent only to the origin of `url` |
253
+ | `include_tags` | string[] | Keep only operations with these tags |
254
+ | `exclude_tags` | string[] | Drop operations with these tags |
255
+ | `path_prefix` | string | Keep only paths starting with this prefix |
256
+ | `max_tokens` | number | Trim the digest to an approximate token budget |
257
+ | `auto_trim` | boolean | Default-cap digests without `max_tokens` (default true) |
258
+ | `write_to_file` | object | `{ directory, filename }` — save instead of returning inline |
175
259
 
176
- ```bash
177
- npx swagger-digest \
178
- --url https://api.example.com/swagger-ui \
179
- --header "Authorization: Bearer eyJhbGciOi..." \
180
- --out-dir ./docs
181
- ```
260
+ ### Live API execution (`api_call`)
182
261
 
183
- Or with a session cookie:
262
+ Beyond reading documentation, the server can execute requests against **one trusted API origin**, turning the workflow from _read → implement → hope_ into _read → call → inspect → correct_. Non-2xx responses are returned as results (not errors) so error bodies can be inspected, and large response bodies are trimmed to a token budget.
184
263
 
185
- ```bash
186
- npx swagger-digest --url https://api.example.com/swagger-ui --cookie "session=abc123"
264
+ It is inactive until configured. Set the trusted origin when starting the server:
265
+
266
+ ```json
267
+ {
268
+ "mcpServers": {
269
+ "swagger-digest": {
270
+ "command": ["npx", "-y", "swagger-digest-mcp"],
271
+ "environment": {
272
+ "SWAGGER_DIGEST_API_BASE_URL": "https://staging.example.com",
273
+ "SWAGGER_DIGEST_API_HEADER": "Authorization: Bearer sk-…",
274
+ "SWAGGER_DIGEST_API_METHODS": "get"
275
+ }
276
+ }
277
+ }
278
+ }
187
279
  ```
188
280
 
189
- You can also supply headers through the `SWAGGER_DIGEST_HEADER` environment variable, with one `Name: value` pair per line. `-H` flags override environment entries on conflicting header names:
281
+ The `api_call` tool:
282
+
283
+ | Argument | Type | Description |
284
+ | ------------ | ------ | ----------------------------------------------------------------- |
285
+ | `path` | string | API path starting with `/`, resolved against the trusted base URL |
286
+ | `url` | string | Absolute URL; its origin must equal the trusted base URL origin |
287
+ | `method` | string | HTTP method (default `get`; writes need explicit opt-in) |
288
+ | `query` | object | Query parameters; array values become repeated keys |
289
+ | `headers` | object | Extra headers; credential-bearing names are rejected |
290
+ | `body` | any | JSON-serializable body (POST/PUT/PATCH/DELETE only) |
291
+ | `max_tokens` | number | Trim large response bodies to an approximate token budget |
292
+
293
+ Either `path` or `url` is required. Provide one, not both.
294
+
295
+ Security model — enforced server-side, not by client approval:
296
+
297
+ - **Single trusted origin.** Requests resolve only against `SWAGGER_DIGEST_API_BASE_URL`. Server URLs declared inside an OpenAPI spec are **never trusted implicitly** — a malicious spec could point at an attacker-controlled host, so absolute `url` arguments must match the configured origin and are refused otherwise.
298
+ - **GET only by default.** Write methods require `SWAGGER_DIGEST_API_METHODS` (e.g. `get,post`); every method name is validated.
299
+ - **Credentials never enter the conversation.** Authentication is injected server-side from `SWAGGER_DIGEST_API_HEADER`; tool arguments containing `authorization`, `cookie`, `x-api-key` and similar are rejected outright. Response bodies are scanned and any echoed credential is redacted.
300
+ - **SSRF guard.** Private/loopback/metadata addresses are rejected per hop, including across redirects (same `SWAGGER_DIGEST_ALLOW_PRIVATE=1` escape hatch as the digest tool, for internal corporate APIs).
301
+ - **Bounded.** Mandatory timeout (default 15 s, hard max 60 s), 1 MB request/response body cap (configurable), token-capped output.
302
+
303
+ `api_call` environment variables:
304
+
305
+ - `SWAGGER_DIGEST_API_BASE_URL` — the single trusted API origin (required; without it the tool explains how to configure itself).
306
+ - `SWAGGER_DIGEST_API_HEADER` — newline-separated `Name: value` authentication headers (same format as `SWAGGER_DIGEST_HEADER`).
307
+ - `SWAGGER_DIGEST_API_METHODS` — comma-separated enabled methods; default `get`.
308
+ - `SWAGGER_DIGEST_API_TIMEOUT_MS` — request timeout (default 15000, hard max 60000).
309
+ - `SWAGGER_DIGEST_API_MAX_BYTES` — request/response body cap in bytes (default 1048576).
310
+
311
+ MCP environment variables:
312
+
313
+ - `SWAGGER_DIGEST_HEADER` — newline-separated `Name: value` pairs, merged with (and overridden by) the call's `headers`. Keeps secrets out of the conversation.
314
+ - `SWAGGER_DIGEST_ALLOW_PRIVATE=1` — opt out of the private-network guard; only for hosts you trust, e.g. internal corporate APIs.
315
+
316
+ MCP-specific behavior: URLs resolving to loopback/private/link-local addresses are rejected by default (MCP clients supply URLs on the model's behalf); diagnostics go to stderr, protocol frames to stdout.
317
+
318
+ ### Authentication headers (CLI)
190
319
 
191
320
  ```bash
321
+ npx swagger-digest --url https://api.example.com/swagger-ui \
322
+ --header "Authorization: Bearer eyJhbGciOi..." --out-dir ./docs
323
+
324
+ # or a session cookie
325
+ npx swagger-digest --url https://api.example.com/swagger-ui --cookie "session=abc123"
326
+
327
+ # or the environment variable (-H flags win on conflicts)
192
328
  export SWAGGER_DIGEST_HEADER=$'Authorization: Bearer eyJ…\nX-Tenant: acme'
193
329
  npx swagger-digest --url https://api.example.com/swagger-ui
194
330
  ```
195
331
 
196
- **Scoping and safety:**
332
+ **Scoping and safety:** custom headers are sent **only** to the origin of the URL you supply; cross-origin requests (redirects, discovered specs on other origins) proceed without them and produce a warning naming the other origin. In the headless-browser fallback, headers are injected per-request for your origin only — they never reach CDNs or third-party scripts. Header values are never logged, written to the digest, or included in error messages. Secrets passed with `-H` can end up in shell history — prefer the environment variable.
197
333
 
198
- - Custom headers are sent **only** to the origin of the URL you supply. If the discovered spec, a redirect, or a Swagger UI script lives on a different origin, that request is made without your headers and a warning naming the other origin is printed to stderr.
199
- - In the headless-browser fallback, headers are injected per-request (only for your origin) — they never reach CDNs or third-party scripts loaded by the page.
200
- - Header values are never logged, written to the digest, or included in error messages.
201
- - ⚠️ **Shell history:** secrets passed with `-H` can end up in your shell history file (`~/.bash_history`, `~/.zsh_history`). Prefer the `SWAGGER_DIGEST_HEADER` environment variable, or prefix the command with a space if your shell is configured to ignore leading-space entries.
334
+ ### Programmatic API
202
335
 
203
- ### Troubleshooting access through a VPN
336
+ ```ts
337
+ import { fetchOpenApiSpec, transformOpenApiToMarkdown, estimateTokens } from 'swagger-digest';
204
338
 
205
- The ordinary HTTP fetches use Node.js networking; they do not automatically reuse a browser's VPN extension, cookies, or signed-in session. If the Swagger page works in Chrome but not in the CLI, first test the same URL from the terminal:
206
-
207
- ```bash
208
- curl -v -L 'https://your-host.example/path/to/swagger-ui' -o /tmp/swagger-page.html
339
+ const { spec, sourceUrl } = await fetchOpenApiSpec('https://api.example.com/openapi.json', {
340
+ blockPrivateNetworks: true, // recommended when URLs come from untrusted input
341
+ headers: { Authorization: 'Bearer …' },
342
+ });
343
+ const markdown = transformOpenApiToMarkdown(spec, sourceUrl, {
344
+ includeTags: ['Subscriptions'],
345
+ maxTokens: 50_000,
346
+ onWarning: console.warn,
347
+ });
348
+ console.log(estimateTokens(markdown).models.claude.tokens);
209
349
  ```
210
350
 
211
- If `curl` cannot connect or receives `401`/`403`, connect a system-level VPN that also routes terminal applications, or configure the relevant corporate proxy/authentication for the terminal. Installing Playwright alone does not make a browser-only VPN extension available to its separate Chromium instance. If terminal access succeeds, the extractor now also reads same-origin Swagger UI initializer/config scripts and tries spec paths relative to the supplied page's context path.
212
-
213
- ## Security notes
351
+ `transformOpenApiToMarkdown` options: `includeTags`, `excludeTags`, `pathPrefix`, `maxTokens`, `autoTrim` (default true), `onWarning`. `fetchOpenApiSpec` options include `specUrl`, `headers`, `maxBytes`, `blockPrivateNetworks`, `useBrowserFallback`.
214
352
 
215
- **What the tool fetches:** the URL you supply (a Swagger UI page or direct OpenAPI JSON/YAML URL), same-origin Swagger UI initializer/config scripts, the spec URL referenced by that page, and a short list of common spec paths on the same origin (`/v3/api-docs`, `/swagger.json`, …). The optional Playwright fallback loads the same page in a headless browser.
353
+ ### Trimming order
216
354
 
217
- **What it does not forward:** cookies or Authorization headers are never sent automatically. Custom headers you pass with `--header`/`--cookie` (see below) are sent **only** to the origin of the URL you supplied. If the page redirects to another origin, or the discovered spec lives on a different origin, those requests are made without your headers and a warning naming the other origin is printed to stderr. Response bodies are capped (10 MB by default, configurable with `--max-bytes`), and redirects are followed at most 5 times.
355
+ When a budget applies (explicit `--max_tokens` or the default cap), the digest is trimmed in this fixed order, re-checked after each step: 1) drop declared example payloads → 2) shorten long descriptions → 3) collapse parameter/response detail per tag (the tag appearing last in the document is collapsed first) → 4) keep only the Quick index. A `## Trimmed` note records exactly which steps applied; schemas referenced only by removed operations are dropped. The estimator is a heuristic — treat budgets as a guide, not a guarantee.
218
356
 
219
- **Embedding in a server:** if you call `fetchOpenApiSpec` from a server that accepts user-supplied URLs, enable `blockPrivateNetworks: true` in the fetch options. Without it, a caller can point the tool at `http://169.254.169.254/…` or other internal addresses from your infrastructure (SSRF). The check rejects loopback, link-local (including the cloud-metadata address 169.254.169.254), private, and unique-local addresses, and applies after redirects. Note the documented DNS-rebinding limitation in the code — for hostile multi-tenant input, use a dedicated egress proxy.
357
+ ## Supported specifications and limitations
220
358
 
221
- ## Markdown format
359
+ **Supported:** OpenAPI 2.0, 3.0 and 3.1, in JSON or YAML, fetched over HTTP(S) from direct spec URLs or discovered through Swagger UI pages.
222
360
 
223
- The generated document includes:
361
+ **Limitations:**
224
362
 
225
- - API title, OpenAPI/Swagger version, API version, base URL(s), and authentication schemes.
226
- - A compact endpoint index (`Method`, `Path`, `Summary`, and `Tags`).
227
- - Endpoint details grouped by tag, including auth requirements, path/query/header parameters, request bodies, success/error responses, and JSON example values.
228
- - TypeScript-like interface/type declarations for schemas reachable from the documented operations, with local `$ref` dependencies expanded as named type references rather than copied JSON Schema trees.
363
+ - The tool downloads specs exposed by the supplied server. It does not bypass authentication, browser access controls, or network restrictions. Ordinary HTTP fetches use Node.js networking — they do not reuse a browser's VPN extension, cookies, or signed-in session. If a page works in Chrome but not here, test `curl -v -L '<url>'` from the terminal; if that fails too, route the terminal through your VPN/proxy.
364
+ - External `$ref` documents are bundled best-effort (one level deep, at most 5 documents); unreachable references degrade to named type references and the digest still succeeds.
365
+ - Very large specs: fetches are capped at 10 MB by default (`--max-bytes`), and digests are capped at ~250k estimated tokens by default (see trimming above).
366
+ - A generated digest is a compact developer reference, not a replacement for the source OpenAPI contract.
229
367
 
230
- OpenAPI 2.0, 3.0, and 3.1 documents are handled through format normalization in the transformer. Schema combinations (`allOf`, `oneOf`, `anyOf`), enums, arrays, nullable fields, and `additionalProperties` receive compact TypeScript-like representations. Unknown or unsupported schema constructs are represented as `unknown` rather than causing the entire digest to fail.
368
+ **Security notes:** the tool fetches the URL you supply, same-origin Swagger UI initializer/config scripts, the spec URL referenced by that page, and a short list of common spec paths on the same origin. Cookies and Authorization headers are never sent automatically. If you call `fetchOpenApiSpec` from a server that accepts user-supplied URLs, enable `blockPrivateNetworks: true` — without it a caller can point the tool at cloud-metadata or other internal addresses (SSRF); the check rejects loopback, link-local, private, and unique-local addresses after redirects, but has a documented DNS-rebinding limitation — for hostile multi-tenant input, use a dedicated egress proxy. The MCP server enables this guard by default.
231
369
 
232
- ## Development
370
+ ## Development and contribution
233
371
 
234
372
  ```bash
235
373
  npm install
236
374
  npm run build
237
- npm test
375
+ npm test # typecheck + test suite (runs offline against fixtures)
238
376
  npm run lint
239
- npm link
240
- swagger-digest
377
+ npm link # then: swagger-digest / swagger-digest-mcp
241
378
  ```
242
379
 
243
- Test against the public Petstore UI:
380
+ Manual check against a live API:
244
381
 
245
382
  ```bash
246
383
  npm run build
247
384
  node dist/cli.js --url https://petstore.swagger.io/ --out-dir ./tmp --filename petstore-digest.md
248
385
  ```
249
386
 
250
- The test suite runs locally against fixtures and does not require a network connection. Network-based checks such as the Petstore example depend on that service being reachable from your environment.
251
-
252
- ## Project layout
387
+ Project layout:
253
388
 
254
389
  ```text
255
390
  src/
@@ -264,13 +399,6 @@ src/
264
399
  digest.ts Tool handler: fetch → transform → tool result
265
400
  ```
266
401
 
267
- ## Limitations
268
-
269
- - The tool downloads specs exposed by the supplied server. It does not bypass authentication, browser access controls, or network restrictions.
270
- - External `$ref` documents are bundled best-effort: references to other HTTP(S) documents are fetched (one level deep, at most 5 documents) and their schemas inlined under namespaced names such as `meta_SharedMeta`. If an external document is unreachable or unparsable, the reference degrades to a named type reference and the digest still succeeds.
271
- - Declared request/response examples are preserved. When an example is absent but a schema exists, a representative JSON example is generated from that schema and clearly marked as illustrative rather than spec-authored. The transformer includes the primary success response and applicable common error responses to balance useful payload context against digest size.
272
- - A generated digest is a compact developer reference, not a replacement for the source OpenAPI contract.
273
-
274
402
  ## License
275
403
 
276
404
  MIT
@@ -265,15 +265,19 @@ async function follow(url, defaultHeaders, userHeaders, options, maxBytes, redir
265
265
  options.onCrossOriginHeadersDropped?.(target.origin);
266
266
  }
267
267
  const requestHeaders = sameOrigin ? { ...defaultHeaders, ...userHeaders } : defaultHeaders;
268
+ const method = (options.method ?? "GET").toUpperCase();
269
+ const carriesBody = options.body !== void 0 && method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && method !== "TRACE";
268
270
  const controller = new AbortController();
269
271
  const timeout = AbortSignal.timeout(options.timeoutMs);
270
272
  timeout.addEventListener("abort", () => controller.abort(), { once: true });
271
273
  let response;
272
274
  try {
273
275
  response = await fetch(target, {
276
+ method,
274
277
  headers: requestHeaders,
275
278
  redirect: "manual",
276
- signal: controller.signal
279
+ signal: controller.signal,
280
+ ...carriesBody ? { body: options.body } : {}
277
281
  });
278
282
  } catch (error) {
279
283
  if (error instanceof FetchError) throw error;
@@ -293,9 +297,11 @@ async function follow(url, defaultHeaders, userHeaders, options, maxBytes, redir
293
297
  } catch {
294
298
  }
295
299
  const next = new URL(response.headers.get("location") ?? "", target);
296
- return follow(next.toString(), defaultHeaders, userHeaders, options, maxBytes, redirectsLeft - 1);
300
+ const downgraded = response.status === 301 || response.status === 302 || response.status === 303;
301
+ const nextOptions = downgraded ? { ...options, method: "GET", body: void 0 } : { ...options };
302
+ return follow(next.toString(), defaultHeaders, userHeaders, nextOptions, maxBytes, redirectsLeft - 1);
297
303
  }
298
- if (!response.ok) {
304
+ if (!response.ok && options.throwOnHttpError !== false) {
299
305
  await discardBodySafely(response);
300
306
  throw new FetchHttpError(`HTTP ${response.status} ${response.statusText} for ${url}`, response.status);
301
307
  }
@@ -314,6 +320,7 @@ async function follow(url, defaultHeaders, userHeaders, options, maxBytes, redir
314
320
  text,
315
321
  contentType: response.headers.get("content-type") ?? "",
316
322
  status: response.status,
323
+ statusText: response.statusText ?? "",
317
324
  url: target.toString()
318
325
  };
319
326
  }
@@ -2188,6 +2195,21 @@ function parseHeaderPairs(raw) {
2188
2195
  }
2189
2196
  return result;
2190
2197
  }
2198
+ var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
2199
+ "authorization",
2200
+ "proxy-authorization",
2201
+ "cookie",
2202
+ "set-cookie",
2203
+ "x-api-key",
2204
+ "api-key",
2205
+ "apikey",
2206
+ "x-auth-token",
2207
+ "x-access-token",
2208
+ "x-session-token"
2209
+ ]);
2210
+ function isSensitiveHeaderName(name) {
2211
+ return SENSITIVE_HEADER_NAMES.has(name.trim().toLowerCase());
2212
+ }
2191
2213
  function mergeHeaders(...sources) {
2192
2214
  const result = {};
2193
2215
  for (const source of sources) {
@@ -2234,6 +2256,7 @@ async function writeDigest(options) {
2234
2256
 
2235
2257
  export {
2236
2258
  VERSION,
2259
+ fetchWithLimits,
2237
2260
  toTypeScriptIdentifier,
2238
2261
  isOpenApiSpec,
2239
2262
  parseSpecText,
@@ -2243,6 +2266,7 @@ export {
2243
2266
  transformOpenApiToMarkdown,
2244
2267
  parseHeaderArgument,
2245
2268
  parseHeaderPairs,
2269
+ isSensitiveHeaderName,
2246
2270
  mergeHeaders,
2247
2271
  defaultOutputDirectory,
2248
2272
  defaultFilename,
@@ -0,0 +1,392 @@
1
+ import {
2
+ VERSION,
3
+ estimateTokens,
4
+ fetchOpenApiSpec,
5
+ fetchWithLimits,
6
+ isSensitiveHeaderName,
7
+ mergeHeaders,
8
+ parseHeaderPairs,
9
+ transformOpenApiToMarkdown,
10
+ writeDigest
11
+ } from "./chunk-2MQ33ITZ.js";
12
+
13
+ // src/mcp/digest.ts
14
+ var TOKEN_HINT_THRESHOLD = 8e4;
15
+ function textContent(text) {
16
+ return [{ type: "text", text }];
17
+ }
18
+ function asError(message) {
19
+ return { content: textContent(message), isError: true };
20
+ }
21
+ function isHttpUrl(value) {
22
+ try {
23
+ const url = new URL(value);
24
+ return url.protocol === "http:" || url.protocol === "https:";
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ function countDigestOperations(markdown) {
30
+ return (markdown.match(/^\| \*\*(?:GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH|TRACE)\*\* \|/gm) ?? []).length;
31
+ }
32
+ function countDigestModels(markdown) {
33
+ return (markdown.match(/^export (?:interface|type) \w+/gm) ?? []).length;
34
+ }
35
+ function buildRequestHeaders(args) {
36
+ const fromEnv = process.env.SWAGGER_DIGEST_HEADER ? parseHeaderPairs(process.env.SWAGGER_DIGEST_HEADER) : {};
37
+ return mergeHeaders(fromEnv, args.headers);
38
+ }
39
+ async function runSwaggerDigest(args, options = {}) {
40
+ if (!isHttpUrl(args.url)) {
41
+ return asError(`Invalid url "${args.url}": use an http:// or https:// URL.`);
42
+ }
43
+ if (args.spec_url !== void 0 && !isHttpUrl(args.spec_url)) {
44
+ return asError(`Invalid spec_url "${args.spec_url}": use an http:// or https:// URL.`);
45
+ }
46
+ if (args.max_tokens !== void 0 && (!Number.isInteger(args.max_tokens) || args.max_tokens <= 0)) {
47
+ return asError(`Invalid max_tokens ${args.max_tokens}: use a positive integer.`);
48
+ }
49
+ const blockPrivateNetworks = options.blockPrivateNetworks ?? process.env.SWAGGER_DIGEST_ALLOW_PRIVATE !== "1";
50
+ const warnings = [];
51
+ const warn = (message) => {
52
+ warnings.push(message);
53
+ };
54
+ let fetchResult;
55
+ try {
56
+ const fetchOptions = {
57
+ useBrowserFallback: false,
58
+ maxBytes: 1e7,
59
+ headers: buildRequestHeaders(args),
60
+ specUrl: args.spec_url,
61
+ blockPrivateNetworks,
62
+ warn
63
+ };
64
+ fetchResult = await fetchOpenApiSpec(args.url, fetchOptions);
65
+ } catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ return asError(`Could not extract an OpenAPI definition from ${args.url}: ${message}`);
68
+ }
69
+ const markdown = transformOpenApiToMarkdown(fetchResult.spec, fetchResult.sourceUrl, {
70
+ includeTags: args.include_tags,
71
+ excludeTags: args.exclude_tags,
72
+ pathPrefix: args.path_prefix,
73
+ maxTokens: args.max_tokens,
74
+ autoTrim: args.auto_trim !== false,
75
+ onWarning: warn
76
+ });
77
+ const operationCount = countDigestOperations(markdown);
78
+ const modelCount = countDigestModels(markdown);
79
+ const tokens = estimateTokens(markdown).models.claude.tokens;
80
+ const metadata = [
81
+ `source: ${fetchResult.sourceUrl} (via ${fetchResult.retrievalMethod})`,
82
+ `${operationCount} operation${operationCount === 1 ? "" : "s"}`,
83
+ `${modelCount} data model${modelCount === 1 ? "" : "s"}`,
84
+ `~${tokens.toLocaleString("en-US")} tokens estimated`
85
+ ].join(" \xB7 ");
86
+ const warningBlock = warnings.length > 0 ? `
87
+
88
+ _Warnings:_
89
+ ${warnings.map((w) => `- ${w}`).join("\n")}` : "";
90
+ const budgetHint = tokens > TOKEN_HINT_THRESHOLD && args.max_tokens === void 0 ? "\n\n_This digest is large. Consider re-running with max_tokens, include_tags, exclude_tags or path_prefix to fit it into context._" : "";
91
+ if (args.write_to_file) {
92
+ try {
93
+ const outputPath = await writeDigest({
94
+ markdown,
95
+ directory: args.write_to_file.directory,
96
+ filename: args.write_to_file.filename
97
+ });
98
+ return { content: textContent(`Digest written to ${outputPath}
99
+
100
+ _${metadata}_${warningBlock}`) };
101
+ } catch (error) {
102
+ const message = error instanceof Error ? error.message : String(error);
103
+ return asError(`Could not write digest: ${message}`);
104
+ }
105
+ }
106
+ return { content: textContent(`${markdown}
107
+
108
+ ---
109
+ _Digest metadata: ${metadata}_${warningBlock}${budgetHint}`) };
110
+ }
111
+
112
+ // src/mcp/server.ts
113
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
114
+ import { z } from "zod";
115
+
116
+ // src/mcp/api-call.ts
117
+ var DEFAULT_TIMEOUT_MS = 15e3;
118
+ var HARD_MAX_TIMEOUT_MS = 6e4;
119
+ var DEFAULT_MAX_BYTES = 1024 * 1024;
120
+ var ALL_METHODS = /* @__PURE__ */ new Set(["get", "head", "post", "put", "patch", "delete"]);
121
+ var BODY_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
122
+ function parsePositiveInt(raw) {
123
+ if (raw === void 0 || raw.trim() === "") return void 0;
124
+ const value = Number(raw);
125
+ return Number.isFinite(value) && value > 0 ? value : void 0;
126
+ }
127
+ function parseMethodsEnv(raw) {
128
+ if (raw === void 0 || raw.trim() === "") return void 0;
129
+ return raw.split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry !== "");
130
+ }
131
+ function isHttpUrl2(value) {
132
+ try {
133
+ const url = new URL(value);
134
+ return url.protocol === "http:" || url.protocol === "https:";
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+ function resolveApiCallConfig(options = {}, env = process.env) {
140
+ const baseUrlRaw = options.baseUrl ?? env.SWAGGER_DIGEST_API_BASE_URL;
141
+ if (baseUrlRaw === void 0 || baseUrlRaw.trim() === "") {
142
+ return {
143
+ error: 'api_call is not configured. Set SWAGGER_DIGEST_API_BASE_URL to the trusted API origin (for example "https://staging.example.com") when starting the MCP server. Requests are only ever sent to this origin \u2014 spec-declared server URLs are not trusted implicitly.'
144
+ };
145
+ }
146
+ let baseUrl;
147
+ try {
148
+ baseUrl = new URL(baseUrlRaw);
149
+ } catch {
150
+ return {
151
+ error: `Invalid SWAGGER_DIGEST_API_BASE_URL "${baseUrlRaw}": use an absolute http:// or https:// URL.`
152
+ };
153
+ }
154
+ if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
155
+ return { error: `Invalid SWAGGER_DIGEST_API_BASE_URL "${baseUrlRaw}": use an http:// or https:// URL.` };
156
+ }
157
+ const methodList = options.methods ?? parseMethodsEnv(env.SWAGGER_DIGEST_API_METHODS) ?? ["get"];
158
+ for (const method of methodList) {
159
+ if (!ALL_METHODS.has(method)) {
160
+ return {
161
+ error: `Unknown method "${method}" in the api_call method list. Allowed: ${[...ALL_METHODS].join(", ")}.`
162
+ };
163
+ }
164
+ }
165
+ const methods = new Set(methodList);
166
+ let authHeaders = {};
167
+ if (options.authHeaders !== void 0) {
168
+ authHeaders = options.authHeaders;
169
+ } else if (env.SWAGGER_DIGEST_API_HEADER !== void 0) {
170
+ try {
171
+ authHeaders = parseHeaderPairs(env.SWAGGER_DIGEST_API_HEADER);
172
+ } catch (error) {
173
+ return {
174
+ error: `Invalid SWAGGER_DIGEST_API_HEADER: ${error instanceof Error ? error.message : String(error)}`
175
+ };
176
+ }
177
+ }
178
+ const requestedTimeout = options.timeoutMs ?? parsePositiveInt(env.SWAGGER_DIGEST_API_TIMEOUT_MS) ?? DEFAULT_TIMEOUT_MS;
179
+ const timeoutMs = Math.min(Math.max(requestedTimeout, 1), HARD_MAX_TIMEOUT_MS);
180
+ const maxBytes = options.maxBytes ?? parsePositiveInt(env.SWAGGER_DIGEST_API_MAX_BYTES) ?? DEFAULT_MAX_BYTES;
181
+ const blockPrivateNetworks = options.blockPrivateNetworks ?? env.SWAGGER_DIGEST_ALLOW_PRIVATE !== "1";
182
+ return { config: { baseUrl, methods, authHeaders, timeoutMs, maxBytes, blockPrivateNetworks } };
183
+ }
184
+ function textContent2(text) {
185
+ return [{ type: "text", text }];
186
+ }
187
+ function asError2(message) {
188
+ return { content: textContent2(message), isError: true };
189
+ }
190
+ function estimateResponseTokens(text) {
191
+ const heuristic = estimateTokens(text).models.claude.tokens;
192
+ return Math.max(heuristic, Math.ceil(text.length / 4));
193
+ }
194
+ function trimToTokenBudget(text, maxTokens) {
195
+ const estimated = estimateResponseTokens(text);
196
+ if (estimated <= maxTokens) return { text, trimmed: false };
197
+ const marker = "\n\u2026[response truncated to fit the max_tokens budget]";
198
+ const keep = Math.max(0, Math.floor(text.length * maxTokens / estimated) - marker.length);
199
+ return { text: text.slice(0, keep) + marker, trimmed: true };
200
+ }
201
+ function redactSecrets(text, secrets) {
202
+ let result = text;
203
+ for (const secret of secrets) {
204
+ if (secret.length >= 8) result = result.split(secret).join("[REDACTED]");
205
+ }
206
+ return result;
207
+ }
208
+ async function runApiCall(args, options = {}) {
209
+ const { config, error } = resolveApiCallConfig(options);
210
+ if (error !== void 0 || config === void 0) return asError2(error ?? "api_call configuration error.");
211
+ if (args.max_tokens !== void 0 && (!Number.isInteger(args.max_tokens) || args.max_tokens <= 0)) {
212
+ return asError2(`Invalid max_tokens ${args.max_tokens}: use a positive integer.`);
213
+ }
214
+ const method = (args.method ?? "get").trim().toLowerCase();
215
+ if (!ALL_METHODS.has(method)) {
216
+ return asError2(
217
+ `Unknown method "${method}". Allowed: ${[...ALL_METHODS].join(", ")}. Enabled methods are controlled by SWAGGER_DIGEST_API_METHODS (default: get).`
218
+ );
219
+ }
220
+ if (!config.methods.has(method)) {
221
+ return asError2(
222
+ `Method "${method}" is not enabled. Enabled methods: ${[...config.methods].sort().join(", ")}. Write methods require an explicit opt-in: set SWAGGER_DIGEST_API_METHODS (e.g. "get,post") when starting the server.`
223
+ );
224
+ }
225
+ let target;
226
+ if (args.url !== void 0 && args.path !== void 0) {
227
+ return asError2("Provide either `url` or `path`, not both.");
228
+ }
229
+ if (args.url !== void 0) {
230
+ if (!isHttpUrl2(args.url)) {
231
+ return asError2(`Invalid url "${args.url}": use an http:// or https:// URL.`);
232
+ }
233
+ target = new URL(args.url);
234
+ } else if (typeof args.path === "string") {
235
+ if (!args.path.startsWith("/")) {
236
+ return asError2(
237
+ `Invalid path "${args.path}": paths must start with "/" and are resolved against the configured base URL.`
238
+ );
239
+ }
240
+ target = new URL(args.path, config.baseUrl);
241
+ } else {
242
+ return asError2(
243
+ 'Provide a target: `path` (e.g. "/v1/refunds", resolved against SWAGGER_DIGEST_API_BASE_URL) or an absolute `url` on the configured trusted origin.'
244
+ );
245
+ }
246
+ if (target.origin !== config.baseUrl.origin) {
247
+ return asError2(
248
+ `Refusing to call ${target.origin}: api_call only sends requests to the configured trusted origin ${config.baseUrl.origin} (SWAGGER_DIGEST_API_BASE_URL). Origins declared inside an OpenAPI specification are never trusted implicitly \u2014 a malicious spec could otherwise redirect credentials.`
249
+ );
250
+ }
251
+ for (const [key, value] of Object.entries(args.query ?? {})) {
252
+ if (Array.isArray(value)) {
253
+ for (const item of value) target.searchParams.append(key, String(item));
254
+ } else {
255
+ target.searchParams.set(key, String(value));
256
+ }
257
+ }
258
+ let bodyText;
259
+ if (args.body !== void 0) {
260
+ if (!BODY_METHODS.has(method)) {
261
+ return asError2(`Method "${method}" cannot carry a request body.`);
262
+ }
263
+ try {
264
+ bodyText = JSON.stringify(args.body);
265
+ } catch {
266
+ return asError2("Invalid body: it must be JSON-serializable.");
267
+ }
268
+ if (bodyText.length > 0 && Buffer.byteLength(bodyText) > config.maxBytes) {
269
+ return asError2(
270
+ `Request body is ${Buffer.byteLength(bodyText)} bytes, exceeding the ${config.maxBytes} byte limit (SWAGGER_DIGEST_API_MAX_BYTES).`
271
+ );
272
+ }
273
+ }
274
+ for (const name of Object.keys(args.headers ?? {})) {
275
+ if (isSensitiveHeaderName(name)) {
276
+ return asError2(
277
+ `Refusing to send "${name}" from tool arguments: authentication headers are injected server-side via SWAGGER_DIGEST_API_HEADER so credentials never enter tool-call transcripts.`
278
+ );
279
+ }
280
+ }
281
+ const requestHeaders = mergeHeaders(config.authHeaders, args.headers);
282
+ if (bodyText !== void 0) {
283
+ requestHeaders["content-type"] = args.headers?.["content-type"] ?? args.headers?.["Content-Type"] ?? "application/json";
284
+ }
285
+ const warnings = [];
286
+ let result;
287
+ try {
288
+ result = await fetchWithLimits(target.toString(), {
289
+ timeoutMs: config.timeoutMs,
290
+ maxBytes: config.maxBytes,
291
+ blockPrivateNetworks: config.blockPrivateNetworks,
292
+ headers: requestHeaders,
293
+ allowedHeaderOrigin: config.baseUrl.origin,
294
+ onCrossOriginHeadersDropped: (origin) => warnings.push(`Request redirected cross-origin to ${origin}; authentication headers were withheld.`),
295
+ method,
296
+ body: bodyText,
297
+ throwOnHttpError: false
298
+ });
299
+ } catch (requestError) {
300
+ const message = requestError instanceof Error ? requestError.message : String(requestError);
301
+ return asError2(`api_call failed: ${message}`);
302
+ }
303
+ let responseBody = redactSecrets(result.text, Object.values(config.authHeaders));
304
+ let wasTrimmed = false;
305
+ if (args.max_tokens !== void 0) {
306
+ const trimmedResult = trimToTokenBudget(responseBody, args.max_tokens);
307
+ responseBody = trimmedResult.text;
308
+ wasTrimmed = trimmedResult.trimmed;
309
+ }
310
+ const tokens = estimateResponseTokens(responseBody);
311
+ const bytes = Buffer.byteLength(result.text);
312
+ const head = [`status: ${result.status}${result.statusText ? ` ${result.statusText}` : ""}`];
313
+ head.push(`url: ${result.url}`);
314
+ head.push(`content-type: ${result.contentType || "unknown"}`);
315
+ const nonOkNote = result.status >= 400 ? `
316
+
317
+ _Non-OK response returned as a result (not an error) so the body can be inspected._` : "";
318
+ const warningBlock = warnings.length > 0 ? `
319
+
320
+ _Warnings:_
321
+ ${warnings.map((w) => `- ${w}`).join("\n")}` : "";
322
+ const metadata = `${bytes.toLocaleString("en-US")} bytes \xB7 ~${tokens.toLocaleString("en-US")} tokens estimated` + (wasTrimmed ? " \xB7 trimmed to max_tokens" : "");
323
+ return {
324
+ content: textContent2(
325
+ `${head.join("\n")}
326
+
327
+ ${responseBody}${nonOkNote}
328
+
329
+ ---
330
+ _api_call metadata: ${metadata}_${warningBlock}`
331
+ )
332
+ };
333
+ }
334
+
335
+ // src/mcp/server.ts
336
+ function createMcpServer(options = {}) {
337
+ const server = new McpServer({ name: "swagger-digest", version: VERSION });
338
+ server.registerTool(
339
+ "swagger_digest",
340
+ {
341
+ title: "Generate API digest",
342
+ description: "Fetch a Swagger UI page or an OpenAPI/Swagger spec (JSON or YAML, versions 2.0/3.0/3.1) and convert it into a compact Markdown API reference optimized for LLM context: endpoint index, request/response details, authentication schemes, and TypeScript-like data types. Handles Swagger UI discovery automatically (initializer scripts, multi-spec dropdowns, common spec paths). Use this whenever the user asks to understand, summarize, or work against an API documented with Swagger/OpenAPI.",
343
+ inputSchema: {
344
+ url: z.string().describe("Swagger UI page URL or direct OpenAPI spec URL (http/https)"),
345
+ spec_url: z.string().optional().describe("Explicit OpenAPI spec URL; overrides discovery from the Swagger UI page"),
346
+ headers: z.record(z.string(), z.string()).optional().describe(
347
+ "Request headers (e.g. Authorization or Cookie). Sent ONLY to the origin of url; cross-origin requests never receive them. Values are not logged."
348
+ ),
349
+ include_tags: z.array(z.string()).optional().describe("Keep only operations carrying at least one of these tags"),
350
+ exclude_tags: z.array(z.string()).optional().describe("Drop operations carrying any of these tags"),
351
+ path_prefix: z.string().optional().describe("Keep only operations whose path starts with this prefix"),
352
+ max_tokens: z.number().int().positive().optional().describe(
353
+ "Trim the digest until an approximate token estimate fits this budget (heuristic, not tokenizer-accurate)"
354
+ ),
355
+ auto_trim: z.boolean().optional().describe(
356
+ "Auto-trim digests exceeding the 250,000-token default budget when max_tokens is not given. Defaults to true."
357
+ ),
358
+ write_to_file: z.object({ directory: z.string(), filename: z.string() }).optional().describe("Save the digest to a file instead of returning it inline")
359
+ }
360
+ },
361
+ (args) => runSwaggerDigest(args, options)
362
+ );
363
+ server.registerTool(
364
+ "api_call",
365
+ {
366
+ title: "Execute an API request",
367
+ description: "Execute an HTTP request against the trusted API origin configured via SWAGGER_DIGEST_API_BASE_URL (set when the MCP server starts; the tool returns a clear configuration error when it is unset). Use it after swagger_digest to verify how the real API behaves: send GET (default) or, when write methods were enabled via SWAGGER_DIGEST_API_METHODS, POST/PUT/PATCH/DELETE. Provide either `path` (resolved against the trusted origin) or an absolute `url` on that same origin \u2014 other origins are refused, and server URLs declared inside an OpenAPI spec are never trusted implicitly. Authentication headers are injected server-side; credential headers in arguments are rejected. Non-2xx responses are returned as results (not errors) so error bodies can be inspected. Responses are size- and token-capped.",
368
+ inputSchema: {
369
+ url: z.string().optional().describe("Absolute http(s) URL whose origin must equal the configured trusted API origin"),
370
+ path: z.string().optional().describe('API path starting with "/", e.g. "/v1/refunds" \u2014 resolved against SWAGGER_DIGEST_API_BASE_URL'),
371
+ method: z.string().optional().describe('HTTP method (default "get"). Write methods require SWAGGER_DIGEST_API_METHODS opt-in'),
372
+ query: z.record(
373
+ z.string(),
374
+ z.union([z.string(), z.number(), z.boolean(), z.array(z.union([z.string(), z.number(), z.boolean()]))])
375
+ ).optional().describe("Query parameters; array values become repeated keys"),
376
+ headers: z.record(z.string(), z.string()).optional().describe(
377
+ "Extra request headers. Credential-bearing names (authorization, cookie, x-api-key, \u2026) are rejected; authentication comes from SWAGGER_DIGEST_API_HEADER"
378
+ ),
379
+ body: z.unknown().optional().describe("JSON-serializable request body (POST/PUT/PATCH/DELETE only)"),
380
+ max_tokens: z.number().int().positive().optional().describe("Trim large response bodies to approximately this token budget")
381
+ }
382
+ },
383
+ (args) => runApiCall(args, options.apiCall)
384
+ );
385
+ return server;
386
+ }
387
+
388
+ export {
389
+ buildRequestHeaders,
390
+ runSwaggerDigest,
391
+ createMcpServer
392
+ };
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  parseHeaderPairs,
11
11
  transformOpenApiToMarkdown,
12
12
  writeDigest
13
- } from "./chunk-MXTHI25H.js";
13
+ } from "./chunk-2MQ33ITZ.js";
14
14
 
15
15
  // src/cli.ts
16
16
  import { input } from "@inquirer/prompts";
package/dist/index.d.ts CHANGED
@@ -97,13 +97,40 @@ declare function buildRequestHeaders(args: Pick<McpDigestArgs, 'headers'>): Reco
97
97
  declare function runSwaggerDigest(args: McpDigestArgs, options?: DigestServerOptions): Promise<CallToolResult>;
98
98
 
99
99
  /**
100
- * Builds the MCP server exposing the `swagger_digest` tool.
100
+ * Server-wide configuration for `api_call`. Tests pass these explicitly; the
101
+ * stdio entry point derives them from the environment (see resolveApiCallConfig).
102
+ */
103
+ interface ApiCallServerOptions {
104
+ /** Reject URLs resolving to loopback/private/metadata addresses. Defaults to true. */
105
+ blockPrivateNetworks?: boolean;
106
+ /** Trusted API origin. Overrides SWAGGER_DIGEST_API_BASE_URL. */
107
+ baseUrl?: string;
108
+ /** Enabled HTTP methods (lowercase). Overrides SWAGGER_DIGEST_API_METHODS. */
109
+ methods?: string[];
110
+ /** Server-injected authentication headers. Overrides SWAGGER_DIGEST_API_HEADER. */
111
+ authHeaders?: Record<string, string>;
112
+ /** Request timeout in ms. Overrides SWAGGER_DIGEST_API_TIMEOUT_MS (hard max 60 s). */
113
+ timeoutMs?: number;
114
+ /** Maximum response/request body size in bytes. Overrides SWAGGER_DIGEST_API_MAX_BYTES. */
115
+ maxBytes?: number;
116
+ }
117
+
118
+ /** Options for the whole MCP server: shared behavior plus per-tool overrides. */
119
+ interface SwaggerDigestMcpOptions extends DigestServerOptions {
120
+ /**
121
+ * Configuration overrides for the `api_call` tool. Tests inject these;
122
+ * production derives them from SWAGGER_DIGEST_API_* environment variables.
123
+ */
124
+ apiCall?: ApiCallServerOptions;
125
+ }
126
+ /**
127
+ * Builds the MCP server exposing the `swagger_digest` and `api_call` tools.
101
128
  *
102
129
  * The instance is transport-agnostic: `src/mcp/index.ts` connects it to a
103
130
  * StdioServerTransport, while tests connect it to an InMemoryTransport pair.
104
131
  * No I/O happens at module scope or during construction.
105
132
  */
106
- declare function createMcpServer(options?: DigestServerOptions): McpServer;
133
+ declare function createMcpServer(options?: SwaggerDigestMcpOptions): McpServer;
107
134
 
108
135
  declare function toTypeScriptIdentifier(value: string): string;
109
136
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  buildRequestHeaders,
3
3
  createMcpServer,
4
4
  runSwaggerDigest
5
- } from "./chunk-DJES77HX.js";
5
+ } from "./chunk-UBR7FEJX.js";
6
6
  import {
7
7
  DEFAULT_MAX_TOKENS,
8
8
  defaultFilename,
@@ -16,7 +16,7 @@ import {
16
16
  toTypeScriptIdentifier,
17
17
  transformOpenApiToMarkdown,
18
18
  writeDigest
19
- } from "./chunk-MXTHI25H.js";
19
+ } from "./chunk-2MQ33ITZ.js";
20
20
  export {
21
21
  DEFAULT_MAX_TOKENS,
22
22
  buildRequestHeaders,
package/dist/mcp/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createMcpServer
4
- } from "../chunk-DJES77HX.js";
5
- import "../chunk-MXTHI25H.js";
4
+ } from "../chunk-UBR7FEJX.js";
5
+ import "../chunk-2MQ33ITZ.js";
6
6
 
7
7
  // src/mcp/index.ts
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swagger-digest",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Turn Swagger UI pages and OpenAPI specs into compact, AI-friendly Markdown API digests.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,147 +0,0 @@
1
- import {
2
- VERSION,
3
- estimateTokens,
4
- fetchOpenApiSpec,
5
- mergeHeaders,
6
- parseHeaderPairs,
7
- transformOpenApiToMarkdown,
8
- writeDigest
9
- } from "./chunk-MXTHI25H.js";
10
-
11
- // src/mcp/digest.ts
12
- var TOKEN_HINT_THRESHOLD = 8e4;
13
- function textContent(text) {
14
- return [{ type: "text", text }];
15
- }
16
- function asError(message) {
17
- return { content: textContent(message), isError: true };
18
- }
19
- function isHttpUrl(value) {
20
- try {
21
- const url = new URL(value);
22
- return url.protocol === "http:" || url.protocol === "https:";
23
- } catch {
24
- return false;
25
- }
26
- }
27
- function countDigestOperations(markdown) {
28
- return (markdown.match(/^\| \*\*(?:GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH|TRACE)\*\* \|/gm) ?? []).length;
29
- }
30
- function countDigestModels(markdown) {
31
- return (markdown.match(/^export (?:interface|type) \w+/gm) ?? []).length;
32
- }
33
- function buildRequestHeaders(args) {
34
- const fromEnv = process.env.SWAGGER_DIGEST_HEADER ? parseHeaderPairs(process.env.SWAGGER_DIGEST_HEADER) : {};
35
- return mergeHeaders(fromEnv, args.headers);
36
- }
37
- async function runSwaggerDigest(args, options = {}) {
38
- if (!isHttpUrl(args.url)) {
39
- return asError(`Invalid url "${args.url}": use an http:// or https:// URL.`);
40
- }
41
- if (args.spec_url !== void 0 && !isHttpUrl(args.spec_url)) {
42
- return asError(`Invalid spec_url "${args.spec_url}": use an http:// or https:// URL.`);
43
- }
44
- if (args.max_tokens !== void 0 && (!Number.isInteger(args.max_tokens) || args.max_tokens <= 0)) {
45
- return asError(`Invalid max_tokens ${args.max_tokens}: use a positive integer.`);
46
- }
47
- const blockPrivateNetworks = options.blockPrivateNetworks ?? process.env.SWAGGER_DIGEST_ALLOW_PRIVATE !== "1";
48
- const warnings = [];
49
- const warn = (message) => {
50
- warnings.push(message);
51
- };
52
- let fetchResult;
53
- try {
54
- const fetchOptions = {
55
- useBrowserFallback: false,
56
- maxBytes: 1e7,
57
- headers: buildRequestHeaders(args),
58
- specUrl: args.spec_url,
59
- blockPrivateNetworks,
60
- warn
61
- };
62
- fetchResult = await fetchOpenApiSpec(args.url, fetchOptions);
63
- } catch (error) {
64
- const message = error instanceof Error ? error.message : String(error);
65
- return asError(`Could not extract an OpenAPI definition from ${args.url}: ${message}`);
66
- }
67
- const markdown = transformOpenApiToMarkdown(fetchResult.spec, fetchResult.sourceUrl, {
68
- includeTags: args.include_tags,
69
- excludeTags: args.exclude_tags,
70
- pathPrefix: args.path_prefix,
71
- maxTokens: args.max_tokens,
72
- autoTrim: args.auto_trim !== false,
73
- onWarning: warn
74
- });
75
- const operationCount = countDigestOperations(markdown);
76
- const modelCount = countDigestModels(markdown);
77
- const tokens = estimateTokens(markdown).models.claude.tokens;
78
- const metadata = [
79
- `source: ${fetchResult.sourceUrl} (via ${fetchResult.retrievalMethod})`,
80
- `${operationCount} operation${operationCount === 1 ? "" : "s"}`,
81
- `${modelCount} data model${modelCount === 1 ? "" : "s"}`,
82
- `~${tokens.toLocaleString("en-US")} tokens estimated`
83
- ].join(" \xB7 ");
84
- const warningBlock = warnings.length > 0 ? `
85
-
86
- _Warnings:_
87
- ${warnings.map((w) => `- ${w}`).join("\n")}` : "";
88
- const budgetHint = tokens > TOKEN_HINT_THRESHOLD && args.max_tokens === void 0 ? "\n\n_This digest is large. Consider re-running with max_tokens, include_tags, exclude_tags or path_prefix to fit it into context._" : "";
89
- if (args.write_to_file) {
90
- try {
91
- const outputPath = await writeDigest({
92
- markdown,
93
- directory: args.write_to_file.directory,
94
- filename: args.write_to_file.filename
95
- });
96
- return { content: textContent(`Digest written to ${outputPath}
97
-
98
- _${metadata}_${warningBlock}`) };
99
- } catch (error) {
100
- const message = error instanceof Error ? error.message : String(error);
101
- return asError(`Could not write digest: ${message}`);
102
- }
103
- }
104
- return { content: textContent(`${markdown}
105
-
106
- ---
107
- _Digest metadata: ${metadata}_${warningBlock}${budgetHint}`) };
108
- }
109
-
110
- // src/mcp/server.ts
111
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
112
- import { z } from "zod";
113
- function createMcpServer(options = {}) {
114
- const server = new McpServer({ name: "swagger-digest", version: VERSION });
115
- server.registerTool(
116
- "swagger_digest",
117
- {
118
- title: "Generate API digest",
119
- description: "Fetch a Swagger UI page or an OpenAPI/Swagger spec (JSON or YAML, versions 2.0/3.0/3.1) and convert it into a compact Markdown API reference optimized for LLM context: endpoint index, request/response details, authentication schemes, and TypeScript-like data types. Handles Swagger UI discovery automatically (initializer scripts, multi-spec dropdowns, common spec paths). Use this whenever the user asks to understand, summarize, or work against an API documented with Swagger/OpenAPI.",
120
- inputSchema: {
121
- url: z.string().describe("Swagger UI page URL or direct OpenAPI spec URL (http/https)"),
122
- spec_url: z.string().optional().describe("Explicit OpenAPI spec URL; overrides discovery from the Swagger UI page"),
123
- headers: z.record(z.string(), z.string()).optional().describe(
124
- "Request headers (e.g. Authorization or Cookie). Sent ONLY to the origin of url; cross-origin requests never receive them. Values are not logged."
125
- ),
126
- include_tags: z.array(z.string()).optional().describe("Keep only operations carrying at least one of these tags"),
127
- exclude_tags: z.array(z.string()).optional().describe("Drop operations carrying any of these tags"),
128
- path_prefix: z.string().optional().describe("Keep only operations whose path starts with this prefix"),
129
- max_tokens: z.number().int().positive().optional().describe(
130
- "Trim the digest until an approximate token estimate fits this budget (heuristic, not tokenizer-accurate)"
131
- ),
132
- auto_trim: z.boolean().optional().describe(
133
- "Auto-trim digests exceeding the 250,000-token default budget when max_tokens is not given. Defaults to true."
134
- ),
135
- write_to_file: z.object({ directory: z.string(), filename: z.string() }).optional().describe("Save the digest to a file instead of returning it inline")
136
- }
137
- },
138
- (args) => runSwaggerDigest(args, options)
139
- );
140
- return server;
141
- }
142
-
143
- export {
144
- buildRequestHeaders,
145
- runSwaggerDigest,
146
- createMcpServer
147
- };