hn-mcp-server

v0.7.0 pre-1.0

Browse Hacker News feeds, threads, and user profiles with full-text search via MCP. STDIO or Streamable HTTP.

hn.caseyjhand.com/mcp
claude mcp add --transport http hn-mcp-server https://hn.caseyjhand.com/mcp
codex mcp add hn-mcp-server --url https://hn.caseyjhand.com/mcp
{
  "mcpServers": {
    "hn-mcp-server": {
      "url": "https://hn.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http hn-mcp-server https://hn.caseyjhand.com/mcp
{
  "mcpServers": {
    "hn-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://hn.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "hn-mcp-server": {
      "type": "http",
      "url": "https://hn.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://hn.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

4

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "hn_get_stories",
    "arguments": {
      "feed": "<feed>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "feed": {
      "type": "string",
      "enum": [
        "top",
        "new",
        "best",
        "ask",
        "show",
        "jobs"
      ],
      "description": "Which HN feed to fetch. \"top\" includes jobs. \"ask\" and \"show\" are Ask HN / Show HN posts."
    },
    "count": {
      "default": 30,
      "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.",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    },
    "offset": {
      "default": 0,
      "description": "Number of stories to skip from the start of the feed. Use with count for pagination.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "feed",
    "count",
    "offset"
  ],
  "additionalProperties": false
}
view source ↗

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.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "hn_get_thread",
    "arguments": {
      "itemId": "<itemId>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "itemId": {
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991,
      "description": "ID of the story, comment, job, poll, or poll option to fetch the thread for."
    },
    "depth": {
      "default": 3,
      "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.",
      "type": "integer",
      "minimum": 0,
      "maximum": 100
    },
    "maxComments": {
      "default": 25,
      "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. When this limit stops the traversal, nextCursor continues it.",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    },
    "cursor": {
      "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.",
      "type": "string"
    }
  },
  "required": [
    "itemId",
    "depth",
    "maxComments"
  ],
  "additionalProperties": false
}
view source ↗

hn_get_user

Get an HN user profile with karma, about, and optionally their most recent submissions resolved into full items.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "hn_get_user",
    "arguments": {
      "username": "<username>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "minLength": 1,
      "description": "HN username. Case-sensitive. Trimmed, and one leading @ is dropped, so @pg looks up pg. Blank input is rejected, as is a name with whitespace inside it — a display name such as \"Paul Graham\", not an account name."
    },
    "includeSubmissions": {
      "default": false,
      "description": "Resolve the user's most recent submissions into full items. Without this, only the submission count is available.",
      "type": "boolean"
    },
    "submissionCount": {
      "default": 10,
      "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.",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    },
    "submissionOffset": {
      "default": 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.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "username",
    "includeSubmissions",
    "submissionCount",
    "submissionOffset"
  ],
  "additionalProperties": false
}
view source ↗

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, minimum points, and minimum comment count.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "hn_search_content",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "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, minComments, or a dateRange bound.",
      "type": "string",
      "minLength": 1
    },
    "tags": {
      "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.",
      "type": "string",
      "enum": [
        "story",
        "comment",
        "poll",
        "job",
        "ask_hn",
        "show_hn",
        "front_page"
      ]
    },
    "author": {
      "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, and one leading @ is dropped, so \"@pg\" searches pg; a name that still holds whitespace is rejected as a display name. Omit the field to search all authors rather than passing a blank string.",
      "type": "string",
      "minLength": 1
    },
    "storyId": {
      "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.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991
    },
    "sort": {
      "default": "relevance",
      "description": "Sort order. \"relevance\" for best match, \"date\" for most recent first.",
      "type": "string",
      "enum": [
        "relevance",
        "date"
      ]
    },
    "dateRange": {
      "description": "Filter to a creation-time window with a start, an end, or both. An empty object is rejected — omit dateRange instead.",
      "type": "object",
      "properties": {
        "start": {
          "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.",
          "type": "string",
          "pattern": "^(\\d{4})(?:-(\\d{2})(?:-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d{1,3})?)?(?:Z|[+-](\\d{2}):(\\d{2}))?)?)?)?$"
        },
        "end": {
          "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.",
          "type": "string",
          "pattern": "^(\\d{4})(?:-(\\d{2})(?:-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d{1,3})?)?(?:Z|[+-](\\d{2}):(\\d{2}))?)?)?)?$"
        }
      },
      "additionalProperties": false
    },
    "minPoints": {
      "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.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "minComments": {
      "description": "Minimum comment count. Applies to stories and polls, including the ask_hn, show_hn, and front_page subsets. Any minComments, 0 included, excludes comments, jobs, and poll options, which carry no comment count in the search index, so an untagged search returns only stories and polls — combining it with tags \"comment\" or \"job\" is rejected.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "count": {
      "default": 10,
      "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.",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    },
    "page": {
      "default": 0,
      "description": "Page number for pagination (0-indexed), counted in pages of count results.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "view": {
      "default": "full",
      "description": "How much of each hit to return. \"full\" includes every field. \"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, parentId, storyUrl, 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.",
      "type": "string",
      "enum": [
        "full",
        "compact"
      ]
    }
  },
  "required": [
    "sort",
    "count",
    "page",
    "view"
  ],
  "additionalProperties": false
}
view source ↗

Extensions

1

io.modelcontextprotocol/skills

{
  "directoryRead": true
}