Skip to main content
Glama
server.pyโ€ข6.92 kB
#!/usr/bin/env python3 """ Hacker News MCP Server A FastMCP server that provides access to Hacker News data. """ import asyncio import json import os from typing import Any, Dict, List, Optional import httpx from fastmcp import FastMCP # Create the FastMCP server mcp = FastMCP("Hacker News MCP Server") # Hacker News API base URL HN_API_BASE = "https://hacker-news.firebaseio.com/v0" @mcp.tool() def get_top_stories(limit: int = 10) -> str: """Get top stories from Hacker News. Args: limit: Number of stories to fetch (default: 10, max: 30) Returns: JSON string with top stories data """ try: limit = min(max(limit, 1), 30) # Clamp between 1 and 30 # Fetch top story IDs response = httpx.get(f"{HN_API_BASE}/topstories.json", timeout=10.0) response.raise_for_status() story_ids = response.json()[:limit] # Fetch story details stories = [] for story_id in story_ids: story_response = httpx.get(f"{HN_API_BASE}/item/{story_id}.json", timeout=5.0) story_response.raise_for_status() story = story_response.json() if story and story.get('type') == 'story': stories.append({ 'id': story.get('id'), 'title': story.get('title', ''), 'url': story.get('url', f"https://news.ycombinator.com/item?id={story_id}"), 'score': story.get('score', 0), 'author': story.get('by', ''), 'comments': story.get('descendants', 0), 'time': story.get('time', 0), 'time_iso': story.get('time') and str(story.get('time')) or None }) return json.dumps(stories, indent=2) except httpx.RequestError as e: return json.dumps({"error": f"Request failed: {str(e)}"}, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) @mcp.tool() def get_new_stories(limit: int = 10) -> str: """Get newest stories from Hacker News. Args: limit: Number of stories to fetch (default: 10, max: 30) Returns: JSON string with newest stories data """ try: limit = min(max(limit, 1), 30) # Clamp between 1 and 30 # Fetch new story IDs response = httpx.get(f"{HN_API_BASE}/newstories.json", timeout=10.0) response.raise_for_status() story_ids = response.json()[:limit] # Fetch story details stories = [] for story_id in story_ids: story_response = httpx.get(f"{HN_API_BASE}/item/{story_id}.json", timeout=5.0) story_response.raise_for_status() story = story_response.json() if story and story.get('type') == 'story': stories.append({ 'id': story.get('id'), 'title': story.get('title', ''), 'url': story.get('url', f"https://news.ycombinator.com/item?id={story_id}"), 'score': story.get('score', 0), 'author': story.get('by', ''), 'time': story.get('time', 0), 'time_iso': story.get('time') and str(story.get('time')) or None }) return json.dumps(stories, indent=2) except httpx.RequestError as e: return json.dumps({"error": f"Request failed: {str(e)}"}, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) @mcp.tool() def get_story(story_id: int) -> str: """Get details of a specific Hacker News story by ID. Args: story_id: The ID of the story to fetch Returns: JSON string with story details """ try: response = httpx.get(f"{HN_API_BASE}/item/{story_id}.json", timeout=10.0) response.raise_for_status() story = response.json() if not story: return json.dumps({"error": f"Story {story_id} not found"}, indent=2) if story.get('type') != 'story': return json.dumps({"error": f"Item {story_id} is not a story"}, indent=2) formatted_story = { 'id': story.get('id'), 'title': story.get('title', ''), 'url': story.get('url', f"https://news.ycombinator.com/item?id={story_id}"), 'score': story.get('score', 0), 'author': story.get('by', ''), 'comments': story.get('descendants', 0), 'time': story.get('time', 0), 'text': story.get('text', ''), 'kids': story.get('kids', []) # Comment IDs } return json.dumps(formatted_story, indent=2) except httpx.RequestError as e: return json.dumps({"error": f"Request failed: {str(e)}"}, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) @mcp.tool() def search_stories(query: str, limit: int = 10) -> str: """Search for stories on Hacker News (using Algolia API). Args: query: Search query string limit: Number of results to return (default: 10, max: 20) Returns: JSON string with search results """ try: limit = min(max(limit, 1), 20) # Clamp between 1 and 20 # Use Algolia search API search_url = "https://hn.algolia.com/api/v1/search" params = { "query": query, "tags": "story", "hitsPerPage": limit } response = httpx.get(search_url, params=params, timeout=10.0) response.raise_for_status() data = response.json() stories = [] for hit in data.get('hits', []): stories.append({ 'id': hit.get('objectID'), 'title': hit.get('title', ''), 'url': hit.get('url', f"https://news.ycombinator.com/item?id={hit.get('objectID')}"), 'score': hit.get('points', 0), 'author': hit.get('author', ''), 'comments': hit.get('num_comments', 0), 'time': hit.get('created_at_i', 0), 'time_iso': hit.get('created_at', '') }) return json.dumps(stories, indent=2) except httpx.RequestError as e: return json.dumps({"error": f"Request failed: {str(e)}"}, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) if __name__ == "__main__": port = int(os.environ.get("PORT", 8000)) mcp.run( transport="http", host="0.0.0.0", port=port, stateless_http=True )

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/akarnik23/mcp-hackernews'

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