{
  "markdown": "# scanmalware-mcp\n\nMinimal Python MCP server that wraps the public ScanMalware.com API.\n\n## Operations\n\nSee `docs/OPERATIONS.md` for deployment, TLS, logging, and how to connect to the DigitalOcean droplet.\n\n## Run locally (Streamable HTTP)\n\n```bash\npython -m venv .venv\nsource .venv/bin/activate\npip install -U pip\npip install .\n\nexport MCP_TRANSPORT=streamable-http\nexport MCP_HOST=127.0.0.1\nexport MCP_PORT=8000\n\nscanmalware-mcp\n```\n\n## Run with Docker\n\n```bash\ndocker build -t scanmalware-mcp .\ndocker run --rm -p 127.0.0.1:8000:8000 \\\\\n  -e MCP_TRANSPORT=streamable-http \\\\\n  -e MCP_HOST=0.0.0.0 \\\\\n  -e MCP_PORT=8000 \\\\\n  scanmalware-mcp\n```\n\nOptional: set `MCP_AUTH_TOKEN` to require `Authorization: Bearer <MCP_AUTH_TOKEN>` for HTTP transports.\n\nOptional auth env vars (only needed for auth-gated endpoints):\n\n- `SCANMALWARE_BEARER_TOKEN`\n\nOther env vars:\n\n- `SCANMALWARE_BASE_URL` (default: `https://scanmalware.com`)\n- `SCANMALWARE_ALLOW_HTTP` (default: `false`)\n- `SCANMALWARE_TIMEOUT_S` (default: `30`)\n- `SCANMALWARE_MAX_DOWNLOAD_BYTES` (default: `10485760`)\n- `SCANMALWARE_ALLOW_PRIVATE_TARGETS` (default: `false`)\n- `SCANMALWARE_CA_CERT` (optional; path to a CA bundle for SSL bump)\n\nMCP server security env vars:\n\n- `MCP_AUTH_TOKEN` (if set, HTTP transports require `Authorization: Bearer <token>`)\n- `MCP_RESOURCE_SERVER_URL` / `MCP_ISSUER_URL` (optional; only used when `MCP_AUTH_TOKEN` is set)\n\nTool note: `submit_scan` does not call `/api/v1/csrf-token`; there is no CSRF token tool.\nTool note: some upstream endpoints are disabled and excluded from the tool list (e.g., `get_improvements`, `find_screenshot_duplicates`, `get_ai_stats`, `search_js_fingerprinter2_code_hash`, `search_js_segments_by_tlsh`).\nSome search tools require at least one filter and will raise a validation error if none are provided.\n\n## Example prompts\n\nPhishing triage (submit → wait → summarize):\n```text\nSubmit a scan for https://example-login-update.com, wait for completion, and\nreturn status, risk_score, and the top indicators. If high risk, include the\nAI analysis and screenshot resource.\n```\n\nBrand abuse monitoring:\n```text\nSearch scans for \"acme login\" (limit 5). For each result, list scan_id,\nstatus, risk_score, and URL. Highlight anything marked high risk.\n```\n\nTLS/certificate inspection:\n```text\nFor scan_id 1234...abcd, fetch TLS details and the certificate PEM download.\nSummarize issuer, subject, validity dates, and SANs; flag mismatches.\n```\n\n## Deploy to DigitalOcean (Debian + Docker + Nginx)\n\nThe deploy bundle lives in `deploy/` and runs two containers:\n- `mcp` (this server, streamable HTTP on port 8000)\n- `nginx` (frontend on port 80; proxies `/mcp` to the MCP server)\n\n### Prereqs\n\n- `doctl` authenticated (`doctl auth init`)\n- SSH key uploaded to DigitalOcean (used by `doctl compute droplet create`)\n\n### Create a small droplet in Germany (Frankfurt)\n\n```bash\nDROPLET_NAME=scanmalware-mcp-small\nREGION=fra1\nSIZE=s-1vcpu-2gb\nIMAGE=debian-12-x64\nSSH_KEYS=$(doctl compute ssh-key list --format ID --no-header | paste -sd, -)\n\ndoctl compute droplet create \"$DROPLET_NAME\" \\\n  --region \"$REGION\" \\\n  --size \"$SIZE\" \\\n  --image \"$IMAGE\" \\\n  --ssh-keys \"$SSH_KEYS\" \\\n  --tag-name scanmalware-mcp \\\n  --wait\n```\n\n### Firewall (public HTTP/HTTPS + SSH)\n\n```bash\ndoctl compute firewall create \\\n  --name scanmalware-mcp-fw \\\n  --inbound-rules \"protocol:tcp,ports:22,address:0.0.0.0/0,address:::0/0\" \\\n  --inbound-rules \"protocol:tcp,ports:80,address:0.0.0.0/0,address:::0/0\" \\\n  --inbound-rules \"protocol:tcp,ports:443,address:0.0.0.0/0,address:::0/0\" \\\n  --outbound-rules \"protocol:icmp,ports:0,address:0.0.0.0/0,address:::0/0\" \\\n  --outbound-rules \"protocol:tcp,ports:0,address:0.0.0.0/0,address:::0/0\" \\\n  --outbound-rules \"protocol:udp,ports:0,address:0.0.0.0/0,address:::0/0\" \\\n  --droplet-ids <droplet-id>\n```\n\n### Install Docker + compose on the droplet\n\n```bash\nssh -i /path/to/key root@<droplet-ip> \\\n  \"apt-get update -y && apt-get install -y docker.io docker-compose\"\n```\n\n### Upload and run\n\n```bash\ntar --exclude=.git --exclude=.venv --exclude=__pycache__ -czf /tmp/scanmalware-mcp.tar.gz -C . .\nscp -i /path/to/key /tmp/scanmalware-mcp.tar.gz root@<droplet-ip>:/tmp/\nssh -i /path/to/key root@<droplet-ip> \\\n  \"mkdir -p /opt/scanmalware-mcp && tar -xzf /tmp/scanmalware-mcp.tar.gz -C /opt/scanmalware-mcp\"\nssh -i /path/to/key root@<droplet-ip> \\\n  \"cd /opt/scanmalware-mcp && docker-compose -f deploy/docker-compose.yml up -d --build\"\n```\n\n### Verify\n\n```bash\ncurl -I https://mcp.scanmalware.com/\ncurl -I https://mcp.scanmalware.com/mcp\n```\n\n`/` should return 200 from Nginx. `/mcp` returns 406 on GET without MCP Accept headers, which is expected.\n\n### Smoke test (MCP initialize + tools/list)\n\n```bash\npython - <<'PY'\nimport json\nimport httpx\n\nURL = \"http://<droplet-ip>/mcp\"\nHEADERS = {\n    \"accept\": \"application/json, text/event-stream\",\n    \"content-type\": \"application/json\",\n}\n\ninit_payload = {\n    \"jsonrpc\": \"2.0\",\n    \"id\": 1,\n    \"method\": \"initialize\",\n    \"params\": {\n        \"protocolVersion\": \"2025-06-18\",\n        \"capabilities\": {},\n        \"clientInfo\": {\"name\": \"mcp-smoke-test\", \"version\": \"0.1.0\"},\n    },\n}\n\nwith httpx.Client(timeout=10) as client:\n    init_resp = client.post(URL, headers=HEADERS, json=init_payload)\n    init_resp.raise_for_status()\n    session_id = init_resp.headers.get(\"mcp-session-id\")\n\n    def extract_sse_data(text: str) -> dict:\n        for line in text.splitlines():\n            if line.startswith(\"data: \"):\n                return json.loads(line[len(\"data: \"):])\n        raise ValueError(\"No SSE data line found\")\n\n    init_message = extract_sse_data(init_resp.text)\n    protocol_version = init_message[\"result\"][\"protocolVersion\"]\n\n    # Send initialized notification\n    client.post(\n        URL,\n        headers={\n            **HEADERS,\n            \"mcp-session-id\": session_id,\n            \"mcp-protocol-version\": protocol_version,\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"notifications/initialized\"},\n    )\n\n    tools_resp = client.post(\n        URL,\n        headers={\n            **HEADERS,\n            \"mcp-session-id\": session_id,\n            \"mcp-protocol-version\": protocol_version,\n        },\n        json={\"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/list\"},\n    )\n    tools_resp.raise_for_status()\n    tools_message = extract_sse_data(tools_resp.text)\n    tool_names = [tool[\"name\"] for tool in tools_message[\"result\"][\"tools\"]]\n\nprint(\"protocol_version:\", protocol_version)\nprint(\"tool_count:\", len(tool_names))\nprint(\"tools:\", \", \".join(tool_names))\nPY\n```\n\n### Redeploy / new deploys\n\nTwo common flows:\n\n1) In-place update (same droplet)\n```bash\ntar --exclude=.git --exclude=.venv --exclude=__pycache__ -czf /tmp/scanmalware-mcp.tar.gz -C . .\nscp -i /path/to/key /tmp/scanmalware-mcp.tar.gz root@<droplet-ip>:/tmp/\nssh -i /path/to/key root@<droplet-ip> \\\n  \"bash /opt/scanmalware-mcp/deploy/redeploy.sh /tmp/scanmalware-mcp.tar.gz\"\n```\nThe redeploy script stops containers before swapping files to avoid bind-mount inode issues.\nIf the script is not on the droplet yet, run the legacy tar + docker-compose command once to install it.\n\nOptional one-shot helper from the repo root:\n```bash\n./deploy/push-redeploy.sh root@<droplet-ip> /path/to/key\n```\n\n2) Rolling deploy (new droplet)\n- Create a new droplet (steps above)\n- Deploy the same bundle\n- Switch DNS to the new IP\n- Destroy the old droplet when ready\n\n```bash\ndoctl compute droplet delete <old-droplet-id> --force\n```\n",
  "bytes": 7485,
  "sha": "0aad1b55997042a9693a9ce89f8adb38df957966cbff7bd7d984d70840ca0156",
  "repo_slug": "scanmalware/mcp-server",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_scanmalware_mcp_scanmalware_mcp_0e236856/readme"
}