Skip to main content
Glama
index.ts18.9 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; // HackerNews API base URL const HN_API_BASE = 'https://hn.algolia.com/api/v1'; // Helper function to make API calls async function fetchHN(endpoint: string): Promise<any> { const response = await fetch(`${HN_API_BASE}${endpoint}`); if (!response.ok) { throw new Error(`HN API error: ${response.status} ${response.statusText}`); } return await response.json(); } // Initialize the MCP server const server = new McpServer({ name: 'hackernews-mcp-server', version: '1.0.0' }); // Tool 1: Search posts by relevance server.registerTool( 'search-posts', { title: 'Search HackerNews Posts', description: 'Search HackerNews posts by relevance (sorted by relevance, then points, then number of comments)', inputSchema: { query: z.string().describe('Search query text'), tags: z.string().optional().describe('Filter tags (e.g., "story", "comment", "poll", "show_hn", "ask_hn", "front_page", "author_USERNAME", "story_ID")'), numericFilters: z.string().optional().describe('Numeric filters (e.g., "points>100", "created_at_i>1672531200")'), page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ query, tags, numericFilters, page, hitsPerPage }) => { const params = new URLSearchParams(); if (query) params.append('query', query); if (tags) params.append('tags', tags); if (numericFilters) params.append('numericFilters', numericFilters); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 2: Search posts by date server.registerTool( 'search-posts-by-date', { title: 'Search HackerNews Posts by Date', description: 'Search HackerNews posts sorted by date (most recent first)', inputSchema: { query: z.string().optional().describe('Search query text (optional)'), tags: z.string().optional().describe('Filter tags (e.g., "story", "comment", "poll", "show_hn", "ask_hn", "front_page", "author_USERNAME", "story_ID")'), numericFilters: z.string().optional().describe('Numeric filters (e.g., "points>100", "created_at_i>1672531200")'), page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ query, tags, numericFilters, page, hitsPerPage }) => { const params = new URLSearchParams(); if (query) params.append('query', query); if (tags) params.append('tags', tags); if (numericFilters) params.append('numericFilters', numericFilters); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 3: Get front page posts server.registerTool( 'get-front-page', { title: 'Get HackerNews Front Page', description: 'Get all stories currently on the HackerNews front page', inputSchema: { page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('tags', 'front_page'); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 4: Get latest stories server.registerTool( 'get-latest-stories', { title: 'Get Latest HackerNews Stories', description: 'Get the most recent stories posted to HackerNews', inputSchema: { page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('tags', 'story'); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 5: Get latest comments server.registerTool( 'get-latest-comments', { title: 'Get Latest HackerNews Comments', description: 'Get the most recent comments posted to HackerNews', inputSchema: { page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('tags', 'comment'); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 6: Get Show HN posts server.registerTool( 'get-show-hn', { title: 'Get Show HN Posts', description: 'Get latest "Show HN" posts where users showcase their projects', inputSchema: { page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('tags', 'show_hn'); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 7: Get Ask HN posts server.registerTool( 'get-ask-hn', { title: 'Get Ask HN Posts', description: 'Get latest "Ask HN" posts where users ask questions', inputSchema: { page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('tags', 'ask_hn'); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 8: Get item by ID server.registerTool( 'get-item', { title: 'Get HackerNews Item by ID', description: 'Get a specific HackerNews item (story, comment, poll, etc.) by its ID', inputSchema: { id: z.number().describe('The ID of the item to retrieve') }, outputSchema: { id: z.number(), created_at: z.string(), author: z.string().optional(), title: z.string().optional(), url: z.string().optional(), text: z.string().optional(), points: z.number().optional(), parent_id: z.number().optional(), children: z.array(z.any()).optional() } }, async ({ id }) => { const endpoint = `/items/${id}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 9: Get user information server.registerTool( 'get-user', { title: 'Get HackerNews User Info', description: 'Get information about a specific HackerNews user', inputSchema: { username: z.string().describe('The username to look up') }, outputSchema: { username: z.string(), about: z.string().optional(), karma: z.number() } }, async ({ username }) => { const endpoint = `/users/${username}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 10: Get posts by author server.registerTool( 'get-posts-by-author', { title: 'Get Posts by Author', description: 'Get all posts (stories, comments, etc.) by a specific author', inputSchema: { username: z.string().describe('The username of the author'), tags: z.string().optional().describe('Filter by type (e.g., "story", "comment")'), page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ username, tags, page, hitsPerPage }) => { const params = new URLSearchParams(); const authorTag = `author_${username}`; const combinedTags = tags ? `${authorTag},${tags}` : authorTag; params.append('tags', combinedTags); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 11: Get comments for a story server.registerTool( 'get-story-comments', { title: 'Get Comments for a Story', description: 'Get all comments for a specific story by story ID', inputSchema: { storyId: z.number().describe('The ID of the story'), page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ storyId, page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('tags', `comment,story_${storyId}`); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 12: Search by URL server.registerTool( 'search-by-url', { title: 'Search Posts by URL', description: 'Search for HackerNews posts that link to a specific URL', inputSchema: { url: z.string().describe('The URL to search for'), page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ url, page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('query', url); params.append('restrictSearchableAttributes', 'url'); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 13: Get polls server.registerTool( 'get-polls', { title: 'Get HackerNews Polls', description: 'Get latest polls posted to HackerNews', inputSchema: { page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('tags', 'poll'); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 14: Advanced search with time range server.registerTool( 'search-by-time-range', { title: 'Search Posts by Time Range', description: 'Search for posts within a specific time range (Unix timestamps)', inputSchema: { query: z.string().optional().describe('Search query text (optional)'), tags: z.string().optional().describe('Filter tags (e.g., "story", "comment")'), startTime: z.number().describe('Start time in Unix timestamp (seconds)'), endTime: z.number().describe('End time in Unix timestamp (seconds)'), page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ query, tags, startTime, endTime, page, hitsPerPage }) => { const params = new URLSearchParams(); if (query) params.append('query', query); if (tags) params.append('tags', tags); params.append('numericFilters', `created_at_i>${startTime},created_at_i<${endTime}`); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Tool 15: Get top stories (high points) server.registerTool( 'get-top-stories', { title: 'Get Top Stories by Points', description: 'Get stories with a minimum number of points, sorted by date', inputSchema: { minPoints: z.number().optional().default(100).describe('Minimum number of points (default: 100)'), page: z.number().optional().describe('Page number for pagination (default: 0)'), hitsPerPage: z.number().optional().describe('Number of results per page (default: 20)') }, outputSchema: { hits: z.array(z.any()), nbHits: z.number(), nbPages: z.number(), page: z.number(), hitsPerPage: z.number() } }, async ({ minPoints = 100, page, hitsPerPage }) => { const params = new URLSearchParams(); params.append('tags', 'story'); params.append('numericFilters', `points>=${minPoints}`); if (page !== undefined) params.append('page', page.toString()); if (hitsPerPage !== undefined) params.append('hitsPerPage', hitsPerPage.toString()); const endpoint = `/search_by_date?${params.toString()}`; const result = await fetchHN(endpoint); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result }; } ); // Connect the server using stdio transport async function main() { const transport = new StdioServerTransport(); await server.connect(transport); // Log to stderr so it doesn't interfere with stdio communication console.error('HackerNews MCP Server running on stdio'); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wei/hn-mcp-server-vibe'

If you have feedback or need assistance with the MCP directory API, please join our Discord server