v2.15.0 (#773)
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
702d0f412e
commit
cafb6d4147
247 changed files with 12401 additions and 3029 deletions
213
tests/core/test_config_mcp_auth.py
Normal file
213
tests/core/test_config_mcp_auth.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from pydantic import ValidationError
|
||||
import pytest
|
||||
|
||||
from vibe.core.config import MCPHttp, MCPOAuth, MCPStaticAuth, MCPStreamableHttp
|
||||
from vibe.core.tools.mcp.registry import MCPRegistry
|
||||
|
||||
HTTP_TRANSPORTS = [
|
||||
pytest.param(MCPHttp, "http", id="http"),
|
||||
pytest.param(MCPStreamableHttp, "streamable-http", id="streamable-http"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||
def test_legacy_top_level_keys_promote_to_static_auth(
|
||||
cls: type[MCPHttp | MCPStreamableHttp],
|
||||
transport: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("MCP_TOKEN", "secret-token")
|
||||
|
||||
srv = cls.model_validate({
|
||||
"name": "remote",
|
||||
"transport": transport,
|
||||
"url": "https://mcp.example.com",
|
||||
"api_key_env": "MCP_TOKEN",
|
||||
})
|
||||
|
||||
assert isinstance(srv.auth, MCPStaticAuth)
|
||||
assert srv.auth.api_key_env == "MCP_TOKEN"
|
||||
assert srv.http_headers() == {"Authorization": "Bearer secret-token"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||
def test_legacy_custom_header_and_format(
|
||||
cls: type[MCPHttp | MCPStreamableHttp],
|
||||
transport: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("MCP_TOKEN", "k")
|
||||
|
||||
srv = cls.model_validate({
|
||||
"name": "remote",
|
||||
"transport": transport,
|
||||
"url": "https://mcp.example.com",
|
||||
"api_key_env": "MCP_TOKEN",
|
||||
"api_key_header": "X-API-Key",
|
||||
"api_key_format": "{token}",
|
||||
})
|
||||
|
||||
assert srv.http_headers() == {"X-API-Key": "k"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||
def test_explicit_static_auth_round_trips(
|
||||
cls: type[MCPHttp | MCPStreamableHttp], transport: str
|
||||
) -> None:
|
||||
srv = cls.model_validate({
|
||||
"name": "remote",
|
||||
"transport": transport,
|
||||
"url": "https://mcp.example.com",
|
||||
"auth": {"type": "static", "api_key_env": "X", "api_key_header": "X-API-Key"},
|
||||
})
|
||||
|
||||
dumped = srv.model_dump()
|
||||
rebuilt = cls.model_validate(dumped)
|
||||
|
||||
assert isinstance(rebuilt.auth, MCPStaticAuth)
|
||||
assert rebuilt.auth.api_key_env == "X"
|
||||
assert rebuilt.auth.api_key_header == "X-API-Key"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||
def test_oauth_auth_parses(
|
||||
cls: type[MCPHttp | MCPStreamableHttp], transport: str
|
||||
) -> None:
|
||||
srv = cls.model_validate({
|
||||
"name": "linear",
|
||||
"transport": transport,
|
||||
"url": "https://mcp.linear.app/mcp",
|
||||
"auth": {"type": "oauth", "scopes": ["read", "write"]},
|
||||
})
|
||||
|
||||
assert isinstance(srv.auth, MCPOAuth)
|
||||
assert srv.auth.scopes == ["read", "write"]
|
||||
assert srv.auth.redirect_port == 47823
|
||||
assert srv.http_headers() == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||
def test_mixing_legacy_keys_with_auth_block_is_rejected(
|
||||
cls: type[MCPHttp | MCPStreamableHttp], transport: str
|
||||
) -> None:
|
||||
with pytest.raises(ValidationError, match="cannot mix top-level"):
|
||||
cls.model_validate({
|
||||
"name": "remote",
|
||||
"transport": transport,
|
||||
"url": "https://mcp.example.com",
|
||||
"api_key_env": "LEGACY",
|
||||
"auth": {"type": "static", "api_key_env": "NEW"},
|
||||
})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||
def test_default_auth_is_static(
|
||||
cls: type[MCPHttp | MCPStreamableHttp], transport: str
|
||||
) -> None:
|
||||
srv = cls.model_validate({
|
||||
"name": "remote",
|
||||
"transport": transport,
|
||||
"url": "https://mcp.example.com",
|
||||
})
|
||||
|
||||
assert isinstance(srv.auth, MCPStaticAuth)
|
||||
assert srv.http_headers() == {}
|
||||
|
||||
|
||||
def test_oauth_client_id_and_metadata_url_mutually_exclusive() -> None:
|
||||
with pytest.raises(ValidationError, match="mutually exclusive"):
|
||||
MCPOAuth.model_validate({
|
||||
"type": "oauth",
|
||||
"scopes": ["read"],
|
||||
"client_id": "abc",
|
||||
"client_metadata_url": "https://example.com/cm.json",
|
||||
})
|
||||
|
||||
|
||||
def test_oauth_client_metadata_url_must_be_http_url() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
MCPOAuth.model_validate({
|
||||
"type": "oauth",
|
||||
"scopes": ["read"],
|
||||
"client_metadata_url": "not-a-url",
|
||||
})
|
||||
|
||||
|
||||
def test_oauth_client_id_rejects_empty_string() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
MCPOAuth.model_validate({"type": "oauth", "scopes": ["read"], "client_id": ""})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("port", [80, 1023, 0, 65536, 70000])
|
||||
def test_oauth_redirect_port_out_of_range(port: int) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
MCPOAuth.model_validate({
|
||||
"type": "oauth",
|
||||
"scopes": ["read"],
|
||||
"redirect_port": port,
|
||||
})
|
||||
|
||||
|
||||
def test_oauth_redirect_port_inside_range() -> None:
|
||||
auth = MCPOAuth.model_validate({
|
||||
"type": "oauth",
|
||||
"scopes": ["read"],
|
||||
"redirect_port": 1024,
|
||||
})
|
||||
assert auth.redirect_port == 1024
|
||||
|
||||
|
||||
def test_static_auth_forbids_extra_keys() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
MCPStaticAuth.model_validate({"type": "static", "headerz": {}})
|
||||
|
||||
|
||||
def test_oauth_forbids_extra_keys() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
MCPOAuth.model_validate({"type": "oauth", "scopes": ["read"], "scope": "x"})
|
||||
|
||||
|
||||
def test_oauth_scopes_required() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
MCPOAuth.model_validate({"type": "oauth"})
|
||||
|
||||
|
||||
def test_oauth_scopes_empty_list_allowed() -> None:
|
||||
auth = MCPOAuth.model_validate({"type": "oauth", "scopes": []})
|
||||
assert auth.scopes == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||
async def test_registry_skips_oauth_servers_with_gated_warning(
|
||||
cls: type[MCPHttp | MCPStreamableHttp],
|
||||
transport: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
srv = cls.model_validate({
|
||||
"name": "linear",
|
||||
"transport": transport,
|
||||
"url": "https://mcp.linear.app/mcp",
|
||||
"auth": {"type": "oauth", "scopes": ["read"]},
|
||||
})
|
||||
registry = MCPRegistry()
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="vibe"):
|
||||
first = await registry.get_tools_async([srv])
|
||||
|
||||
assert first == {}
|
||||
assert (
|
||||
"OAuth support for MCP servers is not yet enabled; coming in a future release"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.WARNING, logger="vibe"):
|
||||
second = await registry.get_tools_async([srv])
|
||||
|
||||
assert second == {}
|
||||
assert "OAuth support" not in caplog.text
|
||||
Loading…
Add table
Add a link
Reference in a new issue