diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d9df8e1..0ef159c 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -33,7 +33,7 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Upgrade npm to latest (trusted publishing requires 11.5.1+)
- run: npm install -g npm@latest
+ run: npm install -g "npm@11"
- name: Verify versions match
run: |
@@ -132,7 +132,11 @@ jobs:
- name: Install mcp-publisher
run: |
- curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher
+ # Download to a file instead of piping curl into tar, so a sha256
+ # verification can slot in once modelcontextprotocol/registry publishes
+ # a checksums file (BSC-003). -f fails the job on an HTTP error.
+ curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" -o mcp-publisher.tar.gz
+ tar xzf mcp-publisher.tar.gz mcp-publisher
chmod +x mcp-publisher
sudo mv mcp-publisher /usr/local/bin/
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index 38e27ca..7eb2a9c 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -9,15 +9,19 @@ on:
permissions:
contents: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
audit:
name: Dependency audit and CI
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
- node-version: '24'
+ node-version-file: ".node-version"
cache: 'npm'
- name: Install dependencies
run: npm ci
diff --git a/.github/workflows/url-health.yml b/.github/workflows/url-health.yml
new file mode 100644
index 0000000..5b80f72
--- /dev/null
+++ b/.github/workflows/url-health.yml
@@ -0,0 +1,23 @@
+name: URL Health
+
+on:
+ schedule:
+ # Mondays 06:00 UTC — catch docs restructures before users hit dead links
+ - cron: "0 6 * * 1"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ check-urls:
+ name: Check curated documentation URLs
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version-file: ".node-version"
+ - name: Check curated URLs
+ run: node scripts/check-urls.mjs
diff --git a/.gitignore b/.gitignore
index 5095a4b..8fc8a62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,10 @@ CLAUDE*.md
*.claude*
.claude/*.local.md
.claude/*.local.json
+
+# local-only draft artifacts (never publish)
+docs/npm-gdpr-request-draft.md
+docs/*.png
+
+# Internal docs — local-only (purged from public history 2026-05-31)
+docs/
diff --git a/.node-version b/.node-version
index 7af24b7..5bf4400 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-22.11.0
+24.15.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 908eb4c..c1aaebf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,85 @@
# Changelog
+## [7.4.0] — 2026-07-15
+
+- fix: adversarially verified hardening across all 14 tools
+- docs: expand 7.3.0 changelog entry
+
+---
+
+## [7.3.0] — 2026-07-10
+
+- feat: gt_compat redesigned on MDN machine-readable data (index.json + BCD API) — exact per-browser version_added incl. Node/Deno/Bun, Baseline status
+- feat: gt_snippets multi-hop traversal (index links, child pages, sitemap) — frameworks whose llms.txt is a link index now yield snippets
+- feat: nested llms.txt pointer following, relative-link index support, path-scoped sitemap discovery, legacy-version-tree ranking penalty
+- feat: gt_search authority-ranked sources (official docs above content farms) + full evidence coverage per source on specific queries
+- feat: gt_migration web-search escalation for upgrade guides at unguessable URLs; gt_examples official-docs fallback (GitHub code search is auth-only)
+- fix: gt_get_docs index-content escalation + latency guard; raceUrls ranks topic-first; dispatch routes natural browser-support phrasing to gt_compat
+
+---
+
+## [7.2.0] — 2026-07-10
+
+- feat: multi-source retrieval, Jina 404 gate, prose-only evidence
+- feat: topic-synonym discovery (migration/upgrade, performance/optimization), index-first deep-fetch, link-list guard
+- feat: weekly URL-health CI workflow; 73 rotted curated URLs replaced with live-verified pages
+- fix: query-meta words no longer count as topic evidence; shared web-search helper in gt_search
+- chore: undici 8.7.0 + hono 4.12.28 (clears 2 high-severity production advisories)
+
+---
+
+## [7.1.0] — 2026-06-12
+
+- feat: evidence engine — verified topic coverage, no generic answers
+
+---
+
+## [7.0.4] — 2026-06-03
+
+- fix: eliminate documentation noise across all MCP tools
+- chore: sync llms.txt to 7.0.3 and auto-stage it on version bump
+
+---
+
+## [7.0.3] — 2026-06-02
+
+- docs: sync generated stats (1198 tests, 14 tools)
+- test: add 78 regression tests for the audit-hardening fixes
+- ci: pin actions, gate release scripts, automate stat writeback
+- refactor: extract server instructions to a testable module
+- perf: cut tokenization cost and fetch fan-out
+- fix: correct resolver, router, scanner and tool defects from audit
+- fix: harden security and reliability from deep audit (wave 2)
+- chore: sync llms.txt stats for 7.0.2
+
+---
+
+## [7.0.2] — 2026-06-02
+
+- fix: backfill registry languages, cover gt_dispatch, sync docs
+- fix: correct resolver, migration band and compat cache defects
+- fix: harden security, reliability and observability from deep audit
+- chore: gitignore docs/ — internal planning docs, local-only
+- fix(scripts): stop version-sweep from rewriting .github action versions
+
+---
+
+## [7.0.1] — 2026-05-30
+
+- chore: ignore local draft artifacts
+- docs: record implementation status (done / skipped-as-wrong / deferred)
+- fix(resolve): key llms.txt probe cache on full path, not origin
+- perf: snippet IDF ranking, cache + lockfile + telemetry cleanup
+- build: upgrade to TypeScript 6.0 + NodeNext, align Node 24
+- fix: reliability, security and protocol hardening
+- fix(audit): use charOffset for repeated-line context windows
+- fix: version-aware migration/changelog pipeline
+- docs: add enterprise upgrade plan
+- chore: shorten server.json description (MCP registry 100-char limit)
+- chore: stats — README library count 444 -> 445
+
+---
+
## [7.0.0] — 2026-05-28
Adds a dispatch tool, hardens the security model, and instruments every tool with telemetry.
diff --git a/README.md b/README.md
index e8bed44..f19beb1 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@
-
+
${"Real rendered documentation content describing the API in detail. ".repeat(5)}
Mail [email protected] us
`; + const out = decodeCloudflareEmails(html); + expect(out).toContain("hi@example.com"); + expect(out).not.toContain("__cf_email__"); + }); + + it("decodes the href-fragment form", () => { + const hex = encodeCf("dev@example.org", 0x10); + const html = `protected`; + expect(decodeCloudflareEmails(html)).toContain("dev@example.org"); + }); + + it("leaves unrelated HTML untouched", () => { + const html = "No cloudflare here
"; + expect(decodeCloudflareEmails(html)).toBe(html); + }); +}); + +describe("stripCloudflareEmailMarkdown (Jina output)", () => { + it("removes the no-hex placeholder link entirely (the live OWASP noise)", () => { + const md = "Contact [[email protected]](/cdn-cgi/l/email-protection) for details."; + const out = stripCloudflareEmailMarkdown(md); + expect(out).not.toContain("cdn-cgi"); + expect(out).not.toMatch(/\[email\s*protected\]/i); + expect(out).toContain("Contact"); + expect(out).toContain("for details."); + }); + + it("recovers the address when a #HEX fragment is present", () => { + const hex = encodeCf("team@acme.io", 0x7b); + const md = `Email [[email protected]](https://acme.io/cdn-cgi/l/email-protection#${hex}) now`; + const out = stripCloudflareEmailMarkdown(md); + expect(out).toContain("team@acme.io"); + expect(out).not.toContain("cdn-cgi"); + }); + + it("leaves unrelated markdown untouched", () => { + const md = "See [the docs](https://example.com/docs) here."; + expect(stripCloudflareEmailMarkdown(md)).toBe(md); + }); +}); diff --git a/src/utils/decode-entities.ts b/src/utils/decode-entities.ts new file mode 100644 index 0000000..27a3e2e --- /dev/null +++ b/src/utils/decode-entities.ts @@ -0,0 +1,147 @@ +/** + * Shared HTML-entity and Cloudflare-email decoding for the documentation + * cleaning pipeline. + * + * Used by both html-to-md.ts (the direct-HTML extraction path) and sanitize.ts + * (the universal post-fetch chokepoint). Jina Reader, llms.txt and GitHub-raw + * content bypass html-to-md entirely and arrive as markdown that still carries + * named/numeric HTML entities (e.g. `¶`, `→`, `©`) and Cloudflare + * email-protection placeholders — sanitize.ts is the only place those get cleaned, + * so the decoder must live in one shared module both can import. + * + * Ordering contract (important): + * - In html-to-md, decode runs AFTER the generic `<[^>]+>` tag strip, so an + * author who wrote `<b>` to SHOW a tag keeps `` as visible text. + * - In sanitize, decode runs BEFORE the SURGICAL strips (script/style/structural + * only). Revealed real `