@fre4x/hn 1.0.51 → 1.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +198 -73
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -42026,6 +42026,14 @@ function applyPagination(items, params) {
42026
42026
  };
42027
42027
  }
42028
42028
 
42029
+ // ../packages/shared/dist/package.js
42030
+ import { createRequire as createJsonRequire } from "node:module";
42031
+ function getPackageVersion(moduleUrl) {
42032
+ const require2 = createJsonRequire(moduleUrl);
42033
+ const packageJson = require2("../package.json");
42034
+ return packageJson.version ?? "0.0.0";
42035
+ }
42036
+
42029
42037
  // src/api.ts
42030
42038
  import https2 from "node:https";
42031
42039
 
@@ -45806,6 +45814,46 @@ var {
45806
45814
  mergeConfig: mergeConfig2
45807
45815
  } = axios_default;
45808
45816
 
45817
+ // src/mock.ts
45818
+ function isMock() {
45819
+ return process.env.MOCK === "true" || process.env.HN_MOCK === "true";
45820
+ }
45821
+ var MOCK_ITEM = {
45822
+ id: 1001,
45823
+ type: "story",
45824
+ by: "fre4x",
45825
+ time: 1712232e3,
45826
+ title: "Mock HN Story",
45827
+ url: "https://example.com/mock-hn-story",
45828
+ score: 42,
45829
+ descendants: 7,
45830
+ kids: [1002, 1003],
45831
+ text: "Mock story content for offline development."
45832
+ };
45833
+ var MOCK_USER = {
45834
+ id: "fre4x",
45835
+ created: 16e8,
45836
+ karma: 1337,
45837
+ about: "Mock Hacker News user for MCP development.",
45838
+ submitted: [1001, 1002, 1003]
45839
+ };
45840
+ var MOCK_UPDATES = {
45841
+ items: [1003, 1002, 1001],
45842
+ profiles: ["fre4x", "b1te"]
45843
+ };
45844
+ var MOCK_FIXTURES = {
45845
+ item: MOCK_ITEM,
45846
+ user: MOCK_USER,
45847
+ maxItem: 1003,
45848
+ topStories: [1003, 1002, 1001],
45849
+ newStories: [1003, 1002, 1001],
45850
+ bestStories: [1001, 1003, 1002],
45851
+ askStories: [2003, 2002, 2001],
45852
+ showStories: [3003, 3002, 3001],
45853
+ jobStories: [4003, 4002, 4001],
45854
+ updates: MOCK_UPDATES
45855
+ };
45856
+
45809
45857
  // src/api.ts
45810
45858
  var BASE_URL = "https://hacker-news.firebaseio.com/v0";
45811
45859
  var httpClient = axios_default.create({
@@ -45842,36 +45890,69 @@ var HNApiClient = class {
45842
45890
  return response.data;
45843
45891
  }
45844
45892
  async getItem(id) {
45893
+ if (isMock()) {
45894
+ return { ...MOCK_FIXTURES.item, id };
45895
+ }
45845
45896
  const response = await httpClient.get(`/item/${id}.json`);
45846
45897
  return response.data;
45847
45898
  }
45848
45899
  async getUser(id) {
45900
+ if (isMock()) {
45901
+ return { ...MOCK_FIXTURES.user, id };
45902
+ }
45849
45903
  const response = await httpClient.get(`/user/${id}.json`);
45850
45904
  return response.data;
45851
45905
  }
45852
45906
  async getMaxItem() {
45907
+ if (isMock()) {
45908
+ return MOCK_FIXTURES.maxItem;
45909
+ }
45853
45910
  const response = await httpClient.get(`/maxitem.json`);
45854
45911
  return response.data;
45855
45912
  }
45856
45913
  async getTopStories() {
45914
+ if (isMock()) {
45915
+ return [...MOCK_FIXTURES.topStories];
45916
+ }
45857
45917
  return this.fetchWithCache(`/topstories`);
45858
45918
  }
45859
45919
  async getNewStories() {
45920
+ if (isMock()) {
45921
+ return [...MOCK_FIXTURES.newStories];
45922
+ }
45860
45923
  return this.fetchWithCache(`/newstories`);
45861
45924
  }
45862
45925
  async getBestStories() {
45926
+ if (isMock()) {
45927
+ return [...MOCK_FIXTURES.bestStories];
45928
+ }
45863
45929
  return this.fetchWithCache(`/beststories`);
45864
45930
  }
45865
45931
  async getAskStories() {
45932
+ if (isMock()) {
45933
+ return [...MOCK_FIXTURES.askStories];
45934
+ }
45866
45935
  return this.fetchWithCache(`/askstories`);
45867
45936
  }
45868
45937
  async getShowStories() {
45938
+ if (isMock()) {
45939
+ return [...MOCK_FIXTURES.showStories];
45940
+ }
45869
45941
  return this.fetchWithCache(`/showstories`);
45870
45942
  }
45871
45943
  async getJobStories() {
45944
+ if (isMock()) {
45945
+ return [...MOCK_FIXTURES.jobStories];
45946
+ }
45872
45947
  return this.fetchWithCache(`/jobstories`);
45873
45948
  }
45874
45949
  async getUpdates() {
45950
+ if (isMock()) {
45951
+ return {
45952
+ items: [...MOCK_FIXTURES.updates.items],
45953
+ profiles: [...MOCK_FIXTURES.updates.profiles]
45954
+ };
45955
+ }
45875
45956
  const response = await httpClient.get(`/updates.json`);
45876
45957
  return response.data;
45877
45958
  }
@@ -45887,22 +45968,28 @@ var CHARACTER_LIMIT = 12e3;
45887
45968
 
45888
45969
  // src/index.ts
45889
45970
  var api = new HNApiClient();
45971
+ var PACKAGE_VERSION = getPackageVersion(import.meta.url);
45890
45972
  var server = new McpServer({
45891
45973
  name: "hackernews-mcp-server",
45892
- version: "1.0.0"
45974
+ version: PACKAGE_VERSION
45893
45975
  });
45894
45976
  function formatItemMarkdown(item) {
45895
45977
  if (!item) return "Item not found.";
45896
45978
  const lines = [];
45897
45979
  if (item.title) lines.push(`# ${item.title}
45898
45980
  `);
45899
- lines.push(formatFields([
45900
- ["By", item.by],
45901
- ["Time", item.time ? new Date(item.time * 1e3).toLocaleString() : void 0],
45902
- ["Score", item.score],
45903
- ["URL", item.url],
45904
- ["Comments", item.descendants]
45905
- ]));
45981
+ lines.push(
45982
+ formatFields([
45983
+ ["By", item.by],
45984
+ [
45985
+ "Time",
45986
+ item.time ? new Date(item.time * 1e3).toLocaleString() : void 0
45987
+ ],
45988
+ ["Score", item.score],
45989
+ ["URL", item.url],
45990
+ ["Comments", item.descendants]
45991
+ ])
45992
+ );
45906
45993
  if (item.text) lines.push(`
45907
45994
  ${item.text}`);
45908
45995
  if (item.kids && item.kids.length > 0) {
@@ -45918,18 +46005,17 @@ function handleLimit(content, format) {
45918
46005
 
45919
46006
  > [!NOTE]
45920
46007
  > Content truncated due to size limit. Use pagination (limit/offset) to fetch specific parts if applicable.`;
45921
- } else {
45922
- return JSON.stringify({
45923
- content: content.substring(0, CHARACTER_LIMIT),
45924
- truncated: true,
45925
- hint: "Content truncated due to size limit. Use limit/offset for lists."
45926
- });
45927
46008
  }
46009
+ return JSON.stringify({
46010
+ content: content.substring(0, CHARACTER_LIMIT),
46011
+ truncated: true,
46012
+ hint: "Content truncated due to size limit. Use limit/offset for lists."
46013
+ });
45928
46014
  }
45929
46015
  var storySchema = paginationSchema.extend({
45930
46016
  response_format: external_exports3.enum(Object.values(ResponseFormat)).default("markdown" /* MARKDOWN */).describe("Output format (markdown or json)")
45931
46017
  });
45932
- async function handleStories(ids, params) {
46018
+ async function handleHNStories(ids, params) {
45933
46019
  const paginated = applyPagination(ids, params);
45934
46020
  const result = {
45935
46021
  total: paginated.total,
@@ -45957,7 +46043,11 @@ async function handleStories(ids, params) {
45957
46043
  ` + formatListItems(
45958
46044
  paginated.items,
45959
46045
  (id, i) => `${i + 1 + paginated.offset}. [${id}](https://news.ycombinator.com/item?id=${id})`
45960
- ) + (paginated.hasMore ? formatPaginationFooter(paginated.offset, paginated.limit, paginated.total) : "");
46046
+ ) + (paginated.hasMore ? formatPaginationFooter(
46047
+ paginated.offset,
46048
+ paginated.limit,
46049
+ paginated.total
46050
+ ) : "");
45961
46051
  return {
45962
46052
  content: [
45963
46053
  {
@@ -45967,6 +46057,45 @@ async function handleStories(ids, params) {
45967
46057
  ]
45968
46058
  };
45969
46059
  }
46060
+ async function handleHNGetTopStories(params) {
46061
+ try {
46062
+ return await handleHNStories(await api.getTopStories(), params);
46063
+ } catch (error48) {
46064
+ return createInternalError(error48);
46065
+ }
46066
+ }
46067
+ async function handleHNGetItem(params) {
46068
+ try {
46069
+ const item = await api.getItem(params.id);
46070
+ if (params.response_format === "json" /* JSON */) {
46071
+ return {
46072
+ content: [
46073
+ {
46074
+ type: "text",
46075
+ text: handleLimit(
46076
+ JSON.stringify(item, null, 2),
46077
+ "json" /* JSON */
46078
+ )
46079
+ }
46080
+ ],
46081
+ structuredContent: item
46082
+ };
46083
+ }
46084
+ return {
46085
+ content: [
46086
+ {
46087
+ type: "text",
46088
+ text: handleLimit(
46089
+ formatItemMarkdown(item),
46090
+ "markdown" /* MARKDOWN */
46091
+ )
46092
+ }
46093
+ ]
46094
+ };
46095
+ } catch (error48) {
46096
+ return createInternalError(error48);
46097
+ }
46098
+ }
45970
46099
  server.registerTool(
45971
46100
  "hn_get_top_stories",
45972
46101
  {
@@ -45979,13 +46108,8 @@ server.registerTool(
45979
46108
  openWorldHint: true
45980
46109
  }
45981
46110
  },
45982
- async (params) => {
45983
- try {
45984
- return await handleStories(await api.getTopStories(), params);
45985
- } catch (error48) {
45986
- return createInternalError(error48);
45987
- }
45988
- }
46111
+ // @ts-expect-error
46112
+ handleHNGetTopStories
45989
46113
  );
45990
46114
  server.registerTool(
45991
46115
  "hn_get_new_stories",
@@ -46001,7 +46125,10 @@ server.registerTool(
46001
46125
  },
46002
46126
  async (params) => {
46003
46127
  try {
46004
- return await handleStories(await api.getNewStories(), params);
46128
+ return await handleHNStories(
46129
+ await api.getNewStories(),
46130
+ params
46131
+ );
46005
46132
  } catch (error48) {
46006
46133
  return createInternalError(error48);
46007
46134
  }
@@ -46021,7 +46148,10 @@ server.registerTool(
46021
46148
  },
46022
46149
  async (params) => {
46023
46150
  try {
46024
- return await handleStories(await api.getBestStories(), params);
46151
+ return await handleHNStories(
46152
+ await api.getBestStories(),
46153
+ params
46154
+ );
46025
46155
  } catch (error48) {
46026
46156
  return createInternalError(error48);
46027
46157
  }
@@ -46041,7 +46171,10 @@ server.registerTool(
46041
46171
  },
46042
46172
  async (params) => {
46043
46173
  try {
46044
- return await handleStories(await api.getAskStories(), params);
46174
+ return await handleHNStories(
46175
+ await api.getAskStories(),
46176
+ params
46177
+ );
46045
46178
  } catch (error48) {
46046
46179
  return createInternalError(error48);
46047
46180
  }
@@ -46061,7 +46194,10 @@ server.registerTool(
46061
46194
  },
46062
46195
  async (params) => {
46063
46196
  try {
46064
- return await handleStories(await api.getShowStories(), params);
46197
+ return await handleHNStories(
46198
+ await api.getShowStories(),
46199
+ params
46200
+ );
46065
46201
  } catch (error48) {
46066
46202
  return createInternalError(error48);
46067
46203
  }
@@ -46081,7 +46217,10 @@ server.registerTool(
46081
46217
  },
46082
46218
  async (params) => {
46083
46219
  try {
46084
- return await handleStories(await api.getJobStories(), params);
46220
+ return await handleHNStories(
46221
+ await api.getJobStories(),
46222
+ params
46223
+ );
46085
46224
  } catch (error48) {
46086
46225
  return createInternalError(error48);
46087
46226
  }
@@ -46102,38 +46241,8 @@ server.registerTool(
46102
46241
  openWorldHint: true
46103
46242
  }
46104
46243
  },
46105
- async (params) => {
46106
- try {
46107
- const item = await api.getItem(params.id);
46108
- if (params.response_format === "json" /* JSON */) {
46109
- return {
46110
- content: [
46111
- {
46112
- type: "text",
46113
- text: handleLimit(
46114
- JSON.stringify(item, null, 2),
46115
- "json" /* JSON */
46116
- )
46117
- }
46118
- ],
46119
- structuredContent: item
46120
- };
46121
- }
46122
- return {
46123
- content: [
46124
- {
46125
- type: "text",
46126
- text: handleLimit(
46127
- formatItemMarkdown(item),
46128
- "markdown" /* MARKDOWN */
46129
- )
46130
- }
46131
- ]
46132
- };
46133
- } catch (error48) {
46134
- return createInternalError(error48);
46135
- }
46136
- }
46244
+ // @ts-expect-error
46245
+ handleHNGetItem
46137
46246
  );
46138
46247
  server.registerTool(
46139
46248
  "hn_get_user",
@@ -46242,15 +46351,23 @@ server.registerTool(
46242
46351
  structuredContent: result
46243
46352
  };
46244
46353
  }
46245
- let markdown = `# HN Recent Updates
46246
-
46247
- `;
46248
- markdown += `## Items
46249
- ` + formatListItems(paginatedItems.items, (id) => `- [${id}](https://news.ycombinator.com/item?id=${id})`) + (paginatedItems.hasMore ? formatPaginationFooter(paginatedItems.offset, paginatedItems.limit, paginatedItems.total) : "") + `
46250
-
46251
- `;
46252
- markdown += `## Profiles
46253
- ` + formatListItems(paginatedProfiles.items, (profile) => `- [${profile}](https://news.ycombinator.com/user?id=${profile})`) + (paginatedProfiles.hasMore ? formatPaginationFooter(paginatedProfiles.offset, paginatedProfiles.limit, paginatedProfiles.total) : "");
46354
+ let markdown = "# HN Recent Updates\n\n";
46355
+ markdown += "## Items\n" + formatListItems(
46356
+ paginatedItems.items,
46357
+ (id) => `- [${id}](https://news.ycombinator.com/item?id=${id})`
46358
+ ) + (paginatedItems.hasMore ? formatPaginationFooter(
46359
+ paginatedItems.offset,
46360
+ paginatedItems.limit,
46361
+ paginatedItems.total
46362
+ ) : "") + "\n\n";
46363
+ markdown += "## Profiles\n" + formatListItems(
46364
+ paginatedProfiles.items,
46365
+ (profile) => `- [${profile}](https://news.ycombinator.com/user?id=${profile})`
46366
+ ) + (paginatedProfiles.hasMore ? formatPaginationFooter(
46367
+ paginatedProfiles.offset,
46368
+ paginatedProfiles.limit,
46369
+ paginatedProfiles.total
46370
+ ) : "");
46254
46371
  return {
46255
46372
  content: [
46256
46373
  {
@@ -46298,10 +46415,18 @@ async function run() {
46298
46415
  await server.connect(transport);
46299
46416
  console.error("Hacker News MCP server running via stdio");
46300
46417
  }
46301
- run().catch((error48) => {
46302
- console.error("Fatal error:", error48);
46303
- process.exit(1);
46304
- });
46418
+ if (process.env.NODE_ENV !== "test") {
46419
+ run().catch((error48) => {
46420
+ console.error("Fatal error:", error48);
46421
+ process.exit(1);
46422
+ });
46423
+ }
46424
+ export {
46425
+ handleHNGetItem,
46426
+ handleHNGetTopStories,
46427
+ handleHNStories,
46428
+ server
46429
+ };
46305
46430
  /*! Bundled license information:
46306
46431
 
46307
46432
  mime-db/index.js:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fre4x/hn",
3
- "version": "1.0.51",
3
+ "version": "1.0.54",
4
4
  "description": "A Hacker News MCP server for LLMs.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,7 @@
11
11
  "dist"
12
12
  ],
13
13
  "scripts": {
14
- "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npx esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node --format=esm --banner:js=\"import{createRequire}from'module';const require=createRequire(import.meta.url);\" && node -e \"const fs=require('fs');const p='dist/index.js';const c=fs.readFileSync(p,'utf8');const next=c.startsWith('#!/usr/bin/env node')?c:'#!/usr/bin/env node\\n'+c;fs.writeFileSync(p,next);fs.chmodSync(p,0o755);\"",
14
+ "build": "node ../scripts/build-package.mjs",
15
15
  "typecheck": "cross-env NODE_OPTIONS=--max-old-space-size=4096 tsc --noEmit",
16
16
  "start": "node dist/index.js",
17
17
  "dev": "tsx src/index.ts",
@@ -28,12 +28,12 @@
28
28
  "author": "fritzprix",
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "@modelcontextprotocol/sdk": "^1.26.0",
31
+ "@modelcontextprotocol/sdk": "^1.27.1",
32
32
  "axios": "^1.13.5",
33
- "zod": "^4.0.0"
33
+ "zod": "^4.3.6"
34
34
  },
35
35
  "devDependencies": {
36
- "@types/node": "^25.3.0",
36
+ "@types/node": "^25.3.5",
37
37
  "tsx": "^4.21.0",
38
38
  "typescript": "^5.9.3",
39
39
  "vitest": "^4.0.18"