Skip to main content
Glama
paabloLC

MCP Hacker News

by paabloLC

getUser

Retrieve Hacker News user profile details including karma, account age, and bio by providing a username.

Instructions

Get user profile information

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
usernameYesThe username to look up

Implementation Reference

  • The main handler (execute function) for the 'getUser' tool. It fetches the user profile from the Hacker News API using fetchFromAPI, formats key fields like creation time with formatTime, computes submission stats, and returns a structured JSON response or error if user not found.
    execute: async (args: any) => {
      const user = await fetchFromAPI<HackerNewsUser>(`/user/${args.username}`);
    
      if (!user) {
        return {
          content: [
            { type: "text", text: JSON.stringify({ error: "User not found" }) },
          ],
        };
      }
    
      const formattedUser = {
        id: user.id,
        karma: user.karma,
        created: user.created ? formatTime(user.created) : "unknown",
        about: user.about,
        submittedCount: user.submitted?.length || 0,
        recentSubmissions: user.submitted?.slice(0, 10) || [],
      };
    
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              {
                message: `User profile for ${args.username}`,
                user: formattedUser,
              },
              null,
              2
            ),
          },
        ],
      };
    },
  • The input schema for the 'getUser' tool, defining a required 'username' string parameter.
    inputSchema: {
      type: "object",
      properties: {
        username: {
          type: "string",
          description: "The username to look up",
        },
      },
      required: ["username"],
    },
  • The fetchFromAPI helper function used by getUser to retrieve user data from Hacker News Firebase API endpoint /user/{username}.json, with caching support.
    export async function fetchFromAPI<T>(endpoint: string): Promise<T | null> {
      const cacheKey = endpoint;
      const cached = getCached<T>(cacheKey);
      if (cached) return cached;
    
      try {
        const response = await fetch(
          `https://hacker-news.firebaseio.com/v0${endpoint}.json`
        );
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
    
        const data = await response.json();
        setCache(cacheKey, data);
        return data;
      } catch (error) {
        console.error(`Error fetching ${endpoint}:`, error);
        return null;
      }
    }
  • The formatTime helper used to format the user's creation timestamp into a human-readable relative time string (e.g., '2d ago').
    // Function to format timestamp to a readable format
    export function formatTime(timestamp: number): string {
      const date = new Date(timestamp * 1000);
      const now = new Date();
      const diff = now.getTime() - date.getTime();
    
      const minutes = Math.floor(diff / (1000 * 60));
      const hours = Math.floor(diff / (1000 * 60 * 60));
      const days = Math.floor(diff / (1000 * 60 * 60 * 24));
    
      if (minutes < 60) return `${minutes}m ago`;
      if (hours < 24) return `${hours}h ago`;
      return `${days}d ago`;
    }
  • src/tools.ts:13-588 (registration)
    The 'getUser' tool is registered by being included in the exported 'tools' array, which is imported and used in src/index.ts for MCP tools/list and tools/call handling.
    export const tools = [
      {
        name: "getTopStories",
        description: "Get top stories from Hacker News (up to 500 available)",
        inputSchema: {
          type: "object",
          properties: {
            limit: {
              type: "number",
              description: "Number of stories to return (default: 10, max: 50)",
              default: 10,
            },
          },
        },
        execute: async (args: any) => {
          const limit = Math.min(args.limit || 10, 50);
          const topIds = await fetchFromAPI<number[]>("/topstories");
    
          if (!topIds) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ error: "Failed to fetch top stories" }),
                },
              ],
            };
          }
    
          const stories = await fetchMultipleItems(topIds, limit);
          const formattedStories = stories.map((story) => ({
            id: story.id,
            title: story.title,
            url: story.url,
            score: story.score,
            author: story.by,
            comments: story.descendants || 0,
            time: story.time ? formatTime(story.time) : "unknown",
            hnUrl: `https://news.ycombinator.com/item?id=${story.id}`,
          }));
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `Top ${limit} stories from Hacker News`,
                    stories: formattedStories,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getBestStories",
        description: "Get best stories from Hacker News (algorithmically ranked)",
        inputSchema: {
          type: "object",
          properties: {
            limit: {
              type: "number",
              description: "Number of stories to return (default: 10, max: 50)",
              default: 10,
            },
          },
        },
        execute: async (args: any) => {
          const limit = Math.min(args.limit || 10, 50);
          const bestIds = await fetchFromAPI<number[]>("/beststories");
    
          if (!bestIds) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ error: "Failed to fetch best stories" }),
                },
              ],
            };
          }
    
          const stories = await fetchMultipleItems(bestIds, limit);
          const formattedStories = stories.map((story) => ({
            id: story.id,
            title: story.title,
            url: story.url,
            score: story.score,
            author: story.by,
            comments: story.descendants || 0,
            time: story.time ? formatTime(story.time) : "unknown",
            hnUrl: `https://news.ycombinator.com/item?id=${story.id}`,
          }));
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `Best ${limit} stories from Hacker News`,
                    stories: formattedStories,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getNewStories",
        description: "Get newest stories from Hacker News",
        inputSchema: {
          type: "object",
          properties: {
            limit: {
              type: "number",
              description: "Number of stories to return (default: 10, max: 50)",
              default: 10,
            },
          },
        },
        execute: async (args: any) => {
          const limit = Math.min(args.limit || 10, 50);
          const newIds = await fetchFromAPI<number[]>("/newstories");
    
          if (!newIds) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ error: "Failed to fetch new stories" }),
                },
              ],
            };
          }
    
          const stories = await fetchMultipleItems(newIds, limit);
          const formattedStories = stories.map((story) => ({
            id: story.id,
            title: story.title,
            url: story.url,
            score: story.score,
            author: story.by,
            comments: story.descendants || 0,
            time: story.time ? formatTime(story.time) : "unknown",
            hnUrl: `https://news.ycombinator.com/item?id=${story.id}`,
          }));
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `Latest ${limit} stories from Hacker News`,
                    stories: formattedStories,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getAskHNStories",
        description: "Get Ask HN stories",
        inputSchema: {
          type: "object",
          properties: {
            limit: {
              type: "number",
              description: "Number of stories to return (default: 10, max: 30)",
              default: 10,
            },
          },
        },
        execute: async (args: any) => {
          const limit = Math.min(args.limit || 10, 30);
          const askIds = await fetchFromAPI<number[]>("/askstories");
    
          if (!askIds) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ error: "Failed to fetch Ask HN stories" }),
                },
              ],
            };
          }
    
          const stories = await fetchMultipleItems(askIds, limit);
          const formattedStories = stories.map((story) => ({
            id: story.id,
            title: story.title,
            score: story.score,
            author: story.by,
            comments: story.descendants || 0,
            time: story.time ? formatTime(story.time) : "unknown",
            hnUrl: `https://news.ycombinator.com/item?id=${story.id}`,
            text: story.text,
          }));
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `Latest ${limit} Ask HN stories`,
                    stories: formattedStories,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getShowHNStories",
        description: "Get Show HN stories",
        inputSchema: {
          type: "object",
          properties: {
            limit: {
              type: "number",
              description: "Number of stories to return (default: 10, max: 30)",
              default: 10,
            },
          },
        },
        execute: async (args: any) => {
          const limit = Math.min(args.limit || 10, 30);
          const showIds = await fetchFromAPI<number[]>("/showstories");
    
          if (!showIds) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({
                    error: "Failed to fetch Show HN stories",
                  }),
                },
              ],
            };
          }
    
          const stories = await fetchMultipleItems(showIds, limit);
          const formattedStories = stories.map((story) => ({
            id: story.id,
            title: story.title,
            url: story.url,
            score: story.score,
            author: story.by,
            comments: story.descendants || 0,
            time: story.time ? formatTime(story.time) : "unknown",
            hnUrl: `https://news.ycombinator.com/item?id=${story.id}`,
          }));
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `Latest ${limit} Show HN stories`,
                    stories: formattedStories,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getJobStories",
        description: "Get job postings from Hacker News",
        inputSchema: {
          type: "object",
          properties: {
            limit: {
              type: "number",
              description: "Number of jobs to return (default: 10, max: 30)",
              default: 10,
            },
          },
        },
        execute: async (args: any) => {
          const limit = Math.min(args.limit || 10, 30);
          const jobIds = await fetchFromAPI<number[]>("/jobstories");
    
          if (!jobIds) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ error: "Failed to fetch job stories" }),
                },
              ],
            };
          }
    
          const jobs = await fetchMultipleItems(jobIds, limit);
          const formattedJobs = jobs.map((job) => ({
            id: job.id,
            title: job.title,
            url: job.url,
            score: job.score,
            author: job.by,
            time: job.time ? formatTime(job.time) : "unknown",
            hnUrl: `https://news.ycombinator.com/item?id=${job.id}`,
            text: job.text,
          }));
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `Latest ${limit} job postings`,
                    jobs: formattedJobs,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getItem",
        description: "Get a specific item (story, comment, job, etc.) by ID",
        inputSchema: {
          type: "object",
          properties: {
            id: {
              type: "number",
              description: "The item ID to fetch",
            },
          },
          required: ["id"],
        },
        execute: async (args: any) => {
          const item = await fetchFromAPI<HackerNewsItem>(`/item/${args.id}`);
    
          if (!item) {
            return {
              content: [
                { type: "text", text: JSON.stringify({ error: "Item not found" }) },
              ],
            };
          }
    
          const formattedItem = {
            id: item.id,
            type: item.type,
            title: item.title,
            url: item.url,
            score: item.score,
            author: item.by,
            time: item.time ? formatTime(item.time) : "unknown",
            comments: item.descendants || 0,
            text: item.text,
            parent: item.parent,
            kids: item.kids,
            hnUrl: `https://news.ycombinator.com/item?id=${item.id}`,
          };
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `Item ${args.id}`,
                    item: formattedItem,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getUser",
        description: "Get user profile information",
        inputSchema: {
          type: "object",
          properties: {
            username: {
              type: "string",
              description: "The username to look up",
            },
          },
          required: ["username"],
        },
        execute: async (args: any) => {
          const user = await fetchFromAPI<HackerNewsUser>(`/user/${args.username}`);
    
          if (!user) {
            return {
              content: [
                { type: "text", text: JSON.stringify({ error: "User not found" }) },
              ],
            };
          }
    
          const formattedUser = {
            id: user.id,
            karma: user.karma,
            created: user.created ? formatTime(user.created) : "unknown",
            about: user.about,
            submittedCount: user.submitted?.length || 0,
            recentSubmissions: user.submitted?.slice(0, 10) || [],
          };
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `User profile for ${args.username}`,
                    user: formattedUser,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getComments",
        description: "Get comments for a specific item",
        inputSchema: {
          type: "object",
          properties: {
            id: {
              type: "number",
              description: "The item ID to fetch comments for",
            },
            depth: {
              type: "number",
              description: "Maximum depth of comments to fetch (default: 2)",
              default: 2,
            },
          },
          required: ["id"],
        },
        execute: async (args: any) => {
          const item = await fetchFromAPI<HackerNewsItem>(`/item/${args.id}`);
    
          if (!item || !item.kids) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ error: "No comments found" }),
                },
              ],
            };
          }
    
          const depth = Math.min(args.depth || 2, 3);
          const comments = await fetchMultipleItems(item.kids, 20);
    
          const formattedComments = comments.map((comment) => ({
            id: comment.id,
            author: comment.by,
            time: comment.time ? formatTime(comment.time) : "unknown",
            text: comment.text,
            parent: comment.parent,
            kids: comment.kids?.length || 0,
            hnUrl: `https://news.ycombinator.com/item?id=${comment.id}`,
          }));
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: `Comments for item ${args.id}`,
                    totalComments: item.descendants || 0,
                    topLevelComments: formattedComments.length,
                    comments: formattedComments,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getMaxItemId",
        description: "Get the current maximum item ID",
        inputSchema: { type: "object", properties: {} },
        execute: async (args: any) => {
          const maxId = await fetchFromAPI<number>("/maxitem");
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: "Current maximum item ID",
                    maxItemId: maxId,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
      {
        name: "getUpdates",
        description: "Get recently updated items and profiles",
        inputSchema: { type: "object", properties: {} },
        execute: async (args: any) => {
          const updates = await fetchFromAPI<HackerNewsUpdates>("/updates");
    
          if (!updates) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ error: "Failed to fetch updates" }),
                },
              ],
            };
          }
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    message: "Recent updates",
                    recentlyUpdatedItems: updates.items.slice(0, 10),
                    recentlyUpdatedProfiles: updates.profiles.slice(0, 10),
                  },
                  null,
                  2
                ),
              },
            ],
          };
        },
      },
    ];
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It states it 'gets' information (implying a read operation) but doesn't mention authentication needs, rate limits, error conditions, or what specific profile information is returned. This leaves significant behavioral gaps for a tool with no annotation coverage.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise with a single, clear sentence that states the core purpose without any wasted words. It's perfectly front-loaded and earns its place efficiently.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations and no output schema, the description is incomplete for a tool that retrieves data. It doesn't explain what 'user profile information' includes, how results are formatted, or any behavioral constraints. For a data retrieval tool with zero structured context, this leaves too many gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage, with the single parameter 'username' clearly documented. The description doesn't add any parameter semantics beyond what the schema provides (like format examples or constraints), so it meets the baseline for high schema coverage without adding value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('Get') and resource ('user profile information'), making it immediately understandable. However, it doesn't differentiate this tool from its siblings (like 'getItem' which might also retrieve user-related data), which prevents a perfect score.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites, context, or how it differs from sibling tools like 'getItem' or 'getComments' that might also retrieve user data, leaving the agent without usage direction.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/paabloLC/mcp-hacker-news'

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