@cyanheads/hn-mcp-server 0.5.13 → 0.5.15

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.
Files changed (41) hide show
  1. package/CLAUDE.md +18 -12
  2. package/Dockerfile +13 -7
  3. package/README.md +2 -1
  4. package/changelog/0.1.x/0.1.0.md +31 -0
  5. package/changelog/0.1.x/0.1.1.md +16 -0
  6. package/changelog/0.1.x/0.1.2.md +12 -0
  7. package/changelog/0.1.x/0.1.3.md +12 -0
  8. package/changelog/0.1.x/0.1.4.md +15 -0
  9. package/changelog/0.1.x/0.1.5.md +14 -0
  10. package/changelog/0.1.x/0.1.6.md +24 -0
  11. package/changelog/0.1.x/0.1.7.md +11 -0
  12. package/changelog/0.1.x/0.1.8.md +11 -0
  13. package/changelog/0.1.x/0.1.9.md +20 -0
  14. package/changelog/0.2.x/0.2.0.md +28 -0
  15. package/changelog/0.3.x/0.3.0.md +18 -0
  16. package/changelog/0.4.x/0.4.0.md +21 -0
  17. package/changelog/0.4.x/0.4.2.md +36 -0
  18. package/changelog/0.4.x/0.4.3.md +23 -0
  19. package/changelog/0.5.x/0.5.0.md +27 -0
  20. package/changelog/0.5.x/0.5.1.md +24 -0
  21. package/changelog/0.5.x/0.5.10.md +24 -0
  22. package/changelog/0.5.x/0.5.11.md +27 -0
  23. package/changelog/0.5.x/0.5.12.md +34 -0
  24. package/changelog/0.5.x/0.5.13.md +18 -0
  25. package/changelog/0.5.x/0.5.14.md +29 -0
  26. package/changelog/0.5.x/0.5.15.md +30 -0
  27. package/changelog/0.5.x/0.5.2.md +28 -0
  28. package/changelog/0.5.x/0.5.3.md +27 -0
  29. package/changelog/0.5.x/0.5.4.md +19 -0
  30. package/changelog/0.5.x/0.5.5.md +11 -0
  31. package/changelog/0.5.x/0.5.6.md +25 -0
  32. package/changelog/0.5.x/0.5.7.md +16 -0
  33. package/changelog/0.5.x/0.5.8.md +17 -0
  34. package/changelog/0.5.x/0.5.9.md +11 -0
  35. package/changelog/template.md +168 -0
  36. package/dist/services/hn/hn-service.d.ts +3 -1
  37. package/dist/services/hn/hn-service.d.ts.map +1 -1
  38. package/dist/services/hn/hn-service.js +36 -46
  39. package/dist/services/hn/hn-service.js.map +1 -1
  40. package/package.json +38 -24
  41. package/server.json +5 -5
@@ -0,0 +1,34 @@
1
+ ---
2
+ summary: "Reject fractional and blank/whitespace-only tool inputs; hn_search_content reports Algolia's own page count instead of a recomputation; hn_get_user's Firebase username segment is percent-encoded; hn_get_thread omits totalAvailable instead of rendering undefined. @cyanheads/mcp-ts-core ^0.10.9 → ^0.11.0."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.12 — 2026-07-28
8
+
9
+ ## Changed
10
+
11
+ - **`bunfig.toml`** — adds `[install.security] scanner = "@socketsecurity/bun-security-scanner"` and a `minimumReleaseAge` of 3 days (`259200`s) on installed package versions, excluding `@cyanheads/mcp-ts-core` via `minimumReleaseAgeExcludes` so framework releases still adopt same-day.
12
+ - **`Dockerfile`** — the production stage's OTEL `bun add` gains `--omit=dev --ignore-scripts`, and both stages gain a BuildKit cache mount (`--mount=type=cache,target=/root/.bun/install/cache`) for Bun's install cache.
13
+ - **`.gitignore`** — `data/` anchored to `/data/`; the unanchored pattern also matched nested paths such as `src/data/`.
14
+ - **`devcheck.config.json`** — `@socketsecurity/bun-security-scanner` added to the depcheck ignore list (install-time-only, never imported); `typescript` dropped from the `outdated` allowlist now that TypeScript 7 is adopted below.
15
+ - **Skill sync** — vendored `skills/` (15 directories) and framework `scripts/*.ts` (7 files) resynced to `@cyanheads/mcp-ts-core` 0.11.0.
16
+
17
+ ## Fixed
18
+
19
+ - **Fractional numeric inputs are rejected** — `count`/`offset` (`hn_get_stories`), `itemId`/`depth`/`maxComments` (`hn_get_thread`), `submissionCount` (`hn_get_user`), `count`/`page`/`minPoints` (`hn_search_content`), and `HN_CONCURRENCY_LIMIT` now require `.int()`. Fractional values previously passed validation and either silently truncated results or produced confusing upstream errors. [#10](https://github.com/cyanheads/hn-mcp-server/issues/10)
20
+ - **Blank and whitespace-only strings are rejected** — `hn_search_content`'s `query` and `author`, and `hn_get_user`'s `username`, are now `.trim()`-aware. A whitespace-only `query` no longer returns HN's global leaderboard, and a whitespace-only `author` no longer silently zeroes out results. [#9](https://github.com/cyanheads/hn-mcp-server/issues/9)
21
+ - **`hn_search_content` reports Algolia's own page count** — `totalPages` now mirrors the Algolia response's `nbPages` instead of recomputing `Math.ceil(nbHits / count)`, which overstated reachable pages by orders of magnitude on broad queries. [#11](https://github.com/cyanheads/hn-mcp-server/issues/11)
22
+ - **`hn_get_user` percent-encodes the username path segment** — `HnService.fetchUser` calls `encodeURIComponent` before building the Firebase URL, so a value like `../item/8863` stays one opaque `/user/` segment instead of escaping to `/item/`. [#13](https://github.com/cyanheads/hn-mcp-server/issues/13)
23
+ - **`hn_get_thread` omits `totalAvailable` instead of rendering it as `undefined`** — comment and job roots don't report `descendants`; the enrichment call now only keys `totalAvailable` when HN provides a value. [#14](https://github.com/cyanheads/hn-mcp-server/issues/14)
24
+
25
+ ## Dependencies
26
+
27
+ - `@cyanheads/mcp-ts-core` ^0.10.9 → ^0.11.0
28
+ - `typescript` ^6.0.3 → ^7.0.2
29
+ - `@biomejs/biome` ^2.5.0 → ^2.5.5
30
+ - `@types/node` ^26.0.0 → ^26.1.1
31
+ - `ignore` ^7.0.5 → ^7.0.6
32
+ - `tsc-alias` ^1.8.17 → ^1.9.1
33
+ - `vitest` ^4.1.9 → ^4.1.10
34
+ - new devDependency `@socketsecurity/bun-security-scanner` ^1.1.2 — the `bunfig.toml` install-time scanner
@@ -0,0 +1,18 @@
1
+ ---
2
+ summary: "hn_get_user gains submissionOffset to page through a user's full submission history; hn_search_content gains a compact view that drops duplicated body text; upstream HTTP failures now classify onto a typed reason and recovery hint instead of a raw fetch error."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.13 — 2026-07-28
8
+
9
+ ## Added
10
+
11
+ - **`hn_get_user`** — `submissionOffset` (integer, min 0, default 0) pages through `user.submitted` in windows of `submissionCount`, so histories beyond the first page are reachable. Enrichment echoes the applied `submissionOffset` and either the next offset to request or the valid range when the offset is past the end. [#12](https://github.com/cyanheads/hn-mcp-server/issues/12)
12
+ - **`hn_search_content`** — `view: "full" | "compact"` (default `"full"`) controls per-hit body projection. `"compact"` omits `text` and `highlights.text` — the pair that can repeat a long comment body twice per hit — while keeping id, title, url, domain, author, points, comment count, timestamp, parent story, title highlight, and `matchedWords`. The projection is applied before either output surface is built, so `structuredContent` and `content[]` stay equivalent. [#15](https://github.com/cyanheads/hn-mcp-server/issues/15)
13
+
14
+ ## Changed
15
+
16
+ - **`HnService`** — every fetch site now routes through a shared `getJson` helper wrapped in `withUpstreamHttpErrors`, which rewrites a non-2xx `FetchHttpError` into a classified `McpError` carrying `data.reason` and `data.recovery.hint`. Reasons: `upstream_rejected` (4xx other than 429, `InvalidParams`), `upstream_rate_limited` (429, `RateLimited`), `upstream_unavailable` (5xx, `ServiceUnavailable`). The wrapper sits outside `withRetry`, so retry eligibility, `Retry-After` handling, timeouts, and caller aborts still classify against the framework's original error. The upstream URL and verbatim response body are dropped from the client-facing message and `data`, staying only on the `cause` chain. [#16](https://github.com/cyanheads/hn-mcp-server/issues/16)
17
+ - **`parseJsonBody`** — a 200 response whose body is neither an HTML error page nor valid JSON now classifies as `upstream_malformed` (`ServiceUnavailable`) instead of letting a raw `SyntaxError` — which quotes the offending body back at the client — escape uncaught. [#16](https://github.com/cyanheads/hn-mcp-server/issues/16)
18
+ - **`hn_get_stories`**, **`hn_get_thread`**, **`hn_get_user`**, **`hn_search_content`** — each tool's `errors[]` contract now declares `upstream_rejected`, `upstream_rate_limited`, `upstream_unavailable`, `upstream_html`, and `upstream_malformed`, matching the reasons `HnService` can now throw.
@@ -0,0 +1,29 @@
1
+ ---
2
+ summary: "MCP SDK v2 compatibility, explicit stateless HTTP serving, and current framework-managed tooling"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.14 — 2026-08-24
8
+
9
+ ## Added
10
+
11
+ - **Community health files** — adds contribution and conduct guidance alongside the existing security policy.
12
+
13
+ ## Changed
14
+
15
+ - **MCP SDK v2 compatibility** — HTTP serves both protocol revision 2026-07-28 and the 2025 era; undeclared top-level tool inputs are rejected by name, and tool failures advertise a structured error envelope.
16
+ - **HTTP session mode** — self-hosted configuration and the container explicitly use stateless serving; the schema's `auto` default remains documented as resolving to stateful.
17
+ - **Container build** — Bun `1.3.14` → `1.4.0`, native build stages use `BUILDPLATFORM`, and production installs omit the framework's optional peer tiers.
18
+ - **TypeScript and tests** — the typecheck covers `tests/`, build emission uses a dedicated config, and the Vitest suite uses typed error-contract contexts across all 274 tests.
19
+ - **Project tooling** — framework-managed scripts, skills, package metadata, plugin metadata, and bundle exclusions are synchronized with the current scaffold.
20
+ - **Security contact** — vulnerability reports use `security@caseyjhand.com`.
21
+
22
+ ## Dependencies
23
+
24
+ - `@cyanheads/mcp-ts-core` `^0.11.0` → `^0.12.3`
25
+ - `@biomejs/biome` `^2.5.5` → `^2.5.10`
26
+ - `@types/node` `^26.1.1` → `^26.2.0`
27
+ - `tsc-alias` `^1.9.1` → `^1.9.2`
28
+ - `vitest` `^4.1.10` → `^4.1.11`
29
+ - `@vitest/coverage-istanbul` `^4.1.11` added; `pino-pretty` `^13.1.3` moved to runtime dependencies; direct `@hono/otel` `^1.1.2` removed in favor of the container's opt-in telemetry install.
@@ -0,0 +1,30 @@
1
+ ---
2
+ summary: "Cleared 6 transitive advisories in fast-uri and qs via overrides; adopted mcp-ts-core's retried upstream 500s and cancellation classification"
3
+ breaking: false
4
+ security: true
5
+ ---
6
+
7
+ # 0.5.15 — 2026-09-04
8
+
9
+ ## Changed
10
+
11
+ - **An upstream Hacker News 500 now retries before the call fails**, where it previously failed on the first attempt — `@cyanheads/mcp-ts-core` 0.12.4 moved 500 into the transient retry set.
12
+ - **A caller that disconnects mid-call is now reported as a cancellation** rather than an internal server error.
13
+ - Shipped skills: `add-tool` 2.21, `api-config` 1.15, `api-context` 2.2, `api-errors` 1.8, `api-linter` 1.13, `api-utils` 2.8, `design-mcp-server` 2.23, `maintenance` 2.6, `release-and-publish` 2.13, `setup` 1.10.
14
+
15
+ ## Fixed
16
+
17
+ - **`HnService.fetchItems` rethrows a caller cancellation** instead of absorbing it as a per-item failure — a batch item fetch now stops when the caller cancels, instead of spending the rest of the id list on fetches nobody is waiting for.
18
+
19
+ ## Security
20
+
21
+ - **`fast-uri` and `qs` pinned via a new `overrides` block** to `^3.1.6` and `^6.16.0` — clears 6 advisories `bun audit` reported through the framework's MCP SDK dependency (4 high in `fast-uri@3.1.5`: SSRF and host confusion; 2 moderate in `qs@6.15.3`). `bun audit` now reports no vulnerabilities across 372 packages.
22
+
23
+ ## Dependencies
24
+
25
+ - `@cyanheads/mcp-ts-core` `^0.12.3` → `^0.12.5`
26
+ - `zod` `^4.4.3` → `^4.5.4`
27
+ - `@biomejs/biome` `^2.5.10` → `^2.5.11`
28
+ - `@types/node` `^26.2.0` → `^26.4.0`
29
+ - `ignore` `^7.0.6` → `^7.0.8`
30
+ - `tsc-alias` `^1.9.2` → `^1.9.3`
@@ -0,0 +1,28 @@
1
+ ---
2
+ summary: "Maintenance + capability release. Adopts the MCP spec server-level `instructions` field on `createApp()` so clients forward orientation text (item types, ID reuse, case-sensitive usernames, sparsity) to the model. Framework `^0.8.20 → ^0.9.1` brings Workers `nodejs_compat` fix, SSRF hardening, gated `tasks` capability, build-time-only lint."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.2 — 2026-05-16
8
+
9
+ Maintenance + capability release. Adopts the MCP spec's server-level `instructions` field on `createApp()` so spec-compliant clients forward orientation text (HN item types, ID reuse across tools, case-sensitive usernames, field sparsity) to the model as session context — previously this text had to live in every tool description. Pulls in the framework's 0.9.x line, which fixes Cloudflare Workers boot under `nodejs_compat`, hardens SSRF DNS validation in Workers, broadens IPv6 SSRF coverage, gates the experimental `tasks` capability on actual task-tool registration (unblocks MCP spec `2025-06-18` strict-parse clients), and moves definition linting to build-time only (faster cold starts). No application code changes beyond the `instructions` adoption; no breaking changes for this server's tool surface.
10
+
11
+ ## Added
12
+
13
+ - `src/index.ts`: server-level `instructions` string on `createApp()` ([cyanheads/mcp-ts-core#91](https://github.com/cyanheads/mcp-ts-core/issues/91)). Sent on every `initialize` response and forwarded by spec-compliant clients to the model. Names the four tools, item type union (`story`, `comment`, `job`, `poll`, `pollopt`), ID reuse across tools, case-sensitive usernames, and the "treat absent fields as unknown" sparsity contract — moves session-level orientation out of per-tool description repetition.
14
+
15
+ ## Changed
16
+
17
+ - Bumped `@cyanheads/mcp-ts-core` from `^0.8.20` to `^0.9.1`. Spans two framework releases:
18
+ - **0.9.0**: `instructions` field on entry points; Workers now boot and serve MCP JSON-RPC under `nodejs_compat` (previous detection used `!process.versions.node`, which is truthy under compat); five-rule cross-vendor portability lint family (`schema-format-portability` error-level default-on; warning-level `schema-anyof-needs-type`, `schema-no-discriminator-keyword`; opt-in `schema-no-defs`, `schema-dialect-tag`); definition linting moved to build-time only (no longer gates `createApp()` startup); RFC 8414 §3 path-suffixed `/.well-known/oauth-protected-resource/{path}` mount; SSRF DNS validation now enforced in Workers and IPv6 blocklist broadened (IPv4-mapped, ULA, link-local, multicast); `FileSystemProvider` re-validates `tenantId` at the storage boundary; rate-limit metric cardinality bounded ([cyanheads/mcp-ts-core#124](https://github.com/cyanheads/mcp-ts-core/issues/124), [#91](https://github.com/cyanheads/mcp-ts-core/issues/91), [#132](https://github.com/cyanheads/mcp-ts-core/issues/132), [#70](https://github.com/cyanheads/mcp-ts-core/issues/70), [#72](https://github.com/cyanheads/mcp-ts-core/issues/72), [#114](https://github.com/cyanheads/mcp-ts-core/issues/114)).
19
+ - **0.9.1**: `tasks` capability advertisement gated on actual task-tool registration (fixes strict-parse MCP `2025-06-18` clients); new `Context.notifyToolListChanged` / `notifyPromptListChanged` notifiers; OTel `deployment.environment.name` migrated to typed `ATTR_DEPLOYMENT_ENVIRONMENT_NAME`; `fast-xml-parser` pinned to exact `5.7.3`; OTel exporter/instrumentation refresh `^0.217 → ^0.218`.
20
+ - Portability lint impact: this server's four tools were already free of `z.url()`/`z.cuid()`/etc. constructs that would trip `schema-format-portability` (default-on, error). `bun run lint:mcp` and `bun run devcheck` pass clean.
21
+ - `scripts/build-changelog.ts` summary cap raised `250 → 350` chars ([cyanheads/mcp-ts-core#129](https://github.com/cyanheads/mcp-ts-core/issues/129)) — synced from upstream; 250 was forcing operator-relevant context out of rollup summaries.
22
+ - `scripts/devcheck.ts` `bun outdated` parser fix — `bun outdated` emits markdown-style rows (`| pkg | … |`) with an empty leading cell, so `split('|')[0]` was always empty and the allowlist never matched. Now reads index `[1]` and strips the `(dev|peer|prod|optional)` workspace marker so bare package names match. Synced from upstream framework devcheck.
23
+ - Refreshed project skills synced from `mcp-ts-core@0.9.0`/`0.9.1` (no version regressions): `add-tool` 2.8 → 2.9 (mutator response design subsection + unit-bearing numeric-name checklist item), `api-errors` 1.5 → 1.6 (`When not to throw` section), `api-linter` 1.2 → 1.3 (full portability rule family documented), `api-workers` 1.3 → 1.4 (`instructions` resolver form on `WorkerHandlerOptions`), `design-mcp-server` 2.10 → 2.11, `field-test` 2.3 → 2.4 (mutator-trigger situational row), `polish-docs-meta` 1.7 → 1.8 (`OTEL_ENABLED` README template now links to framework telemetry docs), `tool-defs-analysis` 1.1 → 1.2 (audit now walks 12 categories — added mutator observability and unit-bearing numeric names). All propagated to `.claude/skills/`.
24
+ - Dependency refresh: `@cyanheads/mcp-ts-core ^0.8.20 → ^0.9.1`, `@types/node ^25.6.2 → ^25.8.0`, `vitest ^4.1.5 → ^4.1.6`.
25
+
26
+ ## Tests
27
+
28
+ - 147 passing. No test changes — `instructions` is verified by the framework's own suite.
@@ -0,0 +1,27 @@
1
+ ---
2
+ summary: "Field-test refinements to two `format()` output paths. `hn_get_stories` now distinguishes offset-past-end from genuinely-empty feeds. `hn_get_user` no longer duplicates the item type when rendering a typeless submission. Also migrates the project to the framework's directory-based changelog convention."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.3 — 2026-05-21
8
+
9
+ ## Changed
10
+
11
+ - `hn_get_stories` `format()`: three-branch empty-result handling. When `stories=[] && total>0 && offset>=total`, render `"<feed> feed — offset N is past the end (feed has M items)"` instead of bare `"<feed> feed — no stories"`. When `offset<total` but the page still came back empty (all items filtered out as dead/deleted), render `"<feed> feed — no live stories on this page (offset:N, feed total:M)"`. The original `"<feed> feed — no stories"` text is now reserved for `total===0`. Closes a parity gap between `structuredContent.total` and the `content[]` text — format-only clients (Claude Desktop) previously had no way to tell an over-paginated query apart from an empty feed.
12
+ - `hn_get_user` `format()`: submission rendering no longer duplicates the item type. The meta line skips the type token when the title fell back to `[${s.type}]`, so a typeless comment now renders as `**[comment]** — id:N | ...` instead of `**[comment]** — id:N | comment | ...`. Titled stories continue to carry the type token in meta (still shown exactly once). Title fallback also switched from `??` to `||` so an empty-string title (theoretical: HN-supplied title that strips to nothing) falls through to `[${s.type}]` instead of producing `**** — id:N | ...` with no type at all. Test invariant: type appears exactly once on every submission line.
13
+
14
+ ## Added
15
+
16
+ - `changelog/<major.minor>.x/<version>.md` directory-based per-version changelog convention (framework standard). All 18 historical entries migrated from the monolithic `CHANGELOG.md` via `scripts/split-changelog.ts` and backfilled with `summary`/`breaking`/`security` frontmatter. `CHANGELOG.md` is now a 75-line navigation index regenerated by `bun run changelog:build`. `bun run changelog:check` gates devcheck against drift.
17
+ - `package.json` scripts: `changelog:build` and `changelog:check`.
18
+ - `changelog/template.md` pristine reference synced from `@cyanheads/mcp-ts-core/templates/changelog/template.md`.
19
+
20
+ ## Tests
21
+
22
+ - 153 passing (up from 150). Added three branches to `get-stories.tool.test.ts` covering offset-past-end (with singular/plural item count), and the all-filtered-on-page case. Added an `it.each` parametric test to `get-user.tool.test.ts` that asserts the item type is rendered exactly once across undefined, empty, and real title shapes.
23
+
24
+ ## Maintenance
25
+
26
+ - `@types/node ^25.8.0 → ^25.9.1`, `vitest ^4.1.6 → ^4.1.7` (both patch). No application impact.
27
+ - `scripts/devcheck.ts` resynced from `@cyanheads/mcp-ts-core` — `bun outdated` parser refactored into two filter passes with an extracted `stripWorkspaceMarker` helper. Functionally equivalent.
@@ -0,0 +1,19 @@
1
+ ---
2
+ summary: "Maintenance. `@cyanheads/mcp-ts-core ^0.9.1 → ^0.9.6`, `zod` added as explicit dependency. `manifest.json` + `.mcpbignore` scaffolded for MCPB bundle support. Install badges added to README. New scripts: `list-skills`, `lint:packaging`, `bundle`, `audit:refresh`."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.4 — 2026-05-23
8
+
9
+ ## Maintenance
10
+
11
+ - `@cyanheads/mcp-ts-core ^0.9.1 → ^0.9.6`. Span across five patch releases; no breaking changes for this server.
12
+ - `zod ^4.4.3` added as an explicit dependency (previously transitive via mcp-ts-core).
13
+ - `manifest.json` scaffolded — MCPB bundle descriptor for Claude Desktop one-click install.
14
+ - `.mcpbignore` scaffolded — excludes source, devDeps, and scripts from the bundle.
15
+ - `package.json` scripts: `list-skills`, `lint:packaging`, `bundle` (`@anthropic-ai/mcpb pack`), `audit:refresh`.
16
+ - `scripts/lint-packaging.ts` + `scripts/list-skills.ts` added (synced from mcp-ts-core).
17
+ - `scripts/devcheck.ts` resynced from mcp-ts-core.
18
+ - README: install badges row (Claude Desktop MCPB, Cursor deeplink, VS Code deeplink). Docker config snippet added to self-hosted section. Badge row reordered; description line tightened.
19
+ - CLAUDE.md: `ctx.tenantId` row added to Context table; `list-skills`, `lint:packaging`, `bundle`, `audit:refresh` added to Commands table.
@@ -0,0 +1,11 @@
1
+ ---
2
+ summary: "Fix duplicate `hono` installs causing `@hono/otel` crash on every HTTP request. `hono ^4.12.22` added as direct dependency to force deduplication."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.5 — 2026-05-23
8
+
9
+ ## Bug Fix
10
+
11
+ - **`hono` added as direct dependency (`^4.12.22`)** — the lockfile resolved `hono` to 4.12.19 at the top level while `mcp-ts-core` needed `^4.12.22`, causing bun to install a second copy nested inside `mcp-ts-core/node_modules/`. Two Hono instances mean two distinct `GET_MATCH_RESULT` symbols; `@hono/otel` imported `matchedRoutes` from the top-level copy but routing ran through the nested copy, so every HTTP request threw `TypeError: undefined is not an object (evaluating 'c.req[GET_MATCH_RESULT][0]')` and returned 500. Adding `hono` as a direct dependency forces a single hoisted copy, eliminating the symbol mismatch.
@@ -0,0 +1,25 @@
1
+ ---
2
+ summary: "@cyanheads/mcp-ts-core ^0.9.6 → ^0.9.13: 413 body cap, HTTP session-init gate, quieter client-error logging, GET /mcp surfaces keywords; dep refresh; package.json keyword additions"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.6 — 2026-05-28
8
+
9
+ ## Changed
10
+
11
+ - **`@cyanheads/mcp-ts-core` ^0.9.6 → ^0.9.13** — framework adoption covering:
12
+ - **`MCP_HTTP_MAX_BODY_BYTES`** — configurable inbound body cap (default 1 MiB); oversized requests rejected with HTTP 413 before session allocation or body parsing.
13
+ - **HTTP session-init gate** — stateful HTTP mode rejects non-`initialize` requests that arrive without an `Mcp-Session-Id` header with HTTP 400, preventing uninitialized sessions from being minted on first contact.
14
+ - **Quieter client-error logging** — expected 401, 403, 400, and 404 responses now logged at `warning` instead of full error-pipeline with stack traces.
15
+ - **`GET /mcp` keywords** — `package.json` `keywords` now included in the status JSON alongside `name`, `version`, and `description`.
16
+ - **`src/index.ts`** — `landing: { requireAuth: false }` added; restores full tool-inventory visibility on the landing page for this public-catalog deployment (framework 0.9.13 default gates inventory behind auth when `MCP_AUTH_MODE` is `jwt` or `oauth`).
17
+ - **`manifest.json`** — `repository`, `homepage`, and `license` fields added.
18
+ - **`package.json` keywords** — `typescript`, `bun`, `stdio`, `streamable-http` added.
19
+ - **Skill sync** — `git-wrapup`, `code-simplifier` skills added; `migrate-mcp-ts-template` removed; bulk refresh across field-test, maintenance, polish-docs-meta, design-mcp-server, api-canvas, api-config, release-and-publish, report-issue-framework.
20
+
21
+ ## Dependencies
22
+
23
+ - `@cyanheads/mcp-ts-core` ^0.9.6 → ^0.9.13
24
+ - `hono` ^4.12.22 → ^4.12.23
25
+ - `@biomejs/biome` ^2.4.15 → ^2.4.16
@@ -0,0 +1,16 @@
1
+ ---
2
+ summary: "enrichment adoption — ranked-feed and search tools surface result context (counts, query echo, empty-result guidance) in a typed enrichment block reaching both structuredContent JSON and content[] markdown trailer"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.7 — 2026-05-30
8
+
9
+ ## Changed
10
+
11
+ - **`hn_get_stories`** — `total`, `offset`, `hasMore` moved from `output` to `enrichment`; `notice` (empty-result guidance) added to `enrichment`. `structuredContent` field `offset` renamed to match its new location; value preserved.
12
+ - **`hn_get_thread`** — `totalLoaded`, `totalAvailable` moved from `output` to `enrichment`; `omitted` object replaced by `notice` string in `enrichment` (consolidates deleted/dead counts and pagination hint into one field).
13
+ - **`hn_search_content`** — `totalHits`, `page`, `totalPages` moved from `output` to `enrichment`; `message` renamed to `notice` in `enrichment`. `query` stays in `output`.
14
+ - **Tests** — updated across all three tool test files to match the revised `output` + `enrichment` shapes.
15
+ - **`@cyanheads/mcp-ts-core` ^0.9.13 → ^0.9.16** — enrichment API adoption.
16
+ - **Skill sync** — bulk refresh across `add-tool`, `add-app-tool`, `add-service`, `api-context`, `api-linter`, `design-mcp-server`, `git-wrapup`, `maintenance`, `polish-docs-meta`, `setup`; `devcheck` skill removed (superseded by the `devcheck.config.json`-driven script).
@@ -0,0 +1,17 @@
1
+ ---
2
+ summary: "@cyanheads/mcp-ts-core ^0.9.16 → ^0.9.21 — per-request log context fix, secret-stripped error messages, withRetry fail-fast on non-retryable errors"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.8 — 2026-06-02
8
+
9
+ ## Changed
10
+
11
+ - **`@cyanheads/mcp-ts-core` ^0.9.16 → ^0.9.21** — framework adoption; see below for notable changes.
12
+ - **HTTP transport per-request log context** — per-request logs and traces now carry fresh request + trace/span IDs instead of the frozen boot context.
13
+ - **`fetchWithTimeout`** — strips query-string secrets (e.g. `?api_key=`) from error messages and logs.
14
+ - **`withRetry`** — fails fast on non-retryable errors; `ctx.fail` auto-populates the `retryable` flag.
15
+ - **Skill sync** — `add-tool`, `add-service`, `api-canvas`, `api-context`, `api-linter`, `api-utils`, `design-mcp-server`, `release-and-publish` updated; `api-mirror` and `orchestrations` added.
16
+ - **Scripts** — `devcheck.ts`, `check-skill-versions.ts`, `release-github.ts` added; `release:github` npm script added.
17
+ - **`vitest` ^4.1.7 → ^4.1.8**
@@ -0,0 +1,11 @@
1
+ ---
2
+ summary: "hn_get_user: surface submission pagination notice via enrichment block"
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.9 — 2026-06-04
8
+
9
+ ## Fixed
10
+
11
+ - **`hn_get_user` pagination notice** — adds `enrichment.notice` and calls `ctx.enrich.notice()` when `includeSubmissions` is true and the resolved (live) submission count is less than `totalSubmissions`. The notice reads: `Showing N of M submissions. Raise submissionCount (max 50) for more.` Resolves [#8](https://github.com/cyanheads/hn-mcp-server/issues/8).
@@ -0,0 +1,168 @@
1
+ ---
2
+ # FORMAT REFERENCE — do not edit. Copy this file to
3
+ # `changelog/<major.minor>.x/<version>.md` (e.g. `changelog/0.8.x/0.8.6.md`)
4
+ # to author a new release. Set that file's H1 to `# <version> — YYYY-MM-DD`
5
+ # with a concrete date.
6
+
7
+ # Required. One-line GitHub Release-style headline. 350 character cap — a
8
+ # ceiling, not a target. Default short and scannable. Don't pad, don't stitch
9
+ # unrelated changes with commas/semicolons into an inventory — pick the
10
+ # headline, like a tag's theme line. Quotes required: unquoted YAML treats
11
+ # `: ` inside the value as a key separator and fails GitHub's strict parser.
12
+ summary: ""
13
+
14
+ # Set `true` when consumers must change code to upgrade: API removals,
15
+ # signature changes, config renames, behavior changes that break existing
16
+ # usage. Flagged as `Breaking` in the rollup.
17
+ breaking: false
18
+
19
+ # Set `true` ONLY for a security fix in THIS project's own source code — a
20
+ # vulnerability or hardening in code you ship. A dependency or transitive CVE
21
+ # bump is routine maintenance, NOT a security release: record it under
22
+ # `## Dependencies` (with the advisory ID) and leave this `false`. When true,
23
+ # pairs with the `## Security` section below and flags `Security` in the rollup.
24
+ security: false
25
+
26
+ # Optional free-form notes for maintenance agents processing this release.
27
+ # Not rendered in CHANGELOG — consumed by agents running `maintenance` on
28
+ # downstream servers. ADOPTION STEPS ONLY — new files to create, fields to
29
+ # populate, one-time migration steps. Never a second rendering of the body:
30
+ # if a body bullet already says it, name the bullet's symbol instead of
31
+ # re-explaining. Omit the field entirely when there's nothing to say.
32
+ # agent-notes: |
33
+ # <instructions for downstream maintenance agents>
34
+ ---
35
+
36
+ # <version> — YYYY-MM-DD
37
+
38
+ <!--
39
+ AUTHORING GUIDE — applies to the new per-version file you create from this
40
+ template.
41
+
42
+ Audience: someone scanning release notes to decide what affects them. Lead
43
+ each bullet with the symbol or concept name in **bold** so they can skip
44
+ what's irrelevant and zoom in on what's not.
45
+
46
+ Tone: terse, fact-dense, not verbose. Bullet shape: **symbol** + what
47
+ changed + at most one consumer-facing caveat. One sentence by default, two
48
+ when the second carries weight — a bullet past ~40 words or three sentences
49
+ is wrong. The depth lives one hop away: the linked issue carries the why,
50
+ the commit diff carries the how. The changelog names what changed and what
51
+ a consumer does about it; a reader who wants mechanism opens the link.
52
+
53
+ Model length on THIS guide, never on the previous entry — entries modeled
54
+ on entries compound.
55
+
56
+ Cut (each has shipped as a wall of text; these are the cruft):
57
+ - History/justification narration — how the bug worked, why the old
58
+ behavior was wrong. One short clause at most; the issue carries the story.
59
+ - Design-rationale defense — "chosen over Y because…", "guarding the
60
+ getter is not enough…". That is the author arguing with a reviewer;
61
+ reviewers read the PR, not the changelog.
62
+ - Defensive unchanged-clauses — "X is unchanged", "byte-identical to
63
+ <prev>". Keep one only where its absence would cause a real misread,
64
+ as a short parenthetical.
65
+ - Edge-case inventories — marker lists, not-flagged lists, escape tables.
66
+ Tests and the issue carry those.
67
+ - Mechanism walkthroughs (JSDoc, CLAUDE.md/AGENTS.md, or the relevant
68
+ skill own those), ceremonial framings ("This release introduces…"),
69
+ backwards-compat paragraphs, file-by-file test enumerations. Prefer
70
+ code/symbol names over English re-explanations.
71
+
72
+ Verified ≠ included: the every-claim-verified-from-the-diff rule bounds
73
+ the TRUTH of what you write, never the AMOUNT.
74
+
75
+ Example — same fact, right size:
76
+
77
+ TOO LONG: **`fetchWithTimeout`'s `timeoutMs` bounds the whole exchange**
78
+ (#341). `fetch` resolves once headers arrive and the deadline was
79
+ cleared as the helper returned, so a peer that answered promptly and
80
+ then stalled the stream held the request open indefinitely. A 2xx
81
+ carrying a body now comes back as a passthrough wrapper that disarms
82
+ the deadline when the body closes, errors, or is cancelled; …
83
+ [+90 more words of mechanism and edge cases]
84
+
85
+ RIGHT: **`fetchWithTimeout`'s `timeoutMs` now bounds the whole
86
+ exchange, not just the headers** (#341). A stalled body aborts with
87
+ the same `Timeout` error; the returned `Response` is a wrapper, so
88
+ identity assertions (`toBe(response)`) no longer hold.
89
+
90
+ Narrative intro: skip by default. Add one short sentence only when the
91
+ release theme genuinely needs framing the bullets can't carry. When many
92
+ bullets share one upgrade consequence, state it ONCE — intro line or
93
+ agent-notes — never per bullet.
94
+
95
+ Sections: Keep a Changelog order — Added, Changed, Deprecated, Removed,
96
+ Fixed, Security. Include only sections with entries; delete the rest
97
+ (including the commented-out scaffolding below). Don't ship empty headers.
98
+
99
+ Include: every distinct fact a reader needs to adopt or audit the release —
100
+ new exports, signatures, lint rule IDs, env vars, breaking changes, version
101
+ bumps on shipped skills. Nothing more.
102
+
103
+ Links: link issues, PRs, docs, or skills where they help a reader jump to
104
+ context. Once per item per entry — don't re-link the same issue in summary,
105
+ narrative, and bullet. Skip links for inline symbol names; code spans speak
106
+ for themselves.
107
+
108
+ Issue/PR URLs: use full URLs. GitHub's bare `#NN` auto-link only resolves
109
+ inside its own UI, not in npm reads or local editors.
110
+
111
+ [#38](https://github.com/cyanheads/mcp-ts-core/issues/38) ← issue
112
+ [#42](https://github.com/cyanheads/mcp-ts-core/pull/42) ← PR
113
+
114
+ Verify numbers exist before linking (`gh issue view NN`, `gh pr view NN`).
115
+ Never speculate on a future number — `#42` for an upcoming PR silently
116
+ resolves to whatever real item already owns 42, and timeline previews pull
117
+ in that unrelated item's metadata.
118
+
119
+ TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
120
+ via `gh release create --notes-from-tag`. The tag is a derivative of this
121
+ changelog entry — a condensed, scannable version, not a copy. Format:
122
+
123
+ <theme — omit version number, GitHub prepends it>
124
+ ← blank line
125
+ <1-2 sentence context: what this release does>
126
+ ← blank line
127
+ Dependency bumps: ← section header
128
+ ← blank line
129
+ - `@cyanheads/mcp-ts-core` ^0.9.1 → ^0.9.6 ← bullet
130
+ ← blank line
131
+ Changed: ← only sections with entries
132
+ ← blank line
133
+ - `format()` output includes `query` in text mode
134
+ ← blank line
135
+ Added:
136
+ ← blank line
137
+ - `manifest.json` scaffolded for MCPB bundle support
138
+ - Install badges (Claude Desktop, Cursor, VS Code)
139
+ ← blank line
140
+ <N> tests pass; `bun run devcheck` clean. ← footer
141
+
142
+ Never a flat comma-separated string. Always structured markdown with
143
+ sections. The tag must scan well as a rendered GitHub Release page.
144
+ -->
145
+
146
+ ## Added
147
+
148
+ -
149
+
150
+ ## Changed
151
+
152
+ -
153
+
154
+ <!-- ## Deprecated
155
+
156
+ - -->
157
+
158
+ <!-- ## Removed
159
+
160
+ - -->
161
+
162
+ ## Fixed
163
+
164
+ -
165
+
166
+ <!-- ## Security
167
+
168
+ - -->
@@ -46,7 +46,9 @@ export declare class HnService {
46
46
  /**
47
47
  * Batch-fetch items with concurrency limiting. Preserves input order.
48
48
  * Per-item failures after exhausted retries are logged and yield `null`
49
- * so a single bad item does not fail the whole batch.
49
+ * so a single bad item does not fail the whole batch. A caller abort is the
50
+ * one failure that is not per-item: it rethrows, ending the batch instead of
51
+ * spending the rest of the id list on fetches nobody is waiting for.
50
52
  */
51
53
  fetchItems(ids: number[], ctx: Context): Promise<(HnItem | null)[]>;
52
54
  /** Search HN via Algolia. Throws on upstream failure after retries. */
@@ -1 +1 @@
1
- {"version":3,"file":"hn-service.d.ts","sourceRoot":"","sources":["../../../src/services/hn/hn-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAKtD,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAsC9E,8CAA8C;AAC9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyB9C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,gDAAgD;AAChD,wBAAgB,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAEpE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOzE;AAED,gDAAgD;AAChD,wBAAgB,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,CAElE;AA8GD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,YAAY,gBAAgB,EAAE,MAAM,EAEnC;IAED;;;;OAIG;IACH,OAAO,CAAC,OAAO;IAkBf,mFAAmF;IACnF,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE1D;IAED;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOhE;IAED,yEAAyE;IACzE,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAG3D;IAED;;;;OAIG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAyBxE;IAED,uEAAuE;IACvE,MAAM,CACJ,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5B,IAAI,EAAE,WAAW,GAAG,MAAM,CAAC;QAC3B,SAAS,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;YAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;SAAE,GAAG,SAAS,CAAC;QACjF,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,EACD,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,eAAe,CAAC,CA4B1B;CACF;AAQD,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED,wBAAgB,YAAY,IAAI,SAAS,CAGxC"}
1
+ {"version":3,"file":"hn-service.d.ts","sourceRoot":"","sources":["../../../src/services/hn/hn-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAWtD,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAmB9E,8CAA8C;AAC9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyB9C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,gDAAgD;AAChD,wBAAgB,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAEpE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOzE;AAED,gDAAgD;AAChD,wBAAgB,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,CAElE;AA4HD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,YAAY,gBAAgB,EAAE,MAAM,EAEnC;IAED;;;;OAIG;IACH,OAAO,CAAC,OAAO;IAmBf,mFAAmF;IACnF,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE1D;IAED;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOhE;IAED,yEAAyE;IACzE,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAG3D;IAED;;;;;;OAMG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CA0BxE;IAED,uEAAuE;IACvE,MAAM,CACJ,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5B,IAAI,EAAE,WAAW,GAAG,MAAM,CAAC;QAC3B,SAAS,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;YAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;SAAE,GAAG,SAAS,CAAC;QACjF,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,EACD,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,eAAe,CAAC,CA4B1B;CACF;AAQD,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED,wBAAgB,YAAY,IAAI,SAAS,CAGxC"}
@@ -2,34 +2,12 @@
2
2
  * @fileoverview HN API service — Firebase and Algolia HTTP clients with HTML stripping utilities.
3
3
  * @module services/hn/hn-service
4
4
  */
5
- import { JsonRpcErrorCode, McpError, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
5
+ import { invalidParams, JsonRpcErrorCode, McpError, rateLimited, serviceUnavailable, } from '@cyanheads/mcp-ts-core/errors';
6
6
  import { fetchWithTimeout, withRetry } from '@cyanheads/mcp-ts-core/utils';
7
7
  import { getServerConfig } from '../../config/server-config.js';
8
8
  const HN_API = 'https://hacker-news.firebaseio.com/v0';
9
9
  const ALGOLIA_API = 'https://hn.algolia.com/api/v1';
10
10
  const REQUEST_TIMEOUT_MS = 10_000;
11
- /**
12
- * Project a handler Context onto a RequestContext shape for framework
13
- * utilities. Runtime-safe — the framework only reads log-binding fields —
14
- * but RequestContext's `[key: string]: unknown` index signature is not
15
- * present on Context, so we materialize a plain object whose literal shape
16
- * satisfies the index signature.
17
- */
18
- function toRequestContext(ctx) {
19
- const base = {
20
- requestId: ctx.requestId,
21
- timestamp: ctx.timestamp,
22
- };
23
- if (ctx.tenantId !== undefined)
24
- base.tenantId = ctx.tenantId;
25
- if (ctx.traceId !== undefined)
26
- base.traceId = ctx.traceId;
27
- if (ctx.spanId !== undefined)
28
- base.spanId = ctx.spanId;
29
- if (ctx.auth !== undefined)
30
- base.auth = ctx.auth;
31
- return base;
32
- }
33
11
  // ---------------------------------------------------------------------------
34
12
  // HTML utilities
35
13
  // ---------------------------------------------------------------------------
@@ -148,31 +126,39 @@ function parseJsonBody(text, upstream) {
148
126
  * The code travels with the reason rather than being taken from the framework's
149
127
  * finer-grained status ladder, so each reason surfaces exactly the code its
150
128
  * tools declare in `errors[]`. That ladder would otherwise split one reason
151
- * across several codes — HN answers an unknown path with 401, and a 500 or 504
152
- * would arrive as `InternalError` or `Timeout`, blurring the two codes that
153
- * mean "this server broke" and "this server's own fetch timed out". The exact
154
- * status stays on `data.status`.
129
+ * across several codes — HN answers an unknown path with 401, and a 504 would
130
+ * arrive as `Timeout`, blurring "this server's own fetch timed out" with an
131
+ * upstream that is merely down. The exact status stays on `data.status`.
155
132
  */
156
- function upstreamFailureFor(upstream, status) {
133
+ function upstreamFailureFor(upstream, status, cause) {
157
134
  if (status === 429) {
158
- return {
159
- code: JsonRpcErrorCode.RateLimited,
135
+ return rateLimited(`${upstream} returned HTTP ${status}.`, {
136
+ upstream,
137
+ status,
160
138
  reason: 'upstream_rate_limited',
161
- hint: `${upstream} is rate-limiting this server. Wait several seconds before retrying the same call, and reduce how often it is called.`,
162
- };
139
+ recovery: {
140
+ hint: `${upstream} is rate-limiting this server. Wait several seconds before retrying the same call, and reduce how often it is called.`,
141
+ },
142
+ }, { cause });
163
143
  }
164
144
  if (status >= 500) {
165
- return {
166
- code: JsonRpcErrorCode.ServiceUnavailable,
145
+ return serviceUnavailable(`${upstream} returned HTTP ${status}.`, {
146
+ upstream,
147
+ status,
167
148
  reason: 'upstream_unavailable',
168
- hint: `${upstream} is failing or in maintenance. Retry after a short delay — no input change helps while the upstream is down.`,
169
- };
149
+ recovery: {
150
+ hint: `${upstream} is failing or in maintenance. Retry after a short delay — no input change helps while the upstream is down.`,
151
+ },
152
+ }, { cause });
170
153
  }
171
- return {
172
- code: JsonRpcErrorCode.InvalidParams,
154
+ return invalidParams(`${upstream} returned HTTP ${status}.`, {
155
+ upstream,
156
+ status,
173
157
  reason: 'upstream_rejected',
174
- hint: `${upstream} rejected the request as malformed. Check the tool input values against the schema; retrying the same input fails identically.`,
175
- };
158
+ recovery: {
159
+ hint: `${upstream} rejected the request as malformed. Check the tool input values against the schema; retrying the same input fails identically.`,
160
+ },
161
+ }, { cause });
176
162
  }
177
163
  /**
178
164
  * Translate `fetchWithTimeout`'s raw non-2xx throw into a classified error
@@ -196,8 +182,7 @@ async function withUpstreamHttpErrors(upstream, fn) {
196
182
  const status = err.data.status;
197
183
  if (typeof status !== 'number')
198
184
  throw err;
199
- const { code, reason, hint } = upstreamFailureFor(upstream, status);
200
- throw new McpError(code, `${upstream} returned HTTP ${status}.`, { upstream, status, reason, recovery: { hint } }, { cause: err });
185
+ throw upstreamFailureFor(upstream, status, err);
201
186
  }
202
187
  }
203
188
  // ---------------------------------------------------------------------------
@@ -214,11 +199,12 @@ export class HnService {
214
199
  * HTML-instead-of-JSON — classified onto a contract reason.
215
200
  */
216
201
  getJson(upstream, operation, url, ctx) {
217
- const rc = toRequestContext(ctx);
218
202
  return withUpstreamHttpErrors(upstream, () => withRetry(async () => {
219
- const res = await fetchWithTimeout(url, REQUEST_TIMEOUT_MS, rc, { signal: ctx.signal });
203
+ const res = await fetchWithTimeout(url, REQUEST_TIMEOUT_MS, ctx, {
204
+ signal: ctx.signal,
205
+ });
220
206
  return parseJsonBody(await res.text(), upstream);
221
- }, { operation, context: rc, signal: ctx.signal }));
207
+ }, { operation, context: ctx, signal: ctx.signal }));
222
208
  }
223
209
  /** Fetch a single item by ID. Returns null when HN reports the item is missing. */
224
210
  fetchItem(id, ctx) {
@@ -241,7 +227,9 @@ export class HnService {
241
227
  /**
242
228
  * Batch-fetch items with concurrency limiting. Preserves input order.
243
229
  * Per-item failures after exhausted retries are logged and yield `null`
244
- * so a single bad item does not fail the whole batch.
230
+ * so a single bad item does not fail the whole batch. A caller abort is the
231
+ * one failure that is not per-item: it rethrows, ending the batch instead of
232
+ * spending the rest of the id list on fetches nobody is waiting for.
245
233
  */
246
234
  async fetchItems(ids, ctx) {
247
235
  if (ids.length === 0)
@@ -258,6 +246,8 @@ export class HnService {
258
246
  results[i] = await this.fetchItem(id, ctx);
259
247
  }
260
248
  catch (err) {
249
+ if (err instanceof McpError && err.code === JsonRpcErrorCode.RequestCancelled)
250
+ throw err;
261
251
  ctx.log.warning('Batch item fetch failed after retries', {
262
252
  id,
263
253
  error: err instanceof Error ? err.message : String(err),