UNPKG

topic-scout-mcp

Version:

MCP Server para buscar notícias e identificar tendências sobre tópicos específicos

378 lines (316 loc) 11.1 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; interface NewsArticle { source: { id: string | null; name: string; }; author: string | null; title: string; description: string | null; url: string; urlToImage: string | null; publishedAt: string; content: string | null; } interface NewsSearchResult { status: string; totalResults: number; articles: NewsArticle[]; } export class TopicScoutMCPServer { private server: Server; private newsApiKey: string; private serverName: string; constructor(serverName?: string) { this.serverName = serverName || "topic-scout-mcp"; this.server = new Server({ name: this.serverName, version: "1.0.0", }); this.newsApiKey = process.env.NEWS_API_KEY || ""; if (!this.newsApiKey) { console.error("⚠️ NEWS_API_KEY não encontrada no ambiente"); } this.setupToolHandlers(); } private setupToolHandlers() { // Listar ferramentas disponíveis this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "search_news_articles", description: "Busca artigos de notícias sobre um tópico específico", inputSchema: { type: "object", properties: { topic: { type: "string", description: "O tópico principal para buscar notícias (ex: 'Real Estate', 'Technology', 'Sports')", }, timeframe: { type: "string", description: "Período de busca: '7d', '30d', '90d' (padrão: '7d')", default: "7d", }, max_articles: { type: "number", description: "Número máximo de artigos para buscar (padrão: 50)", default: 50, }, language: { type: "string", description: "Idioma das notícias: 'en', 'pt' (padrão: 'en')", default: "en", }, }, required: ["topic"], }, }, { name: "get_news_sources", description: "Lista as principais fontes de notícias disponíveis", inputSchema: { type: "object", properties: { category: { type: "string", description: "Categoria de fontes (ex: 'business', 'technology', 'general')", }, language: { type: "string", description: "Idioma das fontes (padrão: 'en')", default: "en", }, }, }, }, ], }; }); // Executar ferramentas this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === "search_news_articles") { return await this.searchNewsArticles(args); } if (name === "get_news_sources") { return await this.getNewsSources(args); } throw new Error(`Ferramenta desconhecida: ${name}`); }); } private async searchNewsArticles(args: any): Promise<any> { try { const { topic, timeframe = "7d", max_articles = 50, language = "en" } = args; if (!topic) { return { content: [ { type: "text", text: "❌ Erro: O parâmetro 'topic' é obrigatório", }, ], }; } console.error(`🔍 Buscando notícias sobre: ${topic}`); // Calcular data de início baseada no timeframe const startDate = this.calculateStartDate(timeframe); // Buscar notícias sobre o tópico const news = await this.searchNews(topic, startDate, max_articles, language); if (!news.articles || news.articles.length === 0) { return { content: [ { type: "text", text: `❌ Nenhuma notícia encontrada sobre "${topic}" nos últimos ${timeframe}. Tente um tópico diferente ou verifique a ortografia.`, }, ], }; } // Formatar resposta com dados brutos para a AI analisar const response = this.formatNewsResponse(topic, news, timeframe); return { content: [ { type: "text", text: response, }, ], }; } catch (error) { console.error("❌ Erro na busca:", error); return { content: [ { type: "text", text: `❌ Erro ao buscar notícias: ${error instanceof Error ? error.message : "Erro desconhecido"}`, }, ], }; } } private async getNewsSources(args: any): Promise<any> { try { const { category, language = "en" } = args; console.error(`📰 Buscando fontes de notícias...`); const sources = await this.fetchNewsSources(category, language); if (!sources.sources || sources.sources.length === 0) { return { content: [ { type: "text", text: "❌ Nenhuma fonte de notícias encontrada.", }, ], }; } const response = this.formatSourcesResponse(sources, category); return { content: [ { type: "text", text: response, }, ], }; } catch (error) { console.error("❌ Erro ao buscar fontes:", error); return { content: [ { type: "text", text: `❌ Erro ao buscar fontes: ${error instanceof Error ? error.message : "Erro desconhecido"}`, }, ], }; } } private calculateStartDate(timeframe: string): string { const now = new Date(); let daysAgo: number; switch (timeframe) { case "7d": daysAgo = 7; break; case "30d": daysAgo = 30; break; case "90d": daysAgo = 90; break; default: daysAgo = 7; } const startDate = new Date(now.getTime() - (daysAgo * 24 * 60 * 60 * 1000)); return startDate.toISOString().split('T')[0]; // Formato YYYY-MM-DD } private async searchNews(query: string, startDate: string, maxResults: number, language: string): Promise<NewsSearchResult> { const url = `https://newsapi.org/v2/everything`; const params = new URLSearchParams({ q: query, from: startDate, sortBy: "publishedAt", pageSize: maxResults.toString(), language: language, apiKey: this.newsApiKey, }); const response = await fetch(`${url}?${params}`, { headers: { "Content-Type": "application/json", }, }); if (!response.ok) { throw new Error(`Erro na API de Notícias: ${response.status} ${response.statusText}`); } return await response.json(); } private async fetchNewsSources(category?: string, language: string = "en"): Promise<any> { const url = `https://newsapi.org/v2/sources`; const params = new URLSearchParams({ language: language, apiKey: this.newsApiKey, }); if (category) { params.append("category", category); } const response = await fetch(`${url}?${params}`, { headers: { "Content-Type": "application/json", }, }); if (!response.ok) { throw new Error(`Erro na API de Fontes: ${response.status} ${response.statusText}`); } return await response.json(); } private formatNewsResponse(topic: string, news: NewsSearchResult, timeframe: string): string { let response = `📰 **NOTÍCIAS SOBRE "${topic.toUpperCase()}"** 📰\n\n`; response += `📊 Encontrados ${news.totalResults} artigos nos últimos ${timeframe}\n`; response += `📋 Analisando ${news.articles.length} artigos mais relevantes\n\n`; response += `📑 **ARTIGOS ENCONTRADOS:**\n\n`; for (let i = 0; i < Math.min(10, news.articles.length); i++) { const article = news.articles[i]; const date = new Date(article.publishedAt).toLocaleDateString('pt-BR'); response += `${i + 1}. **${article.title}**\n`; response += ` 📰 Fonte: ${article.source.name}\n`; response += ` 📅 Data: ${date}\n`; if (article.description) { response += ` 📝 Resumo: ${article.description.substring(0, 150)}...\n`; } response += ` 🔗 URL: ${article.url}\n\n`; } response += `💡 **DADOS PARA ANÁLISE:**\n`; response += `• Total de artigos: ${news.articles.length}\n`; response += `• Período analisado: ${timeframe}\n`; response += `• Fontes principais: ${this.getTopSources(news.articles)}\n\n`; response += `🔍 **PRÓXIMOS PASSOS:**\n`; response += `• Analise os títulos e resumos para identificar temas recorrentes\n`; response += `• Identifique as fontes mais ativas sobre o tópico\n`; response += `• Compare com outros tópicos relacionados se necessário\n`; return response; } private formatSourcesResponse(sources: any, category?: string): string { let response = `📰 **FONTES DE NOTÍCIAS DISPONÍVEIS** 📰\n\n`; if (category) { response += `📂 Categoria: ${category}\n`; } response += `📊 Total de fontes: ${sources.sources.length}\n\n`; response += `🏢 **PRINCIPAIS FONTES:**\n\n`; for (let i = 0; i < Math.min(15, sources.sources.length); i++) { const source = sources.sources[i]; response += `${i + 1}. **${source.name}**\n`; response += ` 🌐 País: ${source.country}\n`; response += ` 📂 Categoria: ${source.category}\n`; response += ` 🔗 URL: ${source.url}\n\n`; } return response; } private getTopSources(articles: NewsArticle[]): string { const sourceCount = new Map<string, number>(); for (const article of articles) { const sourceName = article.source.name; sourceCount.set(sourceName, (sourceCount.get(sourceName) || 0) + 1); } const sortedSources = Array.from(sourceCount.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([name, count]) => `${name} (${count})`); return sortedSources.join(", "); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`🚀 ${this.serverName} MCP Server iniciado`); } } // Executar o servidor apenas se for chamado diretamente if (import.meta.url === `file://${process.argv[1]}`) { const server = new TopicScoutMCPServer(); server.run().catch(console.error); }