b8a107d16c
create_mcp_http_client followed every redirect, so everything configured on a client (headers, auth, request bodies) was re-sent to whatever host a Location header named. Clients built by the factory now follow redirects within the same origin (scheme, host, and port), plus http-to-https upgrades of the same host on default ports, and raise the new RedirectError for anything else - before the next request is sent. - transports resolve a refused redirect in-band: requests get a JSON-RPC error naming the target and the remedy, notifications are delivered to the session's message handler; the standalone GET stream stops retrying an endpoint that keeps redirecting - caller-supplied clients that follow no redirects get the same clear error on POST, GET stream, and SSE connect instead of an opaque content-type error - OAuth discovery, registration, token, refresh, and the identity-assertion token exchange fail loudly on redirect responses instead of silently trying the next URL or abandoning the discovery chain - RedirectError and create_mcp_http_client are exported from the top-level mcp package; migration.md documents the behavior change; docs and examples configure clients through the factory, and the general-purpose fetch example uses a browser-like client of its own
74 lines
2.6 KiB
Python
74 lines
2.6 KiB
Python
"""Shared fixtures for `Dispatcher` contract tests.
|
|
|
|
The `pair_factory` fixture parametrizes contract tests over every `Dispatcher`
|
|
implementation, so the same behavioral assertions run against `DirectDispatcher`
|
|
(in-memory) and `JSONRPCDispatcher` (over crossed anyio memory streams).
|
|
"""
|
|
|
|
from collections.abc import Callable
|
|
|
|
import anyio
|
|
import pytest
|
|
|
|
from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair
|
|
from mcp.shared.dispatcher import Dispatcher
|
|
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
|
|
from mcp.shared.message import SessionMessage
|
|
from mcp.shared.transport_context import TransportContext
|
|
|
|
DispatcherTriple = tuple[Dispatcher[TransportContext], Dispatcher[TransportContext], Callable[[], None]]
|
|
PairFactory = Callable[..., DispatcherTriple]
|
|
|
|
|
|
def direct_pair(*, can_send_request: bool = True) -> DispatcherTriple:
|
|
client, server = create_direct_dispatcher_pair(can_send_request=can_send_request)
|
|
|
|
def close() -> None:
|
|
client.close()
|
|
server.close()
|
|
|
|
return client, server, close
|
|
|
|
|
|
def jsonrpc_pair(*, can_send_request: bool = True) -> DispatcherTriple:
|
|
"""Two `JSONRPCDispatcher`s wired over crossed in-memory streams."""
|
|
c2s_send, c2s_recv = anyio.create_memory_object_stream[SessionMessage | Exception](32)
|
|
s2c_send, s2c_recv = anyio.create_memory_object_stream[SessionMessage | Exception](32)
|
|
|
|
def builder(_meta: object) -> TransportContext:
|
|
return TransportContext(kind="jsonrpc", can_send_request=can_send_request)
|
|
|
|
client: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(s2c_recv, c2s_send, transport_builder=builder)
|
|
server: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(c2s_recv, s2c_send, transport_builder=builder)
|
|
|
|
def close() -> None:
|
|
for s in (c2s_send, c2s_recv, s2c_send, s2c_recv):
|
|
s.close()
|
|
|
|
return client, server, close
|
|
|
|
|
|
@pytest.fixture(
|
|
params=[
|
|
pytest.param(direct_pair, id="direct"),
|
|
pytest.param(jsonrpc_pair, id="jsonrpc"),
|
|
]
|
|
)
|
|
def pair_factory(request: pytest.FixtureRequest) -> PairFactory:
|
|
return request.param
|
|
|
|
|
|
__all__ = ["PairFactory", "direct_pair", "jsonrpc_pair"]
|
|
|
|
|
|
@pytest.fixture
|
|
def no_proxy_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Clear proxy environment variables for tests that swap in a mock transport.
|
|
|
|
Proxy variables make httpx build proxy mounts at client construction, which
|
|
would take precedence over a transport assigned after construction.
|
|
"""
|
|
for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"):
|
|
monkeypatch.delenv(name, raising=False)
|
|
monkeypatch.delenv(name.lower(), raising=False)
|