swagger-digest 0.1.1 → 0.2.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 swagger-digest contributors
3
+ Copyright (c) 2026 VladLegkowski
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -25,8 +25,99 @@ For a direct spec URL:
25
25
  npx swagger-digest --url https://example.com/openapi.yaml --out-dir ./docs
26
26
  ```
27
27
 
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:
29
+
30
+ ```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
33
+ ```
34
+
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.
36
+
37
+ ### Writing to stdout
38
+
39
+ ```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
42
+ ```
43
+
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.
45
+
28
46
  Run `npx swagger-digest --help` for all CLI options. The package requires Node.js 20 or newer.
29
47
 
48
+ ## MCP server
49
+
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.
51
+
52
+ ```bash
53
+ npx swagger-digest-mcp
54
+ ```
55
+
56
+ It speaks the stdio transport: the MCP client spawns the process and communicates over stdin/stdout. Register it in your client's configuration:
57
+
58
+ ```json
59
+ {
60
+ "mcp": {
61
+ "swagger-digest": {
62
+ "command": ["npx", "-y", "swagger-digest-mcp"]
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### The `swagger_digest` tool
69
+
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 |
80
+
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.
82
+
83
+ ### Environment variables
84
+
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).
87
+
88
+ ### Security differences from the CLI
89
+
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.
93
+
94
+ ### Filtering and token budget
95
+
96
+ Large specs produce large digests. The CLI can remove whole operations before rendering and/or trim the remaining digest to an approximate token budget:
97
+
98
+ ```bash
99
+ # Keep only operations tagged "Payments", plus everything under /invoices
100
+ npx swagger-digest --url https://api.example.com/openapi.json \
101
+ --include-tag Payments --path-prefix /payments/ --out-dir ./docs
102
+
103
+ # Drop a noisy tag
104
+ npx swagger-digest --url https://api.example.com/openapi.json --exclude-tag Internal
105
+
106
+ # Fit the digest into a model's context window
107
+ npx swagger-digest --url https://api.example.com/openapi.json --max-tokens 50000
108
+ ```
109
+
110
+ How `--max-tokens` trims (in this fixed order, re-checked against the estimate after each step):
111
+
112
+ 1. Drop declared example payloads (synthetic examples are never emitted).
113
+ 2. Shorten long descriptions.
114
+ 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.
115
+ 4. Keep only the Quick index entries (no endpoint detail, no data types).
116
+
117
+ 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.
118
+
119
+ The same options are available programmatically via the third argument of `transformOpenApiToMarkdown(spec, sourceUrl, { includeTags, excludeTags, pathPrefix, maxTokens, onWarning })`.
120
+
30
121
  ## What it extracts
31
122
 
32
123
  The retrieval strategy deliberately avoids scraping rendered operation accordions or simulating UI clicks:
@@ -45,6 +136,37 @@ npx playwright install chromium
45
136
 
46
137
  Then run the CLI again. Use `--no-browser` to disable the fallback.
47
138
 
139
+ ### Authentication headers
140
+
141
+ Some Swagger UI pages and spec URLs live behind authentication. Pass custom headers with `-H`/`--header` (repeatable) or `--cookie`:
142
+
143
+ ```bash
144
+ npx swagger-digest \
145
+ --url https://api.example.com/swagger-ui \
146
+ --header "Authorization: Bearer eyJhbGciOi..." \
147
+ --out-dir ./docs
148
+ ```
149
+
150
+ Or with a session cookie:
151
+
152
+ ```bash
153
+ npx swagger-digest --url https://api.example.com/swagger-ui --cookie "session=abc123"
154
+ ```
155
+
156
+ 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:
157
+
158
+ ```bash
159
+ export SWAGGER_DIGEST_HEADER=$'Authorization: Bearer eyJ…\nX-Tenant: acme'
160
+ npx swagger-digest --url https://api.example.com/swagger-ui
161
+ ```
162
+
163
+ **Scoping and safety:**
164
+
165
+ - 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.
166
+ - 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.
167
+ - Header values are never logged, written to the digest, or included in error messages.
168
+ - ⚠️ **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.
169
+
48
170
  ### Troubleshooting access through a VPN
49
171
 
50
172
  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:
@@ -55,6 +177,14 @@ curl -v -L 'https://your-host.example/path/to/swagger-ui' -o /tmp/swagger-page.h
55
177
 
56
178
  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.
57
179
 
180
+ ## Security notes
181
+
182
+ **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.
183
+
184
+ **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.
185
+
186
+ **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.
187
+
58
188
  ## Markdown format
59
189
 
60
190
  The generated document includes:
@@ -95,12 +225,16 @@ src/
95
225
  transformer.ts OpenAPI/Swagger to compact Markdown and TypeScript-like types
96
226
  writer.ts Output path, filename defaults and file writing
97
227
  index.ts Public exports
228
+ mcp/
229
+ index.ts MCP stdio entry point (`swagger-digest-mcp` bin)
230
+ server.ts MCP server and tool registration (transport-agnostic)
231
+ digest.ts Tool handler: fetch → transform → tool result
98
232
  ```
99
233
 
100
234
  ## Limitations
101
235
 
102
236
  - The tool downloads specs exposed by the supplied server. It does not bypass authentication, browser access controls, or network restrictions.
103
- - External schema references are retained as named type references where possible; the tool does not currently fetch and bundle arbitrary external documents.
237
+ - 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.
104
238
  - 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.
105
239
  - A generated digest is a compact developer reference, not a replacement for the source OpenAPI contract.
106
240
 
@@ -0,0 +1,143 @@
1
+ import {
2
+ VERSION,
3
+ estimateTokens,
4
+ fetchOpenApiSpec,
5
+ mergeHeaders,
6
+ parseHeaderPairs,
7
+ transformOpenApiToMarkdown,
8
+ writeDigest
9
+ } from "./chunk-U7OLLL6J.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
+ onWarning: warn
73
+ });
74
+ const operationCount = countDigestOperations(markdown);
75
+ const modelCount = countDigestModels(markdown);
76
+ const tokens = estimateTokens(markdown).models.claude.tokens;
77
+ const metadata = [
78
+ `source: ${fetchResult.sourceUrl} (via ${fetchResult.retrievalMethod})`,
79
+ `${operationCount} operation${operationCount === 1 ? "" : "s"}`,
80
+ `${modelCount} data model${modelCount === 1 ? "" : "s"}`,
81
+ `~${tokens.toLocaleString("en-US")} tokens estimated`
82
+ ].join(" \xB7 ");
83
+ const warningBlock = warnings.length > 0 ? `
84
+
85
+ _Warnings:_
86
+ ${warnings.map((w) => `- ${w}`).join("\n")}` : "";
87
+ 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._" : "";
88
+ if (args.write_to_file) {
89
+ try {
90
+ const outputPath = await writeDigest({
91
+ markdown,
92
+ directory: args.write_to_file.directory,
93
+ filename: args.write_to_file.filename
94
+ });
95
+ return { content: textContent(`Digest written to ${outputPath}
96
+
97
+ _${metadata}_${warningBlock}`) };
98
+ } catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ return asError(`Could not write digest: ${message}`);
101
+ }
102
+ }
103
+ return { content: textContent(`${markdown}
104
+
105
+ ---
106
+ _Digest metadata: ${metadata}_${warningBlock}${budgetHint}`) };
107
+ }
108
+
109
+ // src/mcp/server.ts
110
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
111
+ import { z } from "zod";
112
+ function createMcpServer(options = {}) {
113
+ const server = new McpServer({ name: "swagger-digest", version: VERSION });
114
+ server.registerTool(
115
+ "swagger_digest",
116
+ {
117
+ title: "Generate API digest",
118
+ 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.",
119
+ inputSchema: {
120
+ url: z.string().describe("Swagger UI page URL or direct OpenAPI spec URL (http/https)"),
121
+ spec_url: z.string().optional().describe("Explicit OpenAPI spec URL; overrides discovery from the Swagger UI page"),
122
+ headers: z.record(z.string(), z.string()).optional().describe(
123
+ "Request headers (e.g. Authorization or Cookie). Sent ONLY to the origin of url; cross-origin requests never receive them. Values are not logged."
124
+ ),
125
+ include_tags: z.array(z.string()).optional().describe("Keep only operations carrying at least one of these tags"),
126
+ exclude_tags: z.array(z.string()).optional().describe("Drop operations carrying any of these tags"),
127
+ path_prefix: z.string().optional().describe("Keep only operations whose path starts with this prefix"),
128
+ max_tokens: z.number().int().positive().optional().describe(
129
+ "Trim the digest until an approximate token estimate fits this budget (heuristic, not tokenizer-accurate)"
130
+ ),
131
+ write_to_file: z.object({ directory: z.string(), filename: z.string() }).optional().describe("Save the digest to a file instead of returning it inline")
132
+ }
133
+ },
134
+ (args) => runSwaggerDigest(args, options)
135
+ );
136
+ return server;
137
+ }
138
+
139
+ export {
140
+ buildRequestHeaders,
141
+ runSwaggerDigest,
142
+ createMcpServer
143
+ };