Skip to main content
Glama

web-search-mcp

Português | English

Servidor MCP autônomo que pesquisa na web, abre as páginas, lê o conteúdo e devolve um resumo com as fontes para o agente que chamou. Três tools — research_web, analyze_urls e read_url — para qualquer agente (Claude Code, LangChain, LangGraph, etc). Sem framework de agente embutido: só FastMCP.

Não é um MCP para o SearXNG. A busca é própria e tem duas fontes:

  • Google via Tor, a principal. O servidor consulta o Google (pelo endpoint do Google CSE) através de quatro canais Tor, alternando as queries entre eles. Quando o Google barra um canal, a query refaz em outro na hora e o barrado troca de IP em segundo plano.

  • SearXNG, a reserva. Só entra quando o Google falha por todos os caminhos — ou sempre, se você preferir, com SEARCH_BACKEND=searxng.

A stack inteira (quatro containers Tor + SearXNG) vem pronta num docker compose em search-engine/. Detalhes em De onde vêm os links.

A ideia: pesquisar num contexto isolado

A pesquisa inteira acontece fora da janela de contexto do agente principal. Ele manda uma pergunta em linguagem natural e recebe de volta um resumo curto — nunca vê o material bruto.

                     ┌─────────────────────────────────────────────┐
  agente principal   │  web-search-mcp (contexto próprio)          │
  ────────────────   │                                             │
                     │  1. gera variantes de busca (LLM)           │
  "quem foi X?"  ──► │  2. busca em paralelo (Google via Tor;      │
                     │     SearXNG de reserva)                     │
                     │  3. triagem por título/trecho (LLM) e abre  │
                     │     só as páginas escolhidas                │
                     │     + ponte quando nada lido cita o nome    │
                     │  4. monta o dossiê ....... 15-50k chars     │
                     │  5. resume com as fontes (LLM)              │
       resumo   ◄──  │                          ....... ~700 tokens│
     ~700 tokens     └─────────────────────────────────────────────┘

Sem isso, uma pesquisa séria significa despejar dezenas de milhares de tokens de HTML e texto extraído na conversa principal — material que fica lá ocupando espaço em todas as chamadas seguintes, mesmo depois de já ter sido usado. Aqui esse custo é pago num processo separado, com o LLM que você escolher, e o que atravessa é só a resposta.

Na prática:

  • O contexto do agente não incha. Ele gasta ~700 tokens por pesquisa em vez dos 6k-20k tokens do material lido. Conversas longas com muitas pesquisas continuam viáveis.

  • Dá para usar um modelo barato na parte cara. Quem lê 5 páginas e resume pode ser um modelo local pequeno; o agente principal, o caro, só recebe o resultado pronto.

  • A resposta vem citada. O resumo traz as URLs realmente consultadas e um carimbo de data/hora, então dá para conferir a fonte em vez de confiar.

  • Uma chamada resolve. O research_web já busca vários ângulos por dentro e lê em paralelo — o agente não precisa orquestrar rodadas de busca.

O analyze_urls aplica a mesma ideia a links que o usuário já tem: "resuma estes três artigos", "compare as duas propostas" — lê as páginas aqui dentro e devolve só a análise. Quando você quer o texto cru mesmo — "leia este link para mim" — é o read_url que serve, e aí o conteúdo vai inteiro para o agente, sem resumo.

Related MCP server: myscrape

O research_web busca no Google pelo endpoint do Google CSE (o mesmo que o widget de busca embutida usa), através de quatro canais Tor. O SearXNG continua na stack, mas como reserva.

Por quê. Um SearXNG raspa os buscadores e, no uso normal, toma CAPTCHA e limite de taxa: medido em 29/08/2026 e de novo em 10/09/2026, 13 de 15 motores suspensos. O que sobra devolve casamento de nome de marca — duas perguntas técnicas diferentes chegaram a voltar a mesma lista de homepages. No A/B de 10/09/2026 (evals/search_ab.py, 12 perguntas de assuntos sem relação entre si, mesmas variantes de busca nos dois lados):

Fonte

Pesquisas vazias

Resultados no assunto

Páginas lidas

Busca p50

SearXNG

2 de 12

112 / 125

45

2,7 s

Google CSE via Tor

0 de 12

228 / 230

71

4,6 s

Em carga contínua (20 pesquisas seguidas, 80 consultas) 100% saíram pelo Tor, com 5 failovers entre canais e nenhuma queda para o SearXNG; a busca subiu para p50 9,7 s. Isso foi medido com dois canais — os quatro atuais existem para repartir essa carga, e ainda não foram medidos.

Como uma query anda. Nenhum passo espera:

  1. o canal da vez no rodízio;

  2. barrado pelo Google → esse canal troca de circuito em segundo plano (credencial SOCKS nova + NEWNYM) e a query refaz na hora em outro canal;

  3. barrado de novo → um terceiro canal (com só dois, volta ao primeiro, já com circuito novo);

  4. o CSE pelo IP da própria máquina (GOOGLE_CSE_DIRECT_FALLBACK);

  5. o SearXNG (SEARXNG_FALLBACK).

Quando a query chega ao SearXNG e a maioria dos motores dele está suspensa, a resposta começa com um aviso de busca degradada, dizendo ao agente que a cobertura está incompleta por causa da infraestrutura — não porque o assunto não existe — e que repetir a pesquisa não vai adiantar. Sem esse aviso o agente reformula a pergunta em loop.

Só a busca passa pelo Tor. As páginas são abertas direto, desta máquina. SEARCH_BACKEND=searxng desliga o Google e volta ao SearXNG puro — rollback é trocar a variável e reiniciar.

Ressalvas:

  • Não é API documentada. Se o Google mudar o formato, o parse falha alto e a busca cai no SearXNG em vez de quebrar.

  • O CX default é público e de terceiro (blackle.com, o mesmo que o motor google cse do SearXNG usa). Pode sumir; GOOGLE_CSE_CX troca sem mexer no código.

  • O fallback direto não é garantido. O IP de casa também tomou 429 do CSE no dia da medição.

  • Termos de uso. Consulta automatizada ao Google, ainda mais via Tor, contraria os termos do Google. Avalie antes de usar; com SEARCH_BACKEND=searxng nada disso acontece.

Medições, decisões e riscos em plans/tor.md.

Requisitos

  • Python ≥ 3.13

  • uv

  • A stack de busca: quatro containers Tor (a fonte de links é o Google CSE, consultado por eles) e um SearXNG de reserva, com formato JSON habilitado — tudo num docker compose pronto em search-engine/. Sem os Tor a busca ainda funciona (CSE direto, depois SearXNG); SEARCH_BACKEND=searxng usa só o SearXNG

  • Um servidor de LLM com API compatível com OpenAI (/v1/chat/completions e /v1/models) — ex. llama.cpp server, vLLM, ou a própria OpenAI

Infraestrutura de busca via docker compose

search-engine/ traz a stack de busca inteira, já nas portas que são o default do .env.example. Numa cópia nova do repo:

cp .env.example .env          # troque TOR_CONTROL_PASSWORD
cd search-engine
docker compose up -d --wait

A primeira subida compila a imagem Tor localmente (search-engine/tor/: Alpine com o pacote tor fixado em 0.4.9.12-r0), então demora mais e precisa de internet. Se o Alpine tirar essa versão do repositório, a compilação falha — atualize o número no Dockerfile. O --wait segura o comando até os canais Tor ficarem healthy (o bootstrap leva até ~60 s); sem ele, as pesquisas desse intervalo caem no CSE direto e no SearXNG.

Sobe:

Serviço

Porta (host)

Papel

searxng + valkey

8886

SearXNG com search-engine/searxng/settings.yml montado por cima (JSON habilitado e curadoria de fontes já prontos)

tor-a

127.0.0.1:9060 SOCKS, 9061 controle

canal de busca via Tor (plans/tor.md)

tor-b

127.0.0.1:9070 SOCKS, 9071 controle

segundo canal, independente do primeiro

tor-c

127.0.0.1:9080 SOCKS, 9081 controle

terceiro canal

tor-d

127.0.0.1:9090 SOCKS, 9091 controle

quarto canal

mcp-searxng + caddy

8887

servidor MCP de busca de terceiros, independente deste projeto (ver abaixo)

Os containers Tor leem o mesmo .env da raiz que configura o MCP (env_file: ../.env), então a senha do ControlPort é uma só e não há como os dois lados divergirem. Sem o .env o compose falha na hora. As portas Tor só escutam em 127.0.0.1: publicar em 0.0.0.0 transformaria a máquina num proxy aberto. Dentro do container há uma segunda barreira: o torrc só aceita SOCKS vindo de faixas privadas (SocksPolicy), e o ControlPort exige a senha.

Os serviços mcp-searxng e caddy são um servidor MCP de busca de terceiros — este projeto fala com o SearXNG direto, por HTTP. Se você não os usa, pode removê-los do compose sem afetar em nada o web-search-mcp.

Antes de subir isso em qualquer lugar que não seja sua máquina, troque as senhas. Os placeholders estão versionados neste repositório e portanto são públicos:

  • TOR_CONTROL_PASSWORD (no .env) — quem tiver a senha controla o tor

  • SEARXNG_SECRET (em search-engine/docker-compose.yaml) — chave com que o SearXNG assina; deve ser um valor aleatório e longo, ex. openssl rand -hex 32

  • o Bearer password123 do search-engine/searxng/caddy/Caddyfile — é a única autenticação na frente do mcp-searxng, cuja porta 8887 é publicada no host. Quem souber o token usa o serviço

Tudo de uma vez: start.sh

./start.sh

Cria o .env a partir do .env.example se ele não existir, lembra quais variáveis conferir (MODEL_BASE_URL, MODEL_API_KEY, TOR_CONTROL_PASSWORD, MCP_HOST/MCP_PORT) e espera um Enter. Depois confere se as portas e os nomes de container estão livres, sobe o search-engine/ e inicia o MCP em streamable-http. No fim mostra a URL para plugar o cliente (http://127.0.0.1:8765/mcp por padrão). Ctrl+C derruba tudo: o MCP e os containers (docker compose down; volumes preservados). Se o MCP cair sozinho, a stack cai junto. Argumentos extras vão para o web-search-mcp.

O LLM continua externo: se MODEL_BASE_URL não responder, o script avisa e sobe assim mesmo — read_url funciona sem ele, research_web e analyze_urls não.

Instalação

A partir do GitHub (não precisa clonar)

uvx --from git+https://github.com/fabio-barboza/web_search_mcp@v0.2.2 web-search-mcp

Registro no Claude Code:

claude mcp add web-search \
  -e SEARXNG_URL=http://localhost:8886 \
  -e MODEL_BASE_URL=http://localhost:8200/v1 \
  -- uvx --from git+https://github.com/fabio-barboza/web_search_mcp@v0.2.2 web-search-mcp

Instalado assim, o .env não é lido: a configuração inteira entra por -e — ver Configurando o servidor instalado. @v0.2.2 fixa essa versão. Troque por @main para sempre pegar o topo do branch, ou por qualquer outra tag ou commit.

Para deixar o comando fixo no PATH em vez de resolver a cada execução:

uv tool install git+https://github.com/fabio-barboza/web_search_mcp@v0.2.2

A partir do clone (desenvolvimento)

uv sync
cp .env.example .env

Edite o .env: MODEL_BASE_URL do seu servidor de modelo e TOR_CONTROL_PASSWORD (os containers Tor leem este mesmo arquivo). Com a stack do search-engine/ nas portas default, SEARXNG_URL e TOR_CHANNELS já batem. O servidor sobe com os defaults se você não copiar nada, mas sem LLM em MODEL_BASE_URL o research_web e o analyze_urls não funcionam, e sem busca nenhuma no ar (Tor, CSE direto e SearXNG) o research_web não acha links — o read_url funciona sozinho.

Uso

Dois transportes, mesmas tools. A diferença é quem sobe o processo — e isso muda de onde vem a configuração.

stdio

http

Quem sobe o processo

o cliente, a cada sessão

você, uma vez

Quantos clientes

um por processo

vários no mesmo processo

Porta de rede

nenhuma

MCP_HOST:MCP_PORT

Configuração vem de

bloco env do cliente (-e) e/ou .env

ambiente de quem subiu e/ou .env

stdio (padrão — para Claude Code e clientes locais)

uv run web-search-mcp

Na prática você não roda isso à mão: quem executa é o cliente. Registro no Claude Code apontando para o clone:

claude mcp add web-search -- uv --directory /caminho/para/web_search_mcp run web-search-mcp

Para registrar a versão instalada do GitHub, com a configuração no bloco env, ver Configurando o servidor instalado.

HTTP (compartilhar entre vários agentes)

Suba o servidor. Do clone, que lê o .env da raiz:

uv run web-search-mcp --http

Ou instalado, passando a configuração pelo ambiente:

SEARXNG_URL=http://localhost:8886 \
MODEL_BASE_URL=http://localhost:8200/v1 \
uvx --from git+https://github.com/fabio-barboza/web_search_mcp@v0.2.2 web-search-mcp --http

Sobe em http://{MCP_HOST}:{MCP_PORT}/mcp (padrão 127.0.0.1:8765).

Com ele no ar, registre o cliente pela URL:

claude mcp add --transport http web-search http://127.0.0.1:8765/mcp

Que no .mcp.json fica:

{
  "mcpServers": {
    "web-search": {
      "type": "http",
      "url": "http://127.0.0.1:8765/mcp"
    }
  }
}

No modo http o bloco env do cliente não tem efeito. No stdio o cliente spawna o processo, então o -e dele vira o ambiente do servidor; no http o processo é seu e já está rodando quando o cliente conecta, com o ambiente que você deu na hora de subir. Toda a configuração migra para o lado do servidor, e trocar uma variável exige reiniciá-lo — editar o .mcp.json não adianta.

Deixando rodando (systemd de usuário, sem sudo)

# ~/.config/systemd/user/web-search-mcp.service
[Unit]
Description=web-search-mcp (http)
After=network.target

[Service]
Environment=MODEL_BASE_URL=http://localhost:8200/v1
# Busca: Google CSE pelos quatro canais Tor do search-engine/, SearXNG de reserva
Environment=SEARCH_BACKEND=google_tor
Environment=TOR_CHANNELS=127.0.0.1:9060:9061,127.0.0.1:9070:9071,127.0.0.1:9080:9081,127.0.0.1:9090:9091
Environment=SEARXNG_URL=http://localhost:8886
# TOR_CONTROL_PASSWORD fica fora da unit, num arquivo só seu (chmod 600)
EnvironmentFile=%h/.config/web-search-mcp/secrets.env
ExecStart=%h/.local/bin/uvx --from git+https://github.com/fabio-barboza/web_search_mcp@v0.2.2 web-search-mcp --http
Restart=on-failure

[Install]
WantedBy=default.target

A senha do ControlPort é a mesma do .env que os containers Tor leem. Ela não vai na unit porque systemctl --user cat/show exibem cada Environment= para quem olhar:

mkdir -p ~/.config/web-search-mcp
install -m 600 /dev/null ~/.config/web-search-mcp/secrets.env
echo 'TOR_CONTROL_PASSWORD=a-mesma-do-.env-do-search-engine' > ~/.config/web-search-mcp/secrets.env
systemctl --user daemon-reload
systemctl --user enable --now web-search-mcp

A unit não depende dos containers: o search-engine/ sobe sozinho no boot (restart: unless-stopped), e se os canais Tor ainda não estiverem no ar quando a primeira pesquisa chegar, ela cai no CSE direto e depois no SearXNG em vez de falhar. As linhas SEARCH_BACKEND e TOR_CHANNELS repetem os defaults — estão ali para ficar explícito o que mudar se os canais estiverem em outro host ou em outras portas. Rodando do clone (WorkingDirectory= apontando para ele e uv run web-search-mcp --http), o .env da raiz é lido e nenhuma dessas linhas é necessária.

Antes de expor esse endpoint além da sua máquina, note que o servidor não tem autenticação nenhuma. Com o default MCP_HOST=127.0.0.1 só processos locais alcançam, o que é seguro. Trocar para 0.0.0.0 para outra máquina consumir deixa as tools abertas a qualquer um na rede, sem credencial e servindo de proxy de scraping em cima do seu LLM. Nesse caso ponha um reverse proxy com token na frente e restrinja MCP_CORS_ALLOW_ORIGINS às origens que você usa, em vez de deixar *.

Tools

research_web(query: str, recent: bool = False, *, user_message: str) -> str

Pesquisa a pergunta na web (gera variantes de busca — sempre incluindo a pergunta em palavras-chave, com os nomes intactos —, roda em paralelo, faz uma triagem dos candidatos por título e trecho e lê só os escolhidos) e devolve um resumo em português, com carimbo de data/hora. Quando nenhuma página lida contém um nome da pergunta (nome localizado, apelido), faz uma busca curta pela palavra que anda junto com esse nome nos títulos dos resultados e lê essas páginas primeiro — é o que liga "Pai Putrefato" a "Mystic Carrion". Cada fato termina com um link markdown para a página de onde saiu, montado em código a partir da URL realmente lida — não pedido ao modelo. Marcador [n] que o modelo inventar para uma fonte que não existe é apagado, em vez de ficar apontando para o nada. No fim vai a lista numerada das URLs consultadas.

Use recent=True só quando a resposta depende do dia de hoje (clima, cotação, placar, notícia). Para fatos estáveis (história, biografia, conceitos), deixe recent=False — filtrar por data descarta as melhores fontes.

user_message (obrigatório) é a última mensagem do usuário, copiada literalmente. O agente que chama costuma traduzir ou trocar o nome que o usuário escreveu antes mesmo da primeira pesquisa ("Pai Putrefato" vira "Rotting Bride"). Quando a query perdeu um nome que está na mensagem, a pesquisa também busca pelos nomes da mensagem e usa o texto original na triagem e no resumo. Se a mensagem só repete a query, nada muda. Medido em 11/09/2026 com o agente do Open WebUI (qwen3.8:27B): resposta certa em 10 de 10 conversas, contra 6 de 10 sem o campo, com menos pesquisas por conversa. Opcional, o agente só preenchia o campo em 9 de 20 chamadas; por isso ele é obrigatório.

A mesma pergunta reescrita logo em seguida (mesmo conjunto de palavras de conteúdo, em qualquer ordem) devolve o resultado anterior em vez de buscar de novo: a tool já busca vários ângulos por dentro, e repetir só relê as mesmas páginas. Busca degradada vem com o aviso descrito em De onde vêm os links.

Agente que pesquisa em círculo trocando a pergunta a cada volta (o que escapa do cache acima) é freado por sessão: chamadas de research_web e analyze_urls que começam logo depois da anterior contam como o mesmo turno; a partir da 3ª o resultado vem com aviso para parar e pedir o nome exato ao usuário, e a 5ª não é executada.

analyze_urls(urls: list[str], request: str = "Resuma o conteúdo.") -> str

Lê de 1 a 8 URLs fornecidas e devolve só a análise pedida em linguagem natural — resumo, parecer técnico, opinião, comparação —, feita pelo LLM deste servidor. O texto das páginas não entra no contexto do agente. O orçamento de caracteres do dossiê é repartido entre as páginas, para que todas caibam juntas. A resposta lista as URLs analisadas e as que falharam.

read_url(url: str) -> str

Abre uma URL específica e devolve o conteúdo principal da página em Markdown, inteiro, sem busca nem resumo — o texto bruto volta pro agente processar. Vem com um cabeçalho Fonte desta página e um link markdown pronto, para o agente citar o que leu com endereço.

Nas duas tools que leem URLs, se o servidor redirecionar para outro endereço (ex. uma página que não existe mais caindo na home), a resposta avisa e usa a URL final. Sem esse aviso o agente lê a página errada sem saber e tenta de novo em loop. As duas bloqueiam URLs que apontam para IP privado/loopback/link-local (proteção anti-SSRF).

Prompt pesquisador

O servidor também expõe um prompt MCP com a política de uso: pesquisar antes de responder o que não se sabe com certeza, uma chamada por pergunta, responder só com base no resumo e manter os links e as datas.

Configuração

Nenhuma variável é obrigatória. Toda uma tem default no código, e o servidor sobe sem configuração alguma. Você só declara o que desviar do padrão — na prática, MODEL_BASE_URL, se o seu não estiver na porta abaixo, e TOR_CONTROL_PASSWORD, que os containers Tor exigem.

A lista completa, com o default de cada uma:

Log

Variável

Default

O que faz

LOG_LEVEL

INFO

DEBUG | INFO | ERROR. INFO pega logs de info e de erro; DEBUG mostra cada busca, cada URL lida e o dossiê montado. Log sempre sai no stderr, nunca no stdout — um byte no stdout corromperia o protocolo stdio

Modelo (API compatível com OpenAI)

Variável

Default

O que faz

MODEL

(vazio)

Nome do modelo a usar. Vazio = usa o modelo default do servidor, ou seja, o que já está carregado: o servidor consulta GET /models a cada chamada e adota o modelo residente, sem forçar troca nem pagar reload de GPU. Preencha só para fixar um modelo específico, aceitando o reload se ele não for o carregado. Ver Detecção de modelo

MODEL_BASE_URL

http://localhost:8200/v1

Base da API compatível com OpenAI, sem o /chat/completions no fim. Serve llama.cpp, vLLM, Ollama, OpenAI, o que for

MODEL_API_KEY

not-needed

Vai como Authorization: Bearer <valor>. Servidor local normalmente ignora; provider pago exige a chave real

MODEL_TIMEOUT

120

Timeout, em segundos, de cada chamada de LLM. Modelo grande em CPU pode precisar de mais

MODEL_TEMPERATURE

0

Temperatura das chamadas. 0 porque a tarefa é resumir fonte, não criar — temperatura alta aqui vira alucinação

MODEL_CONTEXT_TOKENS

65536

Janela de contexto do modelo, de onde sai o orçamento do dossiê. Com llama.cpp/llama-swap o servidor lê o --ctx-size real do modelo carregado em GET /models a cada pesquisa, e esse valor ganha; esta variável é o fallback para providers que não expõem isso. Nesses, tem que bater com a janela real: declarar mais faz o provider recusar a chamada com HTTP 400 e a pesquisa inteira se perde, depois de já ter pago busca e scraping

MODEL_RESERVE_TOKENS

4096

Quanto da janela fica reservado para o que não é dossiê: instruções, pergunta e a resposta que o modelo ainda vai gerar. O orçamento do dossiê é MODEL_CONTEXT_TOKENS - MODEL_RESERVE_TOKENS

EXTRA_BODY

(vazio)

JSON cru mesclado no payload do /chat/completions, para parâmetro que só o seu provider entende. Ex. desligar reasoning no Qwen3: EXTRA_BODY={"chat_template_kwargs": {"enable_thinking": false}}. JSON inválido derruba o servidor no boot, de propósito

USE_REASONING

false

true liga raciocínio curto só nas chamadas que decidem a qualidade: triagem dos resultados, resumo e analyze_urls. As outras (variantes de busca, seletor da ponte) seguem só com o EXTRA_BODY. Medido com qwen3.8:27B, 10 perguntas × 2 rodadas, notas cegas: +5 pontos e ~+30 s por pesquisa

REASONING_BODY

(vazio)

JSON que liga o raciocínio no seu modelo, mesclado por cima do EXTRA_BODY quando USE_REASONING=true. Vazio = formato do Qwen3.x no llama.cpp: {"chat_template_kwargs": {"enable_thinking": true, "reasoning_effort": "low"}}

EXTRA_SYSTEM_PROMPT

(vazio)

Texto apenso ao fim do system prompt em toda chamada. Existe porque nem todo modelo desliga reasoning por parâmetro de API — em alguns só obedece por instrução. Ex. EXTRA_SYSTEM_PROMPT=Reasoning strength: low. Vale a pena: num modelo que pensa por padrão, gerar 3 linhas de busca custou 2767 tokens / 39s sem, contra 271 / 3s com

Busca (Google CSE via Tor)

O caminho de cada query está em De onde vêm os links.

Variável

Default

O que faz

SEARCH_BACKEND

google_tor

google_tor (Google CSE pelos canais Tor, com CSE direto e SearXNG de reserva) ou searxng (só o SearXNG, como antes). Rollback é trocar e reiniciar

TOR_CHANNELS

127.0.0.1:9060:9061,127.0.0.1:9070:9071,127.0.0.1:9080:9081,127.0.0.1:9090:9091

Um canal por item, host:porta_socks:porta_controle, separados por vírgula. O default bate com os tor-a..tor-d do search-engine/. Aceita qualquer quantidade; com menos de três, o failover repete canal (já com circuito novo)

TOR_CONTROL_PASSWORD

(vazio)

Senha do ControlPort, para o NEWNYM. Os containers Tor não sobem sem ela; o MCP sim — vazia só desliga o NEWNYM, e a troca de circuito pela credencial SOCKS continua funcionando

GOOGLE_CSE_CX

CX público do blackle.com

Qual CSE consultar. Troque por um CX seu sem mexer no código

GOOGLE_CSE_HL

pt-BR

Idioma da interface do CSE. Não restringe o idioma dos resultados

GOOGLE_CSE_TIMEOUT

15

Timeout, em segundos, de cada requisição ao CSE (via Tor leva 2-4 s)

GOOGLE_CSE_DIRECT_FALLBACK

true

Tentar o CSE pelo IP da máquina quando os canais Tor falham. Não conte com ele: o IP de casa também toma 429

SEARXNG_FALLBACK

true

Cair no SearXNG quando o Google falhou por todos os caminhos. Com false, a pesquisa devolve erro de busca nesse caso

SearXNG (reserva)

Usado quando o Google falhou por todos os caminhos, ou sempre, com SEARCH_BACKEND=searxng. Só o SEARXNG_MAX_RESULTS vale também para o Google.

Variável

Default

O que faz

SEARXNG_URL

http://localhost:8886

Base da sua instância SearXNG. Precisa estar com o formato JSON habilitado

SEARXNG_MAX_RESULTS

10

Quantos resultados de cada busca entram na mescla, qualquer que seja a fonte (o Google devolve 20 por consulta; o que passa disso fica de fora). O research_web faz várias buscas e junta, então isso é o teto por busca, não o total

SEARXNG_TIMEOUT

10

Timeout, em segundos, de cada consulta ao SearXNG

SEARXNG_LANGUAGE

auto

Idioma passado na busca. auto deixa o SearXNG detectar pela query (pergunta em inglês busca página em inglês); fixar pt-BR empurra toda busca para página brasileira

SEARXNG_CATEGORIES

general,news

Categorias do SearXNG, separadas por vírgula, repassadas cruas

Scraper

Variável

Default

O que faz

SCRAPER_TIMEOUT

6

Timeout, em segundos, do download de cada página. Baixo de propósito: no research_web uma página lenta não vale segurar a pesquisa inteira, e há links de reserva para tomar o lugar dela

SCRAPER_LIMIT

(vazio)

Corte de caracteres do texto extraído no read_url. Vazio = página inteira. Não afeta o research_web, que usa o RESEARCH_PAGE_CHARS

Research

Variável

Default

O que faz

RESEARCH_PAGE_BUDGET

5

Quantas páginas entram no dossiê de uma pesquisa, somando todas as buscas. É o principal botão de qualidade × latência

RESEARCH_POOL_SIZE

60

Reserva de links candidatos, e o que a triagem por título/trecho enxerga antes de abrir qualquer página. Link morto, bloqueado ou sem texto não gasta vaga do orçamento: cede o lugar para o próximo da reserva. Pool estreito deixa a página certa fora da triagem

RESEARCH_MAX_WAVES

4

Teto de tentativas de leitura antes de desistir. Sem ele, uma sequência ruim de links varreria a reserva inteira e estouraria a latência

RESEARCH_MAX_PER_DOMAIN

2

Máximo de URLs do mesmo domínio na reserva de candidatos. Sem teto, uma busca cujo top-10 é todo de um site enche o dossiê com um veículo só. 0 = sem limite

RESEARCH_PAGE_CHARS

25000

Teto de caracteres por página no dossiê do research_web. Existe para o outlier: uma única página gigante já rendeu 412k caracteres = 103k tokens contra 65k de contexto, e a pesquisa inteira se perdeu. Não aperte muito — dossiê pequeno demais piora o resumo

Servidor MCP

Variável

Default

O que faz

MCP_NAME

web-search

Nome que o servidor anuncia no handshake MCP

MCP_TRANSPORT

stdio

Transporte usado quando você não passa --http. Ver Uso

MCP_HOST

127.0.0.1

Interface de escuta no modo --http. Ver o aviso abaixo antes de trocar

MCP_PORT

8765

Porta de escuta no modo --http

MCP_CORS_ALLOW_ORIGINS

*

Origens liberadas no CORS do modo --http, separadas por vírgula. Só importa para cliente de navegador (ex. MCP Inspector)

TZ

(fuso do host)

Fuso usado no carimbo de data do resumo. Conveniência para container, cujo padrão é UTC. Ex. America/Sao_Paulo

Juiz do eval

Variável

Default

O que faz

EVAL_JUDGE_MODEL

(mesmo do MODEL)

Modelo usado como juiz no eval. Apontar para um modelo diferente do que escreveu o resumo torna a avaliação bem menos complacente

Sobre MCP_HOST e MCP_CORS_ALLOW_ORIGINS: o servidor não tem autenticação nenhuma. Com os defaults ele só escuta em 127.0.0.1, o que restringe o acesso à sua máquina. Trocar MCP_HOST para 0.0.0.0 expõe as tools para qualquer um na rede, sem credencial — o que permite usar seu servidor como proxy de scraping e queimar seu LLM. Se precisar compartilhar na rede, ponha um reverse proxy com autenticação na frente e restrinja MCP_CORS_ALLOW_ORIGINS às origens que você de fato usa.

De onde a configuração vem

Isto não depende do transporte: a leitura acontece no import do config, antes de stdio ou http entrarem em cena. O que decide é onde o config.py está.

Origem

Rodando do clone

Instalado (uvx / uv tool install)

.env na raiz do projeto

vale

ignorado

Variável de ambiente

vale, e sobrescreve o .env

único caminho

Default do código

fallback

fallback

load_dotenv() procura o .env subindo a partir do diretório do config.py. No clone isso chega na raiz do projeto e acha o arquivo; instalado, o config.py mora no site-packages e a busca não encontra nada — nem mesmo um .env que exista no diretório de onde o servidor foi executado. Instalado, portanto, tudo entra por variável de ambiente.

De onde sai essa variável de ambiente, aí sim depende do transporte: no stdio, do bloco env do cliente (-e), que é quem spawna o processo; no http, do ambiente de quem subiu o processo — export, systemd, compose.

A precedência é variável de ambiente > .env > default, porque o load_dotenv() roda sem override. Útil no clone para testar uma variação sem editar arquivo:

MODEL_BASE_URL=http://localhost:8205/v1 uv run web-search-mcp --http

Configurando o servidor instalado

Declare no bloco env do registro:

claude mcp add web-search \
  -e SEARXNG_URL=http://localhost:8886 \
  -e MODEL_BASE_URL=http://localhost:8200/v1 \
  -e MODEL_CONTEXT_TOKENS=65536 \
  -e TZ=America/Sao_Paulo \
  -- uvx --from git+https://github.com/fabio-barboza/web_search_mcp@v0.2.2 web-search-mcp

Note que MODEL não aparece: deixado de fora, o servidor usa o modelo já carregado no seu MODEL_BASE_URL. Passe -e MODEL=nome-do-modelo só para fixar um.

O comando acima grava isto no .mcp.json (escopo de projeto) ou no ~/.claude.json (escopo de usuário, com -s user):

{
  "mcpServers": {
    "web-search": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/fabio-barboza/web_search_mcp@v0.2.2", "web-search-mcp"],
      "env": {
        "SEARXNG_URL": "http://localhost:8886",
        "MODEL_BASE_URL": "http://localhost:8200/v1"
      }
    }
  }
}

O que entra em env fica em texto puro nesse arquivo. Para SEARXNG_URL e MODEL_BASE_URL isso não tem consequência, mas se o seu provedor de LLM exigir uma MODEL_API_KEY real, ela vai parar no .mcp.json — que costuma ser versionado quando está no escopo de projeto. Nesse caso registre com claude mcp add -s user, que grava em ~/.claude.json, fora do repositório.

A busca via Tor não pede nada no bloco env se a stack do search-engine/ estiver na mesma máquina: o default de TOR_CHANNELS já aponta para os quatro canais em 127.0.0.1. O TOR_CONTROL_PASSWORD é opcional para o MCP — sem ele só o NEWNYM desliga — e, se você passar por -e, vale a mesma ressalva da MODEL_API_KEY: fica em texto puro no arquivo.

Detecção de modelo

Quando MODEL fica vazio, cada chamada de LLM consulta GET {MODEL_BASE_URL}/models para achar o modelo já carregado, em vez de forçar um específico (evita pagar reload de GPU a troco de nada). Não há cache: se outro cliente trocou o modelo no servidor, a próxima chamada já usa o novo. Funciona nativamente com routers que expõem status de load (ex. llama.cpp/llama-swap); em providers genéricos, cai no único modelo da lista ou pede pra você preencher MODEL explicitamente se houver ambiguidade.

A mesma consulta traz, nesses routers, os argumentos com que o modelo subiu: o --ctx-size dali define o orçamento do dossiê, no lugar do MODEL_CONTEXT_TOKENS.

Testes

uv run --group test pytest

Tudo determinístico — mocka rede e LLM, não precisa de SearXNG nem de modelo rodando.

Eval

uv run python -m evals.run

Roda um conjunto fixo de perguntas contra a web e o LLM reais, mede faithfulness (afirmações do resumo suportadas pelo dossiê) e relevância, e salva o resultado em evals/results/. Não é teste de CI — a web muda entre execuções — serve para comparar rodadas e pegar alucinação escancarada, não como avaliação independente (o juiz costuma ser o mesmo modelo que escreveu o resumo).

uv run python -m evals.search_ab [--load 20] [--hl pt-BR en]

A/B da fonte de links: SearXNG × Google CSE via Tor na mesma sessão, com as mesmas variantes de busca nos dois lados, seguido de uma carga contínua que mede taxa de bloqueio por canal, failovers e quedas para o CSE direto e o SearXNG. Precisa dos canais Tor e do SearXNG no ar. Foi daqui que saíram os números de De onde vêm os links.

Estrutura

src/web_search_mcp/
  server.py         # FastMCP: tools, prompt, transporte, main()
  config.py         # única fonte de configuração (lê o .env inteiro)
  llm.py            # chat completion via requests, detecção de modelo e de contexto
  tools/
    research.py     # tool: pesquisa, lê páginas, resume com fontes
    analyze.py      # tool: lê URLs fornecidas e devolve só a análise
    read_url.py     # tool: lê uma URL específica
  util/
    search_chain.py # fonte de links: Google via Tor, SearXNG de reserva
    google_cse.py   # cliente do Google CSE, com failover entre canais
    tor.py          # canais Tor: rodízio e troca de circuito
    searxng.py      # cliente do SearXNG
    scraper.py      # download (curl_cffi, impressão TLS do Chrome) + extração (anti-SSRF)
tests/              # pytest, sem rede, sem LLM
evals/              # roda contra web/LLM reais, sob demanda
search-engine/      # docker compose da busca: SearXNG + canais Tor (dependência externa)
plans/              # planos de mudança aprovados
start.sh            # sobe search-engine/ + MCP em --http; Ctrl+C derruba tudo

Layout src/ de propósito: o pacote instalado ocupa um único namespace (web_search_mcp), em vez de despejar server/config/util na raiz do site-packages e colidir com outros pacotes.

Available Tools

3 tools
analyze_urlsA

Lê uma ou mais URLs e devolve uma análise pronta, sem o texto bruto.

Use quando o usuário fornecer a(s) URL(s) e quiser resumo, parecer técnico, opinião ou comparação entre páginas: a leitura e a análise acontecem internamente e só o resultado volta — o conteúdo integral das páginas não entra no seu contexto. Prefira read_url apenas quando o texto completo da página for necessário de verdade.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlsYes1 a 8 URLs completas (http/https), na ordem em que devem ser referidas. Para comparação, passe todas na mesma chamada.
requestNoO pedido em linguagem natural, como o usuário fez ("resuma", "dê um parecer técnico sobre a proposta", "compare os dois produtos e recomende um"). Vazio = resumo.Resuma o conteúdo.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior4/5

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

With no annotations provided, the description carries the behavioral burden and does meaningful work: it discloses that full page content will not enter the agent's context and that only the analysis result returns. It does not cover error or failure behavior, but for a URL-reading tool the key privacy/context behavior is clearly disclosed.

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

Conciseness5/5

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

The description is three sentences with no filler: a crisp purpose statement, a usage condition list, and an explicit pointer to the alternative tool. It is front-loaded and every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The output schema covers return shape, the input schema fully documents both parameters, and the description covers when to use the tool, when to avoid it, and what behavioral guarantee to expect. Although research_web is not named, the phrase 'quando o usuário fornecer a(s) URL(s)' clearly separates this tool from web research.

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?

Schema description coverage is 100% and the schema already documents URL count, ordering, comparison usage, the request default, and examples. The description reinforces the comparison use case but does not add significant parameter-level meaning beyond what the schema provides, so the baseline score of 3 is appropriate.

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

Purpose5/5

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

The first sentence states a specific verb ('Lê'), resource ('URLs'), and outcome ('devolve uma análise pronta, sem o texto bruto'), which makes the tool's function unambiguous. The negative clause about raw text also distinguishes it from read_url.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly says when to use the tool ('quando o usuário fornecer a(s) URL(s) e quiser resumo, parecer técnico, opinião ou comparação') and names the alternative for the opposite case ('Prefira read_url apenas quando o texto completo da página for necessário de verdade'). This gives the agent clear routing guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_urlA

Abre uma URL específica e devolve o conteúdo principal da página.

UMA página por chamada, e o texto bruto inteiro entra no seu contexto. Se você precisa ler VÁRIAS páginas, use analyze_urls (aceita até 8 de uma vez, lê todas, e devolve só a análise) — encadear read_url gasta contexto e tempo à toa.

O texto vem com um cabeçalho "Fonte desta página" e um link markdown pronto. Use esse link ao citar qualquer coisa que tenha lido aqui: nome de página sem endereço não é fonte, é referência que o usuário não consegue conferir.

Use quando o usuário fornecer um link e pedir para você ler, resumir, analisar ou extrair algo dele. Diferente de research_web, aqui não há busca nem resumo interno: a página é lida e o texto bruto (em Markdown) volta para você processar conforme o que foi pedido.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesO endereço completo da página (http/https).

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.7/5.0
Behavior5/5

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

Não há annotations, e a descrição assume integralmente o papel de transparência. Ela revela que uma página por chamada é lida, que o texto bruto inteiro entra no contexto, que o retorno inclui cabeçalho 'Fonte desta página' com link markdown pronto, e que não há resumo interno — comportamento relevante para o agente saber o que esperar.

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

Conciseness5/5

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

Embora mais longa que o mínimo, cada frase agrega valor: scopo, aviso de contexto, alternativa, instrução de citação e comparação com o irmão. A informação essencial está na frente e a estrutura é clara, sem redundância.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Para uma ferramenta com um único parâmetro, output schema presente e um irmão alternativo, a descrição cobre uso, diferenças, retorno e implicações de contexto. Nada essencial para selecionar e invocar corretamente está ausente.

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?

Há apenas um parâmetro, url, com description no schema cobrindo 100% do significado ('O endereço completo da página (http/https)'). A descrição do tool acrescenta pouco além de reforçar que é uma URL específica, então o baseline 3 é adequado já que o schema faz o trabalho.

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

Purpose5/5

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

A descrição usa verbo específico ('Abre uma URL específica') e recurso claro ('devolve o conteúdo principal da página'), diferenciando-se imediatamente dos irmãos: analyze_urls lê várias páginas e research_web faz busca/resumo. A função e o escopo de uma chamada ficam inequívocos.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Há orientação explícita de quando usar: 'quando o usuário fornecer um link e pedir para você ler, resumir, analisar ou extrair algo dele'. Também indica explicitamente a alternativa para múltiplas páginas ('encadear read_url gasta contexto e tempo à toa') e a diferença frente a research_web, que não faz busca nem resumo interno.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

research_webA

Pesquisa na web e devolve um resumo com fontes.

Use para qualquer informação que você não saiba com certeza — e também quando acha que sabe mas o assunto pode ter mudado desde o seu treino: nesses casos, prefira pesquisar a responder de memória. Passe a pergunta completa em linguagem natural — a busca, a leitura das páginas e o resumo são feitos internamente.

UMA CHAMADA POR PERGUNTA. Esta ferramenta já reformula a pergunta em vários ângulos de busca por dentro, roda todos em paralelo e lê as melhores páginas do conjunto. Chamar de novo com a mesma pergunta escrita de outro jeito não traz material novo: relê as mesmas páginas e gasta o mesmo tempo outra vez. Só chame outra vez quando a pergunta for genuinamente outra, ou quando o resumo apontar o que faltou. Pergunta NOVA do usuário = chamada nova, mesmo que seja sobre o mesmo assunto de antes: cada pergunta diferente merece sua própria pesquisa.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryYesA pergunta completa em linguagem natural, do jeito que o usuário faria. Não reduza a palavras-chave nem parta em pedaços: a reformulação em termos de busca é feita aqui dentro, e uma pergunta inteira dá um resultado melhor que um fragmento.
recentNoTrue apenas quando a resposta depende do dia de hoje (clima, cotação, placar, notícia de agora). False para fatos estáveis (história, biografia, conceitos, documentação), pois filtrar por data descarta as fontes boas.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.1/5.0
Behavior5/5

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

With no annotations, the description carries the full burden, and it delivers: it discloses that the tool internally reformulates the query into multiple search angles, runs them in parallel, reads the best pages, and summarizes. It also reveals that calling again with the same question yields no new material, which is a significant behavioral trait. This goes far beyond a basic purpose statement and provides actionable insight for the agent.

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 front-loaded with the core purpose ('Pesquisa na web e devolve um resumo com fontes') and then provides essential usage and behavioral guidance. It is somewhat verbose, especially the repeated explanation of one-call-per-question, but every sentence adds value. It is well-structured into coherent paragraphs, though it could be tightened.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is relatively simple (search + summarize), and the description covers when to use it, how to phrase the query, and the one-call-per-question rule. The output schema exists, so the description does not need to explain return formats. Annotations are absent, but the description provides all necessary behavioral context for an agent to call the tool correctly. Nothing essential is missing.

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?

Schema coverage is 100% – both 'query' and 'recent' have detailed descriptions in the schema. The description's instructions on passing the full natural-language query are already present in the schema, so it adds little over the structured definitions. The baseline of 3 is appropriate because the schema already explains parameters thoroughly.

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

Purpose4/5

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

The description clearly states the tool 'Pesquisa na web e devolve um resumo com fontes' (searches the web and returns a summary with sources), which is a specific verb and resource. It clearly differentiates from 'read_url' and 'analyze_urls' by focusing on autonomous searching rather than processing given URLs, though it does not explicitly name the siblings. The purpose is unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use guidance: 'Use para qualquer informação que você não saiba com certeza' and 'quando acha que sabe mas o assunto pode ter mudado' – effectively stating to use this tool instead of memory when uncertain or potentially outdated. It also provides a clear when-not-to-call repeatedly: 'Só chame outra vez quando a pergunta for genuinamente outra'. However, it does not explicitly mention alternatives like read_url or analyze_urls, so it stops short of full alternative routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.1.0
    • First observedanalyze_urls
    • First observedread_url
    • First observedresearch_web

TDQS

A4.5/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: read_url returns raw content, analyze_urls returns an analysis of provided URLs, and research_web performs a web search with summarized results. Descriptions explicitly cross-reference the tools to prevent confusion.

Naming Consistency5/5

All tool names follow the same verb_noun snake_case pattern: read_url, analyze_urls, research_web. The naming is predictable and immediately communicates the action and target.

Tool Count5/5

Three tools is well-scoped for a web research server: one for raw reading, one for URL analysis, and one for open-ended search. Each tool covers a distinct workflow without unnecessary redundancy.

Completeness5/5

The tool surface covers the full web research workflow: searching, reading a page in full, and analyzing one or more provided URLs. There are no obvious missing operations that would block an agent from completing typical research tasks.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    An MCP server that enables web searching, URL content extraction, and summarization without requiring API keys. It also provides advanced mathematical evaluation and multi-language Wikipedia summary retrieval tools.
    5
    352 npm
    6
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A self-contained web-research MCP server that lets local LLM agents search, fetch, and synthesize web content using tools like web_search, web_fetch, and web_research.
    2
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Multi-purpose research MCP server integrating web search, deep research, web scraping, research methodology routing, and GPT Researcher report generation.
    MIT