ScanMalware.com URL Scanner

Servidor MCP para escaneamento de URLs, detecção de malware e análise do ScanMalware.com

Documentação

scanmalware-mcp

Servidor MCP Python mínimo que encapsula a API pública do ScanMalware.com.

Operações

Consulte docs/OPERATIONS.md para implantação, TLS, registro de logs e como conectar-se ao droplet da DigitalOcean.

Executar localmente (HTTP Streamable)

python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install .

export MCP_TRANSPORT=streamable-http
export MCP_HOST=127.0.0.1
export MCP_PORT=8000

scanmalware-mcp

Executar com Docker

docker build -t scanmalware-mcp .
docker run --rm -p 127.0.0.1:8000:8000 \\
  -e MCP_TRANSPORT=streamable-http \\
  -e MCP_HOST=0.0.0.0 \\
  -e MCP_PORT=8000 \\
  scanmalware-mcp

Opcional: defina MCP_AUTH_TOKEN para exigir Authorization: Bearer <MCP_AUTH_TOKEN> para transportes HTTP.

Variáveis de ambiente de autenticação opcionais (necessárias apenas para endpoints protegidos por autenticação):

  • SCANMALWARE_BEARER_TOKEN

Outras variáveis de ambiente:

  • SCANMALWARE_BASE_URL (padrão: https://scanmalware.com)
  • SCANMALWARE_ALLOW_HTTP (padrão: false)
  • SCANMALWARE_TIMEOUT_S (padrão: 30)
  • SCANMALWARE_MAX_DOWNLOAD_BYTES (padrão: 10485760)
  • SCANMALWARE_ALLOW_PRIVATE_TARGETS (padrão: false)
  • SCANMALWARE_CA_CERT (opcional; caminho para um pacote de certificados CA para SSL bump)

Variáveis de ambiente de segurança do servidor MCP:

  • MCP_AUTH_TOKEN (se definido, transportes HTTP exigem Authorization: Bearer <token>)
  • MCP_RESOURCE_SERVER_URL / MCP_ISSUER_URL (opcional; usado apenas quando MCP_AUTH_TOKEN está definido)

Nota sobre transporte: os transportes HTTP operam sem estado (stateless_http=True), portanto as respostas não carregam nenhum cabeçalho mcp-session-id. Os clientes não devem exigir um. Isso mantém o estado por sessão - e, portanto, a memória - constante.

Nota sobre ferramentas: submit_scan não chama /api/v1/csrf-token; não há ferramenta de token CSRF. Nota sobre ferramentas: alguns endpoints upstream estão desabilitados e excluídos da lista de ferramentas (por exemplo, get_improvements, find_screenshot_duplicates, get_ai_stats, search_js_fingerprinter2_code_hash, search_js_segments_by_tlsh). Algumas ferramentas de busca exigem pelo menos um filtro e gerarão um erro de validação se nenhum for fornecido.

Exemplos de prompts

Triagem de phishing (enviar → aguardar → resumir):

Submit a scan for https://example-login-update.com, wait for completion, and
return status, risk_score, and the top indicators. If high risk, include the
AI analysis and screenshot resource.

Monitoramento de abuso de marca:

Search scans for "acme login" (limit 5). For each result, list scan_id,
status, risk_score, and URL. Highlight anything marked high risk.

Inspeção de TLS/certificados:

For scan_id 1234...abcd, fetch TLS details and the certificate PEM download.
Summarize issuer, subject, validity dates, and SANs; flag mismatches.

Implantar na DigitalOcean (Debian + Docker + Nginx)

O pacote de implantação está em deploy/ e executa dois contêineres:

  • mcp (este servidor, HTTP streamable na porta 8000)
  • nginx (frontend na porta 80; proxy /mcp para o servidor MCP)

Pré-requisitos

  • doctl autenticado (doctl auth init)
  • Chave SSH enviada para a DigitalOcean (usada por doctl compute droplet create)

Criar um droplet pequeno na Alemanha (Frankfurt)

DROPLET_NAME=scanmalware-mcp-small
REGION=fra1
SIZE=s-1vcpu-2gb
IMAGE=debian-12-x64
SSH_KEYS=$(doctl compute ssh-key list --format ID --no-header | paste -sd, -)

doctl compute droplet create "$DROPLET_NAME" \
  --region "$REGION" \
  --size "$SIZE" \
  --image "$IMAGE" \
  --ssh-keys "$SSH_KEYS" \
  --tag-name scanmalware-mcp \
  --wait

Firewall (HTTP/HTTPS público + SSH)

doctl compute firewall create \
  --name scanmalware-mcp-fw \
  --inbound-rules "protocol:tcp,ports:22,address:0.0.0.0/0,address:::0/0" \
  --inbound-rules "protocol:tcp,ports:80,address:0.0.0.0/0,address:::0/0" \
  --inbound-rules "protocol:tcp,ports:443,address:0.0.0.0/0,address:::0/0" \
  --outbound-rules "protocol:icmp,ports:0,address:0.0.0.0/0,address:::0/0" \
  --outbound-rules "protocol:tcp,ports:0,address:0.0.0.0/0,address:::0/0" \
  --outbound-rules "protocol:udp,ports:0,address:0.0.0.0/0,address:::0/0" \
  --droplet-ids <droplet-id>

Instalar Docker + compose no droplet

ssh -i /path/to/key root@<droplet-ip> \
  "apt-get update -y && apt-get install -y docker.io docker-compose"

Enviar e executar

tar --exclude=.git --exclude=.venv --exclude=__pycache__ -czf /tmp/scanmalware-mcp.tar.gz -C . .
scp -i /path/to/key /tmp/scanmalware-mcp.tar.gz root@<droplet-ip>:/tmp/
ssh -i /path/to/key root@<droplet-ip> \
  "mkdir -p /opt/scanmalware-mcp && tar -xzf /tmp/scanmalware-mcp.tar.gz -C /opt/scanmalware-mcp"
ssh -i /path/to/key root@<droplet-ip> \
  "cd /opt/scanmalware-mcp && docker-compose -f deploy/docker-compose.yml up -d --build"

Verificar

curl -I https://mcp.scanmalware.com/
curl -I https://mcp.scanmalware.com/mcp

/ deve retornar 200 do Nginx. /mcp retorna 406 em GET sem cabeçalhos Accept do MCP, o que é esperado.

Teste rápido (inicialização MCP + tools/list)

python - <<'PY'
import json
import httpx

URL = "http://<droplet-ip>/mcp"
HEADERS = {
    "accept": "application/json, text/event-stream",
    "content-type": "application/json",
}

init_payload = {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
        "protocolVersion": "2025-06-18",
        "capabilities": {},
        "clientInfo": {"name": "mcp-smoke-test", "version": "0.1.0"},
    },
}

with httpx.Client(timeout=10) as client:
    init_resp = client.post(URL, headers=HEADERS, json=init_payload)
    init_resp.raise_for_status()
    session_id = init_resp.headers.get("mcp-session-id")

    def extract_sse_data(text: str) -> dict:
        for line in text.splitlines():
            if line.startswith("data: "):
                return json.loads(line[len("data: "):])
        raise ValueError("No SSE data line found")

    init_message = extract_sse_data(init_resp.text)
    protocol_version = init_message["result"]["protocolVersion"]

    # Send initialized notification
    client.post(
        URL,
        headers={
            **HEADERS,
            "mcp-session-id": session_id,
            "mcp-protocol-version": protocol_version,
        },
        json={"jsonrpc": "2.0", "method": "notifications/initialized"},
    )

    tools_resp = client.post(
        URL,
        headers={
            **HEADERS,
            "mcp-session-id": session_id,
            "mcp-protocol-version": protocol_version,
        },
        json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
    )
    tools_resp.raise_for_status()
    tools_message = extract_sse_data(tools_resp.text)
    tool_names = [tool["name"] for tool in tools_message["result"]["tools"]]

print("protocol_version:", protocol_version)
print("tool_count:", len(tool_names))
print("tools:", ", ".join(tool_names))
PY

Reimplantar / novas implantações

Dois fluxos comuns:

  1. Atualização no local (mesmo droplet)
tar --exclude=.git --exclude=.venv --exclude=__pycache__ -czf /tmp/scanmalware-mcp.tar.gz -C . .
scp -i /path/to/key /tmp/scanmalware-mcp.tar.gz root@<droplet-ip>:/tmp/
ssh -i /path/to/key root@<droplet-ip> \
  "bash /opt/scanmalware-mcp/deploy/redeploy.sh /tmp/scanmalware-mcp.tar.gz"

O script de reimplantação interrompe os contêineres antes de trocar os arquivos para evitar problemas de inode com bind-mount. Se o script ainda não estiver no droplet, execute o comando legado de tar + docker-compose uma vez para instalá-lo.

Helper opcional de uma única execução a partir da raiz do repositório:

./deploy/push-redeploy.sh root@<droplet-ip> /path/to/key
  1. Implantação contínua (novo droplet)
  • Crie um novo droplet (etapas acima)
  • Implante o mesmo pacote
  • Alterne o DNS para o novo IP
  • Destrua o droplet antigo quando estiver pronto
doctl compute droplet delete <old-droplet-id> --force