Skip to main content
Glama

Travel MCP

by Erredebe
fetcthNews.ts5.48 kB
import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; export function registerSearchNewsRss(server: McpServer) { server.tool( "search-news-rss", "Search multiple public RSS feeds (no API key) for headlines matching a query", { q: z.string().describe("Query to search in headlines/description"), limit: z .string() .optional() .describe("Max results to return (default 10)"), sources: z .array(z.string()) .optional() .describe( "Optional list of source keys to query. Defaults to all. Keys: 'bbcmundo','elpais','reuters','cnn','ap','euronews'" ), }, async ({ q, limit, sources }) => { try { // Feeds disponibles (sin API key) const FEEDS: Record<string, string> = { bbcmundo: "https://feeds.bbci.co.uk/mundo/rss.xml", elpais: "https://feeds.elpais.com/mrss-s/pages/ep/site/elpais.com/portada", reuters: "http://feeds.reuters.com/reuters/topNews", cnn: "http://rss.cnn.com/rss/edition.rss", ap: "https://apnews.com/rss", // puede variar por sección euronews: "https://es.euronews.com/rss?level=theme_137341", }; const maxResults = Math.max( 1, Math.min(50, Number.isFinite(Number(limit)) ? Number(limit) : 10) ); const selectedKeys = Array.isArray(sources) && sources.length ? sources.filter((k) => FEEDS[k]) : Object.keys(FEEDS); // Normaliza texto para búsqueda (case-insensitive, sin acentos) const normalize = (s: string) => s .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); const qn = normalize(q); // Pequeño parser de RSS con regex (sin dependencias) function parseItems(xml: string) { const items: { title: string; link: string; description?: string; pubDate?: string; }[] = []; const itemBlocks = Array.from( xml.matchAll(/<item>([\s\S]*?)<\/item>/gi) ); for (const m of itemBlocks) { const block = m[1]; // title (maneja CDATA o texto plano) const t = block.match( /<title><!\[CDATA\[([\s\S]*?)\]\]><\/title>|<title>([\s\S]*?)<\/title>/i ) || []; const title = (t[1] || t[2] || "").trim(); // link const l = block.match(/<link>([\s\S]*?)<\/link>/i); const link = (l?.[1] || "").trim(); // description (opcional) const d = block.match( /<description><!\[CDATA\[([\s\S]*?)\]\]><\/description>|<description>([\s\S]*?)<\/description>/i ) || []; const description = (d[1] || d[2] || "").trim(); // pubDate (opcional) const p = block.match(/<pubDate>([\s\S]*?)<\/pubDate>/i); const pubDate = (p?.[1] || "").trim(); if (title || link) { items.push({ title, link, description, pubDate }); } } return items; } // Fetch con timeout async function fetchWithTimeout(url: string, ms = 8000) { const ctrl = new AbortController(); const id = setTimeout(() => ctrl.abort(), ms); try { const res = await fetch(url, { signal: ctrl.signal }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return await res.text(); } catch (err) { throw err; } finally { clearTimeout(id); } } // Consultar todas las fuentes seleccionadas en paralelo const settled = await Promise.allSettled( selectedKeys.map(async (key) => { const xml = await fetchWithTimeout(FEEDS[key]); const items = parseItems(xml); const filtered = items.filter((it) => { const haystack = normalize( `${it.title} ${it.description ?? ""}`.slice(0, 2000) ); return haystack.includes(qn); }); // Anotar la fuente return filtered.map((it) => ({ source: key, ...it })); }) ); // Aplanar resultados y ordenar por pubDate (si existe) const aggregated: Array<{ source: string; title: string; link: string; description?: string; pubDate?: string; }> = []; for (const s of settled) { if (s.status === "fulfilled") aggregated.push(...s.value); } aggregated.sort((a, b) => { const da = Date.parse(a.pubDate || "") || 0; const db = Date.parse(b.pubDate || "") || 0; return db - da; // más recientes primero }); const results = aggregated.slice(0, maxResults); return { content: [ { type: "text", text: JSON.stringify( { query: q, sources: selectedKeys, total_found: aggregated.length, returned: results.length, results, }, null, 2 ), }, ], }; } catch (err: any) { return { content: [ { type: "text", text: JSON.stringify({ error: err.message }), }, ], }; } } ); }

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/Erredebe/travel-mcp'

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