index.ts•11.7 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import { load } from 'cheerio';
import { writeFileSync, mkdirSync } from 'node:fs';
import path from 'path';
import {gerarResumoIA } from './ai/generate';
// Interfaces
interface FormatarDataOptions {
day: '2-digit';
month: 'short';
year: 'numeric';
}
interface FichaLeitura {
url: string;
titulo: string;
autor: string;
imagens: { src: string; legenda: string }[];
resumo: string;
citacao?: string;
}
interface ResultadoBusca {
titulo: string;
url: string;
}
interface ConteudoPagina {
url: string;
titulo: string;
conteudo: string;
imagens: { src: string; legenda: string }[];
autor: string;
citacao: string;
erro?: boolean;
}
// Utilitários
function formatarData(date: Date): string {
const options: FormatarDataOptions = {
day: '2-digit',
month: 'short',
year: 'numeric'
};
const formattedDate = date.toLocaleDateString('pt-BR', options).replace('.', '.');
return `${formattedDate}`;
}
// Funções de scraping
async function rasparTodasPaginasBusca(query: string, todasPaginas: boolean = false): Promise<ResultadoBusca[]> {
let pagina = 1;
let resultados: ResultadoBusca[] = [];
const urlsSet = new Set();
const encodedQuery = encodeURIComponent(query);
while (true) {
const url = pagina === 1
? `https://www.todamateria.com.br/?s=${encodedQuery}`
: `https://www.todamateria.com.br/page/${pagina}/?s=${encodedQuery}`;
try {
const { data: html } = await axios.get(url, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
const $ = load(typeof html === 'string' ? html : String(html));
let encontrou = false;
$('a.card-item').each((_, el) => {
let href = $(el).attr('href');
const titulo = $(el).find('.card-title').text().trim() || $(el).attr('title') || '';
if (href && href.startsWith('/')) {
href = 'https://www.todamateria.com.br' + href;
}
if (href && titulo.length > 0 && !urlsSet.has(href)) {
resultados.push({ titulo, url: href });
urlsSet.add(href);
encontrou = true;
}
});
if (!todasPaginas || !encontrou) break;
pagina++;
} catch (error) {
console.error(`Erro ao buscar página ${pagina}:`, error);
break;
}
}
return resultados;
}
async function rasparConteudoPagina(url: string): Promise<ConteudoPagina> {
try {
const { data: html } = await axios.get(url, {
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://www.todamateria.com.br/'
}
});
const $ = load(typeof html === 'string' ? html : String(html));
const data = new Date();
const dataFormatada = formatarData(data);
// Extrai título
const titulo = $('h1').first().text().trim();
// Extrai parágrafos do conteúdo principal
const paragrafos: string[] = [];
$('.main-content article p, .main-content .content p, article .content p, article p').each((_, el) => {
const txt = $(el).text().trim();
if (txt.length > 0) paragrafos.push(txt);
});
// Se não encontrou nada, tenta pegar todos os <p> exceto os que estão em .sidebar, .footer, .ad-unit
if (paragrafos.length === 0) {
$('p').each((_, el) => {
if (
$(el).parents('.sidebar, .footer, .ad-unit').length === 0 &&
$(el).text().trim().length > 0
) {
paragrafos.push($(el).text().trim());
}
});
}
// Extrai imagens
const imagens: { src: string; legenda: string }[] = [];
$('figure').each((_, fig) => {
const img = $(fig).find('img').first();
let src = img.attr('src') || '';
if (src && src.startsWith('/')) src = 'https://www.todamateria.com.br' + src;
const legenda = $(fig).find('figcaption').text().trim();
if (src) imagens.push({ src, legenda });
});
// Extrai autor
let autor = $('.author-article--b__info__name').first().text().trim() ||
$('.autor, .author, .author-name').first().text().trim() || '';
// Extrai citação
let citacao = '';
const citeCopy = $('#cite-copy .citation');
if (citeCopy.length > 0) {
citacao = citeCopy.text().trim();
}
citacao = `${citacao} ${dataFormatada}`;
return {
url,
titulo,
conteudo: paragrafos.join('\n\n'),
imagens,
autor,
citacao
};
} catch (error) {
console.error(`Erro ao raspar conteúdo de ${url}:`, error);
return { url, titulo: '', conteudo: '', imagens: [], autor: '', citacao: '', erro: true };
}
}
// Simulação da função de criação de ficha (seria substituída pela integração com IA)
async function criarFichaLeitura(conteudo: ConteudoPagina, promptCustomizado?: string): Promise<FichaLeitura> {
// Integrar com uma API de IA
return {
url: conteudo.url,
titulo: conteudo.titulo,
autor: conteudo.autor,
imagens: conteudo.imagens,
resumo: await gerarResumoIA(conteudo.conteudo, promptCustomizado),
citacao: conteudo.citacao,
};
}
// Função principal do fichador
async function fichador(
termoBusca: string,
todasPaginas: boolean,
salvar: boolean = true,
promptCustomizado?: string
): Promise<FichaLeitura[]> {
console.log(`🔍 Buscando artigos para: ${termoBusca} (${todasPaginas ? 'todas as páginas' : 'apenas a primeira página'})`);
let resultados: ResultadoBusca[] = [];
try {
resultados = await rasparTodasPaginasBusca(termoBusca, todasPaginas);
} catch (erro) {
console.error('❌ Erro ao buscar links:', erro);
return [];
}
console.log(`🔗 ${resultados.length} links encontrados. Raspando conteúdos...`);
const fichas: FichaLeitura[] = [];
for (const { url } of resultados) {
try {
const conteudo = await rasparConteudoPagina(url);
if (conteudo.erro) {
console.log(`❌ Erro ao processar ${url}`);
continue;
}
const ficha = await criarFichaLeitura(conteudo, promptCustomizado);
fichas.push(ficha);
console.log(`✅ Ficha criada para: ${ficha.titulo}`);
} catch (erro) {
console.error(`❌ Erro ao processar ${url}:`, erro);
}
}
if (salvar) {
try {
mkdirSync('dados', { recursive: true });
writeFileSync(
path.join('dados', `fichas-leitura-${termoBusca}.json`),
JSON.stringify(fichas, null, 2),
'utf-8'
);
console.log('💾 Fichas salvas em arquivo!');
} catch (erro) {
console.error('❌ Erro ao salvar fichas:', erro);
}
}
console.log('✅ Todas as fichas geradas!');
return fichas;
}
// Configuração do MCP Server
const server = new Server({
name: 'fichador-server',
version: '1.0.0',
capabilities: {
tools: {},
},
});
// Registra as ferramentas
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'buscar_artigos',
description: 'Busca artigos no site todamateria.com.br e retorna os links encontrados',
inputSchema: {
type: 'object',
properties: {
termo_busca: {
type: 'string',
description: 'Termo para buscar nos artigos'
},
todas_paginas: {
type: 'boolean',
description: 'Se deve buscar em todas as páginas ou apenas na primeira',
default: false
}
},
required: ['termo_busca']
}
},
{
name: 'raspar_conteudo',
description: 'Extrai o conteúdo completo de uma página específica',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL da página para extrair o conteúdo'
}
},
required: ['url']
}
},
{
name: 'criar_fichas_leitura',
description: 'Busca artigos, extrai conteúdo e cria fichas de leitura completas',
inputSchema: {
type: 'object',
properties: {
termo_busca: {
type: 'string',
description: 'Termo para buscar nos artigos'
},
todas_paginas: {
type: 'boolean',
description: 'Se deve buscar em todas as páginas ou apenas na primeira',
default: false
},
salvar: {
type: 'boolean',
description: 'Se deve salvar as fichas em arquivo JSON',
default: true
},
prompt_customizado: {
type: 'string',
description: 'Prompt customizado para geração das fichas (opcional)'
}
},
required: ['termo_busca']
}
}
]
};
});
// Manipula as chamadas das ferramentas
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'buscar_artigos': {
const { termo_busca, todas_paginas = false } = args as {
termo_busca: string;
todas_paginas?: boolean;
};
const resultados = await rasparTodasPaginasBusca(termo_busca, todas_paginas);
return {
content: [
{
type: 'text',
text: JSON.stringify({
total_encontrados: resultados.length,
artigos: resultados
}, null, 2)
}
]
};
}
case 'raspar_conteudo': {
const { url } = args as { url: string };
const conteudo = await rasparConteudoPagina(url);
return {
content: [
{
type: 'text',
text: JSON.stringify(conteudo, null, 2)
}
]
};
}
case 'criar_fichas_leitura': {
const {
termo_busca,
todas_paginas = false,
salvar = true,
prompt_customizado
} = args as {
termo_busca: string;
todas_paginas?: boolean;
salvar?: boolean;
prompt_customizado?: string;
};
const fichas = await fichador(termo_busca, todas_paginas, salvar, prompt_customizado);
return {
content: [
{
type: 'text',
text: JSON.stringify({
total_fichas: fichas.length,
fichas: fichas
}, null, 2)
}
]
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Ferramenta desconhecida: ${name}`
);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido';
throw new McpError(ErrorCode.InternalError, `Erro na execução: ${errorMessage}`);
}
});
// Inicia o servidor
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP Server Fichador iniciado e conectado via stdio');
}
main().catch((error) => {
console.error('Erro ao iniciar o servidor:', error);
process.exit(1);
});