@cyanheads/hn-mcp-server 0.6.0 → 0.7.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/AGENTS.md +6 -6
- package/CLAUDE.md +6 -6
- package/README.md +24 -22
- package/changelog/0.6.x/0.6.1.md +34 -0
- package/changelog/0.7.x/0.7.0.md +32 -0
- package/changelog/template.md +10 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-stories.tool.d.ts +15 -0
- package/dist/mcp-server/tools/definitions/get-stories.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-stories.tool.js +47 -24
- package/dist/mcp-server/tools/definitions/get-stories.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-thread.tool.d.ts +10 -1
- package/dist/mcp-server/tools/definitions/get-thread.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-thread.tool.js +235 -277
- package/dist/mcp-server/tools/definitions/get-thread.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-user.tool.d.ts +14 -1
- package/dist/mcp-server/tools/definitions/get-user.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-user.tool.js +43 -72
- package/dist/mcp-server/tools/definitions/get-user.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-content.tool.d.ts +15 -17
- package/dist/mcp-server/tools/definitions/search-content.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-content.tool.js +124 -163
- package/dist/mcp-server/tools/definitions/search-content.tool.js.map +1 -1
- package/dist/mcp-server/tools/username-input.d.ts +23 -0
- package/dist/mcp-server/tools/username-input.d.ts.map +1 -0
- package/dist/mcp-server/tools/username-input.js +30 -0
- package/dist/mcp-server/tools/username-input.js.map +1 -0
- package/dist/services/hn/hn-service.d.ts +57 -8
- package/dist/services/hn/hn-service.d.ts.map +1 -1
- package/dist/services/hn/hn-service.js +128 -16
- package/dist/services/hn/hn-service.js.map +1 -1
- package/dist/services/hn/types.d.ts +4 -0
- package/dist/services/hn/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/server.json +3 -3
- package/skills/hn-guide/SKILL.md +8 -8
- package/dist/mcp-server/tools/response-budget.d.ts +0 -111
- package/dist/mcp-server/tools/response-budget.d.ts.map +0 -1
- package/dist/mcp-server/tools/response-budget.js +0 -204
- package/dist/mcp-server/tools/response-budget.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** hn-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
6
|
-
**Engines:** Bun ≥1.4.
|
|
4
|
+
**Version:** 0.7.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.22`
|
|
6
|
+
**Engines:** Bun ≥1.4.4, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` 2.4.0
|
|
8
8
|
**Zod:** ^4.6.7
|
|
9
9
|
|
|
@@ -203,7 +203,7 @@ import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
|
203
203
|
throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection failed', { pool: 'primary' });
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
**Service-layer:** services don't have `ctx.fail`, but can carry the contract reason via `data: { reason: 'X' }` on a factory throw. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`, and the framework fills the contract's `recovery` for a throw that carries none. `hn-service.ts` throws
|
|
206
|
+
**Service-layer:** services don't have `ctx.fail`, but can carry the contract reason via `data: { reason: 'X' }` on a factory throw. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`, and the framework fills the contract's `recovery` for a throw that carries none. `hn-service.ts` throws five `upstream_*` reasons this way, each with an explicit `recovery: { hint }` that overrides the contract text with runtime context (the upstream's name, the `Retry-After` wait), plus `upstream_no_response` (a stories or user page on which every item failed, most often with no answer from the API), which carries no hint, so the contract's `recovery` fills it, and the declared `retry_deadline_exceeded` (`hn_get_stories`, `hn_get_user`, `hn_get_thread`). Each of their `errors[]` entries carries `thrownBy: 'service'` — `error-contract-unthrown` reads the handler body alone and would otherwise flag them as dead. See framework CLAUDE.md and `api-errors` skill for the full auto-classification table and all factories.
|
|
207
207
|
|
|
208
208
|
---
|
|
209
209
|
|
|
@@ -220,9 +220,9 @@ src/
|
|
|
220
220
|
types.ts # HN domain types
|
|
221
221
|
mcp-server/
|
|
222
222
|
tools/markdown-escape.ts # format() boundary: escape titles, quote bodies
|
|
223
|
-
tools/response-budget.ts # 64,000-byte budget per response surface: measure both surfaces, largest shared body cut, code-point cut
|
|
224
223
|
tools/served-at-cap.ts # notice sentence for a size input served at its per-call cap
|
|
225
224
|
tools/thread-cursor.ts # hn_get_thread cursor codec: packed frontier, JSON format still decoded
|
|
225
|
+
tools/username-input.ts # shared input schema for an HN account name: username and author
|
|
226
226
|
tools/definitions/
|
|
227
227
|
get-stories.tool.ts # hn_get_stories — Fetch stories from an HN feed
|
|
228
228
|
get-thread.tool.ts # hn_get_thread — Get item + comment tree
|
|
@@ -335,7 +335,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
335
335
|
|
|
336
336
|
## Bundling
|
|
337
337
|
|
|
338
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored and local-only files such as `claude-plans/` — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents
|
|
338
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored and local-only files such as `claude-plans/` — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `AGENTS.md`, and `CLAUDE.md` at a package's root, and any `SKILL.md`); platform-specific native bindings; node-gyp build output beside a `binding.gyp`; and the framework's build-only files. The agent-doc selector matches only at a package root because a `skills/` deeper in a package is runtime code: MCP SDK 2.4 ships its Skills extension at `@modelcontextprotocol/{server,core}/dist/ext/skills/`, which the framework imports at startup. `tests/scripts/bundle-agent-docs.test.ts` pins that selector in both `clean-mcpb.ts` and `lint-packaging.ts`, so a script resync that would strip the SDK's modules fails the suite. MCPB is stdio-only — HTTP deployments are unaffected.
|
|
339
339
|
|
|
340
340
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
|
|
341
341
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** hn-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
6
|
-
**Engines:** Bun ≥1.4.
|
|
4
|
+
**Version:** 0.7.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.22`
|
|
6
|
+
**Engines:** Bun ≥1.4.4, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` 2.4.0
|
|
8
8
|
**Zod:** ^4.6.7
|
|
9
9
|
|
|
@@ -203,7 +203,7 @@ import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
|
203
203
|
throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection failed', { pool: 'primary' });
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
**Service-layer:** services don't have `ctx.fail`, but can carry the contract reason via `data: { reason: 'X' }` on a factory throw. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`, and the framework fills the contract's `recovery` for a throw that carries none. `hn-service.ts` throws
|
|
206
|
+
**Service-layer:** services don't have `ctx.fail`, but can carry the contract reason via `data: { reason: 'X' }` on a factory throw. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`, and the framework fills the contract's `recovery` for a throw that carries none. `hn-service.ts` throws five `upstream_*` reasons this way, each with an explicit `recovery: { hint }` that overrides the contract text with runtime context (the upstream's name, the `Retry-After` wait), plus `upstream_no_response` (a stories or user page on which every item failed, most often with no answer from the API), which carries no hint, so the contract's `recovery` fills it, and the declared `retry_deadline_exceeded` (`hn_get_stories`, `hn_get_user`, `hn_get_thread`). Each of their `errors[]` entries carries `thrownBy: 'service'` — `error-contract-unthrown` reads the handler body alone and would otherwise flag them as dead. See framework CLAUDE.md and `api-errors` skill for the full auto-classification table and all factories.
|
|
207
207
|
|
|
208
208
|
---
|
|
209
209
|
|
|
@@ -220,9 +220,9 @@ src/
|
|
|
220
220
|
types.ts # HN domain types
|
|
221
221
|
mcp-server/
|
|
222
222
|
tools/markdown-escape.ts # format() boundary: escape titles, quote bodies
|
|
223
|
-
tools/response-budget.ts # 64,000-byte budget per response surface: measure both surfaces, largest shared body cut, code-point cut
|
|
224
223
|
tools/served-at-cap.ts # notice sentence for a size input served at its per-call cap
|
|
225
224
|
tools/thread-cursor.ts # hn_get_thread cursor codec: packed frontier, JSON format still decoded
|
|
225
|
+
tools/username-input.ts # shared input schema for an HN account name: username and author
|
|
226
226
|
tools/definitions/
|
|
227
227
|
get-stories.tool.ts # hn_get_stories — Fetch stories from an HN feed
|
|
228
228
|
get-thread.tool.ts # hn_get_thread — Get item + comment tree
|
|
@@ -335,7 +335,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
335
335
|
|
|
336
336
|
## Bundling
|
|
337
337
|
|
|
338
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored and local-only files such as `claude-plans/` — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents
|
|
338
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored and local-only files such as `claude-plans/` — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `AGENTS.md`, and `CLAUDE.md` at a package's root, and any `SKILL.md`); platform-specific native bindings; node-gyp build output beside a `binding.gyp`; and the framework's build-only files. The agent-doc selector matches only at a package root because a `skills/` deeper in a package is runtime code: MCP SDK 2.4 ships its Skills extension at `@modelcontextprotocol/{server,core}/dist/ext/skills/`, which the framework imports at startup. `tests/scripts/bundle-agent-docs.test.ts` pins that selector in both `clean-mcpb.ts` and `lint-packaging.ts`, so a script resync that would strip the SDK's modules fails the suite. MCPB is stdio-only — HTTP deployments are unaffected.
|
|
339
339
|
|
|
340
340
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
|
|
341
341
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/hn-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/hn-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -38,7 +38,7 @@ Hacker News feeds, threads, and profiles. Browse ranked feeds, read full comment
|
|
|
38
38
|
| `hn_get_stories` | Fetch stories from an HN feed (top, new, best, ask, show, jobs), with title, URL, score, author, and comment count |
|
|
39
39
|
| `hn_get_thread` | Get an item and its comment tree as a threaded discussion, with depth and comment-count controls and a cursor to page through long threads |
|
|
40
40
|
| `hn_get_user` | Fetch a user profile with karma, about, and optionally a page of resolved submissions |
|
|
41
|
-
| `hn_search_content` | Search stories, comments, polls, and jobs via Algolia by keyword, by filters alone, or both — type, author, parent story, date range, and minimum
|
|
41
|
+
| `hn_search_content` | Search stories, comments, polls, and jobs via Algolia by keyword, by filters alone, or both — type, author, parent story, date range, minimum points, and minimum comment count |
|
|
42
42
|
|
|
43
43
|
### Agent skill
|
|
44
44
|
|
|
@@ -51,49 +51,51 @@ Hacker News feeds, threads, and profiles. Browse ranked feeds, read full comment
|
|
|
51
51
|
### `hn_get_stories` <sub>tool</sub>
|
|
52
52
|
|
|
53
53
|
- Six feed types: `top`, `new`, `best`, `ask`, `show`, `jobs`; `count` (default 30) and `offset` for pagination. One call serves at most 100 stories; a `count` up to 1,000 is served at 100, with a `notice` naming the next `offset`
|
|
54
|
-
- Returns id, type, title, url, domain, score, author, timestamp, comment count, and body text — each field omitted (not null) when HN doesn't provide it
|
|
55
|
-
- Enrichment reports `total`, `offset`, and `hasMore`; while more stories remain, `truncated` is set and `notice` names the next `offset`, and a `notice` explains empty pages (empty feed, offset past end, or
|
|
56
|
-
- Items whose fetch failed after retries, or ran past the call's 50-second time budget, are listed in `failedIds` with a `notice` naming the retry; a page where every item failed returns the classified
|
|
54
|
+
- Returns rank, id, type, title, url, domain, score, author, timestamp, comment count, and body text — each field but `rank`, `id`, and `type` omitted (not null) when HN doesn't provide it. `rank` is the story's 1-based position in the feed across pages, counted before omitted or failed items drop out, so a gap between consecutive ranks marks one
|
|
55
|
+
- Enrichment reports `total`, `offset`, and `hasMore`; while more stories remain, `truncated` is set and `notice` names the next `offset`, and a `notice` explains empty pages (empty feed, offset past end, or no live item on the page). On every page that drops deleted, dead, or missing items, the final one included, `notice` counts them by kind. The last page carries no truncation
|
|
56
|
+
- Items whose fetch failed after retries, or ran past the call's 50-second time budget, are listed in `failedIds` with a `notice` naming the retry; a page where every item failed returns the classified error of its dominant cause instead of an empty list: `upstream_rate_limited` when any item was rate-limited, otherwise the most frequent declared upstream reason, or `upstream_no_response` (`Timeout`) when no answer from the HN API at all was the most common failure
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
60
60
|
### `hn_get_thread` <sub>tool</sub>
|
|
61
61
|
|
|
62
|
-
- `itemId` plus `depth` (default 3; 0 returns the item with no comments) and `maxComments` (default
|
|
62
|
+
- `itemId` plus `depth` (default 3; 0 returns the item with no comments) and `maxComments` (default 25) capping one response across all levels; every comment is returned whole. One call resolves at most 10 levels and 200 comments; a `depth` up to 100 is served at 10 and a `maxComments` up to 1,000 at 200, with a `notice` naming the comment to pass as `itemId` or the `nextCursor` that reaches the rest
|
|
63
63
|
- Breadth-first traversal ranked like HN — top-ranked top-level comments resolve first, replies fill in only after the level above is exhausted
|
|
64
|
-
- Resumable: a response stopped by `maxComments`,
|
|
65
|
-
- The root carries its relationships and state: `parent
|
|
64
|
+
- Resumable: a response stopped by `maxComments`, an upstream rate limit, or the call's 50-second time budget returns `nextCursor`; passing it back as `cursor` with the same `itemId` continues at the next unseen comment. The cursor carries the unvisited comments themselves, so re-ranked replies cannot shift it, and the server keeps no session state. A cursor the tool did not issue, an altered one, or one passed with a different `itemId` fails with `invalid_cursor`
|
|
65
|
+
- The root carries its relationships and state: `parent`, `storyId` (the story or poll the discussion belongs to), and, when that story is found, `storyTitle` and `storyUrl` (absent for a text post) for a comment, `childCount` (its direct replies, dead and deleted included) at every `depth`, `poll` for a poll option, `parts` and resolved `options` (text and votes, at every depth) for a poll, and `deleted`/`dead` markers. The `content[]` heading names the type — a title, `Comment by <author>`, `Poll option by <author> on poll <id>` — and marks a deleted or dead root
|
|
66
66
|
- Flat comment list carries `depth`/`parentId` for tree reconstruction, plus `childCount` and an `isOp` flag when the comment author submitted the thread's story — on a comment root, the story at `storyId`, never the comment's own author. Dead and deleted comments are left out, but their live replies are still returned at their true `depth`, with `parentId` naming the omitted comment
|
|
67
|
-
- `truncated` and `truncationReason` (`count`, `
|
|
67
|
+
- `truncated` and `truncationReason` (`count`, `rate_limited`, `time`, or `depth` when replies lie below the depth limit, at `depth: 0` any reply of the root) are set only while comments remain unread — never on a terminal page, including a thread that loads in full at exactly the cap. `notice` reports deleted/dead comments omitted during traversal and the next step: the cursor, or a larger `depth` or a comment id as `itemId`, naming by ID any omitted comment whose replies the depth limit cut
|
|
68
68
|
- Comments and poll options whose fetch failed after retries or ran past the call's time budget are listed in `failedIds`, and `notice` names them with the retry; failed comments ride in `nextCursor`, so paging on retries them rather than skipping them. The root item is still returned, and a failure is never passed off as a `maxComments`/`depth` limit
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
|
72
72
|
### `hn_get_user` <sub>tool</sub>
|
|
73
73
|
|
|
74
|
-
- `username` is case-sensitive and
|
|
75
|
-
- Profile includes karma, creation date, and about text (HTML stripped); submissions filter out dead/deleted items
|
|
74
|
+
- `username` is case-sensitive, trimmed, and loses one leading `@` (`@pg` looks up `pg`); a name that still holds whitespace is rejected as a display name before any upstream call; `includeSubmissions` (default false) resolves recent submissions, with `submissionCount` (default 10) and `submissionOffset` paging through a long history. One call resolves at most 50 submissions; a `submissionCount` up to 1,000 is served at 50, with a `notice` naming the next `submissionOffset`
|
|
75
|
+
- Profile includes karma, creation date, and about text (HTML stripped); submissions filter out dead/deleted items, and `notice` counts what a window drops, by kind
|
|
76
76
|
- A comment submission carries its `parent` and a poll-option submission its `poll`, so either links back to what it answered
|
|
77
77
|
- Enrichment echoes `submissionOffset` and the offset to send next, or a notice when the requested offset is past the end of the history
|
|
78
|
-
- Submissions whose fetch failed after retries, or that the call's 50-second time budget cut, are listed in `failedIds` with a `notice` naming the retry (and the budget, when it cut them); a window where every submission failed returns the classified
|
|
79
|
-
-
|
|
78
|
+
- Submissions whose fetch failed after retries, or that the call's 50-second time budget cut, are listed in `failedIds` with a `notice` naming the retry (and the budget, when it cut them); a window where every submission failed returns the classified error of its dominant cause, as `hn_get_stories` does, with `upstream_no_response` (`Timeout`) when no answer was the most common failure, the budget cutting them all included
|
|
79
|
+
- A final window that drops dead, deleted, or absent submissions gets a `notice` counting them by kind, with no next-offset hint; one with none live also gets the `submissionOffset` and `total`
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
83
83
|
### `hn_search_content` <sub>tool</sub>
|
|
84
84
|
|
|
85
|
-
- Free-text `query` — typo-tolerant, with the last term also matching as a prefix; wrap a term in double quotes to match it exactly and put `-` before one to exclude it — plus `tags` (`story`/`comment`/`poll`/`job`/`ask_hn`/`show_hn`/`front_page`), `author` (the exact, case-sensitive username), `storyId`, `dateRange`, and `
|
|
85
|
+
- Free-text `query` — typo-tolerant, with the last term also matching as a prefix; wrap a term in double quotes to match it exactly and put `-` before one to exclude it — plus `tags` (`story`/`comment`/`poll`/`job`/`ask_hn`/`show_hn`/`front_page`), `author` (the exact, case-sensitive username; trimmed, with one leading `@` dropped, and a name that still holds whitespace is rejected as a display name), `storyId`, `dateRange`, `minPoints`, and `minComments` filters; `sort` by relevance or date; `count` (default 10) and `page` for pagination. One call serves at most 50 hits; a `count` up to 1,000 is served as the page of 50 starting at the requested page's first hit, with a `notice` naming the `count: 50` page to pass next; a non-zero `page` whose first hit begins no page of 50 fails with `count_page_unaligned`, naming a `count` and `page` that start there and, when that count is under 25, the page of 50 that holds that hit
|
|
86
86
|
- `tags: "front_page"` is the front page right now (about 30 stories), never past front pages; for an earlier window, use `tags: "story"` with `dateRange` and `minPoints`
|
|
87
87
|
- `query` is optional when a filter is set — list Ask HN, Show HN, polls, or jobs on their own, or pass a story's `storyId` with `tags: "comment"` to search within one thread. No query and no filter fails with `missing_query_or_filter`
|
|
88
88
|
- To check whether a page was discussed, pass its URL alone as `query`: the leading `http://`, `https://`, or `www.` is dropped before searching, so submissions under either scheme, with or without `www.`, all match. The output `query` and the notice show what was searched
|
|
89
89
|
- `dateRange` bounds take ISO 8601 (`YYYY`, `YYYY-MM`, `YYYY-MM-DD`, or a date-time with optional offset), read as UTC and both exclusive; an empty or out-of-order range fails with `invalid_date_range`
|
|
90
90
|
- `minPoints` applies to stories and polls — comments and jobs carry no points, so pairing it with `tags: "comment"` or `"job"` fails with `min_points_unscored_type`
|
|
91
|
-
- `
|
|
91
|
+
- `minComments` applies to stories and polls the same way: any value, 0 included, excludes comments, jobs, and poll options, so an untagged search returns only stories and polls, and pairing it with `tags: "comment"` or `"job"` fails with `min_comments_unscored_type`. It filters; it does not sort
|
|
92
|
+
- A comment hit carries `parentId` (its story's id for a top-level comment, otherwise the parent comment's id, which `hn_get_thread` reads) and, when its story links out, `storyUrl`; in `content[]` the meta line shows `parent:<id>` and the story link sits on its own line
|
|
93
|
+
- `view: "compact"` drops the two body-text fields (`text`, `highlights.text`), which otherwise repeat a long comment twice per hit — pass a hit id to `hn_get_thread` to read the body. In `full`, every hit carries its whole body
|
|
92
94
|
- Each hit carries its `type` (`story`, `comment`, `poll`, `pollopt`, `job`) when Algolia records one. Untagged searches also return poll options, which carry only an id, author, vote count, and date — `hn_get_thread` reads an option's text and poll. A poll hit has no `storyId`; its own `id` is the discussion root
|
|
93
95
|
- Job descriptions come back as `text`, though keywords don't search them
|
|
94
|
-
- Highlight metadata (`highlights.title`, `highlights.text`, `matchedWords`) shows which terms matched and where; `highlights.text` is the matched body in full with `<em>` markers
|
|
95
|
-
- Enrichment echoes the applied `criteria` — each filter set (`author` trimmed, `dateRange` as the exclusive UTC bounds applied, to the second), plus `sort`, `count`, and `view` with defaults filled — so a saved result says what it answers
|
|
96
|
-
- Enrichment reports `totalHits`, `page`, and the actual reachable `totalPages` — not derived from `totalHits`, since broad queries report far more hits than Algolia will serve. `totalHitsExact: false` marks `totalHits` as an estimate, and `reachableHits: 1000` marks a search that matches more than Algolia's 1,000-hit ceiling; the last page of such a search gets a notice naming the ceiling and narrower `dateRange` windows to reach the rest. While more pages remain, `truncated` is set and `notice` names the next `page`; a page past the end gets a notice naming the last valid page (or `page: 0` past the ceiling)
|
|
96
|
+
- Highlight metadata (`highlights.title`, `highlights.text`, `matchedWords`) shows which terms matched and where; `highlights.text` is the matched body in full with `<em>` markers, absent when Algolia returned no body highlight, which does not mean the body lacks the terms; on a hit Algolia left unhighlighted, `highlights` and `matchedWords` are absent and `text` still holds the body
|
|
97
|
+
- Enrichment echoes the applied `criteria` — each filter set (`author` as searched, trimmed with one leading `@` dropped, `dateRange` as the exclusive UTC bounds applied, to the second), plus `sort`, `count`, and `view` with defaults filled — so a saved result says what it answers
|
|
98
|
+
- Enrichment reports `totalHits`, `page`, and the actual reachable `totalPages` — not derived from `totalHits`, since broad queries report far more hits than Algolia will serve. `totalHitsExact: false` marks `totalHits` as an estimate, and `reachableHits: 1000` marks a search that matches more than Algolia's 1,000-hit ceiling; the last page of such a search gets a notice naming the ceiling and narrower `dateRange` windows to reach the rest. While more pages remain, `truncated` is set and `notice` names the next `page`; a page past the end gets a notice naming the last valid page (or `page: 0` past the ceiling). Each page a notice names comes with the `count` it is counted in unless that is the default, so the call works as written
|
|
97
99
|
|
|
98
100
|
## Features
|
|
99
101
|
|
|
@@ -105,14 +107,14 @@ HN-specific:
|
|
|
105
107
|
- Concurrent batch fetching with configurable parallelism for item resolution (`HN_CONCURRENCY_LIMIT`)
|
|
106
108
|
- HTML bodies stripped to plain text — entities decoded once, code blocks kept verbatim, each link rendered once as its full URL — while titles, which are not HTML though some carry entities, are entity-decoded and never tag-stripped
|
|
107
109
|
- Server-level `instructions` forwarded to LLM clients on `initialize` — item types, ID reuse across tools, case-sensitive usernames, field sparsity
|
|
108
|
-
- No API keys required — both upstream APIs are public
|
|
110
|
+
- No API keys required — both upstream APIs are public; every request identifies itself with a `User-Agent` of `hn-mcp-server/<version>` and the repository URL
|
|
109
111
|
|
|
110
112
|
Agent-friendly output:
|
|
111
113
|
|
|
112
|
-
- Graceful partial failure — an item whose fetch fails after retries is reported by ID in `failedIds`, apart from deleted, dead, or nonexistent items, rather than silently shrinking the result; each `hn_get_thread`, `hn_get_stories`, and `hn_get_user` call runs under one 50-second time budget, so hung fetches cannot hold it past an MCP client's 60-second timeout; `hn_get_thread`
|
|
114
|
+
- Graceful partial failure — an item whose fetch fails after retries is reported by ID in `failedIds`, apart from deleted, dead, or nonexistent items, rather than silently shrinking the result; each `hn_get_thread`, `hn_get_stories`, and `hn_get_user` call runs under one 50-second time budget, so hung fetches cannot hold it past an MCP client's 60-second timeout; `hn_get_thread` counts the deleted and dead comments it drops, and `hn_get_stories` and `hn_get_user` the deleted, dead, and missing items, in `notice`; a rate-limited item stops the batch instead of sending every remaining item through its own retry ladder
|
|
113
115
|
- Discriminated output contracts — typed error reasons (`item_not_found`, `upstream_rate_limited`, `upstream_html`, …) with per-reason recovery text (a rate limit passes on the upstream's `Retry-After` as `retryAfter`), and a `depth`/`parentId` pair on every comment so callers reconstruct the tree without guessing nesting
|
|
114
116
|
- Pagination provenance — every paged listing echoes the offset it used (`offset`, `submissionOffset`, `page`) plus the exact next-offset value in `notice`, `hn_search_content` also echoes the `criteria` it searched with, and `hn_get_thread` returns a self-contained `nextCursor`, so an agent can resume without recomputing state
|
|
115
|
-
- Response shaping — HTML stripping, URL normalization, and domain extraction remove upstream markup noise; `hn_search_content`'s `view: "compact"` drops the two body-text fields that otherwise duplicate a hit's full text
|
|
117
|
+
- Response shaping — HTML stripping, URL normalization, and domain extraction remove upstream markup noise; `hn_search_content`'s `view: "compact"` drops the two body-text fields that otherwise duplicate a hit's full text
|
|
116
118
|
- Fenced upstream text — in `content[]`, every line of a body is block-quoted and titles are escaped only where they could break the surrounding Markdown, so a comment cannot pass for a server-authored line
|
|
117
119
|
|
|
118
120
|
## Getting started
|
|
@@ -190,7 +192,7 @@ Or with Docker:
|
|
|
190
192
|
|
|
191
193
|
### Prerequisites
|
|
192
194
|
|
|
193
|
-
- [Bun v1.4.
|
|
195
|
+
- [Bun v1.4.4](https://bun.sh/) or higher (or Node.js >= 24)
|
|
194
196
|
|
|
195
197
|
### Installation
|
|
196
198
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "hn_search_content gains a minComments filter and parentId and storyUrl on comment hits; a stories or user page on which every item failed throws its dominant cause, with a new upstream_no_response reason when the HN API gave no answer."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.6.1 — 2026-11-12
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`hn_search_content` `minComments` filter**: an integer ≥ 0, sent to Algolia as `num_comments>=N`. It applies to stories and polls and is rejected with `tags: "comment"` or `"job"` as `min_comments_unscored_type`. (#54)
|
|
12
|
+
- **`hn_search_content` comment hits carry `parentId` and `storyUrl`**: the id of the item the comment replies to (read it with `hn_get_thread`), and the link its story submitted, absent for a text post. (#53)
|
|
13
|
+
- **`User-Agent` on every Firebase and Algolia request**: `hn-mcp-server/<version> (+https://github.com/cyanheads/hn-mcp-server)`.
|
|
14
|
+
|
|
15
|
+
## Changed
|
|
16
|
+
|
|
17
|
+
- **`hn_get_user` notice for an empty final window**: a window whose submissions are all dead, deleted, or absent now says so, with its `submissionOffset` and `total`.
|
|
18
|
+
- **`hn-guide` skill 1.1**: corrects the hiring-thread posting dates and adds `minComments` guidance.
|
|
19
|
+
- **Framework adoption** (mcp-ts-core 0.13.19–0.13.22): Bun ≥1.4.4 is required; on Node.js below 24 the server now exits at startup, naming the running version; `initialize`'s `serverInfo` carries `description`; a rate-limit error's trailer adds `retry after <value>`; installed as a project dependency, the server resolves its own `skills/` and logs directory; the `.mcpb` bundle no longer ships dependency-root `AGENTS.md` and `CLAUDE.md`. `framework-skills/`, `scripts/`, and `changelog/template.md` are resynced.
|
|
20
|
+
|
|
21
|
+
## Fixed
|
|
22
|
+
|
|
23
|
+
- **A stories or user page on which every item failed throws its dominant cause** instead of the first slot's error: a rate limit first, otherwise the most frequent declared upstream reason. When no answer from the HN API is the most common failure, it throws the new `upstream_no_response` (`Timeout`, with `data.itemCount` and `data.budgetCut`), declared on `hn_get_stories` and `hn_get_user`. (#51)
|
|
24
|
+
- **`retry_deadline_exceeded` is declared** in `errors[]` on `hn_get_stories`, `hn_get_user`, and `hn_get_thread`.
|
|
25
|
+
|
|
26
|
+
## Dependencies
|
|
27
|
+
|
|
28
|
+
- `@cyanheads/mcp-ts-core` ^0.13.18 → ^0.13.22
|
|
29
|
+
- Dev: `@biomejs/biome` ^2.6.3 → ^2.6.6
|
|
30
|
+
- Dev: `@types/node` ^26.6.5 → ^26.6.6
|
|
31
|
+
- Dev: `@vitest/coverage-istanbul` ^5.0.5 → ^5.1.0
|
|
32
|
+
- Dev: `vitest` ^5.0.5 → ^5.1.0
|
|
33
|
+
- Dev: `tsc-alias` ^1.9.9 → ^1.9.10
|
|
34
|
+
- `engines.bun` >=1.4.0 → >=1.4.4
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "The 64,000-byte response budget is gone: pages hold whole records at the caller's count, and hn_get_thread maxComments and hn_search_content count now default to 25 and 10. Feed stories carry rank, omitted items are counted, thread roots report replies and story title, and `@name` usernames resolve."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.7.0 — 2026-11-13
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`hn_get_thread` `item.storyTitle` and `item.storyUrl` for a comment root**: the title and link of the story or poll at `storyId`. `storyUrl` is absent for a text post, and both are absent when the story can't be loaded. A continuation page fetches that story once more, so `item` matches on every page. (#60)
|
|
12
|
+
|
|
13
|
+
## Changed
|
|
14
|
+
|
|
15
|
+
- **A page holds the count the caller asked for, never a byte cut** (#55): `hn_get_thread` stops only on `maxComments`, the 1,000-item ceiling, the time budget, a rate limit, or depth, and `hn_search_content` and `hn_get_user` bodies are always whole. The per-call caps stay at 200 comments, 50 hits, and 50 submissions.
|
|
16
|
+
- **Defaults**: `hn_get_thread` `maxComments` 50 → 25 and `hn_search_content` `count` 30 → 10, so a `page` sent without `count` counts in tens. (#55)
|
|
17
|
+
- **Breaking: output fields removed** (#55): `hn_search_content` `hits[].textTruncated` and the `highlightsTextOmitted` enrichment field, `hn_get_user` `submissions[].textTruncated`, and `size` from `hn_get_thread`'s `truncationReason`. Read each body whole from `text`.
|
|
18
|
+
- **`hn_get_user` `username` and `hn_search_content` `author` drop one leading `@`**: `@pg` looks up and searches `pg`, and `criteria.author` shows the name as searched. (#61)
|
|
19
|
+
- **Breaking: `hn_search_content` `author` that holds whitespace after trimming and dropping one `@`, or is `@` alone, is rejected as `invalid_arguments`** (#61); it returned zero hits before. `hn_get_user` rejects the same forms, which already failed there as `user_not_found` or `upstream_rejected`.
|
|
20
|
+
- **`hn-guide` skill 1.2**: covers the `@` and display-name handling, the 25-comment default page, and hits Algolia left unhighlighted.
|
|
21
|
+
|
|
22
|
+
## Fixed
|
|
23
|
+
|
|
24
|
+
- **`hn_get_thread` reports replies at `depth: 0`** (#57): `item.childCount` counts the root's direct replies on every call, dead and deleted ones included, and a root with replies now sets `truncated: true` and `truncationReason: "depth"` with a notice naming a larger `depth`.
|
|
25
|
+
- **`hn_get_stories` carries `stories[].rank`** (#58): the story's 1-based position in the feed, counted before omitted or failed items drop, rendered as `[rank]` in `content[]`.
|
|
26
|
+
- **`hn_get_user` and `hn_get_stories` count the items a page omits** (#59): `notice` reads like `3 of the 40 submissions in this window are omitted: 2 deleted, 1 dead.`, by kind (deleted, dead, missing), on final pages too.
|
|
27
|
+
- **`hn_search_content` page notice names `count` on every page it points to**, unless `count` is the default, and offers a larger `count` only on page 0, where the larger page still starts at hit 0.
|
|
28
|
+
- **`hn_search_content` `highlights` and `highlights.text` descriptions** no longer read an absent highlight as a non-match: Algolia leaves some bodies that hold every query term unhighlighted.
|
|
29
|
+
|
|
30
|
+
## Dependencies
|
|
31
|
+
|
|
32
|
+
- Dev: `@biomejs/biome` ^2.6.6 → ^2.6.7
|
package/changelog/template.md
CHANGED
|
@@ -116,14 +116,21 @@ security: false
|
|
|
116
116
|
resolves to whatever real item already owns 42, and timeline previews pull
|
|
117
117
|
in that unrelated item's metadata.
|
|
118
118
|
|
|
119
|
+
Outside reporters: when the issue or PR came from outside the project
|
|
120
|
+
(`git-wrapup` step 5 says how to tell), thank its author in the same
|
|
121
|
+
parenthetical, profile linked by full URL for the same reason:
|
|
122
|
+
|
|
123
|
+
([#38](https://github.com/cyanheads/mcp-ts-core/issues/38), thanks [@octocat](https://github.com/octocat)!)
|
|
124
|
+
|
|
119
125
|
TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
|
|
120
126
|
via `gh release create --notes-from-tag`. It is a condensed digest of this
|
|
121
127
|
entry, never a copy, and its format is owned by the `release-and-publish`
|
|
122
128
|
skill (step 4, "Create the annotated tag"): a short subject line without the
|
|
123
129
|
version, flat headline bullets — no Keep-a-Changelog section headers, no
|
|
124
|
-
gates line — at most one deps line, issue backlinks
|
|
125
|
-
last. In release-PR mode the `git-wrapup`
|
|
126
|
-
PR body's `## Changes` and the tag copies
|
|
130
|
+
gates line — at most one deps line, issue backlinks with any outside-reporter
|
|
131
|
+
thanks, and the changelog link last. In release-PR mode the `git-wrapup`
|
|
132
|
+
skill authors those bullets as the PR body's `## Changes` and the tag copies
|
|
133
|
+
them.
|
|
127
134
|
-->
|
|
128
135
|
|
|
129
136
|
## Added
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ await createApp({
|
|
|
13
13
|
name: 'hn-mcp-server',
|
|
14
14
|
title: 'hn-mcp-server',
|
|
15
15
|
tools: [getStories, getThread, getUser, searchHn],
|
|
16
|
-
instructions: 'Use the hn_* tools to access Hacker News via the Firebase API and Algolia search: ranked feeds; search by keyword, by type/author/story/date/score filters alone, or both; user profiles; and items with their comment trees. Items are addressed by integer ID, reused across tools and item types (story, comment, job, poll, pollopt); passing a comment id to hn_get_thread drills into that subtree. Usernames are case-sensitive; HN sparsely populates fields, so treat absent values as unknown.',
|
|
16
|
+
instructions: 'Use the hn_* tools to access Hacker News via the Firebase API and Algolia search: ranked feeds; search by keyword, by type/author/story/date/score/comment-count filters alone, or both; user profiles; and items with their comment trees. Items are addressed by integer ID, reused across tools and item types (story, comment, job, poll, pollopt); passing a comment id to hn_get_thread drills into that subtree. Usernames are case-sensitive; HN sparsely populates fields, so treat absent values as unknown.',
|
|
17
17
|
landing: { requireAuth: false },
|
|
18
18
|
sessionMode: 'stateless',
|
|
19
19
|
skills: 'skills',
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,oDAAoD,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,mDAAmD,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,iDAAiD,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,uDAAuD,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,eAAe;IACrB,KAAK,EAAE,eAAe;IACtB,KAAK,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC;IACjD,YAAY,EACV,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,oDAAoD,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,mDAAmD,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,iDAAiD,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,uDAAuD,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,eAAe;IACrB,KAAK,EAAE,eAAe;IACtB,KAAK,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC;IACjD,YAAY,EACV,wfAAwf;IAC1f,OAAO,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE;IAC/B,WAAW,EAAE,WAAW;IACxB,MAAM,EAAE,QAAQ;IAChB,KAAK;QACH,aAAa,EAAE,CAAC;IAClB,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -17,6 +17,7 @@ export declare const getStories: import("@cyanheads/mcp-ts-core").ToolDefinition
|
|
|
17
17
|
offset: z.ZodDefault<z.ZodNumber>;
|
|
18
18
|
}, z.core.$strip>, z.ZodObject<{
|
|
19
19
|
stories: z.ZodArray<z.ZodObject<{
|
|
20
|
+
rank: z.ZodNumber;
|
|
20
21
|
id: z.ZodNumber;
|
|
21
22
|
type: z.ZodString;
|
|
22
23
|
title: z.ZodOptional<z.ZodString>;
|
|
@@ -63,6 +64,20 @@ export declare const getStories: import("@cyanheads/mcp-ts-core").ToolDefinition
|
|
|
63
64
|
readonly recovery: "Retry after a brief delay; no input change helps while the upstream serves this.";
|
|
64
65
|
readonly retryable: true;
|
|
65
66
|
readonly thrownBy: "service";
|
|
67
|
+
}, {
|
|
68
|
+
readonly reason: "upstream_no_response";
|
|
69
|
+
readonly code: JsonRpcErrorCode.Timeout;
|
|
70
|
+
readonly when: "Every item on the page failed, none with HTTP 429, and no answer from the HN API was the most common failure — timed out after retries, could not connect, or ran past this call's 50-second time budget.";
|
|
71
|
+
readonly recovery: "Retry the same call after a short delay; when the time budget cut any of the items, pass a smaller count so fewer items share it.";
|
|
72
|
+
readonly retryable: true;
|
|
73
|
+
readonly thrownBy: "service";
|
|
74
|
+
}, {
|
|
75
|
+
readonly reason: "retry_deadline_exceeded";
|
|
76
|
+
readonly code: JsonRpcErrorCode.Timeout;
|
|
77
|
+
readonly when: "The HN API was too slow to return the feed: its retries ran past this call's 50-second time budget.";
|
|
78
|
+
readonly recovery: "Retry the same call after a short delay; the HN API answered too slowly to finish within this call's time budget.";
|
|
79
|
+
readonly retryable: true;
|
|
80
|
+
readonly thrownBy: "service";
|
|
66
81
|
}], {
|
|
67
82
|
readonly total: z.ZodNumber;
|
|
68
83
|
readonly offset: z.ZodNumber;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-stories.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/get-stories.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"get-stories.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/get-stories.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAqBjE,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2SrB,CAAC"}
|
|
@@ -6,7 +6,7 @@ import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
|
6
6
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
7
|
import { escapeInline, quoteBody } from '../markdown-escape.js';
|
|
8
8
|
import { servedAtCap } from '../served-at-cap.js';
|
|
9
|
-
import { CALL_BUDGET_MS, decodeHtmlEntities, extractDomain, getHnService, normalizeUrl, settlePage, stripHtml, timeBudgetCutSentence, } from '../../../services/hn/hn-service.js';
|
|
9
|
+
import { CALL_BUDGET_MS, decodeHtmlEntities, extractDomain, getHnService, normalizeUrl, omittedSentence, settlePage, stripHtml, timeBudgetCutSentence, } from '../../../services/hn/hn-service.js';
|
|
10
10
|
/** Most stories one call serves; a larger `count` is served at this cap. */
|
|
11
11
|
const COUNT_CAP = 100;
|
|
12
12
|
/** Largest `count` accepted; anything above it is rejected. */
|
|
@@ -54,6 +54,22 @@ export const getStories = tool('hn_get_stories', {
|
|
|
54
54
|
retryable: true,
|
|
55
55
|
thrownBy: 'service',
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
reason: 'upstream_no_response',
|
|
59
|
+
code: JsonRpcErrorCode.Timeout,
|
|
60
|
+
when: `Every item on the page failed, none with HTTP 429, and no answer from the HN API was the most common failure — timed out after retries, could not connect, or ran past this call's ${CALL_BUDGET_MS / 1000}-second time budget.`,
|
|
61
|
+
recovery: 'Retry the same call after a short delay; when the time budget cut any of the items, pass a smaller count so fewer items share it.',
|
|
62
|
+
retryable: true,
|
|
63
|
+
thrownBy: 'service',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
reason: 'retry_deadline_exceeded',
|
|
67
|
+
code: JsonRpcErrorCode.Timeout,
|
|
68
|
+
when: `The HN API was too slow to return the feed: its retries ran past this call's ${CALL_BUDGET_MS / 1000}-second time budget.`,
|
|
69
|
+
recovery: "Retry the same call after a short delay; the HN API answered too slowly to finish within this call's time budget.",
|
|
70
|
+
retryable: true,
|
|
71
|
+
thrownBy: 'service',
|
|
72
|
+
},
|
|
57
73
|
],
|
|
58
74
|
input: z.object({
|
|
59
75
|
feed: z
|
|
@@ -77,6 +93,9 @@ export const getStories = tool('hn_get_stories', {
|
|
|
77
93
|
stories: z
|
|
78
94
|
.array(z
|
|
79
95
|
.object({
|
|
96
|
+
rank: z
|
|
97
|
+
.number()
|
|
98
|
+
.describe("1-based position in the feed's ranking when fetched: offset plus the story's slot on this page, counted before omitted or failed items drop out. A gap between consecutive ranks marks an item omitted as deleted, dead, or missing, or one in failedIds."),
|
|
80
99
|
id: z.number().describe('Item ID — use with hn_get_thread to read comments.'),
|
|
81
100
|
type: z.string().describe('Item type (story, job).'),
|
|
82
101
|
title: z
|
|
@@ -138,7 +157,7 @@ export const getStories = tool('hn_get_stories', {
|
|
|
138
157
|
notice: z
|
|
139
158
|
.string()
|
|
140
159
|
.optional()
|
|
141
|
-
.describe(`Agent guidance: the offset to pass for the next page while more stories remain, the IDs that failed to load and how to retry them (naming the call's ${CALL_BUDGET_MS / 1000}-second time budget when it cut them)
|
|
160
|
+
.describe(`Agent guidance: the offset to pass for the next page while more stories remain; how many items on this page were omitted, by kind (deleted, dead, missing); the IDs that failed to load and how to retry them (naming the call's ${CALL_BUDGET_MS / 1000}-second time budget when it cut them); why a page came back empty (an empty feed, or offset past its end); and when count was served at its per-call cap. Absent on the last page of a non-empty result when nothing on it was omitted or failed.`),
|
|
142
161
|
},
|
|
143
162
|
enrichmentTrailer: {
|
|
144
163
|
failedIds: { render: (ids) => `**Failed to load:** ${(ids ?? []).join(', ')}` },
|
|
@@ -151,11 +170,12 @@ export const getStories = tool('hn_get_stories', {
|
|
|
151
170
|
const count = Math.min(input.count, COUNT_CAP);
|
|
152
171
|
const sliced = feedIds.slice(input.offset, input.offset + count);
|
|
153
172
|
const slots = await hn.fetchItems(sliced, ctx, deadline);
|
|
154
|
-
const {
|
|
155
|
-
const stories =
|
|
173
|
+
const { live, failedIds, omitted } = settlePage(slots);
|
|
174
|
+
const stories = live.map(({ item, slot }) => {
|
|
156
175
|
const url = normalizeUrl(item.url);
|
|
157
176
|
const domain = extractDomain(url);
|
|
158
177
|
return {
|
|
178
|
+
rank: input.offset + slot + 1,
|
|
159
179
|
id: item.id,
|
|
160
180
|
type: item.type,
|
|
161
181
|
...(item.title && { title: decodeHtmlEntities(item.title) }),
|
|
@@ -172,6 +192,7 @@ export const getStories = tool('hn_get_stories', {
|
|
|
172
192
|
feed: input.feed,
|
|
173
193
|
count: stories.length,
|
|
174
194
|
failed: failedIds.length,
|
|
195
|
+
omitted,
|
|
175
196
|
});
|
|
176
197
|
const total = feedIds.length;
|
|
177
198
|
const nextOffset = input.offset + count;
|
|
@@ -182,36 +203,39 @@ export const getStories = tool('hn_get_stories', {
|
|
|
182
203
|
hasMore,
|
|
183
204
|
...(failedIds.length > 0 && { failedIds }),
|
|
184
205
|
});
|
|
185
|
-
const deadPageMessage = `No live stories on this page of the ${input.feed} feed (offset:${input.offset}, total:${total}). Items may have been deleted or flagged.`;
|
|
186
206
|
const failureMessage = failedIds.length > 0
|
|
187
207
|
? `Could not fetch ${failedIds.length} of ${sliced.length} items on this page (id${failedIds.length === 1 ? '' : 's'} ${failedIds.join(', ')}).${timeBudgetCutSentence(slots)} Retry with offset: ${input.offset}, or pass an id as itemId to hn_get_thread to fetch that story alone.`
|
|
188
208
|
: undefined;
|
|
209
|
+
/** A page with nothing live and nothing failed: every slot on it was omitted, which the next sentence counts. */
|
|
210
|
+
const emptyPage = stories.length === 0 && !failureMessage
|
|
211
|
+
? `No live stories on this page of the ${input.feed} feed (offset:${input.offset}, total:${total}).`
|
|
212
|
+
: undefined;
|
|
213
|
+
const omittedMessage = omittedSentence(omitted, sliced.length, 'item', 'on this page');
|
|
189
214
|
if (hasMore) {
|
|
190
215
|
const raiseCount = input.count < COUNT_CAP ? `, or raise count (max ${COUNT_CAP})` : '';
|
|
191
216
|
const nextPage = `Pass offset: ${nextOffset} for the next page${raiseCount}.`;
|
|
192
|
-
const pageStatus =
|
|
193
|
-
|
|
194
|
-
: `Showing items ${input.offset + 1}–${nextOffset} of ${total} in the ${input.feed} feed.`;
|
|
217
|
+
const pageStatus = emptyPage ??
|
|
218
|
+
`Showing items ${input.offset + 1}–${nextOffset} of ${total} in the ${input.feed} feed.`;
|
|
195
219
|
const overCap = input.count > COUNT_CAP ? servedAtCap('count', input.count, COUNT_CAP) : '';
|
|
196
220
|
ctx.enrich.truncated({
|
|
197
221
|
shown: stories.length,
|
|
198
222
|
cap: count,
|
|
199
|
-
guidance: [pageStatus, overCap, failureMessage, nextPage]
|
|
223
|
+
guidance: [pageStatus, omittedMessage, overCap, failureMessage, nextPage]
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.join(' '),
|
|
200
226
|
});
|
|
201
227
|
}
|
|
202
|
-
else if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
ctx.enrich.notice(`Offset ${input.offset} is past the end of the ${input.feed} feed (${total} item${total === 1 ? '' : 's'}). Reset offset below ${total}.`);
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
ctx.enrich.notice(failureMessage ?? deadPageMessage);
|
|
211
|
-
}
|
|
228
|
+
else if (total === 0) {
|
|
229
|
+
ctx.enrich.notice(`${input.feed} feed is empty.`);
|
|
230
|
+
}
|
|
231
|
+
else if (input.offset >= total) {
|
|
232
|
+
ctx.enrich.notice(`Offset ${input.offset} is past the end of the ${input.feed} feed (${total} item${total === 1 ? '' : 's'}). Reset offset below ${total}.`);
|
|
212
233
|
}
|
|
213
|
-
else
|
|
214
|
-
|
|
234
|
+
else {
|
|
235
|
+
/** The final page: a drop or a failure is not a pagination gap, so nothing here names a next offset. */
|
|
236
|
+
const notice = [emptyPage, omittedMessage, failureMessage].filter(Boolean).join(' ');
|
|
237
|
+
if (notice)
|
|
238
|
+
ctx.enrich.notice(notice);
|
|
215
239
|
}
|
|
216
240
|
return {
|
|
217
241
|
stories,
|
|
@@ -222,8 +246,7 @@ export const getStories = tool('hn_get_stories', {
|
|
|
222
246
|
if (result.stories.length === 0) {
|
|
223
247
|
return [{ type: 'text', text: `${result.feed} feed — no stories` }];
|
|
224
248
|
}
|
|
225
|
-
const lines = result.stories.map((s
|
|
226
|
-
const rank = i + 1;
|
|
249
|
+
const lines = result.stories.map((s) => {
|
|
227
250
|
const date = s.time != null
|
|
228
251
|
? `${new Date(s.time * 1000).toISOString().slice(0, 10)} (t:${s.time})`
|
|
229
252
|
: null;
|
|
@@ -241,7 +264,7 @@ export const getStories = tool('hn_get_stories', {
|
|
|
241
264
|
const text = s.text ? `\n${quoteBody(s.text)}` : '';
|
|
242
265
|
const title = s.title ? escapeInline(s.title) : `[${s.type}]`;
|
|
243
266
|
const domain = s.domain ? ` (${s.domain})` : '';
|
|
244
|
-
return `[${rank}] ${title}${domain}\n${meta}${url}${text}`;
|
|
267
|
+
return `[${s.rank}] ${title}${domain}\n${meta}${url}${text}`;
|
|
245
268
|
});
|
|
246
269
|
const header = `## ${result.feed} stories`;
|
|
247
270
|
return [{ type: 'text', text: `${header}\n\n${lines.join('\n\n')}` }];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-stories.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/get-stories.tool.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"get-stories.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/get-stories.tool.ts"],"names":[],"mappings":"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"}
|