mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
feat(remote): add --verify flag for TLS certificate verification (#4369)
This commit is contained in:
parent
7f2d034f4d
commit
a13e48ea6f
4 changed files with 147 additions and 3 deletions
|
|
@ -113,6 +113,26 @@ For local development servers over plain HTTP, disable OAuth when the server is
|
|||
uvx fastmcp-remote http://localhost:8000/mcp --auth none
|
||||
```
|
||||
|
||||
## Self-Signed Certificates
|
||||
|
||||
For servers behind a self-signed certificate, point `--verify` at a CA bundle that trusts the certificate:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://internal.example.com/mcp --verify /path/to/ca-bundle.pem
|
||||
```
|
||||
|
||||
To disable certificate verification entirely, pass `--verify false`. This is insecure and should only be used for trusted servers on private networks:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://internal.example.com/mcp --verify false
|
||||
```
|
||||
|
||||
To trust a CA bundle without a flag, set the standard `SSL_CERT_FILE` environment variable, which OpenSSL reads automatically:
|
||||
|
||||
```bash
|
||||
SSL_CERT_FILE=/path/to/ca-bundle.pem uvx fastmcp-remote https://internal.example.com/mcp
|
||||
```
|
||||
|
||||
## OAuth Storage
|
||||
|
||||
OAuth tokens are stored under `~/.fastmcp/remote` by default. Set `FASTMCP_REMOTE_CONFIG_DIR` to use another directory:
|
||||
|
|
@ -140,6 +160,7 @@ uvx fastmcp-remote https://example.com/mcp 3334 --host 127.0.0.1
|
|||
| `--transport` | Choose `http` or `sse`. Defaults to `http`. |
|
||||
| `--header` | Add a header to upstream requests, for example `--header "Authorization: Bearer <token>"`. Values may contain colons. Quote headers whose values contain spaces. Use `${VAR}` to expand environment variables inside values. Repeat for multiple headers. |
|
||||
| `--auth` | Choose `oauth` or `none`. The default uses OAuth unless an `Authorization` header is provided. |
|
||||
| `--verify` | Control TLS certificate verification. Pass a path to a CA bundle to trust a self-signed certificate, or `false` to disable verification (insecure). Defaults to verification enabled. |
|
||||
| `--resource` | Isolate OAuth token storage for a named remote resource. |
|
||||
| `--host` | Set the OAuth callback hostname. Defaults to `localhost`. |
|
||||
| `--auth-timeout` | Set how long to wait for the OAuth callback. Defaults to 300 seconds. |
|
||||
|
|
|
|||
|
|
@ -73,6 +73,24 @@ Use `--auth none` for unauthenticated development servers:
|
|||
uvx fastmcp-remote http://localhost:8000/mcp --auth none
|
||||
```
|
||||
|
||||
For servers behind a self-signed certificate, point `--verify` at a CA bundle that trusts the certificate:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://internal.example.com/mcp --verify /path/to/ca-bundle.pem
|
||||
```
|
||||
|
||||
To disable certificate verification entirely (insecure, only for trusted private networks), pass `--verify false`:
|
||||
|
||||
```bash
|
||||
uvx fastmcp-remote https://internal.example.com/mcp --verify false
|
||||
```
|
||||
|
||||
A CA bundle can also be supplied through the standard `SSL_CERT_FILE` environment variable, which OpenSSL reads automatically:
|
||||
|
||||
```bash
|
||||
SSL_CERT_FILE=/path/to/ca-bundle.pem uvx fastmcp-remote https://internal.example.com/mcp
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `--transport`: Choose `http` or `sse`. Defaults to `http`.
|
||||
|
|
@ -82,5 +100,6 @@ uvx fastmcp-remote http://localhost:8000/mcp --auth none
|
|||
- `--auth-timeout`: Set how long to wait for the OAuth callback. Defaults to 300 seconds.
|
||||
- `--ignore-tool`: Hide tools whose names match a glob pattern.
|
||||
- `--auth`: Choose `oauth` or `none`. The default uses OAuth unless an `Authorization` header is provided.
|
||||
- `--verify`: Control TLS certificate verification. Pass a path to a CA bundle to trust a self-signed certificate, or `false` to disable verification (insecure). Defaults to verification enabled.
|
||||
|
||||
OAuth tokens are stored under `~/.fastmcp/remote` by default. Set `FASTMCP_REMOTE_CONFIG_DIR` to use another directory.
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class RemoteConfig:
|
|||
ignore_tools: tuple[str, ...]
|
||||
show_banner: bool
|
||||
log_level: str | None
|
||||
verify: bool | str | None
|
||||
|
||||
|
||||
class IgnoreTools(Transform):
|
||||
|
|
@ -84,6 +85,16 @@ def parse_header(value: str) -> tuple[str, str]:
|
|||
return name.strip(), expanded_value.strip()
|
||||
|
||||
|
||||
def parse_verify(value: str) -> bool | str:
|
||||
"""Interpret the --verify value as a boolean toggle or a CA bundle path."""
|
||||
lowered = value.strip().lower()
|
||||
if lowered in {"false", "0", "no", "off"}:
|
||||
return False
|
||||
if lowered in {"true", "1", "yes", "on"}:
|
||||
return True
|
||||
return value
|
||||
|
||||
|
||||
def default_storage_dir(resource: str | None = None) -> Path:
|
||||
if config_dir := os.environ.get("FASTMCP_REMOTE_CONFIG_DIR"):
|
||||
base = Path(config_dir).expanduser()
|
||||
|
|
@ -147,6 +158,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
default=[],
|
||||
help="Hide tools matching this glob pattern. Repeat for multiple patterns.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify",
|
||||
type=parse_verify,
|
||||
default=None,
|
||||
metavar="VERIFY",
|
||||
help=(
|
||||
"SSL certificate verification. Pass a path to a CA bundle file, or "
|
||||
"'false' to disable verification (insecure, for self-signed "
|
||||
"certificates). Defaults to verification enabled."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
|
|
@ -190,6 +212,7 @@ def parse_args(argv: Sequence[str] | None = None) -> RemoteConfig:
|
|||
ignore_tools=tuple(args.ignore_tool),
|
||||
show_banner=not args.silent,
|
||||
log_level=log_level,
|
||||
verify=args.verify,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -228,8 +251,12 @@ def resolve_auth(config: RemoteConfig) -> OAuth | None:
|
|||
def build_transport(config: RemoteConfig) -> SSETransport | StreamableHttpTransport:
|
||||
auth = resolve_auth(config)
|
||||
if config.transport == "sse":
|
||||
return SSETransport(config.url, headers=config.headers, auth=auth)
|
||||
return StreamableHttpTransport(config.url, headers=config.headers, auth=auth)
|
||||
return SSETransport(
|
||||
config.url, headers=config.headers, auth=auth, verify=config.verify
|
||||
)
|
||||
return StreamableHttpTransport(
|
||||
config.url, headers=config.headers, auth=auth, verify=config.verify
|
||||
)
|
||||
|
||||
|
||||
async def run(config: RemoteConfig) -> None:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ from fastmcp.client.auth import OAuth
|
|||
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
||||
from fastmcp.tools import FunctionTool
|
||||
from fastmcp.utilities.versions import VersionSpec
|
||||
from fastmcp_remote.cli import IgnoreTools, build_transport, parse_args, parse_header
|
||||
from fastmcp_remote.cli import (
|
||||
IgnoreTools,
|
||||
build_transport,
|
||||
parse_args,
|
||||
parse_header,
|
||||
parse_verify,
|
||||
)
|
||||
|
||||
|
||||
def sample_tool() -> str:
|
||||
|
|
@ -164,6 +170,77 @@ def test_sse_transport_strategy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
|
|||
assert isinstance(transport, SSETransport)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["false", "False", "0", "no", "off"])
|
||||
def test_parse_verify_disables_verification(value: str):
|
||||
assert parse_verify(value) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["true", "True", "1", "yes", "on"])
|
||||
def test_parse_verify_enables_verification(value: str):
|
||||
assert parse_verify(value) is True
|
||||
|
||||
|
||||
def test_parse_verify_treats_other_values_as_ca_bundle_path():
|
||||
assert parse_verify("/etc/ssl/ca-bundle.pem") == "/etc/ssl/ca-bundle.pem"
|
||||
|
||||
|
||||
def test_verify_defaults_to_none(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
|
||||
config = parse_args(["https://example.com/mcp", "--auth", "none"])
|
||||
|
||||
assert config.verify is None
|
||||
assert build_transport(config).verify is None
|
||||
|
||||
|
||||
def test_verify_false_disables_verification_on_transport(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
|
||||
config = parse_args(
|
||||
["https://example.com/mcp", "--auth", "none", "--verify", "false"]
|
||||
)
|
||||
|
||||
transport = build_transport(config)
|
||||
|
||||
assert config.verify is False
|
||||
assert isinstance(transport, StreamableHttpTransport)
|
||||
assert transport.verify is False
|
||||
|
||||
|
||||
def test_verify_ca_bundle_path_passes_to_transport(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
|
||||
ca_bundle = "/etc/ssl/custom-ca.pem"
|
||||
config = parse_args(
|
||||
["https://example.com/mcp", "--auth", "none", "--verify", ca_bundle]
|
||||
)
|
||||
|
||||
assert build_transport(config).verify == ca_bundle
|
||||
|
||||
|
||||
def test_verify_passes_to_sse_transport(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
|
||||
config = parse_args(
|
||||
[
|
||||
"https://example.com/sse",
|
||||
"--transport",
|
||||
"sse",
|
||||
"--auth",
|
||||
"none",
|
||||
"--verify",
|
||||
"false",
|
||||
]
|
||||
)
|
||||
|
||||
transport = build_transport(config)
|
||||
|
||||
assert isinstance(transport, SSETransport)
|
||||
assert transport.verify is False
|
||||
|
||||
|
||||
async def test_ignore_tools_transform_filters_matching_names():
|
||||
tool = FunctionTool.from_function(sample_tool, name="delete_user")
|
||||
transform = IgnoreTools(["delete*"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue