Skip to content

Commit 96ce880

Browse files
committed
fix(fetch): add configurable SSL certificate verification
Resolves #508 Added MCP_FETCH_SSL_VERIFY environment variable to control SSL certificate verification. This allows the fetch server to work with internal servers that use self-signed certificates. ## Changes - Added SSL_VERIFY configuration via MCP_FETCH_SSL_VERIFY env var (default: true) - Added verify=SSL_VERIFY to both AsyncClient instances - Added comprehensive SSL error handling with helpful error messages - Error messages guide users to set MCP_FETCH_SSL_VERIFY=false for self-signed certs ## Usage ```bash # For servers with self-signed certificates: export MCP_FETCH_SSL_VERIFY=false ``` ## Security Note Disabling SSL verification reduces security. Only use in trusted environments with internal servers that have self-signed certificates.
1 parent 862e717 commit 96ce880

File tree

6 files changed

+343
-7
lines changed

6 files changed

+343
-7
lines changed

src/fetch/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,27 @@ This can be customized by adding the argument `--user-agent=YourUserAgent` to th
170170

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

173+
### Customization - SSL Verification
174+
175+
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`:
176+
177+
```json
178+
{
179+
"mcpServers": {
180+
"fetch": {
181+
"command": "uvx",
182+
"args": ["mcp-server-fetch"],
183+
"env": {
184+
"MCP_FETCH_SSL_VERIFY": "false"
185+
}
186+
}
187+
}
188+
}
189+
```
190+
191+
> [!WARNING]
192+
> Disabling SSL verification reduces security. Only use this option in trusted environments with internal servers that have self-signed certificates.
193+
173194
## Windows Configuration
174195

175196
If you're experiencing timeout issues on Windows, you may need to set the `PYTHONIOENCODING` environment variable to ensure proper character encoding:

src/fetch/pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ requires = ["hatchling"]
3333
build-backend = "hatchling.build"
3434

3535
[tool.uv]
36-
dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3"]
36+
dev-dependencies = [
37+
"pyright>=1.1.389",
38+
"pytest>=9.0.2",
39+
"pytest-asyncio>=1.3.0",
40+
"ruff>=0.7.3",
41+
]

src/fetch/src/mcp_server_fetch/server.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
import ssl
13
from typing import Annotated, Tuple
24
from urllib.parse import urlparse, urlunparse
35

@@ -20,6 +22,14 @@
2022
from protego import Protego
2123
from pydantic import BaseModel, Field, AnyUrl
2224

25+
# =============================================================================
26+
# SSL CONFIGURATION
27+
# =============================================================================
28+
# Set MCP_FETCH_SSL_VERIFY=false to disable SSL certificate verification.
29+
# This is useful for internal servers with self-signed certificates.
30+
# WARNING: Disabling SSL verification reduces security. Only use in trusted environments.
31+
SSL_VERIFY = os.getenv("MCP_FETCH_SSL_VERIFY", "true").lower() == "true"
32+
2333
DEFAULT_USER_AGENT_AUTONOMOUS = "ModelContextProtocol/1.0 (Autonomous; +https://github.com/modelcontextprotocol/servers)"
2434
DEFAULT_USER_AGENT_MANUAL = "ModelContextProtocol/1.0 (User-Specified; +https://github.com/modelcontextprotocol/servers)"
2535

@@ -68,18 +78,41 @@ async def check_may_autonomously_fetch_url(url: str, user_agent: str, proxy_url:
6878
Check if the URL can be fetched by the user agent according to the robots.txt file.
6979
Raises a McpError if not.
7080
"""
71-
from httpx import AsyncClient, HTTPError
81+
import httpx
7282

7383
robot_txt_url = get_robots_txt_url(url)
7484

75-
async with AsyncClient(proxies=proxy_url) as client:
85+
async with httpx.AsyncClient(proxies=proxy_url, verify=SSL_VERIFY) as client:
7686
try:
7787
response = await client.get(
7888
robot_txt_url,
7989
follow_redirects=True,
8090
headers={"User-Agent": user_agent},
91+
timeout=30,
8192
)
82-
except HTTPError:
93+
except ssl.SSLError as e:
94+
raise McpError(ErrorData(
95+
code=INTERNAL_ERROR,
96+
message=f"SSL Certificate verification failed for {robot_txt_url}. "
97+
f"If this is an internal server with a self-signed certificate, "
98+
f"set MCP_FETCH_SSL_VERIFY=false in your environment. "
99+
f"Error details: {str(e)}",
100+
))
101+
except httpx.ConnectError as e:
102+
error_str = str(e).lower()
103+
if "ssl" in error_str or "certificate" in error_str or "verify" in error_str:
104+
raise McpError(ErrorData(
105+
code=INTERNAL_ERROR,
106+
message=f"SSL Certificate verification failed for {robot_txt_url}. "
107+
f"If this is an internal server with a self-signed certificate, "
108+
f"set MCP_FETCH_SSL_VERIFY=false in your environment. "
109+
f"Error details: {str(e)}",
110+
))
111+
raise McpError(ErrorData(
112+
code=INTERNAL_ERROR,
113+
message=f"Failed to connect to {robot_txt_url}: {str(e)}",
114+
))
115+
except httpx.HTTPError:
83116
raise McpError(ErrorData(
84117
code=INTERNAL_ERROR,
85118
message=f"Failed to fetch robots.txt {robot_txt_url} due to a connection issue",
@@ -114,17 +147,39 @@ async def fetch_url(
114147
"""
115148
Fetch the URL and return the content in a form ready for the LLM, as well as a prefix string with status information.
116149
"""
117-
from httpx import AsyncClient, HTTPError
150+
import httpx
118151

119-
async with AsyncClient(proxies=proxy_url) as client:
152+
async with httpx.AsyncClient(proxies=proxy_url, verify=SSL_VERIFY) as client:
120153
try:
121154
response = await client.get(
122155
url,
123156
follow_redirects=True,
124157
headers={"User-Agent": user_agent},
125158
timeout=30,
126159
)
127-
except HTTPError as e:
160+
except ssl.SSLError as e:
161+
raise McpError(ErrorData(
162+
code=INTERNAL_ERROR,
163+
message=f"SSL Certificate verification failed for {url}. "
164+
f"If this is an internal server with a self-signed certificate, "
165+
f"set MCP_FETCH_SSL_VERIFY=false in your environment. "
166+
f"Error details: {str(e)}",
167+
))
168+
except httpx.ConnectError as e:
169+
error_str = str(e).lower()
170+
if "ssl" in error_str or "certificate" in error_str or "verify" in error_str:
171+
raise McpError(ErrorData(
172+
code=INTERNAL_ERROR,
173+
message=f"SSL Certificate verification failed for {url}. "
174+
f"If this is an internal server with a self-signed certificate, "
175+
f"set MCP_FETCH_SSL_VERIFY=false in your environment. "
176+
f"Error details: {str(e)}",
177+
))
178+
raise McpError(ErrorData(
179+
code=INTERNAL_ERROR,
180+
message=f"Failed to connect to {url}: {str(e)}",
181+
))
182+
except httpx.HTTPError as e:
128183
raise McpError(ErrorData(code=INTERNAL_ERROR, message=f"Failed to fetch {url}: {e!r}"))
129184
if response.status_code >= 400:
130185
raise McpError(ErrorData(

src/fetch/tests/__init__.py

Whitespace-only changes.

src/fetch/tests/test_ssl.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Tests for SSL certificate verification configuration.
3+
4+
These tests verify that the MCP_FETCH_SSL_VERIFY environment variable
5+
correctly controls SSL certificate verification behavior.
6+
"""
7+
8+
import os
9+
import importlib
10+
import pytest
11+
12+
13+
class TestSSLConfiguration:
14+
"""Tests for SSL_VERIFY environment variable configuration."""
15+
16+
def test_ssl_verify_default_is_true(self, monkeypatch):
17+
"""SSL verification should be enabled by default."""
18+
monkeypatch.delenv("MCP_FETCH_SSL_VERIFY", raising=False)
19+
20+
# Re-import to pick up new env var
21+
import mcp_server_fetch.server as server_module
22+
importlib.reload(server_module)
23+
24+
assert server_module.SSL_VERIFY is True
25+
26+
def test_ssl_verify_explicit_true(self, monkeypatch):
27+
"""SSL verification should be enabled when explicitly set to 'true'."""
28+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "true")
29+
30+
import mcp_server_fetch.server as server_module
31+
importlib.reload(server_module)
32+
33+
assert server_module.SSL_VERIFY is True
34+
35+
def test_ssl_verify_explicit_True_uppercase(self, monkeypatch):
36+
"""SSL verification should be enabled when set to 'True' (uppercase)."""
37+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "True")
38+
39+
import mcp_server_fetch.server as server_module
40+
importlib.reload(server_module)
41+
42+
assert server_module.SSL_VERIFY is True
43+
44+
def test_ssl_verify_false_lowercase(self, monkeypatch):
45+
"""SSL verification should be disabled when set to 'false'."""
46+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "false")
47+
48+
import mcp_server_fetch.server as server_module
49+
importlib.reload(server_module)
50+
51+
assert server_module.SSL_VERIFY is False
52+
53+
def test_ssl_verify_False_uppercase(self, monkeypatch):
54+
"""SSL verification should be disabled when set to 'False' (uppercase)."""
55+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "False")
56+
57+
import mcp_server_fetch.server as server_module
58+
importlib.reload(server_module)
59+
60+
assert server_module.SSL_VERIFY is False
61+
62+
def test_ssl_verify_FALSE_all_caps(self, monkeypatch):
63+
"""SSL verification should be disabled when set to 'FALSE' (all caps)."""
64+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "FALSE")
65+
66+
import mcp_server_fetch.server as server_module
67+
importlib.reload(server_module)
68+
69+
assert server_module.SSL_VERIFY is False
70+
71+
def test_ssl_verify_invalid_value_defaults_to_disabled(self, monkeypatch):
72+
"""Invalid values should result in SSL verification being disabled (not 'true')."""
73+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "invalid")
74+
75+
import mcp_server_fetch.server as server_module
76+
importlib.reload(server_module)
77+
78+
# Since "invalid".lower() != "true", SSL_VERIFY will be False
79+
assert server_module.SSL_VERIFY is False
80+
81+
def test_ssl_verify_empty_string_defaults_to_disabled(self, monkeypatch):
82+
"""Empty string should result in SSL verification being disabled."""
83+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "")
84+
85+
import mcp_server_fetch.server as server_module
86+
importlib.reload(server_module)
87+
88+
assert server_module.SSL_VERIFY is False
89+
90+
def test_ssl_verify_0_is_disabled(self, monkeypatch):
91+
"""'0' should result in SSL verification being disabled."""
92+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "0")
93+
94+
import mcp_server_fetch.server as server_module
95+
importlib.reload(server_module)
96+
97+
assert server_module.SSL_VERIFY is False
98+
99+
def test_ssl_verify_1_is_disabled(self, monkeypatch):
100+
"""'1' should result in SSL verification being disabled (only 'true' enables it)."""
101+
monkeypatch.setenv("MCP_FETCH_SSL_VERIFY", "1")
102+
103+
import mcp_server_fetch.server as server_module
104+
importlib.reload(server_module)
105+
106+
# Only "true" (case-insensitive) enables SSL verification
107+
assert server_module.SSL_VERIFY is False
108+
109+
110+
class TestSSLErrorHandling:
111+
"""Tests for SSL error message formatting."""
112+
113+
def test_ssl_error_message_format(self):
114+
"""Verify SSL error messages are properly formatted."""
115+
import ssl
116+
117+
# Create a sample SSL error
118+
ssl_error = ssl.SSLCertVerificationError(
119+
1, "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed"
120+
)
121+
122+
# The error message should contain useful information
123+
error_str = str(ssl_error)
124+
assert "CERTIFICATE_VERIFY_FAILED" in error_str or "certificate" in error_str.lower()
125+

0 commit comments

Comments
 (0)