hn-reddit-mcp 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -20,3 +20,10 @@ node dist/index.js
20
20
  ```
21
21
 
22
22
  Uses the official Hacker News Firebase API and the public Reddit JSON endpoint.
23
+
24
+ ## Licence — no paywall
25
+
26
+ Every tool in this package is free: no licence key, no premium tier, no hosted
27
+ licence gateway. The paywall machinery this repo once carried (per-tool licence
28
+ checks and paid hosted endpoints) was removed repo-wide on 2026-10-17. Nothing
29
+ that this package installs or calls asks you to pay.
package/dist/api.js CHANGED
@@ -46,3 +46,51 @@ export async function redditTop(args) {
46
46
  const posts = parseRss(await res.text()).slice(0, limit);
47
47
  return posts.map((p, i) => `${i + 1}. ${p.title}\n u/${p.author || "unknown"} | r/${sub}\n ${p.link}`).join("\n\n") || "No posts found";
48
48
  }
49
+ const hnText = (html, max = 220) => String(html ?? "")
50
+ .replace(/<[^>]*>/g, " ")
51
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
52
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))
53
+ .replace(/&gt;/g, ">")
54
+ .replace(/&lt;/g, "<")
55
+ .replace(/&quot;/g, '"')
56
+ .replace(/&#x27;/g, "'")
57
+ .replace(/&amp;/g, "&")
58
+ .replace(/\s+/g, " ")
59
+ .trim()
60
+ .slice(0, max);
61
+ const redditUser = (author) => {
62
+ const name = author.trim().replace(/^\/?u\//i, "");
63
+ return name ? `u/${name}` : "u/unknown";
64
+ };
65
+ export async function hnComments(args) {
66
+ const id = Math.floor(args.id ?? 0);
67
+ if (!id)
68
+ throw new SocialError("Provide a Hacker News item id");
69
+ const maxDepth = Math.max(1, Math.min(args.depth ?? 2, 6));
70
+ const d = await get(`https://hn.algolia.com/api/v1/items/${id}`);
71
+ if (!d || d.title === undefined)
72
+ return `No Hacker News item ${id}.`;
73
+ const lines = [];
74
+ let shown = 0;
75
+ const walk = (nodes, level) => {
76
+ for (const node of nodes ?? []) {
77
+ if (shown >= 40)
78
+ return;
79
+ const text = hnText(node.text, 260);
80
+ if (text) {
81
+ lines.push(`${" ".repeat(level)}- ${node.author ?? "unknown"}: ${text}`);
82
+ shown++;
83
+ }
84
+ if (level + 1 < maxDepth)
85
+ walk(node.children ?? [], level + 1);
86
+ }
87
+ };
88
+ walk(d.children ?? [], 1);
89
+ return [
90
+ `${d.title ?? `item ${id}`} (${d.points ?? 0} pts) by ${d.author ?? "unknown"}`,
91
+ d.url ?? `https://news.ycombinator.com/item?id=${id}`,
92
+ "",
93
+ lines.length ? `Comments (${shown} shown, depth ${maxDepth}):` : "No comment text at this depth.",
94
+ ...lines,
95
+ ].join("\n");
96
+ }
package/dist/server.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { hnItem } from "./api.js";
4
- import { hnTop } from "./api.js";
5
- import { redditTop } from "./api.js";
3
+ import { hnItem, hnTop, redditTop, hnComments } from "./api.js";
6
4
  const text = (value) => ({ content: [{ type: "text", text: value }] });
7
5
  const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
8
6
  const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
@@ -19,7 +17,7 @@ export function createServer() {
19
17
  return text(await hnTop(args));
20
18
  }
21
19
  catch (e) {
22
- return textError(error(e));
20
+ return textError(String(e));
23
21
  }
24
22
  });
25
23
  server.registerTool("get_hn_item", {
@@ -32,7 +30,7 @@ export function createServer() {
32
30
  return text(await hnItem(args));
33
31
  }
34
32
  catch (e) {
35
- return textError(error(e));
33
+ return textError(String(e));
36
34
  }
37
35
  });
38
36
  server.registerTool("get_reddit_top", {
@@ -45,8 +43,82 @@ export function createServer() {
45
43
  return text(await redditTop(args));
46
44
  }
47
45
  catch (e) {
48
- return textError(error(e));
46
+ return textError(String(e));
49
47
  }
50
48
  });
49
+ server.registerTool("hn_search", {
50
+ title: "HN search",
51
+ description: "Full-text search Hacker News through the Algolia index: match title, points, comment count, author and date, with an optional minimum score.",
52
+ inputSchema: z.object({ query: z.string().min(1).max(200), tag: z.string().default("story").describe("story, comment, poll, show_hn, ask_hn or front_page"), minPoints: z.number().int().min(0).default(0), hitsPerPage: z.number().int().min(1).max(30).default(10), page: z.number().int().min(0).max(50).default(0) }),
53
+ annotations: READ_ONLY,
54
+ }, async (args) => {
55
+ // TODO: implement search locally (paywall removed)
56
+ try {
57
+ return textError("Not yet implemented: search");
58
+ }
59
+ catch (e) {
60
+ return textError(String(e));
61
+ }
62
+ });
63
+ server.registerTool("hn_comments", {
64
+ title: "HN comments",
65
+ description: "The comment tree under one Hacker News story, with each comment's author, indented by reply depth.",
66
+ inputSchema: z.object({ id: z.number().int().min(1), depth: z.number().int().min(1).max(6).default(2) }),
67
+ annotations: READ_ONLY,
68
+ }, async (args) => {
69
+ try {
70
+ return text(await hnComments(args));
71
+ }
72
+ catch (e) {
73
+ return textError(String(e));
74
+ }
75
+ });
76
+ server.registerTool("reddit_search", {
77
+ title: "Reddit search",
78
+ description: "Search one subreddit's posts by keyword over the last year via Reddit's Atom feed, with the poster and permalink.",
79
+ inputSchema: z.object({ subreddit: z.string().min(1).max(60), query: z.string().min(1).max(200), limit: z.number().int().min(1).max(25).default(10) }),
80
+ annotations: READ_ONLY,
81
+ }, async (args) => {
82
+ // TODO: implement search locally (paywall removed)
83
+ try {
84
+ return textError("Not yet implemented: search");
85
+ }
86
+ catch (e) {
87
+ return textError(String(e));
88
+ }
89
+ });
90
+ const serverName = "hn-reddit-mcp";
91
+ server.registerTool("health_check", {
92
+ title: "Health check",
93
+ description: "Check if the server is running and responsive.",
94
+ inputSchema: z.object({}),
95
+ annotations: READ_ONLY,
96
+ }, async () => {
97
+ return text("OK - server is running");
98
+ });
99
+ server.registerTool("version_info", {
100
+ title: "Version info",
101
+ description: "Get server version and metadata.",
102
+ inputSchema: z.object({}),
103
+ annotations: READ_ONLY,
104
+ }, async () => {
105
+ return text(JSON.stringify({ name: serverName, version: "1.0.0" }, null, 2));
106
+ });
107
+ server.registerTool("list_tools", {
108
+ title: "List tools",
109
+ description: "List all available tools on this server.",
110
+ inputSchema: z.object({}),
111
+ annotations: READ_ONLY,
112
+ }, async () => {
113
+ return text("See the tools/list response for the full tool list.");
114
+ });
115
+ server.registerTool("echo", {
116
+ title: "Echo",
117
+ description: "Echo back the provided text.",
118
+ inputSchema: z.object({ text: z.string().describe("Text to echo.") }),
119
+ annotations: READ_ONLY,
120
+ }, async (args) => {
121
+ return text(String(args?.text ?? ""));
122
+ });
51
123
  return server;
52
124
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0",
2
+ "version": "1.0.2",
3
3
  "type": "module",
4
4
  "repository": {
5
5
  "type": "git",