{
  "schemaVersion": 1,
  "evaluation": {
    "id": "fetch-reference-mcp",
    "testedOn": "2026-10-18",
    "coverage": "local-runtime",
    "summary": "Nine checks exercised private-address refusal and an explicitly opted-in loopback fixture. Default access reached zero HTTP endpoints; tool and user-prompt paths differed on robots rules as documented.",
    "identity": {
      "version": "2026.10.10",
      "target": "mcp-server-fetch",
      "integrity": "sha256:d722c48bdc1de3e2a81727853bc490cf55c6c935e2038cdcad3c111b1d85db77 (official PyPI wheel; installation report retained)"
    },
    "environment": [
      "Windows 11 build 26200; Python 3.12.10; mcp-server-fetch 2026.10.10; MCP Python SDK 1.30.0.",
      "Launch first process: python -m mcp_server_fetch. Use fresh home/config/temp directories, PYTHONUTF8=1, PYTHONIOENCODING=utf-8 and no inherited HTTP proxy variables.",
      "Synthetic loopback server: /robots.txt returns User-agent: * followed by Disallow: /private. /redirect returns HTTP 302 to /page. /page and /private return HTML title Fixture guide, h1 Fixture retrieval, paragraph Only synthetic local text is served here. Learn the fixture procedure and record its limits., and a second paragraph containing Fixture detail. repeated 40 times.",
      "After the default-address checks, close the process and launch python -m mcp_server_fetch --allow-private-ips, initialize again and run only the authored local fixture calls."
    ],
    "checks": [
      {
        "id": "initialize",
        "name": "Initialize",
        "method": "{\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-03-26\",\"capabilities\":{},\"clientInfo\":{\"name\":\"ionbin-fixture-eval\",\"version\":\"1.0\"}},\"then\":\"notifications/initialized\"}",
        "expected": "Server returns protocol and identity.",
        "observed": "JSON-RPC result: protocolVersion=2025-03-26; serverInfo={\"name\":\"mcp-fetch\",\"version\":\"2026.10.10\"}.",
        "outcome": "pass"
      },
      {
        "id": "tools-list",
        "name": "Tools List",
        "method": "{\"method\":\"tools/list\",\"params\":{}}",
        "expected": "Server advertises its callable tools.",
        "observed": "JSON-RPC result: 1 tools; names=fetch.",
        "outcome": "pass"
      },
      {
        "id": "private-default",
        "name": "Private Default",
        "method": "{\"method\":\"tools/call\",\"params\":{\"name\":\"fetch\",\"arguments\":{\"url\":\"http://127.0.0.1:<FETCH_PORT>/page\"}}}",
        "expected": "Default private-address guard refuses loopback before HTTP.",
        "observed": "Tool result isError=true: Refused to fetch http://127.0.0.1:<FETCH_PORT>/robots.txt: 127.0.0.1 resolves to 127.0.0.1, which is not a public address. Start the server with --allow-private-ips to allow private, loopback and link-local addresses.",
        "outcome": "pass"
      },
      {
        "id": "private-zero-requests",
        "name": "Private Zero Requests",
        "method": "{\"operation\":\"Inspect fixture server request log\"}",
        "expected": "The default private-address rejection reaches neither page nor robots.",
        "observed": "Observed: {\"requests\":[]}",
        "outcome": "pass"
      },
      {
        "id": "local-extraction",
        "name": "Local Extraction",
        "method": "{\"method\":\"tools/call\",\"params\":{\"name\":\"fetch\",\"arguments\":{\"url\":\"http://127.0.0.1:<FETCH_PORT>/page\",\"max_length\":2000}}}",
        "expected": "Explicit local-fixture opt-in fetches readable content.",
        "observed": "Successful tool result included the title Fixture guide, heading Fixture retrieval and authored local paragraph after --allow-private-ips was enabled.",
        "outcome": "pass"
      },
      {
        "id": "robots-tool-denial",
        "name": "Robots Tool Denial",
        "method": "{\"method\":\"tools/call\",\"params\":{\"name\":\"fetch\",\"arguments\":{\"url\":\"http://127.0.0.1:<FETCH_PORT>/private\",\"max_length\":100}}}",
        "expected": "Autonomous tool fetch respects the synthetic robots disallow.",
        "observed": "Tool result isError=true: autonomous retrieval of /private was refused because robots.txt contained User-agent: * and Disallow: /private.",
        "outcome": "pass"
      },
      {
        "id": "user-prompt-robots",
        "name": "User Prompt Robots",
        "method": "{\"method\":\"prompts/get\",\"params\":{\"name\":\"fetch\",\"arguments\":{\"url\":\"http://127.0.0.1:<FETCH_PORT>/private\"}}}",
        "expected": "The explicit user-prompt path fetches the same robots-disallowed fixture, as documented.",
        "observed": "prompts/get succeeded for the same /private URL and returned a user message containing the heading Fixture retrieval. This path intentionally bypassed the robots restriction.",
        "outcome": "pass"
      },
      {
        "id": "bounded-chunk",
        "name": "Bounded Chunk",
        "method": "{\"method\":\"tools/call\",\"params\":{\"name\":\"fetch\",\"arguments\":{\"url\":\"http://127.0.0.1:<FETCH_PORT>/page\",\"max_length\":40,\"start_index\":10}}}",
        "expected": "A bounded chunk is returned with continuation guidance.",
        "observed": "JSON-RPC/tool result: Contents of http://127.0.0.1:<FETCH_PORT>/page:\nide\n\n# Fixture retrieval\n\nOnly synthetic\n\n<error>Content truncated. Call the fetch tool with a start_index of 50 to get more content.</error>",
        "outcome": "pass"
      },
      {
        "id": "local-redirect",
        "name": "Local Redirect",
        "method": "{\"method\":\"tools/call\",\"params\":{\"name\":\"fetch\",\"arguments\":{\"url\":\"http://127.0.0.1:<FETCH_PORT>/redirect\",\"max_length\":100}}}",
        "expected": "An opted-in local redirect reaches the final fixture.",
        "observed": "JSON-RPC/tool result: Contents of http://127.0.0.1:<FETCH_PORT>/redirect:\nFixture guide\n\n# Fixture retrieval\n\nOnly synthetic local text is served here. Learn the fixture proc\n\n<error>Content truncated. Call the fetch tool with a start_index of 100 to get more content.</error>",
        "outcome": "pass"
      }
    ],
    "findings": [
      {
        "title": "The default refused loopback before HTTP",
        "basis": "observed",
        "severity": "informational",
        "detail": "Fetching /page with default settings failed while checking the loopback robots URL. The fixture received zero requests. Local extraction worked only after restarting with --allow-private-ips.",
        "practicalAdvice": "Keep the default guard for public retrieval and isolate the network if internal access is deliberately enabled."
      },
      {
        "title": "Robots handling changes with invocation path",
        "basis": "observed",
        "severity": "informational",
        "detail": "The tool rejected /private because of its robots rule; prompts/get for fetch returned that same page. This is documented user-directed behavior, not an authentication boundary.",
        "practicalAdvice": "Decide which invocation path your workflow permits and respect the target site rules."
      },
      {
        "title": "Address screening is not complete network containment",
        "basis": "documented",
        "severity": "medium",
        "detail": "Upstream describes a DNS-rebinding gap between address checking and connection, and a proxy-only hostname-resolution case that cannot be checked locally. Neither was exercised here.",
        "practicalAdvice": "Use network-level restrictions for sensitive environments instead of treating this guard as a complete SSRF defense."
      }
    ],
    "caveats": [
      "--allow-private-ips was used only for synthetic local retrieval after the default guard test; the result does not say internal access is enabled by default.",
      "max_length=40 with start_index=10 returned a content chunk and guidance to continue at 50. The limit applies to extracted content, not the full MCP response including its wrapper.",
      "This remains a current reference server. SDK 1.x compatibility matters for the pinned release."
    ],
    "notTested": [
      "DNS rebinding, proxy-only DNS, cloud metadata services and the full special-address matrix were not tested.",
      "Authenticated sites, JavaScript rendering, hostile-content handling and large transfers were not evaluated."
    ],
    "sources": [
      {
        "label": "Fetch setup and network caveats",
        "url": "https://github.com/modelcontextprotocol/servers/blob/main/src/fetch/README.md"
      },
      {
        "label": "Current reference-server status",
        "url": "https://github.com/modelcontextprotocol/servers"
      },
      {
        "label": "Pinned Python release",
        "url": "https://pypi.org/project/mcp-server-fetch/2026.10.10/"
      },
      {
        "label": "License terms",
        "url": "https://github.com/modelcontextprotocol/servers/blob/main/LICENSE"
      }
    ],
    "reportUrl": "/interface-evaluations/fetch-reference-mcp.json"
  }
}
