hn-mcp-server
An MCP server that allows agents to browse Hacker News feeds, threads, and user profiles with full-text search. It supports fetching stories from various feeds, retrieving threaded discussions with pagination, looking up user profiles with karma and submissions, and searching content via Algolia.
Endpoint and access
No sign-in to connect and list tools. Individual tools may still need a vendor account, credentials, or permissions; check the documented prerequisites.
https://hn.caseyjhand.com/mcpWhat its tools do
4 tools, grouped by the hints the server advertises. Hints are self-declared, not verified behavior.
Checks and signals
Each line is separate evidence, not a trust score.
- Passed Endpoint answered the live check ·
- Availability 1 of 1 checks passed in 30 days
What you can do with it
- Fetch stories from HN feeds like top, new, best, ask, show, and jobs
- Retrieve threaded comment discussions with depth and pagination controls
- Look up user profiles including karma, about section, and recent submissions
- Search stories, comments, polls, and jobs by keyword or filters
Source and maintenance
- Maintenance checked
Protocol and capabilities
- Server
- hn-mcp-server 0.6.0
- MCP protocol negotiated in check
- 2025-11-25
- Tools
- 4 advertised
- Resources
- 1 advertised
- Resource templates
- 1 advertised
- Prompts
- 0 advertised
{
"tools": {
"listChanged": true
},
"logging": {},
"prompts": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"extensions": {
"io.modelcontextprotocol/skills": {
"directoryRead": true
}
}
}Server-provided instructions
Untrusted server output shown for inspection. It is not Koskamo guidance and is excluded from knowledge search.
Tools
4 advertised
hn_get_stories — Hn Get Stories
Fetch stories from an HN feed (top, new, best, ask, show, jobs), with title, URL, score, author, and comment count for each story.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"feed"
],
"properties": {
"feed": {
"enum": [
"top",
"new",
"best",
"ask",
"show",
"jobs"
],
"type": "string",
"description": "Which HN feed to fetch. \"top\" includes jobs. \"ask\" and \"show\" are Ask HN / Show HN posts."
},
"count": {
"type": "integer",
"default": 30,
"maximum": 1000,
"minimum": 1,
"description": "Number of stories to return, at most 100 per call. A larger value is served at 100, and the notice names the offset for the next page. Larger counts take longer."
},
"offset": {
"type": "integer",
"default": 0,
"maximum": 9007199254740991,
"minimum": 0,
"description": "Number of stories to skip from the start of the feed. Use with count for pagination."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"stories",
"feed",
"total",
"offset",
"hasMore"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"cap": {
"type": "number",
"description": "The count value served: as passed, or 100 when count was above that cap."
},
"feed": {
"type": "string",
"description": "Which feed was fetched."
},
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"upstream_rejected",
"upstream_rate_limited",
"upstream_unavailable",
"upstream_html",
"upstream_malformed"
],
"description": "Machine-readable failure mode. Declared by this tool: `upstream_rejected`: The HN API answered with a 4xx status other than 429 — it rejected the request as built. `upstream_rate_limited`: The HN API answered with HTTP 429. `upstream_unavailable`: The HN API answered with a 5xx status. `upstream_html`: The HN API served an HTML error page with a 200 status, which it does under rate limiting or maintenance. `upstream_malformed`: The HN API answered with a 200 status and a body that is not JSON. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"shown": {
"type": "number",
"description": "Number of stories returned on this page."
},
"total": {
"type": "number",
"description": "Total items in the feed (up to 500 for top/new/best, 200 for ask/show/jobs)."
},
"notice": {
"type": "string",
"description": "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 50-second time budget when it cut them), or why a page came back empty — offset past the end of the feed, an empty feed, or every item on the page deleted or flagged — noting when count was served at its per-call cap. Absent on the last page of a non-empty result with no failed IDs."
},
"offset": {
"type": "number",
"description": "Offset that was applied to this page."
},
"hasMore": {
"type": "boolean",
"description": "Whether more stories are available beyond this page."
},
"stories": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"type"
],
"properties": {
"by": {
"type": "string",
"description": "Author username when provided by HN. Omitted when unknown."
},
"id": {
"type": "number",
"description": "Item ID — use with hn_get_thread to read comments."
},
"url": {
"type": "string",
"description": "External link URL. Absent for Ask HN / text posts."
},
"text": {
"type": "string",
"description": "Body text for Ask HN / text posts. Use hn_get_thread for full discussion."
},
"time": {
"type": "number",
"description": "Unix timestamp when provided by HN. Omitted when unknown."
},
"type": {
"type": "string",
"description": "Item type (story, job)."
},
"score": {
"type": "number",
"description": "Upvote count when provided by HN. Omitted when unknown."
},
"title": {
"type": "string",
"description": "Story title when provided by HN. Omitted when unknown."
},
"domain": {
"type": "string",
"description": "Bare hostname derived from url (e.g. \"github.com\", with leading \"www.\" stripped). Absent when url is missing or unparseable."
},
"descendants": {
"type": "number",
"description": "HN's count of live comments in the thread (descendants); dead and deleted comments are not counted. Absent for jobs."
}
},
"description": "A single story or job posting.",
"additionalProperties": false
},
"description": "Stories from the feed, ordered by HN ranking."
},
"failedIds": {
"type": "array",
"items": {
"type": "number"
},
"description": "IDs on this page whose fetch failed after retries or ran past the call's 50-second time budget. They are not deleted or missing and may load on a later call: retry the page with the same offset, or pass an ID to hn_get_thread to fetch that story alone. Absent when every item on the page loaded."
},
"truncated": {
"type": "boolean",
"description": "True when more stories remain beyond this page (hasMore). Absent on the last page of the feed."
}
},
"additionalProperties": false
}{
"readOnlyHint": true
}hn_get_thread — Hn Get Thread
Get an item and its comment tree as a threaded discussion, with child comments resolved recursively. Use depth 0 for an item-only lookup. A long thread comes back in pages: pass the returned nextCursor as cursor to continue.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"itemId"
],
"properties": {
"depth": {
"type": "integer",
"default": 3,
"maximum": 100,
"minimum": 0,
"description": "How many levels of replies to resolve, at most 10 per call. 0 = just the item, no comments. 1 = direct replies only. A larger value is served at 10. Replies below the depth served are never fetched — to read them, raise depth up to 10, or call again with a comment from the deepest level returned, or one notice names, as itemId to drill into its subtree. Ignored when cursor is set: the depth the cursor was issued with applies."
},
"cursor": {
"type": "string",
"description": "nextCursor from a previous call, passed with the same itemId, to continue the traversal at the next unseen comment without repeating or skipping any. Omit to start from the top."
},
"itemId": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "ID of the story, comment, job, poll, or poll option to fetch the thread for."
},
"maxComments": {
"type": "integer",
"default": 50,
"maximum": 1000,
"minimum": 1,
"description": "Maximum comments in one response, across all depth levels, at most 200 per call. A larger value is served at 200, and nextCursor continues from there. Highest-ranked top-level comments resolve first; replies fill in only after the level above is exhausted. A response also stops between comments to stay within a fixed 64,000-byte budget on each surface — all content[] text, nextCursor and the rest of the trailer included, and the structuredContent JSON — though its first comment is always returned whole. When either limit stops the traversal, nextCursor continues it."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"item",
"comments",
"totalLoaded"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"cap": {
"type": "number",
"description": "The maxComments value served: as passed, or 200 when maxComments was above that cap."
},
"item": {
"type": "object",
"required": [
"id",
"type"
],
"properties": {
"by": {
"type": "string",
"description": "Author username."
},
"id": {
"type": "number",
"description": "Item ID."
},
"url": {
"type": "string",
"description": "External link URL."
},
"dead": {
"type": "boolean",
"const": true,
"description": "Present and `true` when the root is dead (flagged or killed); its replies are still walked. Omitted otherwise."
},
"poll": {
"type": "number",
"description": "For a poll-option root, the ID of its poll."
},
"text": {
"type": "string",
"description": "Body text (HTML stripped)."
},
"time": {
"type": "number",
"description": "Unix timestamp."
},
"type": {
"type": "string",
"description": "Item type: story | comment | job | poll | pollopt."
},
"parts": {
"type": "array",
"items": {
"type": "number"
},
"description": "For a poll root, its option IDs in HN order."
},
"score": {
"type": "number",
"description": "Upvote count."
},
"title": {
"type": "string",
"description": "Story/job/poll title."
},
"parent": {
"type": "number",
"description": "For a comment root, the ID of the story or comment it replies to — pass it as itemId for the context above."
},
"deleted": {
"type": "boolean",
"const": true,
"description": "Present and `true` when HN reports the root deleted; its author and text are gone, but its replies are still walked. Omitted otherwise."
},
"options": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "number",
"description": "Poll option ID."
},
"dead": {
"type": "boolean",
"const": true,
"description": "Present and `true` when the option is dead (flagged or killed). Omitted otherwise."
},
"text": {
"type": "string",
"description": "Option text (HTML stripped). Absent once deleted."
},
"score": {
"type": "number",
"description": "Votes for this option."
},
"deleted": {
"type": "boolean",
"const": true,
"description": "Present and `true` when the option was deleted. Omitted otherwise."
}
},
"description": "One poll option.",
"additionalProperties": false
},
"description": "For a poll root, its options with text and votes in parts order, resolved at every depth including 0. Deleted and dead options are kept and marked; an option whose fetch failed is listed in failedIds instead. Options do not count toward maxComments or totalLoaded."
},
"storyId": {
"type": "number",
"description": "For a comment root, the ID of the story or poll its discussion belongs to — pass it as itemId for the whole thread, or as storyId to hn_search_content. Absent for other roots, and when the story could not be found (notice says why)."
},
"descendants": {
"type": "number",
"description": "HN's count of live comments in the thread; dead and deleted comments are not counted."
}
},
"description": "The root item: a story, comment, job, poll, or poll option.",
"additionalProperties": false
},
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"item_not_found",
"invalid_cursor",
"upstream_rejected",
"upstream_rate_limited",
"upstream_unavailable",
"upstream_html",
"upstream_malformed"
],
"description": "Machine-readable failure mode. Declared by this tool: `item_not_found`: HN reports no item exists for the given itemId. `invalid_cursor`: The cursor is not one this tool issued, was altered, or was issued for a different itemId than the one passed with it. `upstream_rejected`: The HN API answered with a 4xx status other than 429 — it rejected the request as built. `upstream_rate_limited`: The HN API answered with HTTP 429. `upstream_unavailable`: The HN API answered with a 5xx status. `upstream_html`: The HN API served an HTML error page with a 200 status, which it does under rate limiting or maintenance. `upstream_malformed`: The HN API answered with a 200 status and a body that is not JSON. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"shown": {
"type": "number",
"description": "Number of comments returned."
},
"notice": {
"type": "string",
"description": "Traversal context: counts of deleted/dead comments dropped, the IDs that failed to load and how to retry them, then what stopped the traversal and how to continue — nextCursor for a count, size, rate-limit, or time-budget stop, or a larger depth or a comment id as itemId for a depth cut — noting when the maxComments or depth that stopped it was served at its per-call cap. Dead or deleted comments whose replies the depth limit cut are named by id, to pass as itemId, on the page that reaches them, since comments omits them. Absent when nothing was dropped, failed, or left unread."
},
"comments": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"depth",
"parentId",
"childCount"
],
"properties": {
"by": {
"type": "string",
"description": "Author username."
},
"id": {
"type": "number",
"description": "Comment ID."
},
"isOp": {
"type": "boolean",
"const": true,
"description": "Present and `true` when the comment author submitted the story or poll the thread belongs to (OP replying within their own thread) — for a comment root, the story at item.storyId. Omitted otherwise, including when either author is missing. Most threads carry no OP replies, so absence usually means \"not OP\"; it means unknown when the thread's root has no author (a deleted story, for example) or when notice says replies are not marked."
},
"text": {
"type": "string",
"description": "Comment text (HTML stripped)."
},
"time": {
"type": "number",
"description": "Unix timestamp."
},
"depth": {
"type": "number",
"description": "Nesting level (0 = direct reply to root)."
},
"parentId": {
"type": "number",
"description": "Parent item ID: the root or another comment. It can name a dead or deleted comment, which this list omits while still returning its replies; pass that ID as itemId to see it marked."
},
"childCount": {
"type": "number",
"description": "Number of direct child comments (may exceed what was resolved)."
}
},
"description": "A single comment in the thread with its tree position.",
"additionalProperties": false
},
"description": "Flat comment list ordered breadth-first by rank: highest-ranked top-level comments first, then their replies. Use depth/parentId to reconstruct nesting. With cursor, the page continues the same order."
},
"failedIds": {
"type": "array",
"items": {
"type": "number"
},
"description": "Comment and poll-option IDs whose fetch failed after retries or ran past the call's 50-second time budget, so neither they nor their replies are in this response. They are not deleted or missing and may load on a later call. Failed comments ride in nextCursor and are retried when you continue; without one, call again, or pass an ID as itemId to fetch that comment and its replies. Absent when everything the traversal reached loaded."
},
"truncated": {
"type": "boolean",
"description": "True when comments remain unread: reachable through nextCursor (truncationReason count, size, rate_limited, or time), or below the depth limit (depth). Absent on a terminal page — including a thread that loads in full at exactly maxComments."
},
"nextCursor": {
"type": "string",
"description": "Pass as cursor, with the same itemId, to continue from the next unseen comment. Present when truncationReason is count, size, rate_limited, or time; absent on the last page."
},
"totalLoaded": {
"type": "number",
"description": "Number of comments in this response (this page, when paging with cursor)."
},
"totalAvailable": {
"type": "number",
"description": "HN's count of live comments in the thread (descendants); dead and deleted comments are not counted. Absent for comment and job roots."
},
"truncationReason": {
"enum": [
"count",
"size",
"depth",
"rate_limited",
"time"
],
"type": "string",
"description": "Why the traversal stopped short: count = maxComments (or the 1,000-item ceiling per call) was reached; size = the page stopped between comments to keep content[] and structuredContent each within the 64,000-byte response budget; rate_limited = the HN API throttled the fetches; time = the call's 50-second time budget ran short because the HN API answered slowly or not at all; depth = replies lie below the depth limit, which nextCursor does not reach — raise depth or pass a comment id as itemId. When a cursor reason and a depth cut both apply, the cursor reason is reported."
}
},
"additionalProperties": false
}{
"readOnlyHint": true
}hn_get_user — Hn Get User
Get an HN user profile with karma, about, and optionally their most recent submissions resolved into full items.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"username"
],
"properties": {
"username": {
"type": "string",
"minLength": 1,
"description": "HN username. Case-sensitive. Trimmed; blank or whitespace-only input is rejected."
},
"submissionCount": {
"type": "integer",
"default": 10,
"maximum": 1000,
"minimum": 1,
"description": "Page size — how many submissions to resolve per call, at most 50 per call. A larger value is served at 50, and the notice names the submissionOffset for the next page. Only used when includeSubmissions is true."
},
"submissionOffset": {
"type": "integer",
"default": 0,
"maximum": 9007199254740991,
"minimum": 0,
"description": "How many submissions to skip before resolving, counting back from the most recent. Use with submissionCount to page through a long history: request offset 0, then advance by the page size served, and so on. The enrichment block echoes submissionOffset and, when more remain, the offset to send next. Only used when includeSubmissions is true."
},
"includeSubmissions": {
"type": "boolean",
"default": false,
"description": "Resolve the user's most recent submissions into full items. Without this, only the submission count is available."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"user"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"cap": {
"type": "number",
"description": "The submissionCount value served: as passed, or 50 when submissionCount was above that cap."
},
"user": {
"type": "object",
"required": [
"id",
"karma",
"created",
"totalSubmissions"
],
"properties": {
"id": {
"type": "string",
"description": "Username."
},
"about": {
"type": "string",
"description": "Self-description (HTML stripped)."
},
"karma": {
"type": "number",
"description": "Karma score."
},
"created": {
"type": "number",
"description": "Account creation time (Unix timestamp)."
},
"totalSubmissions": {
"type": "number",
"description": "Total number of submissions."
}
},
"description": "User profile.",
"additionalProperties": false
},
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"user_not_found",
"upstream_rejected",
"upstream_rate_limited",
"upstream_unavailable",
"upstream_html",
"upstream_malformed"
],
"description": "Machine-readable failure mode. Declared by this tool: `user_not_found`: HN reports no user account exists for the given username. `upstream_rejected`: The HN API answered with a 4xx status other than 429 — it rejected the request as built, which a username outside HN’s charset can cause. `upstream_rate_limited`: The HN API answered with HTTP 429. `upstream_unavailable`: The HN API answered with a 5xx status. `upstream_html`: The HN API served an HTML error page with a 200 status, which it does under rate limiting or maintenance. `upstream_malformed`: The HN API answered with a 200 status and a body that is not JSON. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"shown": {
"type": "number",
"description": "Number of submissions returned."
},
"notice": {
"type": "string",
"description": "Pagination context — which window of the history this page covers and the submissionOffset to send next, the submission IDs that failed to load and how to retry them, or a warning that the offset is past the end — noting when submissionCount was served at its per-call cap or the call's 50-second time budget cut the failed fetches, and closing with the submissions whose text was cut to keep the page within the 64,000-byte response budget. Absent when the page reaches the end of the history with no failed IDs and no cut text, or when no submissions were resolved."
},
"failedIds": {
"type": "array",
"items": {
"type": "number"
},
"description": "Submission IDs in this window whose fetch failed after retries or ran past the call's 50-second time budget. They are not deleted or missing and may load on a later call: retry with the same submissionOffset, or pass an ID to hn_get_thread to fetch that item alone. Absent when every submission in the window loaded."
},
"truncated": {
"type": "boolean",
"description": "True when submissions remain beyond this page."
},
"submissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"type"
],
"properties": {
"id": {
"type": "number",
"description": "Item ID — use with hn_get_thread to read comments."
},
"url": {
"type": "string",
"description": "External link URL."
},
"poll": {
"type": "number",
"description": "For a poll option, the ID of its poll."
},
"text": {
"type": "string",
"description": "Body text (HTML stripped). Cut short when the page would otherwise exceed the 64,000-byte response budget, with textTruncated set."
},
"time": {
"type": "number",
"description": "Unix timestamp."
},
"type": {
"type": "string",
"description": "Item type (story, comment, job, poll, pollopt)."
},
"score": {
"type": "number",
"description": "Score/upvotes."
},
"title": {
"type": "string",
"description": "Title (stories/jobs/polls)."
},
"parent": {
"type": "number",
"description": "For a comment, the ID of the story or comment it replied to — pass it to hn_get_thread for the context."
},
"descendants": {
"type": "number",
"description": "HN's count of live comments in the thread (descendants); dead and deleted comments are not counted. Present for stories and polls."
},
"textTruncated": {
"type": "boolean",
"description": "True when text is a prefix of the body, cut short to keep the page within the 64,000-byte response budget — hn_get_thread with this id returns it whole. Absent when text is whole."
}
},
"description": "A single submission by the user (story, comment, job, poll, or poll option).",
"additionalProperties": false
},
"description": "One page of submissions, most recent first, starting at submissionOffset. Absent when includeSubmissions is false or the user has never submitted. Empty when the page holds no live items — either the offset is past the end, or every item in the window was deleted, flagged, or failed to load (failedIds lists the failures)."
},
"submissionOffset": {
"type": "number",
"description": "The offset this page started at. Absent when includeSubmissions is false or the user has never submitted."
}
},
"additionalProperties": false
}{
"readOnlyHint": true
}hn_search_content — Hn Search Content
Search Hacker News stories, comments, polls, and jobs via Algolia — by keyword, by filters alone, or both. Filterable by content type, author, parent story, date range, and minimum points.
{
"type": "object",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"page": {
"type": "integer",
"default": 0,
"maximum": 9007199254740991,
"minimum": 0,
"description": "Page number for pagination (0-indexed), counted in pages of count results."
},
"sort": {
"enum": [
"relevance",
"date"
],
"type": "string",
"default": "relevance",
"description": "Sort order. \"relevance\" for best match, \"date\" for most recent first."
},
"tags": {
"enum": [
"story",
"comment",
"poll",
"job",
"ask_hn",
"show_hn",
"front_page"
],
"type": "string",
"description": "Filter results by content type: \"story\", \"comment\", \"poll\", or \"job\", or the story subsets \"ask_hn\", \"show_hn\", and \"front_page\". \"front_page\" is the HN front page right now (about 30 stories); no filter selects stories from past front pages. The nearest proxy is \"story\" with dateRange and minPoints — without a query, the default relevance sort ranks by points. Omit to search all types, which also returns poll options (type \"pollopt\"): records with only an id, author, vote count, and date — hn_get_thread reads an option's text and poll."
},
"view": {
"enum": [
"full",
"compact"
],
"type": "string",
"default": "full",
"description": "How much of each hit to return. \"full\" includes every field, within a 64,000-byte budget on each response surface: a page over it drops highlights.text from every hit (highlightsTextOmitted), and if still over, cuts the longest text bodies to one shared length (textTruncated) — every hit stays on the page. \"compact\" omits the two body-text fields — `text` and `highlights.text` — which together can repeat a long comment twice per hit; everything else (id, title, url, domain, author, points, comment count, timestamp, parent story, title highlight, matchedWords) is unchanged. Use \"compact\" to scan many results, then pass a hit id to hn_get_thread to read the body you skipped."
},
"count": {
"type": "integer",
"default": 30,
"maximum": 1000,
"minimum": 1,
"description": "Number of results to return, at most 50 per call. A larger value is served at 50: the search runs at count 50 from the hit where the requested page starts, and the notice names the count and page to pass next. With page above 0, that hit (page × count) must begin a page of 50 hits."
},
"query": {
"type": "string",
"minLength": 1,
"description": "Search terms. Supports simple keywords — Algolia handles stemming and relevance, and a hit must contain every term. Matching is typo-tolerant and the last term also matches as a prefix (zig matches Zigbee); wrap a term in double quotes to match it exactly (\"zig\"), or put - before one to exclude it (\"zig\" -devlog). Trimmed before searching; blank or whitespace-only input is rejected. To find discussions of a page, pass its URL alone: a query with no spaces that starts with http://, https://, or www. is searched without that prefix, so submissions under either scheme, with or without www., all match. Omit for a filter-only search, which needs at least one of tags, author, storyId, minPoints, or a dateRange bound."
},
"author": {
"type": "string",
"minLength": 1,
"description": "Filter results to a specific author — useful for finding a user's posts on a topic. Matches the HN username exactly and is case-sensitive, so a re-cased name finds nothing. Trimmed before filtering; omit the field to search all authors rather than passing a blank string."
},
"storyId": {
"type": "integer",
"maximum": 9007199254740991,
"description": "Restrict results to one discussion: the id of a story or poll root, combined with the other filters. Pair with tags \"comment\" to search within a thread. Take it from hits[].storyId, from a poll hit's own id (poll hits carry no storyId), or from hn_get_thread — the root item's id, or its item.storyId when the root is a comment. A comment id matches nothing.",
"exclusiveMinimum": 0
},
"dateRange": {
"type": "object",
"properties": {
"end": {
"type": "string",
"pattern": "^(\\d{4})(?:-(\\d{2})(?:-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d{1,3})?)?(?:Z|[+-](\\d{2}):(\\d{2}))?)?)?)?$",
"description": "Exclusive upper bound — only items created strictly before this instant match. Same formats and UTC reading as start, and must be later than start. A date-only end excludes that whole UTC day: to include it, pass the next day or a full timestamp."
},
"start": {
"type": "string",
"pattern": "^(\\d{4})(?:-(\\d{2})(?:-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d{1,3})?)?(?:Z|[+-](\\d{2}):(\\d{2}))?)?)?)?$",
"description": "Exclusive lower bound — only items created strictly after this instant match. ISO 8601: YYYY, YYYY-MM, YYYY-MM-DD, or YYYY-MM-DDThh:mm[:ss[.sss]] with an optional Z or ±hh:mm offset. Reduced and date-only forms mean UTC midnight at the start of that period; a date-time without an offset is read as UTC."
}
},
"description": "Filter to a creation-time window with a start, an end, or both. An empty object is rejected — omit dateRange instead."
},
"minPoints": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": 0,
"description": "Minimum score. Applies to stories and polls, including the ask_hn, show_hn, and front_page subsets. Comments and jobs carry no points in the search index, so any minPoints excludes them — combining it with tags \"comment\" or \"job\" is rejected."
}
},
"additionalProperties": false
}{
"type": "object",
"anyOf": [
{
"not": {
"required": [
"error"
]
},
"required": [
"hits",
"criteria",
"totalHits",
"page",
"totalPages"
]
},
{
"required": [
"error"
]
}
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"cap": {
"type": "number",
"description": "The count value served: as passed, or 50 when count was above that cap."
},
"hits": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"author",
"createdAt"
],
"properties": {
"id": {
"type": "number",
"description": "HN item ID — use with hn_get_thread to read the discussion."
},
"url": {
"type": "string",
"description": "External link URL."
},
"text": {
"type": "string",
"description": "Body text, HTML stripped: a comment's text, a text post's body, or a job's description. A job description is returned but not keyword-searched or highlighted. Cut short when the page would otherwise exceed the 64,000-byte response budget, with textTruncated set. Absent when the hit has no body, and always absent under view \"compact\" — call hn_get_thread with this id to read it."
},
"type": {
"enum": [
"story",
"comment",
"poll",
"pollopt",
"job"
],
"type": "string",
"description": "Item type: story (Ask HN, Show HN, and front-page stories included), comment, poll, pollopt (one option of a poll), or job. Absent when the search record names no type."
},
"title": {
"type": "string",
"description": "Item title (present for stories, polls, and jobs)."
},
"author": {
"type": "string",
"description": "Author username."
},
"domain": {
"type": "string",
"description": "Bare hostname derived from url (e.g. \"github.com\", with leading \"www.\" stripped). Absent when url is missing or unparseable."
},
"points": {
"type": "number",
"description": "Score/upvotes."
},
"storyId": {
"type": "number",
"description": "Parent story ID for comment hits; equals `id` for story hits. Absent for polls, poll options, and jobs — a poll's own `id` is its discussion root."
},
"createdAt": {
"type": "string",
"description": "Creation time (ISO 8601)."
},
"highlights": {
"type": "object",
"required": [
"matchedWords"
],
"properties": {
"text": {
"type": "string",
"description": "The matched body in full, not a snippet (comment_text or story_text), HTML stripped, with matched terms wrapped in `<em>…</em>` — the same text as `text` plus its markers. A literal `<em>` typed into the body is indistinguishable from a marker. Absent when the body did not match, when highlightsTextOmitted is true, and always under view \"compact\" — matchedWords still lists what matched."
},
"title": {
"type": "string",
"description": "Title snippet with matched terms wrapped in `<em>…</em>`. Titles are not HTML, though some arrive entity-encoded; entities are decoded and no tags are stripped, so a literal `<em>` in the title is indistinguishable from a marker. Absent when the title did not match."
},
"matchedWords": {
"type": "array",
"items": {
"type": "string"
},
"description": "Deduplicated union of matched terms across all searchable fields (title, url, author, comment_text, story_text, story_title)."
}
},
"description": "Algolia per-field highlight metadata showing which terms matched and where. Absent when no fields produced a match.",
"additionalProperties": false
},
"storyTitle": {
"type": "string",
"description": "Parent story title (present for comment results)."
},
"numComments": {
"type": "number",
"description": "Comment count."
},
"textTruncated": {
"type": "boolean",
"description": "True when text is a prefix of the body, cut short to keep the page within the 64,000-byte response budget — hn_get_thread with this id returns it whole. Absent when text is whole."
}
},
"description": "A single Algolia search hit (story, comment, poll, poll option, or job).",
"additionalProperties": false
},
"description": "Search results ranked by sort order."
},
"page": {
"type": "number",
"description": "Current page number (0-indexed), in pages of criteria.count hits. When count was above the per-call cap, the page of 50 where the requested page starts."
},
"error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "JSON-RPC error code for this failure."
},
"data": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"examples": [
"missing_query_or_filter",
"invalid_date_range",
"min_points_unscored_type",
"count_page_unaligned",
"upstream_rejected",
"upstream_rate_limited",
"upstream_unavailable",
"upstream_html",
"upstream_malformed"
],
"description": "Machine-readable failure mode. Declared by this tool: `missing_query_or_filter`: Neither a query nor any filter was supplied, so there is nothing to search by. `invalid_date_range`: dateRange was supplied with neither bound, or with start not before end. `min_points_unscored_type`: minPoints was combined with tags \"comment\" or \"job\", record types Algolia stores without points. `count_page_unaligned`: count was above the per-call cap of 50 with a page whose first hit (page × count) begins no page of 50 hits, so the requested page cannot be served from its start at the cap. `upstream_rejected`: Algolia answered with a 4xx status other than 429 — it rejected the request as built. `upstream_rate_limited`: Algolia answered with HTTP 429. `upstream_unavailable`: Algolia answered with a 5xx status. `upstream_html`: Algolia served an HTML error page with a 200 status, which it does under rate limiting or maintenance. `upstream_malformed`: Algolia answered with a 200 status and a body that is not JSON. Other values are possible when a failure originates below the handler."
},
"recovery": {
"type": "object",
"required": [
"hint"
],
"properties": {
"hint": {
"type": "string"
}
},
"description": "Actionable next step for the caller.",
"additionalProperties": {}
},
"retryable": {
"type": "boolean",
"description": "Whether retrying may succeed."
}
},
"additionalProperties": {}
},
"message": {
"type": "string",
"description": "Human-readable description of what went wrong."
}
},
"description": "Present when the call failed. Absent on success.",
"additionalProperties": {}
},
"query": {
"type": "string",
"description": "The query that was searched — for a URL query, without its leading scheme and www. Absent for a filter-only search."
},
"shown": {
"type": "number",
"description": "Number of hits returned."
},
"notice": {
"type": "string",
"description": "Agent guidance, one string. It opens with the page state: the next page to request while more pages remain, the 1,000-hit ceiling on the last page of a search that matches more, the last valid page when the requested page is past the end, or the filters to relax when a first page comes back empty. Notes on the inputs follow it, then, on a view \"full\" page fitted to the 64,000-byte response budget, a sentence naming what was omitted and the ids of hits whose text was cut. Absent when none applies."
},
"criteria": {
"type": "object",
"required": [
"sort",
"count",
"view"
],
"properties": {
"sort": {
"enum": [
"relevance",
"date"
],
"type": "string",
"description": "Sort order used."
},
"tags": {
"enum": [
"story",
"comment",
"poll",
"job",
"ask_hn",
"show_hn",
"front_page"
],
"type": "string",
"description": "Content-type filter."
},
"view": {
"enum": [
"full",
"compact"
],
"type": "string",
"description": "Projection applied to each hit."
},
"count": {
"type": "number",
"description": "Hits per page sent: count as passed, or 50 when count was above that cap."
},
"author": {
"type": "string",
"description": "Author filter — the username matched, trimmed."
},
"storyId": {
"type": "number",
"description": "Discussion filter: a story or poll root id."
},
"dateRange": {
"type": "object",
"properties": {
"end": {
"type": "string",
"description": "Items match only when created strictly before this instant: the end bound in ISO 8601 UTC, rounded up to the whole second."
},
"start": {
"type": "string",
"description": "Items match only when created strictly after this instant: the start bound in ISO 8601 UTC, rounded down to the whole second."
}
},
"description": "Creation-time window as applied, with only the bounds that were set.",
"additionalProperties": false
},
"minPoints": {
"type": "number",
"description": "Minimum score."
}
},
"description": "The criteria this search ran with: each filter that was set, as sent — author trimmed, dateRange bounds as the exclusive UTC instants applied — then sort, count, and view, defaults included. Unset filters are absent. The searched query is in query.",
"additionalProperties": false
},
"totalHits": {
"type": "number",
"description": "Matching results across all pages, as Algolia counts them. An estimate when totalHitsExact is false — broad searches can be off by orders of magnitude — and paging reaches only the first 1,000 (see reachableHits)."
},
"truncated": {
"type": "boolean",
"description": "True when more pages remain after this one (page + 1 < totalPages). Absent on the last page Algolia serves."
},
"totalPages": {
"type": "number",
"description": "Number of pages of criteria.count hits Algolia will actually serve for this query, covering at most the first 1,000 hits. Not derived from totalHits — broad queries report a totalHits far larger than the reachable page range, so paginate against this value."
},
"reachableHits": {
"type": "number",
"description": "How many of totalHits paging can reach: Algolia serves at most 1,000 hits per search. Present only when totalHits exceeds that; split the search into narrower dateRange windows to reach the rest."
},
"totalHitsExact": {
"type": "boolean",
"description": "True when totalHits is an exact count, false when it is an estimate, as it is for keyword searches and broad filter-only searches. Absent when Algolia does not say, and on a page past the 1,000-hit ceiling, where totalHits reads 0."
},
"highlightsTextOmitted": {
"type": "boolean",
"description": "True when highlights.text was dropped from every hit to keep this view \"full\" page within the 64,000-byte response budget; highlights.title and matchedWords remain. Absent otherwise."
}
},
"additionalProperties": false
}{
"readOnlyHint": true
}Prompts
0 advertised
Resources
1 advertised
hn-guide
Find the Hacker News thread behind a link, search topics without Algolia's fuzzy matches, read large comment trees in full, and search what one user said, with the hn_* tools. Use when a task asks what HN thinks of something, needs the discussion of a URL or story, a user's posts on a topic, the most-discussed front-page stories, or the monthly Who is hiring threads.
skill://hn-guide/SKILL.mdio.modelcontextprotocol/skills
skill://{+path}90-day uptime
A lightweight connection check about every six hours, sampled on hosts with many listings. It shows the server answered, not that its tools work.
- 2026-11-07: 1 of 1 checks answered
30-day availability
Aggregated from read-only remote checks; this is not a tool-execution or security guarantee.
- Successful checks
- 1 of 1 (100%)
- Average latency
- 608 ms
- 95th percentile latency
- 608 ms
Maintenance history
Public check and source-refresh events. Editorial and ownership records remain private.
- · probe · passed · 608 ms · metadata changed · 4 tool(s), 1 resource(s), 1 resource template(s), 0 prompt(s)
Connect from your client
Each snippet registers the same endpoint; pick the client you use.
Claude Code
claude mcp add --transport http hn-mcp-server https://hn.caseyjhand.com/mcpCodex
codex mcp add hn-mcp-server --url https://hn.caseyjhand.com/mcpCursor
{
"mcpServers": {
"hn-mcp-server": {
"url": "https://hn.caseyjhand.com/mcp"
}
}
}Cline
{
"mcpServers": {
"hn-mcp-server": {
"type": "streamableHttp",
"url": "https://hn.caseyjhand.com/mcp",
"disabled": false
}
}
}VS Code
{
"servers": {
"hn-mcp-server": {
"type": "http",
"url": "https://hn.caseyjhand.com/mcp"
}
}
}Gemini CLI
gemini mcp add --transport http hn-mcp-server https://hn.caseyjhand.com/mcpClaude Desktop
https://hn.caseyjhand.com/mcpWindsurf
{
"mcpServers": {
"hn-mcp-server": {
"serverUrl": "https://hn.caseyjhand.com/mcp"
}
}
}Let your agent find its tools
Find servers like hn-mcp-server from your agent. Add Koskamo once to search the index for endpoints, access requirements, and client setup. Public discovery needs no key.