Files
Max Isbey b8a107d16c Scope HTTP client redirect following to the request's origin
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
2026-07-07 19:41:45 +00:00

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)