Skip to main content
Glama

rss

Access Hacker News via simplified commands to fetch latest posts, top stories, best articles, comment history, and more with optional parameters for detailed results.

Instructions

Interfaz principal para Hacker News con comandos simplificados

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
commandYesComando a ejecutar (latest, top, best, history, comments)
paramNoParámetro opcional, número precedido por -- (ejemplo: --10, --50)

Implementation Reference

  • Main handler function for the 'rss' tool within the CallToolRequestSchema handler. Parses command and optional limit parameter, dispatches to RSSAggregator methods based on command (latest, list, specific feed, category), and formats the response using formatItemsResponse.
        if (name === "rss") {
          const command = (typeof args?.command === 'string' ? args.command : '').toLowerCase() || '';
          const param = (typeof args?.param === 'string' ? args?.param : '');
          
          let limit = 10;
          if (param.startsWith('--')) {
            const limitMatch = param.match(/--(\d+)/);
            if (limitMatch && limitMatch[1]) {
              limit = parseInt(limitMatch[1], 10);
              limit = Math.min(Math.max(limit, 1), 50);
            }
          }
          
          if (command === 'latest') {
            const items = await rssAggregator.getAllFeedItems(undefined, limit);
            return formatItemsResponse(items, `Latest ${limit} articles from all feeds`);
          } 
          else if (command === 'top' || command === 'best') {
            const items = await rssAggregator.getAllFeedItems(undefined, limit);
            return formatItemsResponse(items, `Top ${limit} articles from all feeds`);
          }
          else if (command === 'history') {
            const items = await rssAggregator.getAllFeedItems(undefined, limit);
            return formatItemsResponse(items, `Recent history (${limit} articles)`);
          }
          else if (command === 'comments') {
            return {
              content: [
                {
                  type: "text",
                  text: "Comments functionality is currently not supported in this version."
                }
              ]
            };
          }
          else if (command.startsWith('--')) {
            const feedId = command.slice(2);
            try {
              const items = await rssAggregator.getFeedItems(feedId, limit);
              return formatItemsResponse(items, `Latest ${limit} articles from ${items[0]?.source || feedId}`);
            } catch (error) {
              return {
                content: [
                  {
                    type: "text",
                    text: `Error: Feed '${feedId}' not found or couldn't be fetched. Use 'rss list' to see available feeds.`
                  }
                ]
              };
            }
          }
          else if (command === 'list') {
            return {
              content: [
                {
                  type: "text",
                  text: rssAggregator.getFeedsList()
                }
              ]
            };
          }
          else if (command === 'set-feeds-path' && param) {
            try {
              const feedsPath = param.replace(/^--/, '');
              rssAggregator.setFeedsPath(feedsPath);
              return {
                content: [
                  {
                    type: "text",
                    text: `Successfully set feeds path to '${feedsPath}' and loaded feeds.`
                  }
                ]
              };
            } catch (error) {
              return {
                content: [
                  {
                    type: "text",
                    text: `Error setting feeds path: ${error.message}`
                  }
                ]
              };
            }
          }
          else {
            const matchedCategory = rssAggregator.getCategoryByKeyword(command);
            
            if (matchedCategory) {
              console.error(`Matched category "${matchedCategory}" from query "${command}"`);
              const items = await rssAggregator.getAllFeedItems(matchedCategory, limit);
              return formatItemsResponse(items, `Latest ${limit} articles in ${matchedCategory}`);
            }
            
            if (command.includes('news') || command.includes('tech') || 
                command.includes('sport') || command.includes('science') || 
                command.includes('business') || command.includes('health')) {
              console.error(`Using keyword query for "${command}"`);
              const items = await rssAggregator.getAllFeedItems(command, limit);
              return formatItemsResponse(items, `Latest ${limit} articles matching '${command}'`);
            }
    
            const words = command.split(/\s+/);
            if (words.length > 1) {
              for (const word of words) {
                if (word.length < 3) continue;
                
                const matchedKeywordCategory = rssAggregator.getCategoryByKeyword(word);
                if (matchedKeywordCategory) {
                  console.error(`Matched category "${matchedKeywordCategory}" from partial keyword "${word}" in query "${command}"`);
                  const items = await rssAggregator.getAllFeedItems(matchedKeywordCategory, limit);
                  return formatItemsResponse(items, `Latest ${limit} articles in ${matchedKeywordCategory} matching '${command}'`);
                }
              }
            }
            
            return {
              content: [
                {
                  type: "text",
                  text: `Unknown command: '${command}'. Available commands are:
    - latest: Latest articles from all feeds
    - top or best: Top articles from all feeds
    - list: Show all available feeds
    - [category name]: Show latest articles from a specific category
    - --[feed-id]: Show articles from a specific feed (use 'rss list' to see feed IDs)
    - set-feeds-path --[path]: Set the path to your OPML or JSON feeds file
    
    You can specify the number of articles to show with --N parameter (e.g., 'rss latest --20').`
                }
              ]
            };
          }
        }
  • Input schema definition for the 'rss' tool returned in ListTools response, specifying 'command' (required) and optional 'param'.
      type: "object",
      properties: {
        command: {
          type: "string",
          description: "Comando a ejecutar (latest, top, best, history, comments)"
        },
        param: {
          type: "string",
          description: "Parámetro opcional, número precedido por -- (ejemplo: --10, --50)"
        }
      },
      required: ["command"]
    }
  • src/index.ts:387-410 (registration)
    Registration of the 'rss' tool in the ListToolsRequestSchema handler, providing name, description, and inputSchema.
    server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          {
            name: "rss",
            description: "Interfaz principal para Hacker News con comandos simplificados",
            inputSchema: {
              type: "object",
              properties: {
                command: {
                  type: "string",
                  description: "Comando a ejecutar (latest, top, best, history, comments)"
                },
                param: {
                  type: "string",
                  description: "Parámetro opcional, número precedido por -- (ejemplo: --10, --50)"
                }
              },
              required: ["command"]
            }
          }
        ]
      };
    });
  • Core helper class RSSAggregator that manages RSS feeds: loads from OPML/JSON/Claude config, parses feeds, fetches items from specific feeds or all/by category, generates feeds list.
    class RSSAggregator {
      private feeds: Map<string, Feed>;
      private parser: Parser;
      private xmlParser: XMLParser;
      private configPath: string;
      private feedsPath: string | null;
    
      constructor() {
        this.feeds = new Map();
        this.parser = new Parser({
          customFields: {
            item: [
              ['creator', 'creator'],
              ['dc:creator', 'creator']
            ]
          }
        });
        this.xmlParser = new XMLParser({
          ignoreAttributes: false,
          attributeNamePrefix: "@_"
        });
        
        this.configPath = DEFAULT_CONFIG_PATHS[process.platform] || DEFAULT_CONFIG_PATHS.darwin;
        this.feedsPath = null;
        
        this.initializeFeeds();
      }
    
      private initializeFeeds() {
        try {
          this.getFeedsPathFromClaudeConfig();
          
          if (this.feedsPath && fs.existsSync(this.feedsPath)) {
            this.loadFeedsFromFile(this.feedsPath);
          } else {
            this.loadFeedsFromFile(SAMPLE_OPML_PATH);
            console.error('No feeds file found in Claude config. Using sample feeds.');
          }
        } catch (error) {
          console.error('Error initializing feeds:', error);
          this.addDefaultFeeds();
        }
      }
      
      private getFeedsPathFromClaudeConfig() {
        try {
          if (fs.existsSync(this.configPath)) {
            const configContent = fs.readFileSync(this.configPath, 'utf-8');
            const config = JSON.parse(configContent);
            
            if (config.mcpServers?.rssAggregator?.feedsPath) {
              this.feedsPath = config.mcpServers.rssAggregator.feedsPath;
            }
          }
        } catch (error) {
          console.error('Error reading Claude Desktop config:', error);
        }
      }
      
      private loadFeedsFromFile(filePath: string) {
        try {
          const fileContent = fs.readFileSync(filePath, 'utf-8');
          const fileExt = path.extname(filePath).toLowerCase();
          
          if (fileExt === '.opml') {
            this.parseOPMLFeeds(fileContent);
          } else if (fileExt === '.json') {
            this.parseJSONFeeds(fileContent);
          } else {
            throw new Error(`Unsupported file format: ${fileExt}`);
          }
        } catch (error) {
          console.error(`Error loading feeds from ${filePath}:`, error);
          throw error;
        }
      }
      
      private parseOPMLFeeds(opmlContent: string) {
        try {
          const result = this.xmlParser.parse(opmlContent);
          
          if (!result.opml || !result.opml.body || !result.opml.body.outline) {
            throw new Error('Invalid OPML format');
          }
          
          const processOutline = (outline: any, category: string = '') => {
            if (Array.isArray(outline)) {
              outline.forEach(item => processOutline(item, category));
              return;
            }
            
            if (outline['@_xmlUrl']) {
              const feedId = this.createFeedId(outline['@_xmlUrl']);
              this.feeds.set(feedId, {
                title: outline['@_title'] || outline['@_text'] || 'Unnamed Feed',
                url: outline['@_xmlUrl'],
                htmlUrl: outline['@_htmlUrl'],
                category: category
              });
            } 
            else if (outline.outline) {
              const newCategory = outline['@_title'] || outline['@_text'] || category;
              processOutline(outline.outline, newCategory);
            }
          };
          
          processOutline(result.opml.body.outline);
          
          console.error(`Loaded ${this.feeds.size} feeds from OPML file`);
        } catch (error) {
          console.error('Error parsing OPML:', error);
          throw error;
        }
      }
      
      private parseJSONFeeds(jsonContent: string) {
        try {
          const feeds: {title: string, url: string, htmlUrl?: string, category?: string}[] = JSON.parse(jsonContent);
          
          feeds.forEach(feed => {
            const feedId = this.createFeedId(feed.url);
            this.feeds.set(feedId, {
              title: feed.title,
              url: feed.url,
              htmlUrl: feed.htmlUrl,
              category: feed.category
            });
          });
          
          console.error(`Loaded ${this.feeds.size} feeds from JSON file`);
        } catch (error) {
          console.error('Error parsing JSON feeds:', error);
          throw error;
        }
      }
      
      private addDefaultFeeds() {
        this.feeds.clear();
        
        this.feeds.set('hackernews', {
          title: 'Hacker News',
          url: 'https://news.ycombinator.com/rss',
          htmlUrl: 'https://news.ycombinator.com/',
          category: 'Tech News'
        });
        
        this.feeds.set('techcrunch', {
          title: 'TechCrunch',
          url: 'https://techcrunch.com/feed/',
          htmlUrl: 'https://techcrunch.com/',
          category: 'Tech News'
        });
        
        console.error('Added default feeds');
      }
      
      private createFeedId(url: string): string {
        try {
          const domain = new URL(url).hostname
            .replace('www.', '')
            .replace(/\./g, '-');
          
          return domain;
        } catch (e) {
          return url
            .replace(/https?:\/\//g, '')
            .replace(/[^a-zA-Z0-9]/g, '-')
            .toLowerCase();
        }
      }
    
      async getFeedItems(feedId: string, limit: number = 10): Promise<FeedItem[]> {
        const feed = this.feeds.get(feedId);
        if (!feed) {
          throw new Error(`Feed '${feedId}' not found`);
        }
        
        try {
          const parsedFeed = await this.parser.parseURL(feed.url);
          
          return parsedFeed.items.slice(0, limit).map(item => ({
            title: item.title || 'No title',
            link: item.link || '',
            pubDate: item.pubDate || item.isoDate || new Date().toISOString(),
            content: item.content,
            contentSnippet: item.contentSnippet,
            creator: item.creator || parsedFeed.title,
            categories: item.categories,
            isoDate: item.isoDate,
            source: feed.title,
            sourceUrl: feed.htmlUrl || feed.url
          }));
        } catch (error) {
          console.error(`Error fetching feed ${feedId}:`, error);
          throw error;
        }
      }
    
      async getAllFeedItems(category?: string, limit: number = 30): Promise<FeedItem[]> {
        const feedPromises: Promise<FeedItem[]>[] = [];
        const itemsPerFeed = Math.ceil(limit / this.feeds.size);
        
        this.feeds.forEach((feed, id) => {
          if (!category || 
              (feed.category && feed.category.toLowerCase() === category.toLowerCase()) ||
              (feed.category && feed.category.toLowerCase().includes(category.toLowerCase())) ||
              (feed.title && feed.title.toLowerCase().includes(category.toLowerCase()))) {
            feedPromises.push(this.getFeedItems(id, itemsPerFeed));
          }
        });
        
        try {
          const results = await Promise.allSettled(feedPromises);
          
          const allItems = results
            .filter((result): result is PromiseFulfilledResult<FeedItem[]> => result.status === 'fulfilled')
            .flatMap(result => result.value);
          
          allItems.sort((a, b) => {
            const dateA = new Date(a.isoDate || a.pubDate);
            const dateB = new Date(b.isoDate || b.pubDate);
            return dateB.getTime() - dateA.getTime();
          });
          
          return allItems.slice(0, limit);
        } catch (error) {
          console.error('Error fetching all feeds:', error);
          throw error;
        }
      }
      
      setFeedsPath(path: string): void {
        if (!fs.existsSync(path)) {
          throw new Error(`File not found: ${path}`);
        }
        
        this.feedsPath = path;
        this.loadFeedsFromFile(path);
      }
    
      getFeedsList(): string {
        let result = 'Available RSS Feeds:\n\n';
        
        const categorizedFeeds: Record<string, Feed[]> = {};
        
        this.feeds.forEach(feed => {
          const category = feed.category || 'Uncategorized';
          if (!categorizedFeeds[category]) {
            categorizedFeeds[category] = [];
          }
          categorizedFeeds[category].push(feed);
        });
        
        Object.keys(categorizedFeeds).sort().forEach(category => {
          result += `${category}:\n`;
          
          categorizedFeeds[category].sort((a, b) => a.title.localeCompare(b.title)).forEach(feed => {
            const feedId = this.createFeedId(feed.url);
            result += `- ${feed.title} (use: rss --${feedId})\n`;
          });
          
          result += '\n';
        });
        
        return result;
      }
      
      getCategories(): string[] {
        const categories = new Set<string>();
        
        this.feeds.forEach(feed => {
          if (feed.category) {
            categories.add(feed.category);
          }
        });
        
        return Array.from(categories).sort();
      }
      
      getCategoryByKeyword(keyword: string): string | null {
        const categories = this.getCategories();
        const lowercaseKeyword = keyword.toLowerCase();
        
        const exactMatch = categories.find(c => c.toLowerCase() === lowercaseKeyword);
        if (exactMatch) return exactMatch;
        
        const partialMatch = categories.find(c => 
          c.toLowerCase().includes(lowercaseKeyword) || 
          lowercaseKeyword.includes(c.toLowerCase().split(' ')[0]));
        if (partialMatch) return partialMatch;
        
        const keywordMap: Record<string, string[]> = {
          'tech': ['tech', 'technology', 'programming', 'software', 'developer', 'ai'],
          'news': ['news', 'headlines', 'current'],
          'business': ['business', 'finance', 'economy', 'market'],
          'health': ['health', 'medical', 'wellness', 'fitness'],
          'science': ['science', 'research', 'study', 'discovery'],
          'sports': ['sports', 'game', 'team', 'player']
        };
        
        for (const [category, keywords] of Object.entries(keywordMap)) {
          if (keywords.some(k => lowercaseKeyword.includes(k))) {
            const categoryMatch = categories.find(c => 
              c.toLowerCase().includes(category) || 
              c.toLowerCase() === category);
            if (categoryMatch) return categoryMatch;
          }
        }
        
        return null;
      }
    }
  • Helper function to format the list of FeedItem objects into a response, preferring an external RSS formatter service, with fallback to simple markdown.
    async function formatItemsResponse(items: FeedItem[], title: string) {
      if (items.length === 0) {
        return {
          content: [
            {
              type: "text",
              text: "No articles found."
            }
          ]
        };
      }
      
      try {
        const formatterUrl = "https://rss-formatter.vercel.app/format";
        
        const response = await axios.post(formatterUrl, {
          items,
          title
        });
        
        const claudePrompt = `
    <rss_feed>
    ${response.data.formattedResponse}
    </rss_feed>
    
    The text inside the <rss_feed> tags above contains RSS news items with their complete details. When displaying this RSS feed content to the user:
    
    1. DO NOT summarize or reword the articles
    2. MAINTAIN all links exactly as they appear
    3. PRESERVE all article metadata (dates, authors, sources, category)
    4. KEEP the formatting of each article exactly as shown
    
    Present the feed content as I've formatted it above, without modification. If the user asks for more details about a specific article, point them to the article link provided.
    `;
        
        return {
          content: [
            {
              type: "text",
              text: claudePrompt
            }
          ]
        };
      } catch (error) {
        console.error('Error calling formatter service:', error)
        let text = `# ${title}\n\n`;
        
        items.forEach((item, index) => {
          text += `${index + 1}. **${item.title}** (${item.source})\n`;
          text += `Link: ${item.link}\n\n`;
        });
        
        return {
          content: [
            {
              type: "text",
              text: text
            }
          ]
        };
      }
    }
Behavior2/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions 'interfaz principal' (main interface) and 'comandos simplificados' (simplified commands), implying it's a read-only tool for accessing Hacker News data, but it doesn't specify details like rate limits, authentication needs, error handling, or output format. This leaves significant gaps in understanding how the tool behaves beyond basic functionality.

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

Conciseness4/5

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

The description is a single, concise sentence in Spanish that efficiently conveys the tool's general purpose. It's front-loaded with the main idea and avoids unnecessary details. However, it could be slightly more structured by explicitly stating the tool's actions, but overall, it's appropriately sized with no wasted words.

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 the complexity of a tool with 2 parameters and no output schema or annotations, the description is incomplete. It lacks information on what the tool returns (e.g., list of posts, comments), error conditions, or behavioral traits. While the schema covers parameters well, the overall context for effective use is insufficient, especially for a tool that likely involves data retrieval from an external source like Hacker News.

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 schema description coverage is 100%, with clear descriptions for both parameters ('command' and 'param'). The description adds minimal value beyond the schema, as it doesn't explain parameter meanings or usage further. Since the schema adequately documents the parameters, a baseline score of 3 is appropriate, reflecting that the description doesn't compensate but also doesn't detract from the schema's information.

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

Purpose3/5

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

The description states 'Interfaz principal para Hacker News con comandos simplificados' which translates to 'Main interface for Hacker News with simplified commands.' This indicates the tool provides access to Hacker News content through commands, giving a general purpose. However, it's somewhat vague about what specific actions it performs (e.g., fetching posts, comments) and doesn't specify verbs like 'fetch' or 'retrieve,' making it less precise than ideal.

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. Since there are no sibling tools mentioned, this might be acceptable, but it still lacks context such as prerequisites, use cases, or limitations. Without any usage instructions, users must infer from the command list in the schema, which is insufficient for clear decision-making.

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

Related 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/imprvhub/mcp-rss-aggregator'

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