mcp-hn

MIT License
6
  • Apple
import mcp_hn.hn as hn from mcp.server.models import InitializationOptions import mcp.types as types from mcp.server import NotificationOptions, Server import mcp.server.stdio import json DEFAULT_NUM_STORIES = 10 server = Server("hn") @server.list_tools() async def handle_list_tools() -> list[types.Tool]: """ List available tools. Each tool specifies its arguments using JSON Schema validation. """ return [ types.Tool( name="get_stories", description="Get stories from Hacker News. The options are `top`, `new`, `ask_hn`, `show_hn` for types of stories. This doesn't include the comments. Use `get_story_info` to get the comments.", inputSchema={ "type": "object", "properties": { "story_type": { "type": "string", "description": "Type of stories to get, one of: `top`, `new`, `ask_hn`, `show_hn`", }, "num_stories": { "type": "integer", "description": "Number of stories to get", }, }, }, ), types.Tool( name="get_user_info", description="Get user info from Hacker News, including the stories they've submitted", inputSchema={ "type": "object", "properties": { "user_name": { "type": "string", "description": "Username of the user", }, "num_stories": { "type": "integer", "description": f"Number of stories to get, defaults to {DEFAULT_NUM_STORIES}", }, }, "required": ["user_name"], }, ), types.Tool( name="search_stories", description="Search stories from Hacker News. It is generally recommended to use simpler queries to get a broader set of results (less than 5 words). Very targetted queries may not return any results.", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "Search query", }, "search_by_date": { "type": "boolean", "description": "Search by date, defaults to False. If this is False, then we search by relevance, then points, then number of comments.", }, "num_results": { "type": "integer", "description": f"Number of results to get, defaults to {DEFAULT_NUM_STORIES}", }, }, "required": ["query"], }, ), types.Tool( name="get_story_info", description="Get detailed story info from Hacker News, including the comments", inputSchema={ "type": "object", "properties": { "story_id": { "type": "integer", "description": "Story ID", }, }, }, ), ] @server.call_tool() async def handle_call_tool( name: str, arguments: dict | None ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: """ Handle tool execution requests. """ if name == "get_stories": story_type = arguments.get("story_type", "top") num_stories = arguments.get("num_stories", DEFAULT_NUM_STORIES) output = hn.get_stories(story_type, num_stories) return [types.TextContent(type="text", text=json.dumps(output, indent=2))] elif name == "search_stories": query = arguments.get("query") search_by_date = arguments.get("search_by_date", False) num_results = arguments.get("num_results", DEFAULT_NUM_STORIES) output = json.dumps(hn.search_stories(query, num_results, search_by_date), indent=2) return [types.TextContent(type="text", text=output)] elif name == "get_story_info": story_id = int(arguments.get("story_id")) output = json.dumps(hn.get_story_info(story_id), indent=2) return [types.TextContent(type="text", text=output)] elif name == "get_user_info": user_name = arguments.get("user_name") num_stories = arguments.get("num_stories", DEFAULT_NUM_STORIES) output = json.dumps(hn.get_user_info(user_name, num_stories), indent=2) return [types.TextContent(type="text", text=output)] else: raise ValueError(f"Unknown tool: {name}") async def main(): # Run the server using stdin/stdout streams async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationOptions( server_name="hn", server_version="0.1.0", capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), )