
Se você leu meu último post, sabe que eu construí o searxng-web-fetch-mcp — um servidor MCP em Crystal que dá a assistentes de IA busca na web e fetching de conteúdo. Funcionou. Foi rápido. Eu shippinguei em uma noite.
Mas depois de algumas semanas de uso diário, as coisas começaram a me incomodar.
A extração era boa, mas não ótima. Alguns artigos voltavam com lixo de navegação. Outros tinham o conteúdo principal enterrado sob texto de sidebar. E o batch fetch, embora rápido, não me dava o controle que eu queria sobre o que voltava e como.
Eu ficava remendando. Então percebi: eu não estava remendando um bug. Eu estava remendando uma arquitetura.
O Problema, Revisitado
Aqui está o que continuava me incomodando:
- Qualidade de extração — A versão em Crystal usava uma heurística portada do go-trafilatura. Funcionava para a maioria dos artigos, mas o scoring não era agressivo o suficiente. Sidebars com texto longo ainda passavam despercebidas.
- Estrutura de resposta — O original retornava strings JSON puras. A IA tinha que parsear texto dentro de texto. Sem conteúdo estruturado. Sem schema de metadata. Sem jeito do cliente saber o que era título e o que era corpo do texto.
- Controle de batch — Batch fetch era tudo-ou-nada. Sem timeouts por URL. Sem limites de truncamento. Sem jeito de dizer “busca essas 10 URLs mas só me dê 4000 caracteres cada.”
- A história das dependências — Crystal é bonito, mas o ecossistema é pequeno. Toda vez que eu precisava de um novo feature de parsing HTML, eu estava escrevendo do zero ou portando de outra linguagem.
E o maior: o protocolo MCP estava evoluindo. Conteúdo estruturado, campos _meta, versionamento de schema — a spec estava caminhando para response shapes mais ricas. Meu código em Crystal não foi construído para isso.
Então fiz o que qualquer desenvolvedor razoável faz às 11 da noite com um projeto funcionando: reescrevi.
Por Que Go
Eu já uso Go para o navi-agent. Eu já penso em Go quando alcanço concorrência. E a standard library do Go — especificamente golang.org/x/net/html — te dá um parser HTML de produção grade direto da caixa. Sem porting. Sem FFI. Sem prece.
Três razões, igual da última vez mas diferentes:
- Ecossistema — O parser net/html do Go, suas primitivas de concorrência, seu framework de testes. Tudo incluído.
- Respostas estruturadas — O sistema de tipos do Go torna trivial definir response shapes exatas com JSON tags. Sem ginástica de tipos em runtime.
- Binário único —
go installe pronto. Mesma história de deployment que Crystal, mas com uma standard library mais profunda atrás.
Conheça o Better Search MCP
Better Search MCP é a reescrita em Go que faz tudo que a versão em Crystal fazia — e faz melhor. Mesmas duas ferramentas: busca e fetch. Mesma stack SearXNG + Byparr. Mas a extração é mais inteligente, as respostas são estruturadas, e o batch fetching tem controle real.
| |
Um comando. Binário cai em $HOME/go/bin/better-search. Pronto.
O Que Mudou
Extração de Conteúdo Mais Inteligente
O extrator em Go usa um algoritmo de scoring diferente. Em vez de só checar nomes de classe, ele caminha o DOM e pontua cada nó candidato baseado em:
- Densidade de texto — Quanto texto real vs. tags HTML
- Densidade de links — Razão link-para-texto alta? Provavelmente navegação, não conteúdo
- Padrões de boost — Nomes de classe e IDs como “content”, “article”, “post”, “entry”, “main”
- Padrões de penalidade — Nomes de classe como “comment”, “sidebar”, “footer”, “widget”, “ad”, “social”
- Preferência de tag —
<article>,<main>,<section>recebem boosts naturais sobre sopa de<div>genérico
O resultado: extrações mais limpas em sites bagunçados. O tipo de site que tem três sidebars, um banner de cookies, um popup de newsletter e um parágrafo de conteúdo real.
Respostas com Conteúdo Estruturado
Essa é a grande. Toda resposta de ferramenta agora inclui structuredContent junto com o campo content legado:
| |
O cliente de IA não precisa parsear JSON de uma string dentro de uma string. Ele recebe um objeto real com campos reais. Título é título. Autor é autor. Data é data. Sem achismo.
Erros são estruturados também:
| |
Sem mais parsear mensagens de erro de texto freeform. O cliente sempre sabe o que aconteceu.
Batch Fetch Com Controle Real
A versão em Crystal tinha batch fetching. A versão em Go tem batch fetching configurável:
| |
Todo parâmetro que você quereria:
| Parâmetro | Propósito | Padrão |
|---|---|---|
url | Fetch de URL única | — |
urls | Batch fetch (até 25) | — |
include_metadata | Título, autor, data, idioma | true |
timeout_seconds | Timeout por request (1-120) | 30 |
max_content_chars | Truncar output | Sem limite |
preserve_links | Manter links Markdown | true |
raw_html | Incluir HTML extraído | false |
prefer_readable_text | Foco em artigo vs. página completa | true |
fail_fast | Parar batch no primeiro erro | false |
URLs em batch preservam ordem. URLs duplicadas são mantidas. O array de resultados tem a mesma cardinalidade que a entrada. Sem surpresas.
Concorrência Que Escala
O batch fetch usa um padrão de semáforo — canais do Go agindo como semáforo contador:
| |
Mesma ideia que as fibers do Crystal, mas goroutines do Go são igualmente leves e o padrão é mais explícito. A env var MAX_CONCURRENT_REQUESTS (padrão: 30) controla o teto.
Validação de Config na Inicialização
Configuração inválida falha rápido. Se SEARXNG_URL ou BYPARR_URL não é uma URL HTTP/HTTPS válida com um host, o servidor se recusa a iniciar:
configuration error: SEARXNG_URL must be a valid http or https URL with a host
Sem misconfiguração silenciosa. Sem sessões de debug de “por que a busca retorna resultados vazios?” às 2 da manhã.
Logging de Requests
Cada request é logada com atributos estruturados:
| |
Set LOG_LEVEL=DEBUG para ver tudo. Set LOG_LEVEL=ERROR para silêncio. Produção fica limpa. Desenvolvimento fica observável.
A Stack
| Componente | Tecnologia | Por Quê |
|---|---|---|
| Linguagem | Go 1.23+ | Parser HTML da stdlib, goroutines, binário único |
| Parsing HTML | golang.org/x/net/html | DOM walking de produção grade |
| Scoring de Conteúdo | Motor heurístico custom | Boost/penalidade por pattern matching |
| Rendering Markdown | Conversor DOM-based | Preserva estrutura, não só texto |
| Protocolo MCP | JSON-RPC sobre stdio | Padrão, sem overhead de HTTP |
| Clientes HTTP | Clientes SearXNG + Byparr | Separação limpa, testável |
Stats de Código
2146 linhas de Go em todo o projeto.
Isso é um MCP server completo com busca, batch fetch, extração de conteúdo, rendering Markdown, respostas estruturadas, validação de config e logging de requests. Em 2146 linhas.
A standard library do Go faz boa parte do trabalho pesado. Sem framework. Sem ORM. Sem mágica.
A Arquitetura
cmd/server/ → Entrypoint do binário
internal/clients/ → Clientes HTTP para SearXNG e Byparr
internal/config/ → Carregamento e validação de config
internal/extractor/ → Extração de conteúdo e rendering Markdown
internal/mcp/ → Servidor JSON-RPC / MCP e registro de tools
internal/tools/ → Orquestração de tools e modelos de resposta
Cada package tem uma única responsabilidade. O extrator não sabe sobre MCP. O servidor MCP não sabe sobre Byparr. O package tools orquestra as peças. Limpo, testável e fácil de extender.
Como Funciona
Assistente de IA → Request MCP → better-search
↓
searxng_web_search() ou web_fetch()
↓
Busca → SearXNG → Resultados
Fetch → Byparr → HTML → Extrair → Markdown
↓
Resposta estruturada → De volta para a IA
Mesmo fluxo de antes. Execução melhor.
O Que Eu Aprendi
1. Reescreva quando a arquitetura está errada, não o código
A versão em Crystal funcionava. O código era limpo. Mas a arquitetura não suportava respostas estruturadas, opções de fetch por URL, ou a spec MCP em evolução. Remendar teria levado mais tempo que reescrever.
2. O parser net/html do Go é subestimado
Eu portei um algoritmo de extração de Go para Crystal para a primeira versão. Então reescrevi tudo em Go e usei o parser original diretamente. A API de DOM walking é limpa, o modelo de memória é previsível, e o package golang.org/x/net/html lida com edge cases que eu nem sabia que existiam.
3. Respostas estruturadas são o futuro do MCP
Texto-puro-em-JSON funciona para demos. Para produção, o cliente de IA precisa saber exatamente o que está recebendo. structuredContent com campos tipados e _meta com versões de schema — é assim que ferramentas MCP devem ser construídas daqui para frente.
4. Validação de config economiza horas
A pergunta de suporte mais comum para a versão em Crystal era “busca retorna vazio.” A resposta era sempre uma URL mal configurada. Agora o servidor não inicia com config inválida. Problema eliminado na raiz.
5. A standard library é o framework
Sem framework web. Sem router. Sem chain de middleware. O servidor MCP lê JSON-RPC do stdin, despacha para tools, escreve respostas no stdout. É isso. As primitivas de concorrência do Go cuidam do resto. Quando seu protocolo é simples assim, adicionar um framework só adiciona complexidade.
Configuração
| |
Adicione na sua config do OpenCode ou Claude Code. Reinicie. Sua IA agora pode buscar e fetchar — com respostas estruturadas.
Experimente
| |
O repositório no GitHub está aberto. Issues e PRs são bem-vindos.
E você? Tem alguma ferramenta que você reescreveu porque a arquitetura não correspondia mais ao problema? Me conta nos comentários.
Se achou útil, compartilha com outros desenvolvedores. Ajuda mais do que você imagina.
See you in the Wired.