Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/fetch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,27 @@ This can be customized by adding the argument `--user-agent=YourUserAgent` to th

The server can be configured to use a proxy by using the `--proxy-url` argument.

### Customization - SSL Verification

By default, the server verifies SSL certificates for all HTTPS requests. For internal servers with self-signed certificates, you can disable SSL verification by setting the `MCP_FETCH_SSL_VERIFY` environment variable to `false`:

```json
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"],
"env": {
"MCP_FETCH_SSL_VERIFY": "false"
}
}
}
}
```

> [!WARNING]
> Disabling SSL verification reduces security. Only use this option in trusted environments with internal servers that have self-signed certificates.

## Windows Configuration

If you're experiencing timeout issues on Windows, you may need to set the `PYTHONIOENCODING` environment variable to ensure proper character encoding:
Expand Down
7 changes: 6 additions & 1 deletion src/fetch/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3"]
dev-dependencies = [
"pyright>=1.1.389",
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"ruff>=0.7.3",
]
68 changes: 62 additions & 6 deletions src/fetch/src/mcp_server_fetch/server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os
import ssl
from typing import Annotated, Tuple
from urllib.parse import urlparse, urlunparse

Expand All @@ -20,6 +22,15 @@
from protego import Protego
from pydantic import BaseModel, Field, AnyUrl

# =============================================================================
# SSL CONFIGURATION
# =============================================================================
# Set MCP_FETCH_SSL_VERIFY=false to disable SSL certificate verification.
# This is useful for internal servers with self-signed certificates.
# WARNING: Disabling SSL verification reduces security. Only use in trusted environments.
# NOTE: Only explicit "false" disables verification; any other value keeps it enabled (fail-secure).
SSL_VERIFY = os.getenv("MCP_FETCH_SSL_VERIFY", "true").lower() != "false"

DEFAULT_USER_AGENT_AUTONOMOUS = "ModelContextProtocol/1.0 (Autonomous; +https://github.com/modelcontextprotocol/servers)"
DEFAULT_USER_AGENT_MANUAL = "ModelContextProtocol/1.0 (User-Specified; +https://github.com/modelcontextprotocol/servers)"

Expand Down Expand Up @@ -68,18 +79,41 @@ async def check_may_autonomously_fetch_url(url: str, user_agent: str, proxy_url:
Check if the URL can be fetched by the user agent according to the robots.txt file.
Raises a McpError if not.
"""
from httpx import AsyncClient, HTTPError
import httpx

robot_txt_url = get_robots_txt_url(url)

async with AsyncClient(proxies=proxy_url) as client:
async with httpx.AsyncClient(proxies=proxy_url, verify=SSL_VERIFY) as client:
try:
response = await client.get(
robot_txt_url,
follow_redirects=True,
headers={"User-Agent": user_agent},
timeout=30,
)
except HTTPError:
except ssl.SSLError as e:
raise McpError(ErrorData(
code=INTERNAL_ERROR,
message=f"SSL Certificate verification failed for {robot_txt_url}. "
f"If this is an internal server with a self-signed certificate, "
f"set MCP_FETCH_SSL_VERIFY=false in your environment. "
f"Error details: {str(e)}",
))
except httpx.ConnectError as e:
error_str = str(e).lower()
if "ssl" in error_str or "certificate" in error_str or "verify" in error_str:
raise McpError(ErrorData(
code=INTERNAL_ERROR,
message=f"SSL Certificate verification failed for {robot_txt_url}. "
f"If this is an internal server with a self-signed certificate, "
f"set MCP_FETCH_SSL_VERIFY=false in your environment. "
f"Error details: {str(e)}",
))
raise McpError(ErrorData(
code=INTERNAL_ERROR,
message=f"Failed to connect to {robot_txt_url}: {str(e)}",
))
except httpx.HTTPError:
raise McpError(ErrorData(
code=INTERNAL_ERROR,
message=f"Failed to fetch robots.txt {robot_txt_url} due to a connection issue",
Expand Down Expand Up @@ -114,17 +148,39 @@ async def fetch_url(
"""
Fetch the URL and return the content in a form ready for the LLM, as well as a prefix string with status information.
"""
from httpx import AsyncClient, HTTPError
import httpx

async with AsyncClient(proxies=proxy_url) as client:
async with httpx.AsyncClient(proxies=proxy_url, verify=SSL_VERIFY) as client:
try:
response = await client.get(
url,
follow_redirects=True,
headers={"User-Agent": user_agent},
timeout=30,
)
except HTTPError as e:
except ssl.SSLError as e:
raise McpError(ErrorData(
code=INTERNAL_ERROR,
message=f"SSL Certificate verification failed for {url}. "
f"If this is an internal server with a self-signed certificate, "
f"set MCP_FETCH_SSL_VERIFY=false in your environment. "
f"Error details: {str(e)}",
))
except httpx.ConnectError as e:
error_str = str(e).lower()
if "ssl" in error_str or "certificate" in error_str or "verify" in error_str:
raise McpError(ErrorData(
code=INTERNAL_ERROR,
message=f"SSL Certificate verification failed for {url}. "
f"If this is an internal server with a self-signed certificate, "
f"set MCP_FETCH_SSL_VERIFY=false in your environment. "
f"Error details: {str(e)}",
))
raise McpError(ErrorData(
code=INTERNAL_ERROR,
message=f"Failed to connect to {url}: {str(e)}",
))
except httpx.HTTPError as e:
raise McpError(ErrorData(code=INTERNAL_ERROR, message=f"Failed to fetch {url}: {e!r}"))
if response.status_code >= 400:
raise McpError(ErrorData(
Expand Down
Empty file added src/fetch/tests/__init__.py
Empty file.
147 changes: 147 additions & 0 deletions src/fetch/tests/test_ssl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
Tests for SSL certificate verification configuration.

These tests verify that the MCP_FETCH_SSL_VERIFY environment variable
correctly controls SSL certificate verification behavior.
"""

import os
import importlib
import pytest


class TestSSLConfiguration:
"""Tests for SSL_VERIFY environment variable configuration."""

def test_ssl_verify_default_is_true(self, monkeypatch):
"""SSL verification should be enabled by default."""
monkeypatch.delenv("MCP_FETCH_SSL_VERIFY", raising=False)

# Re-import to pick up new env var
import mcp_server_fetch.server as server_module
importlib.reload(server_module)

assert server_module.SSL_VERIFY is True

def test_ssl_verify_explicit_true(self, monkeypatch):
"""SSL verification should be enabled when explicitly set to 'true'."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "true")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

assert server_module.SSL_VERIFY is True

def test_ssl_verify_explicit_True_uppercase(self, monkeypatch):
"""SSL verification should be enabled when set to 'True' (uppercase)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "True")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

assert server_module.SSL_VERIFY is True

def test_ssl_verify_false_lowercase(self, monkeypatch):
"""SSL verification should be disabled when set to 'false'."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "false")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

assert server_module.SSL_VERIFY is False

def test_ssl_verify_False_uppercase(self, monkeypatch):
"""SSL verification should be disabled when set to 'False' (uppercase)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "False")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

assert server_module.SSL_VERIFY is False

def test_ssl_verify_FALSE_all_caps(self, monkeypatch):
"""SSL verification should be disabled when set to 'FALSE' (all caps)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "FALSE")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

assert server_module.SSL_VERIFY is False

def test_ssl_verify_invalid_value_stays_enabled(self, monkeypatch):
"""Invalid/unknown values should keep SSL verification ENABLED (fail-secure)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "invalid")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

# Fail-secure: only explicit "false" disables SSL verification
assert server_module.SSL_VERIFY is True

def test_ssl_verify_empty_string_stays_enabled(self, monkeypatch):
"""Empty string should keep SSL verification ENABLED (fail-secure)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

# Fail-secure: only explicit "false" disables SSL verification
assert server_module.SSL_VERIFY is True

def test_ssl_verify_0_stays_enabled(self, monkeypatch):
"""'0' should keep SSL verification ENABLED (fail-secure, only 'false' disables)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "0")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

# Fail-secure: only explicit "false" disables SSL verification
assert server_module.SSL_VERIFY is True

def test_ssl_verify_1_stays_enabled(self, monkeypatch):
"""'1' should keep SSL verification ENABLED (fail-secure)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "1")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

# Fail-secure: only explicit "false" disables SSL verification
assert server_module.SSL_VERIFY is True

def test_ssl_verify_yes_stays_enabled(self, monkeypatch):
"""'yes' should keep SSL verification ENABLED (fail-secure)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "yes")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

# Fail-secure: only explicit "false" disables SSL verification
assert server_module.SSL_VERIFY is True

def test_ssl_verify_no_stays_enabled(self, monkeypatch):
"""'no' should keep SSL verification ENABLED (fail-secure, only 'false' disables)."""
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "no")

import mcp_server_fetch.server as server_module
importlib.reload(server_module)

# Fail-secure: only explicit "false" disables SSL verification
assert server_module.SSL_VERIFY is True


class TestSSLErrorHandling:
"""Tests for SSL error message formatting."""

def test_ssl_error_message_format(self):
"""Verify SSL error messages are properly formatted."""
import ssl

# Create a sample SSL error
ssl_error = ssl.SSLCertVerificationError(
1, "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed"
)

# The error message should contain useful information
error_str = str(ssl_error)
assert "CERTIFICATE_VERIFY_FAILED" in error_str or "certificate" in error_str.lower()

Loading