Files
wehub-resource-sync 60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:59 +08:00

255 lines
7.3 KiB
Python

from pathlib import Path
import pytest
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,
parse_verify,
)
def sample_tool() -> str:
return "ok"
def test_parse_header_accepts_spaced_value():
assert parse_header("Authorization: Bearer token") == (
"Authorization",
"Bearer token",
)
def test_parse_header_preserves_spaces_inside_value():
assert parse_header("X-Client-Name: My MCP Host") == (
"X-Client-Name",
"My MCP Host",
)
def test_parse_header_preserves_colons_inside_value():
assert parse_header("X-Callback-Url: https://example.com/oauth/callback") == (
"X-Callback-Url",
"https://example.com/oauth/callback",
)
def test_parse_header_expands_environment_variables_in_value(
monkeypatch: pytest.MonkeyPatch,
):
monkeypatch.setenv("AUTH_HEADER", "Bearer token with spaces")
assert parse_header("Authorization:${AUTH_HEADER}") == (
"Authorization",
"Bearer token with spaces",
)
def test_parse_header_rejects_missing_environment_variable():
with pytest.raises(SystemExit):
parse_args(["https://example.com/mcp", "--header", "Authorization:${MISSING}"])
def test_parse_header_accepts_unspaced_value():
assert parse_header("Authorization:Bearer token") == (
"Authorization",
"Bearer token",
)
def test_parse_header_rejects_missing_colon():
with pytest.raises(SystemExit):
parse_args(["https://example.com/mcp", "--header", "Authorization"])
def test_http_urls_are_allowed():
config = parse_args(["http://localhost:8000/mcp", "--auth", "none"])
assert config.url == "http://localhost:8000/mcp"
def test_auth_defaults_to_oauth(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
config = parse_args(["https://example.com/mcp"])
transport = build_transport(config)
assert isinstance(transport, StreamableHttpTransport)
assert isinstance(transport.auth, OAuth)
def test_authorization_header_disables_oauth_by_default(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
config = parse_args(
[
"https://example.com/mcp",
"--header",
"Authorization: Bearer token",
]
)
transport = build_transport(config)
assert isinstance(transport, StreamableHttpTransport)
assert transport.auth is None
assert transport.headers == {"Authorization": "Bearer token"}
def test_explicit_oauth_keeps_oauth_with_authorization_header(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
config = parse_args(
[
"https://example.com/mcp",
"--header",
"Authorization: Bearer token",
"--auth",
"oauth",
]
)
transport = build_transport(config)
assert isinstance(transport.auth, OAuth)
def test_oauth_callback_options_pass_to_fastmcp_oauth(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
config = parse_args(
[
"https://example.com/mcp",
"8765",
"--host",
"127.0.0.1",
"--auth-timeout",
"12.5",
]
)
transport = build_transport(config)
assert isinstance(transport.auth, OAuth)
assert transport.auth.context.client_metadata.redirect_uris is not None
assert str(transport.auth.context.client_metadata.redirect_uris[0]) == (
"http://127.0.0.1:8765/callback"
)
assert transport.auth._callback_timeout == 12.5
def test_resource_isolates_token_storage(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
default_config = parse_args(["https://example.com/mcp"])
resource_config = parse_args(
["https://example.com/mcp", "--resource", "linear-prod"]
)
assert default_config.storage_dir == tmp_path
assert resource_config.storage_dir.parent == tmp_path / "resources"
assert resource_config.storage_dir != default_config.storage_dir
def test_sse_transport_strategy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("FASTMCP_REMOTE_CONFIG_DIR", str(tmp_path))
config = parse_args(["https://example.com/sse", "--transport", "sse"])
transport = build_transport(config)
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*"])
async def call_next(
name: str, *, version: VersionSpec | None = None
) -> FunctionTool:
return tool
assert await transform.list_tools([tool]) == []
assert await transform.get_tool("delete_user", call_next) is None