| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -38,7 +38,7 @@ | |||
| 38 | 38 | from typing import Any, cast | |
| 39 | 39 | from urllib.parse import parse_qs, urlparse | |
| 40 | 40 | ||
| 41 | - import httpx | ||
| 41 | + import httpx2 | ||
| 42 | 42 | import mcp_types as types | |
| 43 | 43 | from mcp_types.version import MODERN_PROTOCOL_VERSIONS | |
| 44 | 44 | from pydantic import AnyUrl | |
@@ -151,7 +151,7 @@ async def handle_redirect(self, authorization_url: str) -> None: | |||
| 151 | 151 | """Fetch the authorization URL and extract the auth code from the redirect.""" | |
| 152 | 152 | logger.debug(f"Fetching authorization URL: {authorization_url}") | |
| 153 | 153 | ||
| 154 | - async with httpx.AsyncClient() as client: | ||
| 154 | + async with httpx2.AsyncClient() as client: | ||
| 155 | 155 | response = await client.get( | |
| 156 | 156 | authorization_url, | |
| 157 | 157 | follow_redirects=False, | |
@@ -486,13 +486,13 @@ async def run_enterprise_managed_authorization(server_url: str) -> None: | |||
| 486 | 486 | # learn it from the harness's PRM document (RFC 9728); production | |
| 487 | 487 | # deployments would supply it as static configuration instead. | |
| 488 | 488 | prm_url = build_protected_resource_metadata_discovery_urls(None, server_url)[0] | |
| 489 | - async with httpx.AsyncClient(timeout=30.0) as http: | ||
| 489 | + async with httpx2.AsyncClient(timeout=30.0) as http: | ||
| 490 | 490 | prm = (await http.get(prm_url)).raise_for_status().json() | |
| 491 | 491 | as_issuer = prm["authorization_servers"][0] | |
| 492 | 492 | ||
| 493 | 493 | async def fetch_id_jag(audience: str, resource: str) -> str: | |
| 494 | 494 | """Leg 1 - RFC 8693 token-exchange at the enterprise IdP.""" | |
| 495 | - async with httpx.AsyncClient(timeout=30.0) as http: | ||
| 495 | + async with httpx2.AsyncClient(timeout=30.0) as http: | ||
| 496 | 496 | resp = await http.post( | |
| 497 | 497 | idp_token_endpoint, | |
| 498 | 498 | data={ | |
@@ -563,9 +563,9 @@ async def run_auth_code_client(server_url: str) -> None: | |||
| 563 | 563 | await _run_auth_session(server_url, oauth_auth) | |
| 564 | 564 | ||
| 565 | 565 | ||
| 566 | - async def _run_auth_session(server_url: str, oauth_auth: httpx.Auth) -> None: | ||
| 566 | + async def _run_auth_session(server_url: str, oauth_auth: httpx2.Auth) -> None: | ||
| 567 | 567 | """Common session logic for all OAuth flows.""" | |
| 568 | - http_client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0) | ||
| 568 | + http_client = httpx2.AsyncClient(auth=oauth_auth, timeout=30.0) | ||
| 569 | 569 | transport = streamable_http_client(url=server_url, http_client=http_client) | |
| 570 | 570 | async with Client(transport, mode=client_mode(), elicitation_callback=default_elicitation_callback) as client: | |
| 571 | 571 | logger.debug("Initialized successfully") | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,7 +19,7 @@ Everything below is the second request: the client that sends it and the authori | |||
| 19 | 19 | ||
| 20 | 20 | ## The client | |
| 21 | 21 | ||
| 22 | - **`IdentityAssertionOAuthProvider`** lives in `mcp.client.auth.extensions.identity_assertion`. Like every provider in **[OAuth clients](oauth-clients.md)** it is an `httpx.Auth`: construct one, put it on `auth=`, hand the `httpx.AsyncClient` to the transport. | ||
| 22 | + **`IdentityAssertionOAuthProvider`** lives in `mcp.client.auth.extensions.identity_assertion`. Like every provider in **[OAuth clients](oauth-clients.md)** it is an `httpx2.Auth`: construct one, put it on `auth=`, hand the `httpx2.AsyncClient` to the transport. | ||
| 23 | 23 | ||
| 24 | 24 | ```python title="client.py" hl_lines="49-50 53-61" | |
| 25 | 25 | --8<-- "docs_src/identity_assertion/tutorial001.py" | |
@@ -139,7 +139,7 @@ And notice what the returned `OAuthToken` does not carry: a refresh token. The I | |||
| 139 | 139 | ||
| 140 | 140 | * [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) lets the enterprise identity provider, not the end user, decide which MCP servers a client may reach. The IdP signs that decision into an **ID-JAG**. | |
| 141 | 141 | * Obtaining the ID-JAG is an [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange against *your IdP*, and the SDK does not make it. Presenting it to the MCP authorization server is the [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) `jwt-bearer` grant, and the SDK does both sides of that. | |
| 142 | - * `IdentityAssertionOAuthProvider` is another `httpx.Auth`: a pre-registered confidential client, a pinned `issuer`, and one `assertion_provider(audience, resource)` callback. No browser, no registration, no refresh token. | ||
| 142 | + * `IdentityAssertionOAuthProvider` is another `httpx2.Auth`: a pre-registered confidential client, a pinned `issuer`, and one `assertion_provider(audience, resource)` callback. No browser, no registration, no refresh token. | ||
| 143 | 143 | * The authorization server is never discovered from the resource server. Configure `issuer` to exactly the string its metadata document serves; the comparison is character for character. | |
| 144 | 144 | * Server side, `identity_assertion_enabled=True` plus `exchange_identity_assertion`. The SDK authenticates the client and gates the grant; validating the ID-JAG is entirely yours, and the issued token is bound to the ID-JAG's `resource`, not the request's. | |
| 145 | 145 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Some MCP servers are protected. Send them a request without a token and they answer `401 Unauthorized`. | |
| 4 | 4 | ||
| 5 | - **`OAuthClientProvider`** is how you get the token. It is not an MCP object at all. It is an `httpx.Auth`, the standard httpx hook for "do something to every request". You attach it to an `httpx.AsyncClient`, hand that client to the Streamable HTTP transport, and stop thinking about it. | ||
| 5 | + **`OAuthClientProvider`** is how you get the token. It is not an MCP object at all. It is an `httpx2.Auth`, the standard httpx2 hook for "do something to every request". You attach it to an `httpx2.AsyncClient`, hand that client to the Streamable HTTP transport, and stop thinking about it. | ||
| 6 | 6 | ||
| 7 | 7 | This page is the client side. Making your own server demand a token is **[Authorization](../run/authorization.md)**. | |
| 8 | 8 | ||
@@ -68,9 +68,9 @@ A real client runs a small local HTTP server on the redirect URI instead of call | |||
| 68 | 68 | ||
| 69 | 69 | ### Into the `Client` | |
| 70 | 70 | ||
| 71 | - Look at `main()`. The provider goes on the **httpx client**, the httpx client goes into `streamable_http_client(url, http_client=...)`, and that transport goes into `Client`. | ||
| 71 | + Look at `main()`. The provider goes on the **httpx2 client**, the httpx2 client goes into `streamable_http_client(url, http_client=...)`, and that transport goes into `Client`. | ||
| 72 | 72 | ||
| 73 | - `streamable_http_client` has no `auth=` keyword. Anything HTTP-level (auth, headers, timeouts, proxies) belongs on the `httpx.AsyncClient` you bring. That layering is **[Client transports](transports.md)**. | ||
| 73 | + `streamable_http_client` has no `auth=` keyword. Anything HTTP-level (auth, headers, timeouts, proxies) belongs on the `httpx2.AsyncClient` you bring. That layering is **[Client transports](transports.md)**. | ||
| 74 | 74 | ||
| 75 | 75 | ## What the provider does for you | |
| 76 | 76 | ||
@@ -103,7 +103,7 @@ The URL must be HTTPS with a non-root path; anything else is a `ValueError` at c | |||
| 103 | 103 | ||
| 104 | 104 | A nightly job, a CI step, another service. There is no browser and nobody to click "allow". That is the **client credentials** grant: you already hold a `client_id` and a `client_secret`, and the token endpoint is the whole flow. | |
| 105 | 105 | ||
| 106 | - `ClientCredentialsOAuthProvider` is the same `httpx.Auth`, minus the human: | ||
| 106 | + `ClientCredentialsOAuthProvider` is the same `httpx2.Auth`, minus the human: | ||
| 107 | 107 | ||
| 108 | 108 | ```python title="client.py" hl_lines="4 27-33" | |
| 109 | 109 | --8<-- "docs_src/oauth_clients/tutorial002.py" | |
@@ -113,7 +113,7 @@ What changed: | |||
| 113 | 113 | ||
| 114 | 114 | * No `OAuthClientMetadata`, no handlers. You pass `client_id` and `client_secret`; the provider builds a minimal `client_credentials` registration around them and skips dynamic registration entirely. | |
| 115 | 115 | * `scopes` is a space-separated string, the OAuth wire format. | |
| 116 | - * Everything downstream is identical: the same `TokenStorage`, the same `httpx.AsyncClient(auth=...)`, the same `streamable_http_client`. | ||
| 116 | + * Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`. | ||
| 117 | 117 | ||
| 118 | 118 | By default the secret travels as HTTP Basic auth on the token request (`client_secret_basic`). Pass `token_endpoint_auth_method="client_secret_post"` to put it in the form body instead. Some authorization servers only accept one of the two. | |
| 119 | 119 | ||
@@ -133,11 +133,11 @@ There is one more no-human situation: the client belongs to an enterprise whose | |||
| 133 | 133 | ||
| 134 | 134 | When the OAuth flow goes wrong, the provider raises an `OAuthFlowError` from `mcp.client.auth`. It has two subclasses. `OAuthRegistrationError` means the authorization server refused to register you. `OAuthTokenError` means the token endpoint said no. One `except OAuthFlowError:` covers discovery, registration, authorization, and exchange. | |
| 135 | 135 | ||
| 136 | - Not everything is a flow error. The network can still fail; those are ordinary `httpx` exceptions and pass through untouched. | ||
| 136 | + Not everything is a flow error. The network can still fail; those are ordinary `httpx2` exceptions and pass through untouched. | ||
| 137 | 137 | ||
| 138 | 138 | ## Recap | |
| 139 | 139 | ||
| 140 | - * `OAuthClientProvider` is an `httpx.Auth`. Put it on an `httpx.AsyncClient`, pass that to `streamable_http_client(url, http_client=...)`, and `Client` never knows OAuth happened. | ||
| 140 | + * `OAuthClientProvider` is an `httpx2.Auth`. Put it on an `httpx2.AsyncClient`, pass that to `streamable_http_client(url, http_client=...)`, and `Client` never knows OAuth happened. | ||
| 141 | 141 | * You supply four things: the server URL, an `OAuthClientMetadata`, a `TokenStorage`, and the redirect/callback handler pair. | |
| 142 | 142 | * `TokenStorage` is a `Protocol`: four async methods, no base class. Persist `client_info` as well as the tokens. | |
| 143 | 143 | * Discovery, registration (dynamic, or via a **Client ID Metadata Document**), PKCE, the `state` and `iss` checks, and token refresh are the provider's job, not yours. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -29,7 +29,7 @@ Pass a URL string and you get **Streamable HTTP**, the transport you deploy behi | |||
| 29 | 29 | --8<-- "docs_src/client_transports/tutorial002.py" | |
| 30 | 30 | ``` | |
| 31 | 31 | ||
| 32 | - That is the whole production client. `Client` wraps the URL in `streamable_http_client(...)` for you, on top of an `httpx.AsyncClient` configured the way MCP needs: `follow_redirects=True`, a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open. | ||
| 32 | + That is the whole production client. `Client` wraps the URL in `streamable_http_client(...)` for you, on top of an `httpx2.AsyncClient` configured the way MCP needs: `follow_redirects=True`, a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open. | ||
| 33 | 33 | ||
| 34 | 34 | !!! check | |
| 35 | 35 | A `Client` you have constructed is **not** connected. Construction only picks the transport; | |
@@ -41,19 +41,26 @@ That is the whole production client. `Client` wraps the URL in `streamable_http_ | |||
| 41 | 41 | ||
| 42 | 42 | Nothing was resolved, fetched or spawned when you wrote `Client("http://...")`. That line is free. | |
| 43 | 43 | ||
| 44 | - ### Bring your own `httpx.AsyncClient` | ||
| 44 | + ### Bring your own `httpx2.AsyncClient` | ||
| 45 | 45 | ||
| 46 | - The moment you need an `Authorization` header, a cookie, a proxy, mTLS, or a different timeout, build the `httpx.AsyncClient` yourself and hand it to `streamable_http_client`: | ||
| 46 | + The moment you need an `Authorization` header, a cookie, a proxy, mTLS, or a different timeout, build the `httpx2.AsyncClient` yourself and hand it to `streamable_http_client`: | ||
| 47 | 47 | ||
| 48 | 48 | ```python title="client.py" hl_lines="8-14" | |
| 49 | 49 | --8<-- "docs_src/client_transports/tutorial003.py" | |
| 50 | 50 | ``` | |
| 51 | 51 | ||
| 52 | 52 | Two things to notice: | |
| 53 | 53 | ||
| 54 | - * You own the `httpx.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create. | ||
| 54 | + * You own the `httpx2.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create. | ||
| 55 | 55 | * `streamable_http_client(url, http_client=...)` returns a transport, and `Client(transport)` accepts it like anything else. | |
| 56 | 56 | ||
| 57 | + One TLS note: `httpx2` verifies certificates against the operating system trust store (via | ||
| 58 | + [`truststore`](https://pypi.org/project/truststore/)), not a bundled CA list. In an environment with | ||
| 59 | + no usable system CA store (some minimal containers), set the standard `SSL_CERT_FILE`/`SSL_CERT_DIR` | ||
| 60 | + environment variables or pass an explicit `verify=ssl_context` to your `httpx2.AsyncClient` | ||
| 61 | + (background in | ||
| 62 | + [`httpx` and `httpx-sse` replaced by `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). | ||
| 63 | + | ||
| 57 | 64 | !!! warning | |
| 58 | 65 | `streamable_http_client` used to take `headers=` and `timeout=` directly. It does not any more: | |
| 59 | 66 | its only parameters are `url`, `http_client` and `terminate_on_close`. Reach for `headers=` out | |
@@ -63,12 +70,13 @@ Two things to notice: | |||
| 63 | 70 | TypeError: streamable_http_client() got an unexpected keyword argument 'headers' | |
| 64 | 71 | ``` | |
| 65 | 72 | ||
| 66 | - Everything HTTP-shaped now lives on the one `httpx.AsyncClient` you pass in. | ||
| 73 | + Everything HTTP-shaped now lives on the one `httpx2.AsyncClient` you pass in. | ||
| 67 | 74 | ||
| 68 | 75 | !!! info | |
| 69 | - If you know `httpx`, you already know how to do auth, proxies, event hooks, retries and connection | ||
| 70 | - limits here. The SDK adds nothing on top and takes nothing away. It is also where OAuth plugs in: | ||
| 71 | - `httpx.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](oauth-clients.md)**. | ||
| 76 | + `httpx2` keeps the familiar `httpx` API, so if you know `httpx` you already know how to do auth, | ||
| 77 | + proxies, event hooks, retries and connection limits here. The SDK adds nothing on top and takes | ||
| 78 | + nothing away. It is also where OAuth plugs in: | ||
| 79 | + `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](oauth-clients.md)**. | ||
| 72 | 80 | ||
| 73 | 81 | ## stdio | |
| 74 | 82 | ||
@@ -106,7 +114,7 @@ A **transport** is any async context manager that yields a `(read, write)` pair | |||
| 106 | 114 | ||
| 107 | 115 | * `Client(mcp)` (the server object) connects in memory. Use it for tests and for embedding. | |
| 108 | 116 | * `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport. | |
| 109 | - * Headers, auth, proxies and timeouts belong on an `httpx.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword. | ||
| 117 | + * Headers, auth, proxies and timeouts belong on an `httpx2.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword. | ||
| 110 | 118 | * stdio is `Client(stdio_client(StdioServerParameters(...)))`, never the parameters object alone. | |
| 111 | 119 | * The subprocess gets an allow-listed environment, not yours; `env=` adds to it. | |
| 112 | 120 | * A transport is anything you can `async with x as (read, write)`. `Client` hands anything that isn't a server object or a URL straight to that protocol. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,7 +35,7 @@ You don't need to know any of this to use the SDK, but if you're wondering what | |||
| 35 | 35 | * [`anyio`](https://anyio.readthedocs.io/): the async runtime. The whole SDK is written against anyio, so it runs on either `asyncio` or `trio`. | |
| 36 | 36 | * [`pydantic`](https://docs.pydantic.dev/): what every `mcp_types` model is built on, plus all schema generation and validation. | |
| 37 | 37 | * [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/): server configuration via `MCP_*` environment variables and `.env` files. | |
| 38 | - * [`httpx`](https://www.python-httpx.org/) and [`httpx-sse`](https://pypi.org/project/httpx-sse/): the HTTP client behind the Streamable HTTP and SSE *client* transports. | ||
| 38 | + * [`httpx2`](https://pypi.org/project/httpx2/): the HTTP client behind the Streamable HTTP and SSE *client* transports, with server-sent events support built in. | ||
| 39 | 39 | * [`starlette`](https://www.starlette.io/), [`uvicorn`](https://www.uvicorn.org/), [`sse-starlette`](https://pypi.org/project/sse-starlette/), and [`python-multipart`](https://pypi.org/project/python-multipart/): the HTTP *server* transports. | |
| 40 | 40 | * [`jsonschema`](https://pypi.org/project/jsonschema/): validates a tool's structured output against its declared output schema. | |
| 41 | 41 | * [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/): OAuth token handling for authorization. | |
| Back | FazBrowse Home | New Git URL |
0 commit comments