Replace httpx and httpx-sse with httpx2 (#2972)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
parent
1216c53693
commit
2713b53b12
7 changed files with 1029 additions and 948 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import inspect
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from mcp_types import TextContent
|
||||
from starlette.applications import Starlette
|
||||
|
|
@ -50,8 +50,8 @@ async def test_streamable_http_app_takes_runs_options_except_port() -> None:
|
|||
|
||||
async def test_a_request_before_the_session_manager_runs_is_rejected() -> None:
|
||||
"""The `!!! check`: nothing starts the session manager except its lifespan."""
|
||||
transport = httpx.ASGITransport(app=tutorial001.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
transport = httpx2.ASGITransport(app=tutorial001.app)
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
with pytest.raises(RuntimeError, match=r"Task group is not initialized\. Make sure to use run\(\)\."):
|
||||
await http.post("/mcp")
|
||||
|
||||
|
|
@ -76,12 +76,12 @@ async def test_a_root_mount_swallows_routes_listed_after_it() -> None:
|
|||
listed_after = Starlette(routes=[Mount("/", app=mcp_app), Route("/about", about)])
|
||||
listed_before = Starlette(routes=[Route("/about", about), Mount("/", app=mcp_app)])
|
||||
|
||||
transport = httpx.ASGITransport(app=listed_after)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
transport = httpx2.ASGITransport(app=listed_after)
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
assert (await http.get("/about")).status_code == 404
|
||||
|
||||
transport = httpx.ASGITransport(app=listed_before)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
transport = httpx2.ASGITransport(app=listed_before)
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
assert (await http.get("/about")).status_code == 200
|
||||
|
||||
|
||||
|
|
@ -127,8 +127,8 @@ async def test_cors_exposes_the_session_id_header() -> None:
|
|||
"""tutorial005: the browser origin gets the three MCP methods and can read `Mcp-Session-Id`."""
|
||||
(middleware,) = tutorial005.app.user_middleware
|
||||
assert middleware.cls is CORSMiddleware
|
||||
transport = httpx.ASGITransport(app=tutorial005.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
transport = httpx2.ASGITransport(app=tutorial005.app)
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
preflight = await http.options(
|
||||
"/mcp",
|
||||
headers={"Origin": "https://app.example.com", "Access-Control-Request-Method": "POST"},
|
||||
|
|
@ -152,8 +152,8 @@ async def test_custom_route_lands_next_to_the_mcp_endpoint() -> None:
|
|||
|
||||
async def test_the_health_check_answers_outside_the_protocol() -> None:
|
||||
"""tutorial006: `GET /health` is ordinary HTTP, with no session manager and no MCP."""
|
||||
transport = httpx.ASGITransport(app=tutorial006.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
transport = httpx2.ASGITransport(app=tutorial006.app)
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
|
||||
response = await http.get("/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
|
@ -174,11 +174,11 @@ async def test_the_default_app_is_localhost_only() -> None:
|
|||
before any MCP code runs."""
|
||||
bare = MCPServer("Notes")
|
||||
app = bare.streamable_http_app()
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
transport = httpx2.ASGITransport(app=app)
|
||||
async with bare.session_manager.run():
|
||||
async with httpx.AsyncClient(transport=transport, base_url="https://mcp.example.com") as http:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="https://mcp.example.com") as http:
|
||||
wrong_host = await http.post("/mcp", json=INITIALIZE, headers=MCP_HEADERS)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://localhost:8000") as http:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://localhost:8000") as http:
|
||||
wrong_origin = await http.post(
|
||||
"/mcp", json=INITIALIZE, headers={**MCP_HEADERS, "Origin": "https://app.example.com"}
|
||||
)
|
||||
|
|
@ -189,9 +189,9 @@ async def test_the_default_app_is_localhost_only() -> None:
|
|||
async def test_the_documented_browser_origin_works_end_to_end() -> None:
|
||||
"""tutorial005: the page's scenario for real. The public hostname, the browser origin, a
|
||||
realistic preflight naming the `Mcp-*` headers, then the actual request."""
|
||||
transport = httpx.ASGITransport(app=tutorial005.app)
|
||||
transport = httpx2.ASGITransport(app=tutorial005.app)
|
||||
async with tutorial005.lifespan(tutorial005.app):
|
||||
async with httpx.AsyncClient(transport=transport, base_url="https://mcp.example.com") as http:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="https://mcp.example.com") as http:
|
||||
preflight = await http.options(
|
||||
"/mcp",
|
||||
headers={
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""`docs/run/authorization.md`: every claim the page makes, proved against the real SDK."""
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp_types import TextContent
|
||||
|
|
@ -40,8 +40,8 @@ async def test_the_app_grows_a_protected_resource_metadata_route() -> None:
|
|||
|
||||
async def test_the_metadata_document_is_built_from_auth_settings() -> None:
|
||||
"""tutorial001: `GET` on the well-known route returns the Protected Resource Metadata the page shows."""
|
||||
transport = httpx.ASGITransport(app=tutorial001.mcp.streamable_http_app())
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as http_client:
|
||||
transport = httpx2.ASGITransport(app=tutorial001.mcp.streamable_http_app())
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as http_client:
|
||||
response = await http_client.get("/.well-known/oauth-protected-resource/mcp")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == snapshot(
|
||||
|
|
@ -56,8 +56,8 @@ async def test_the_metadata_document_is_built_from_auth_settings() -> None:
|
|||
|
||||
async def test_a_request_without_a_token_never_reaches_the_protocol() -> None:
|
||||
"""The `!!! check`: no `Authorization` header means a 401 that points at the metadata document."""
|
||||
transport = httpx.ASGITransport(app=tutorial001.mcp.streamable_http_app())
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as http_client:
|
||||
transport = httpx2.ASGITransport(app=tutorial001.mcp.streamable_http_app())
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as http_client:
|
||||
response = await http_client.post("/mcp", json={})
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {"error": "invalid_token", "error_description": "Authentication required"}
|
||||
|
|
@ -69,8 +69,8 @@ async def test_a_request_without_a_token_never_reaches_the_protocol() -> None:
|
|||
|
||||
async def test_a_token_the_verifier_rejects_gets_the_same_401() -> None:
|
||||
"""tutorial001: `verify_token` returning `None` and a missing header are indistinguishable to the caller."""
|
||||
transport = httpx.ASGITransport(app=tutorial001.mcp.streamable_http_app())
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as http_client:
|
||||
transport = httpx2.ASGITransport(app=tutorial001.mcp.streamable_http_app())
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as http_client:
|
||||
response = await http_client.post("/mcp", json={}, headers={"Authorization": "Bearer not-a-real-token"})
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {"error": "invalid_token", "error_description": "Authentication required"}
|
||||
|
|
@ -86,11 +86,11 @@ async def test_get_access_token_is_none_outside_an_authenticated_request() -> No
|
|||
async def test_get_access_token_is_the_callers_access_token() -> None:
|
||||
"""tutorial002: over Streamable HTTP a valid bearer token reaches the tool as an `AccessToken`."""
|
||||
url = "http://127.0.0.1:8000/mcp"
|
||||
transport = httpx.ASGITransport(app=tutorial002.mcp.streamable_http_app())
|
||||
transport = httpx2.ASGITransport(app=tutorial002.mcp.streamable_http_app())
|
||||
headers = {"Authorization": "Bearer alice-token"}
|
||||
async with tutorial002.mcp.session_manager.run():
|
||||
async with (
|
||||
httpx.AsyncClient(transport=transport, base_url=url, headers=headers) as http_client,
|
||||
httpx2.AsyncClient(transport=transport, base_url=url, headers=headers) as http_client,
|
||||
Client(streamable_http_client(url, http_client=http_client)) as client,
|
||||
):
|
||||
result = await client.call_tool("whoami", {})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""`docs/run/deploy.md`: every claim the page makes, proved against the real SDK."""
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from mcp_types import (
|
||||
INVALID_PARAMS,
|
||||
|
|
@ -44,18 +44,18 @@ async def test_the_default_app_rejects_a_real_hostname_before_mcp_runs() -> None
|
|||
bare = MCPServer("Notes")
|
||||
app = bare.streamable_http_app()
|
||||
async with bare.session_manager.run():
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="https://api.example.com") as h:
|
||||
async with httpx2.AsyncClient(transport=httpx2.ASGITransport(app=app), base_url="https://api.example.com") as h:
|
||||
response = await h.post("/mcp", json=INITIALIZE, headers=MCP_HEADERS)
|
||||
assert (response.status_code, response.text) == (421, "Invalid Host header")
|
||||
|
||||
|
||||
async def test_the_allowlisted_app_serves_its_hostname_and_still_rejects_others() -> None:
|
||||
"""tutorial001: `allowed_hosts=` opens exactly the hostname you named, and nothing else."""
|
||||
transport = httpx.ASGITransport(app=tutorial001.app)
|
||||
transport = httpx2.ASGITransport(app=tutorial001.app)
|
||||
async with tutorial001.mcp.session_manager.run():
|
||||
async with httpx.AsyncClient(transport=transport, base_url="https://mcp.example.com") as http:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="https://mcp.example.com") as http:
|
||||
allowed = await http.post("/mcp", json=INITIALIZE, headers=MCP_HEADERS)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="https://api.example.com") as http:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="https://api.example.com") as http:
|
||||
rejected = await http.post("/mcp", json=INITIALIZE, headers=MCP_HEADERS)
|
||||
assert allowed.status_code == 200
|
||||
assert allowed.headers["mcp-session-id"]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import inspect
|
||||
from urllib.parse import parse_qsl
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import jwt
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
|
|
@ -27,21 +27,21 @@ pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDepre
|
|||
MCP_SERVER_URL = "http://localhost:8001/mcp"
|
||||
|
||||
|
||||
class RecordingASGITransport(httpx.ASGITransport):
|
||||
"""An `httpx.ASGITransport` that appends every (method, path, body) it carries to a shared log."""
|
||||
class RecordingASGITransport(httpx2.ASGITransport):
|
||||
"""An `httpx2.ASGITransport` that appends every (method, path, body) it carries to a shared log."""
|
||||
|
||||
def __init__(self, app: Starlette, log: list[tuple[str, str, bytes]]) -> None:
|
||||
super().__init__(app=app)
|
||||
self.log = log
|
||||
|
||||
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
||||
async def handle_async_request(self, request: httpx2.Request) -> httpx2.Response:
|
||||
self.log.append((request.method, request.url.path, request.content))
|
||||
return await super().handle_async_request(request)
|
||||
|
||||
|
||||
async def test_the_provider_is_an_httpx_auth_but_not_an_oauth_client_provider() -> None:
|
||||
"""tutorial001: same `auth=` slot as the rest of OAuth clients, but nothing is discovered or registered."""
|
||||
assert isinstance(tutorial001.oauth, httpx.Auth)
|
||||
assert isinstance(tutorial001.oauth, httpx2.Auth)
|
||||
assert not isinstance(tutorial001.oauth, OAuthClientProvider)
|
||||
|
||||
|
||||
|
|
@ -132,8 +132,8 @@ async def test_a_replayed_assertion_is_rejected() -> None:
|
|||
|
||||
async def test_the_metadata_advertises_the_grant_type_and_the_id_jag_profile() -> None:
|
||||
"""tutorial002: the flag turns on both the `jwt-bearer` grant type and the grant-profile advertisement."""
|
||||
transport = httpx.ASGITransport(app=tutorial002.auth_app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="https://auth.example.com") as http_client:
|
||||
transport = httpx2.ASGITransport(app=tutorial002.auth_app)
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="https://auth.example.com") as http_client:
|
||||
response = await http_client.get("/.well-known/oauth-authorization-server")
|
||||
assert response.status_code == 200
|
||||
metadata = response.json()
|
||||
|
|
@ -167,7 +167,7 @@ async def test_the_whole_grant_is_one_token_request() -> None:
|
|||
mounts = {"https://auth.example.com": RecordingASGITransport(tutorial002.auth_app, log)}
|
||||
async with mcp.session_manager.run():
|
||||
async with (
|
||||
httpx.AsyncClient(auth=tutorial001.oauth, transport=transport, mounts=mounts) as http_client,
|
||||
httpx2.AsyncClient(auth=tutorial001.oauth, transport=transport, mounts=mounts) as http_client,
|
||||
Client(streamable_http_client(MCP_SERVER_URL, http_client=http_client)) as client,
|
||||
):
|
||||
result = await client.call_tool("whoami", {})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import inspect
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from mcp_types import INVALID_REQUEST, ResourceUpdatedNotification, TextContent
|
||||
|
||||
|
|
@ -64,8 +64,8 @@ async def test_a_legacy_session_is_minted_in_process_and_a_stray_session_id_is_a
|
|||
app = MCPServer("Bookshop").streamable_http_app()
|
||||
async with (
|
||||
app.router.lifespan_context(app),
|
||||
httpx.ASGITransport(app) as transport,
|
||||
httpx.AsyncClient(transport=transport, base_url="http://localhost:8000") as http,
|
||||
httpx2.ASGITransport(app) as transport,
|
||||
httpx2.AsyncClient(transport=transport, base_url="http://localhost:8000") as http,
|
||||
):
|
||||
opened = await http.post("/mcp", json=INITIALIZE, headers=MCP_HEADERS)
|
||||
assert opened.status_code == 200
|
||||
|
|
@ -80,8 +80,8 @@ async def test_stateless_http_never_mints_a_session() -> None:
|
|||
app = MCPServer("Bookshop").streamable_http_app(stateless_http=True)
|
||||
async with (
|
||||
app.router.lifespan_context(app),
|
||||
httpx.ASGITransport(app) as transport,
|
||||
httpx.AsyncClient(transport=transport, base_url="http://localhost:8000") as http,
|
||||
httpx2.ASGITransport(app) as transport,
|
||||
httpx2.AsyncClient(transport=transport, base_url="http://localhost:8000") as http,
|
||||
):
|
||||
opened = await http.post("/mcp", json=INITIALIZE, headers=MCP_HEADERS)
|
||||
assert opened.status_code == 200
|
||||
|
|
@ -93,8 +93,8 @@ async def test_stateless_http_kills_the_legacy_back_channel_and_only_the_legacy_
|
|||
the legacy client's call fails as the top-level `MCPError` the `!!! check` quotes."""
|
||||
async with (
|
||||
tutorial002.app.router.lifespan_context(tutorial002.app),
|
||||
httpx.ASGITransport(tutorial002.app) as transport,
|
||||
httpx.AsyncClient(transport=transport) as http,
|
||||
httpx2.ASGITransport(tutorial002.app) as transport,
|
||||
httpx2.AsyncClient(transport=transport) as http,
|
||||
):
|
||||
modern_target = streamable_http_client(URL, http_client=http)
|
||||
async with Client(modern_target, elicitation_callback=tutorial001.answer) as modern:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import inspect
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from pydantic import AnyUrl, ValidationError
|
||||
|
||||
|
|
@ -42,8 +42,8 @@ async def test_storage_round_trips_tokens_and_client_info() -> None:
|
|||
|
||||
|
||||
async def test_the_provider_is_an_httpx_auth() -> None:
|
||||
"""tutorial001: `OAuthClientProvider` plugs into httpx, not into MCP."""
|
||||
assert isinstance(tutorial001.oauth, httpx.Auth)
|
||||
"""tutorial001: `OAuthClientProvider` plugs into httpx2, not into MCP."""
|
||||
assert isinstance(tutorial001.oauth, httpx2.Auth)
|
||||
|
||||
|
||||
async def test_the_metadata_defaults_are_the_authorization_code_flow() -> None:
|
||||
|
|
@ -66,9 +66,9 @@ async def test_the_redirect_handler_receives_the_authorization_url(capsys: pytes
|
|||
|
||||
|
||||
async def test_client_credentials_provider_has_no_human_in_the_loop() -> None:
|
||||
"""tutorial002: `ClientCredentialsOAuthProvider` is the same `httpx.Auth`, minus the handlers."""
|
||||
"""tutorial002: `ClientCredentialsOAuthProvider` is the same `httpx2.Auth`, minus the handlers."""
|
||||
assert isinstance(tutorial002.oauth, OAuthClientProvider)
|
||||
assert isinstance(tutorial002.oauth, httpx.Auth)
|
||||
assert isinstance(tutorial002.oauth, httpx2.Auth)
|
||||
assert tutorial002.oauth.context.redirect_handler is None
|
||||
assert tutorial002.oauth.context.callback_handler is None
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ async def test_the_three_remaining_keyword_arguments_have_defaults() -> None:
|
|||
|
||||
|
||||
async def test_the_one_more_provider_is_private_key_jwt() -> None:
|
||||
"""The `!!! info`: `PrivateKeyJWTOAuthProvider` is the same `httpx.Auth`, built the same way."""
|
||||
"""The `!!! info`: `PrivateKeyJWTOAuthProvider` is the same `httpx2.Auth`, built the same way."""
|
||||
provider = PrivateKeyJWTOAuthProvider(
|
||||
server_url="http://localhost:8001/mcp",
|
||||
storage=tutorial002.InMemoryTokenStorage(),
|
||||
|
|
@ -100,7 +100,7 @@ async def test_the_one_more_provider_is_private_key_jwt() -> None:
|
|||
assertion_provider=static_assertion_provider("a.prebuilt.jwt"),
|
||||
)
|
||||
assert isinstance(provider, OAuthClientProvider)
|
||||
assert isinstance(provider, httpx.Auth)
|
||||
assert isinstance(provider, httpx2.Auth)
|
||||
assert provider.context.client_metadata.token_endpoint_auth_method == "private_key_jwt"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from mcp_types import (
|
||||
INVALID_PARAMS,
|
||||
|
|
@ -125,10 +125,10 @@ async def test_the_default_streamable_http_app_answers_a_real_hostname_with_421(
|
|||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""tutorial003: one 421, three spellings. The page presents all three as the same event."""
|
||||
transport = httpx.ASGITransport(app=tutorial003.app)
|
||||
transport = httpx2.ASGITransport(app=tutorial003.app)
|
||||
async with tutorial003.mcp.session_manager.run():
|
||||
# What curl (or the reverse proxy's access log) shows: the status and the plain-text body.
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://mcp.example.com") as raw:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://mcp.example.com") as raw:
|
||||
with caplog.at_level(logging.WARNING, logger="mcp.server.transport_security"):
|
||||
response = await raw.post("/mcp", json=INITIALIZE, headers=MCP_HEADERS)
|
||||
assert (response.status_code, response.text) == (421, "Invalid Host header")
|
||||
|
|
@ -137,7 +137,7 @@ async def test_the_default_streamable_http_app_answers_a_real_hostname_with_421(
|
|||
# What the server operator finds by grepping the server log.
|
||||
assert "Invalid Host header: mcp.example.com" in caplog.messages
|
||||
# What the python `Client` raises instead: the generic stand-in, wrapped by the task group.
|
||||
async with httpx.AsyncClient(transport=transport) as http_client:
|
||||
async with httpx2.AsyncClient(transport=transport) as http_client:
|
||||
client = Client(streamable_http_client("http://mcp.example.com/mcp", http_client=http_client))
|
||||
with pytest.raises(Exception) as exc_info: # pragma: no branch
|
||||
await client.__aenter__() # the connection attempt itself is what fails
|
||||
|
|
@ -147,9 +147,9 @@ async def test_the_default_streamable_http_app_answers_a_real_hostname_with_421(
|
|||
|
||||
async def test_an_allowlisted_hostname_connects_and_calls_a_tool() -> None:
|
||||
"""tutorial004: `transport_security=` names the deployed hostname, and the same client connects."""
|
||||
transport = httpx.ASGITransport(app=tutorial004.app)
|
||||
transport = httpx2.ASGITransport(app=tutorial004.app)
|
||||
async with tutorial004.mcp.session_manager.run():
|
||||
async with httpx.AsyncClient(transport=transport) as http_client:
|
||||
async with httpx2.AsyncClient(transport=transport) as http_client:
|
||||
allowed = streamable_http_client("http://mcp.example.com/mcp", http_client=http_client)
|
||||
async with Client(allowed) as c: # pragma: no branch
|
||||
assert c.protocol_version == "2026-07-28"
|
||||
|
|
@ -159,8 +159,8 @@ async def test_an_allowlisted_hostname_connects_and_calls_a_tool() -> None:
|
|||
|
||||
async def test_a_mounted_app_without_a_lifespan_fails_on_the_first_request() -> None:
|
||||
"""tutorial005: Starlette never runs a mounted sub-app's lifespan, so nothing starts the manager."""
|
||||
transport = httpx.ASGITransport(app=tutorial005.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as http:
|
||||
transport = httpx2.ASGITransport(app=tutorial005.app)
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as http:
|
||||
with pytest.raises(RuntimeError, match=r"Task group is not initialized\. Make sure to use run\(\)\."):
|
||||
await http.post("/mcp")
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ async def test_a_session_id_the_server_never_issued_gets_a_404_session_not_found
|
|||
mcp = MCPServer("Weather")
|
||||
app = mcp.streamable_http_app()
|
||||
async with mcp.session_manager.run():
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://127.0.0.1:8000") as h:
|
||||
async with httpx2.AsyncClient(transport=httpx2.ASGITransport(app=app), base_url="http://127.0.0.1:8000") as h:
|
||||
response = await h.post(
|
||||
"/mcp",
|
||||
json={"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}},
|
||||
|
|
@ -243,9 +243,9 @@ async def test_a_legacy_ctx_elicit_without_a_callback_says_elicitation_not_suppo
|
|||
|
||||
async def test_ctx_elicit_over_stateless_http_has_no_back_channel() -> None:
|
||||
"""tutorial008: `stateless_http=True` leaves the server no channel to send `elicitation/create`."""
|
||||
transport = httpx.ASGITransport(app=tutorial008.app)
|
||||
transport = httpx2.ASGITransport(app=tutorial008.app)
|
||||
async with tutorial008.mcp.session_manager.run():
|
||||
async with httpx.AsyncClient(transport=transport) as http_client:
|
||||
async with httpx2.AsyncClient(transport=transport) as http_client:
|
||||
stateless = streamable_http_client("http://127.0.0.1:8000/mcp", http_client=http_client)
|
||||
async with Client(stateless) as c: # pragma: no branch
|
||||
with pytest.raises(MCPError) as exc_info: # pragma: no branch
|
||||
|
|
|
|||
Loading…
Reference in a new issue