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
110 changed files with 1029 additions and 948 deletions
12
.github/actions/conformance/client.py
vendored
12
.github/actions/conformance/client.py
vendored
|
|
@ -38,7 +38,7 @@ from collections.abc import Callable, Coroutine
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx2
|
||||||
import mcp_types as types
|
import mcp_types as types
|
||||||
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
|
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
|
||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
|
|
@ -151,7 +151,7 @@ class ConformanceOAuthCallbackHandler:
|
||||||
"""Fetch the authorization URL and extract the auth code from the redirect."""
|
"""Fetch the authorization URL and extract the auth code from the redirect."""
|
||||||
logger.debug(f"Fetching authorization URL: {authorization_url}")
|
logger.debug(f"Fetching authorization URL: {authorization_url}")
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx2.AsyncClient() as client:
|
||||||
response = await client.get(
|
response = await client.get(
|
||||||
authorization_url,
|
authorization_url,
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
|
|
@ -486,13 +486,13 @@ async def run_enterprise_managed_authorization(server_url: str) -> None:
|
||||||
# learn it from the harness's PRM document (RFC 9728); production
|
# learn it from the harness's PRM document (RFC 9728); production
|
||||||
# deployments would supply it as static configuration instead.
|
# deployments would supply it as static configuration instead.
|
||||||
prm_url = build_protected_resource_metadata_discovery_urls(None, server_url)[0]
|
prm_url = build_protected_resource_metadata_discovery_urls(None, server_url)[0]
|
||||||
async with httpx.AsyncClient(timeout=30.0) as http:
|
async with httpx2.AsyncClient(timeout=30.0) as http:
|
||||||
prm = (await http.get(prm_url)).raise_for_status().json()
|
prm = (await http.get(prm_url)).raise_for_status().json()
|
||||||
as_issuer = prm["authorization_servers"][0]
|
as_issuer = prm["authorization_servers"][0]
|
||||||
|
|
||||||
async def fetch_id_jag(audience: str, resource: str) -> str:
|
async def fetch_id_jag(audience: str, resource: str) -> str:
|
||||||
"""Leg 1 - RFC 8693 token-exchange at the enterprise IdP."""
|
"""Leg 1 - RFC 8693 token-exchange at the enterprise IdP."""
|
||||||
async with httpx.AsyncClient(timeout=30.0) as http:
|
async with httpx2.AsyncClient(timeout=30.0) as http:
|
||||||
resp = await http.post(
|
resp = await http.post(
|
||||||
idp_token_endpoint,
|
idp_token_endpoint,
|
||||||
data={
|
data={
|
||||||
|
|
@ -563,9 +563,9 @@ async def run_auth_code_client(server_url: str) -> None:
|
||||||
await _run_auth_session(server_url, oauth_auth)
|
await _run_auth_session(server_url, oauth_auth)
|
||||||
|
|
||||||
|
|
||||||
async def _run_auth_session(server_url: str, oauth_auth: httpx.Auth) -> None:
|
async def _run_auth_session(server_url: str, oauth_auth: httpx2.Auth) -> None:
|
||||||
"""Common session logic for all OAuth flows."""
|
"""Common session logic for all OAuth flows."""
|
||||||
http_client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0)
|
http_client = httpx2.AsyncClient(auth=oauth_auth, timeout=30.0)
|
||||||
transport = streamable_http_client(url=server_url, http_client=http_client)
|
transport = streamable_http_client(url=server_url, http_client=http_client)
|
||||||
async with Client(transport, mode=client_mode(), elicitation_callback=default_elicitation_callback) as client:
|
async with Client(transport, mode=client_mode(), elicitation_callback=default_elicitation_callback) as client:
|
||||||
logger.debug("Initialized successfully")
|
logger.debug("Initialized successfully")
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Everything below is the second request: the client that sends it and the authori
|
||||||
|
|
||||||
## The client
|
## The client
|
||||||
|
|
||||||
**`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.
|
**`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.
|
||||||
|
|
||||||
```python title="client.py" hl_lines="49-50 53-61"
|
```python title="client.py" hl_lines="49-50 53-61"
|
||||||
--8<-- "docs_src/identity_assertion/tutorial001.py"
|
--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
|
||||||
|
|
||||||
* [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**.
|
* [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**.
|
||||||
* 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.
|
* 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.
|
||||||
* `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.
|
* `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.
|
||||||
* 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.
|
* 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.
|
||||||
* 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.
|
* 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Some MCP servers are protected. Send them a request without a token and they answer `401 Unauthorized`.
|
Some MCP servers are protected. Send them a request without a token and they answer `401 Unauthorized`.
|
||||||
|
|
||||||
**`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.
|
**`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.
|
||||||
|
|
||||||
This page is the client side. Making your own server demand a token is **[Authorization](../run/authorization.md)**.
|
This page is the client side. Making your own server demand a token is **[Authorization](../run/authorization.md)**.
|
||||||
|
|
||||||
|
|
@ -68,9 +68,9 @@ A real client runs a small local HTTP server on the redirect URI instead of call
|
||||||
|
|
||||||
### Into the `Client`
|
### Into the `Client`
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
`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)**.
|
`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)**.
|
||||||
|
|
||||||
## What the provider does for you
|
## What the provider does for you
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ The URL must be HTTPS with a non-root path; anything else is a `ValueError` at c
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
`ClientCredentialsOAuthProvider` is the same `httpx.Auth`, minus the human:
|
`ClientCredentialsOAuthProvider` is the same `httpx2.Auth`, minus the human:
|
||||||
|
|
||||||
```python title="client.py" hl_lines="4 27-33"
|
```python title="client.py" hl_lines="4 27-33"
|
||||||
--8<-- "docs_src/oauth_clients/tutorial002.py"
|
--8<-- "docs_src/oauth_clients/tutorial002.py"
|
||||||
|
|
@ -113,7 +113,7 @@ What changed:
|
||||||
|
|
||||||
* 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.
|
* 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.
|
||||||
* `scopes` is a space-separated string, the OAuth wire format.
|
* `scopes` is a space-separated string, the OAuth wire format.
|
||||||
* Everything downstream is identical: the same `TokenStorage`, the same `httpx.AsyncClient(auth=...)`, the same `streamable_http_client`.
|
* Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
@ -133,11 +133,11 @@ There is one more no-human situation: the client belongs to an enterprise whose
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
Not everything is a flow error. The network can still fail; those are ordinary `httpx` exceptions and pass through untouched.
|
Not everything is a flow error. The network can still fail; those are ordinary `httpx2` exceptions and pass through untouched.
|
||||||
|
|
||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
* `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.
|
* `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.
|
||||||
* You supply four things: the server URL, an `OAuthClientMetadata`, a `TokenStorage`, and the redirect/callback handler pair.
|
* You supply four things: the server URL, an `OAuthClientMetadata`, a `TokenStorage`, and the redirect/callback handler pair.
|
||||||
* `TokenStorage` is a `Protocol`: four async methods, no base class. Persist `client_info` as well as the tokens.
|
* `TokenStorage` is a `Protocol`: four async methods, no base class. Persist `client_info` as well as the tokens.
|
||||||
* 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.
|
* 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.
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ Pass a URL string and you get **Streamable HTTP**, the transport you deploy behi
|
||||||
--8<-- "docs_src/client_transports/tutorial002.py"
|
--8<-- "docs_src/client_transports/tutorial002.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
!!! check
|
!!! check
|
||||||
A `Client` you have constructed is **not** connected. Construction only picks the transport;
|
A `Client` you have constructed is **not** connected. Construction only picks the transport;
|
||||||
|
|
@ -41,9 +41,9 @@ That is the whole production client. `Client` wraps the URL in `streamable_http_
|
||||||
|
|
||||||
Nothing was resolved, fetched or spawned when you wrote `Client("http://...")`. That line is free.
|
Nothing was resolved, fetched or spawned when you wrote `Client("http://...")`. That line is free.
|
||||||
|
|
||||||
### Bring your own `httpx.AsyncClient`
|
### Bring your own `httpx2.AsyncClient`
|
||||||
|
|
||||||
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`:
|
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`:
|
||||||
|
|
||||||
```python title="client.py" hl_lines="8-14"
|
```python title="client.py" hl_lines="8-14"
|
||||||
--8<-- "docs_src/client_transports/tutorial003.py"
|
--8<-- "docs_src/client_transports/tutorial003.py"
|
||||||
|
|
@ -51,9 +51,16 @@ The moment you need an `Authorization` header, a cookie, a proxy, mTLS, or a dif
|
||||||
|
|
||||||
Two things to notice:
|
Two things to notice:
|
||||||
|
|
||||||
* You own the `httpx.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create.
|
* You own the `httpx2.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create.
|
||||||
* `streamable_http_client(url, http_client=...)` returns a transport, and `Client(transport)` accepts it like anything else.
|
* `streamable_http_client(url, http_client=...)` returns a transport, and `Client(transport)` accepts it like anything else.
|
||||||
|
|
||||||
|
One TLS note: `httpx2` verifies certificates against the operating system trust store (via
|
||||||
|
[`truststore`](https://pypi.org/project/truststore/)), not a bundled CA list. In an environment with
|
||||||
|
no usable system CA store (some minimal containers), set the standard `SSL_CERT_FILE`/`SSL_CERT_DIR`
|
||||||
|
environment variables or pass an explicit `verify=ssl_context` to your `httpx2.AsyncClient`
|
||||||
|
(background in
|
||||||
|
[`httpx` and `httpx-sse` replaced by `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)).
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
`streamable_http_client` used to take `headers=` and `timeout=` directly. It does not any more:
|
`streamable_http_client` used to take `headers=` and `timeout=` directly. It does not any more:
|
||||||
its only parameters are `url`, `http_client` and `terminate_on_close`. Reach for `headers=` out
|
its only parameters are `url`, `http_client` and `terminate_on_close`. Reach for `headers=` out
|
||||||
|
|
@ -63,12 +70,13 @@ Two things to notice:
|
||||||
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
|
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
|
||||||
```
|
```
|
||||||
|
|
||||||
Everything HTTP-shaped now lives on the one `httpx.AsyncClient` you pass in.
|
Everything HTTP-shaped now lives on the one `httpx2.AsyncClient` you pass in.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
If you know `httpx`, you already know how to do auth, proxies, event hooks, retries and connection
|
`httpx2` keeps the familiar `httpx` API, so if you know `httpx` you already know how to do auth,
|
||||||
limits here. The SDK adds nothing on top and takes nothing away. It is also where OAuth plugs in:
|
proxies, event hooks, retries and connection limits here. The SDK adds nothing on top and takes
|
||||||
`httpx.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](oauth-clients.md)**.
|
nothing away. It is also where OAuth plugs in:
|
||||||
|
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](oauth-clients.md)**.
|
||||||
|
|
||||||
## stdio
|
## stdio
|
||||||
|
|
||||||
|
|
@ -106,7 +114,7 @@ A **transport** is any async context manager that yields a `(read, write)` pair
|
||||||
|
|
||||||
* `Client(mcp)` (the server object) connects in memory. Use it for tests and for embedding.
|
* `Client(mcp)` (the server object) connects in memory. Use it for tests and for embedding.
|
||||||
* `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport.
|
* `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport.
|
||||||
* Headers, auth, proxies and timeouts belong on an `httpx.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword.
|
* Headers, auth, proxies and timeouts belong on an `httpx2.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword.
|
||||||
* stdio is `Client(stdio_client(StdioServerParameters(...)))`, never the parameters object alone.
|
* stdio is `Client(stdio_client(StdioServerParameters(...)))`, never the parameters object alone.
|
||||||
* The subprocess gets an allow-listed environment, not yours; `env=` adds to it.
|
* The subprocess gets an allow-listed environment, not yours; `env=` adds to it.
|
||||||
* 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.
|
* 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.
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ You don't need to know any of this to use the SDK, but if you're wondering what
|
||||||
* [`anyio`](https://anyio.readthedocs.io/): the async runtime. The whole SDK is written against anyio, so it runs on either `asyncio` or `trio`.
|
* [`anyio`](https://anyio.readthedocs.io/): the async runtime. The whole SDK is written against anyio, so it runs on either `asyncio` or `trio`.
|
||||||
* [`pydantic`](https://docs.pydantic.dev/): what every `mcp_types` model is built on, plus all schema generation and validation.
|
* [`pydantic`](https://docs.pydantic.dev/): what every `mcp_types` model is built on, plus all schema generation and validation.
|
||||||
* [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/): server configuration via `MCP_*` environment variables and `.env` files.
|
* [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/): server configuration via `MCP_*` environment variables and `.env` files.
|
||||||
* [`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.
|
* [`httpx2`](https://pypi.org/project/httpx2/): the HTTP client behind the Streamable HTTP and SSE *client* transports, with server-sent events support built in.
|
||||||
* [`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.
|
* [`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.
|
||||||
* [`jsonschema`](https://pypi.org/project/jsonschema/): validates a tool's structured output against its declared output schema.
|
* [`jsonschema`](https://pypi.org/project/jsonschema/): validates a tool's structured output against its declared output schema.
|
||||||
* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/): OAuth token handling for authorization.
|
* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/): OAuth token handling for authorization.
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ v2 raises the minimum versions of several shared dependencies and adds new requi
|
||||||
| pywin32 (Windows) | `>=310` (Python <3.14) | `>=311` | floor raised on Python <3.14 |
|
| pywin32 (Windows) | `>=310` (Python <3.14) | `>=311` | floor raised on Python <3.14 |
|
||||||
| opentelemetry-api | not a dependency | `>=1.28.0` | new required dependency |
|
| opentelemetry-api | not a dependency | `>=1.28.0` | new required dependency |
|
||||||
| mcp-types | not a dependency | `==<exact mcp version>` | new, exact-pinned |
|
| mcp-types | not a dependency | `==<exact mcp version>` | new, exact-pinned |
|
||||||
|
| httpx | `>=0.27.1,<1.0.0` | removed | see [`httpx` and `httpx-sse` replaced by `httpx2`](#httpx-and-httpx-sse-replaced-by-httpx2) |
|
||||||
|
| httpx-sse | `>=0.4` | removed | see [`httpx` and `httpx-sse` replaced by `httpx2`](#httpx-and-httpx-sse-replaced-by-httpx2) |
|
||||||
|
| httpx2 | not a dependency | `>=2.5.0` | new required dependency |
|
||||||
| `ws` extra | `websockets>=15.0.1` | removed | see [WebSocket transport removed](#websocket-transport-removed) |
|
| `ws` extra | `websockets>=15.0.1` | removed | see [WebSocket transport removed](#websocket-transport-removed) |
|
||||||
|
|
||||||
**Before (v1):**
|
**Before (v1):**
|
||||||
|
|
@ -89,6 +92,68 @@ dependencies = [
|
||||||
|
|
||||||
Relax or bump any conflicting pins when upgrading. sse-starlette jumps two majors, so a project that imports `sse_starlette` itself must also work through that library's own breaking changes to co-install with mcp v2. `opentelemetry-api` is a new hard dependency because every outbound request now carries a `_meta` envelope used for OpenTelemetry trace propagation; see [Every outbound request now carries a `_meta` envelope](#every-outbound-request-now-carries-a-_meta-envelope-opentelemetry-is-on-by-default). `mcp-types` is exact-pinned to the SDK version; nothing in a v1 tree can conflict with it, but do not pin `mcp-types` independently of `mcp`.
|
Relax or bump any conflicting pins when upgrading. sse-starlette jumps two majors, so a project that imports `sse_starlette` itself must also work through that library's own breaking changes to co-install with mcp v2. `opentelemetry-api` is a new hard dependency because every outbound request now carries a `_meta` envelope used for OpenTelemetry trace propagation; see [Every outbound request now carries a `_meta` envelope](#every-outbound-request-now-carries-a-_meta-envelope-opentelemetry-is-on-by-default). `mcp-types` is exact-pinned to the SDK version; nothing in a v1 tree can conflict with it, but do not pin `mcp-types` independently of `mcp`.
|
||||||
|
|
||||||
|
### `httpx` and `httpx-sse` replaced by `httpx2`
|
||||||
|
|
||||||
|
The SDK now depends on [`httpx2`](https://pypi.org/project/httpx2/) instead of
|
||||||
|
`httpx` and `httpx-sse`. `httpx2` is the next-generation HTTP client (a fork of
|
||||||
|
`httpx`) with server-sent events support built in, so the separate `httpx-sse`
|
||||||
|
dependency is gone.
|
||||||
|
|
||||||
|
The swap itself does not change any SDK signatures - `streamable_http_client`
|
||||||
|
and `sse_client` accept the same arguments as elsewhere in v2 - but the client
|
||||||
|
type they expect is now `httpx2.AsyncClient`. If you construct your own client to pass as
|
||||||
|
`http_client` (or build an `httpx2.Auth` subclass for `auth`), import from
|
||||||
|
`httpx2`:
|
||||||
|
|
||||||
|
**Before (v1):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
http_client = httpx.AsyncClient(follow_redirects=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (v2):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx2
|
||||||
|
|
||||||
|
http_client = httpx2.AsyncClient(follow_redirects=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
`httpx2` is API-compatible with `httpx`, so usually only the import name
|
||||||
|
changes. To consume SSE directly, use `httpx2.EventSource` (or
|
||||||
|
`AsyncClient.sse()`) instead of the `httpx-sse` helpers.
|
||||||
|
|
||||||
|
Exception handlers need the same rename: the SDK now raises `httpx2`
|
||||||
|
exceptions (`httpx2.ConnectError`, `httpx2.HTTPStatusError`, and so on), and
|
||||||
|
this failure mode is silent. `httpx` usually stays installed as a transitive
|
||||||
|
dependency of other packages, so an old `except httpx.ConnectError:` block
|
||||||
|
keeps importing fine and simply never matches again. Audit `except httpx.`
|
||||||
|
clauses and `isinstance` checks along with the imports. The same identity
|
||||||
|
split applies to objects: `httpx` and `httpx2` types are not interchangeable
|
||||||
|
at runtime, so an `httpx.AsyncClient` passed as `http_client` degrades in
|
||||||
|
subtle ways (server-initiated messages stop arriving) instead of raising
|
||||||
|
immediately.
|
||||||
|
|
||||||
|
The client also identifies itself differently: the default User-Agent is now
|
||||||
|
`python-httpx2/<version>`, and log lines come from the `httpx2` and
|
||||||
|
`httpcore2.*` loggers, so a `logging.getLogger("httpx")` or
|
||||||
|
`logging.getLogger("httpcore")` suppression no longer matches anything.
|
||||||
|
Telemetry integrations keyed to the `httpx` module (such as OpenTelemetry's
|
||||||
|
httpx instrumentation) stop seeing the SDK's traffic as well.
|
||||||
|
|
||||||
|
TLS verification also changes: `httpx` validated certificates against the
|
||||||
|
bundled `certifi` CA list, while `httpx2` validates against the operating
|
||||||
|
system trust store via [`truststore`](https://pypi.org/project/truststore/).
|
||||||
|
If your environment has no usable system CA store (some minimal containers),
|
||||||
|
or you relied on certifi's bundle specifically, point the standard
|
||||||
|
`SSL_CERT_FILE` or `SSL_CERT_DIR` environment variable at a CA bundle
|
||||||
|
(`httpx2` honors these before falling back to the system store), or pass an
|
||||||
|
explicit `verify=ssl_context` to your `httpx2.AsyncClient`. Passing a CA
|
||||||
|
bundle path as `verify="ca.pem"` or using the `cert=` parameter is deprecated
|
||||||
|
in `httpx2`; build an `ssl.SSLContext` and configure it instead.
|
||||||
|
|
||||||
### `mcp dev` and `mcp install` pin the spawned environment to your SDK version
|
### `mcp dev` and `mcp install` pin the spawned environment to your SDK version
|
||||||
|
|
||||||
Both commands run your server through a fresh `uv run --with ...` environment. In v1 the
|
Both commands run your server through a fresh `uv run --with ...` environment. In v1 the
|
||||||
|
|
@ -111,7 +176,7 @@ unchanged. Only the `mcp.types` submodule and `mcp.shared.version` were removed.
|
||||||
package's API reference is at [`mcp_types`](api/mcp_types/index.md).
|
package's API reference is at [`mcp_types`](api/mcp_types/index.md).
|
||||||
|
|
||||||
**Why:** keeping the wire types in their own package lets tooling and lightweight clients
|
**Why:** keeping the wire types in their own package lets tooling and lightweight clients
|
||||||
depend on the protocol schema without pulling in `httpx`, `starlette`, `uvicorn`, and the
|
depend on the protocol schema without pulling in `httpx2`, `starlette`, `uvicorn`, and the
|
||||||
rest of the server/transport stack.
|
rest of the server/transport stack.
|
||||||
|
|
||||||
**Before (v1):**
|
**Before (v1):**
|
||||||
|
|
@ -1594,13 +1659,13 @@ async with streamablehttp_client(
|
||||||
**After (v2):**
|
**After (v2):**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import httpx
|
import httpx2
|
||||||
from mcp.client.streamable_http import streamable_http_client
|
from mcp.client.streamable_http import streamable_http_client
|
||||||
|
|
||||||
# Configure headers, timeout, and auth on the httpx.AsyncClient
|
# Configure headers, timeout, and auth on the httpx2.AsyncClient
|
||||||
http_client = httpx.AsyncClient(
|
http_client = httpx2.AsyncClient(
|
||||||
headers={"Authorization": "Bearer token"},
|
headers={"Authorization": "Bearer token"},
|
||||||
timeout=httpx.Timeout(30, read=300),
|
timeout=httpx2.Timeout(30, read=300),
|
||||||
auth=my_auth,
|
auth=my_auth,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
|
@ -1613,7 +1678,7 @@ async with http_client:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
v1's internal client set `follow_redirects=True`; set it explicitly when supplying your own `httpx.AsyncClient` to preserve that behavior.
|
v1's internal client set `follow_redirects=True`; set it explicitly when supplying your own `httpx2.AsyncClient` to preserve that behavior.
|
||||||
|
|
||||||
### `get_session_id` callback removed from `streamable_http_client`
|
### `get_session_id` callback removed from `streamable_http_client`
|
||||||
|
|
||||||
|
|
@ -1621,7 +1686,7 @@ The `get_session_id` callback (third element of the returned tuple) has been rem
|
||||||
|
|
||||||
The `GetSessionIdCallback` type alias is gone as well, so `from mcp.client.streamable_http import GetSessionIdCallback` now raises `ImportError`. Drop the annotation, or inline `Callable[[], str | None]` if your own wrapper code still needs the type.
|
The `GetSessionIdCallback` type alias is gone as well, so `from mcp.client.streamable_http import GetSessionIdCallback` now raises `ImportError`. Drop the annotation, or inline `Callable[[], str | None]` if your own wrapper code still needs the type.
|
||||||
|
|
||||||
If you need to capture the session ID (e.g., for session resumption testing), you can use httpx event hooks to capture it from the response headers:
|
If you need to capture the session ID (e.g., for session resumption testing), you can use httpx2 event hooks to capture it from the response headers:
|
||||||
|
|
||||||
**Before (v1):**
|
**Before (v1):**
|
||||||
|
|
||||||
|
|
@ -1637,7 +1702,7 @@ async with streamable_http_client(url) as (read_stream, write_stream, get_sessio
|
||||||
**After (v2):**
|
**After (v2):**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import httpx
|
import httpx2
|
||||||
from mcp.client.streamable_http import streamable_http_client
|
from mcp.client.streamable_http import streamable_http_client
|
||||||
|
|
||||||
# Option 1: Simply ignore if you don't need the session ID
|
# Option 1: Simply ignore if you don't need the session ID
|
||||||
|
|
@ -1645,15 +1710,15 @@ async with streamable_http_client(url) as (read_stream, write_stream):
|
||||||
async with ClientSession(read_stream, write_stream) as session:
|
async with ClientSession(read_stream, write_stream) as session:
|
||||||
await session.initialize()
|
await session.initialize()
|
||||||
|
|
||||||
# Option 2: Capture session ID via httpx event hooks if needed
|
# Option 2: Capture session ID via httpx2 event hooks if needed
|
||||||
captured_session_ids: list[str] = []
|
captured_session_ids: list[str] = []
|
||||||
|
|
||||||
async def capture_session_id(response: httpx.Response) -> None:
|
async def capture_session_id(response: httpx2.Response) -> None:
|
||||||
session_id = response.headers.get("mcp-session-id")
|
session_id = response.headers.get("mcp-session-id")
|
||||||
if session_id:
|
if session_id:
|
||||||
captured_session_ids.append(session_id)
|
captured_session_ids.append(session_id)
|
||||||
|
|
||||||
http_client = httpx.AsyncClient(
|
http_client = httpx2.AsyncClient(
|
||||||
event_hooks={"response": [capture_session_id]},
|
event_hooks={"response": [capture_session_id]},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
|
@ -1667,7 +1732,7 @@ async with http_client:
|
||||||
|
|
||||||
### `StreamableHTTPTransport` parameters removed
|
### `StreamableHTTPTransport` parameters removed
|
||||||
|
|
||||||
The `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters have been removed from `StreamableHTTPTransport`. Configure these on the `httpx.AsyncClient` instead (see example above).
|
The `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters have been removed from `StreamableHTTPTransport`. Configure these on the `httpx2.AsyncClient` instead (see example above).
|
||||||
|
|
||||||
Note: `sse_client` retains its `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters — only the streamable HTTP transport changed.
|
Note: `sse_client` retains its `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters — only the streamable HTTP transport changed.
|
||||||
|
|
||||||
|
|
@ -1730,7 +1795,7 @@ async with streamable_http_client(url) as (read, write):
|
||||||
raise
|
raise
|
||||||
```
|
```
|
||||||
|
|
||||||
Move HTTP-status failure handling from around the transport context to around the individual calls, catching `MCPError` (see [`McpError` renamed to `MCPError`](#mcperror-renamed-to-mcperror)). Connect-level failures such as `httpx.ConnectError` still escape the transport context as before; keep context-level handling for those only.
|
Move HTTP-status failure handling from around the transport context to around the individual calls, catching `MCPError` (see [`McpError` renamed to `MCPError`](#mcperror-renamed-to-mcperror)). Connect-level failures such as `httpx2.ConnectError` still escape the transport context as before; keep context-level handling for those only.
|
||||||
|
|
||||||
### `terminate_windows_process` removed
|
### `terminate_windows_process` removed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ The renames announce themselves. These do not:
|
||||||
* **URI templates are real RFC 6570 now.** `{+path}`, `{?query}` and friends work, matching is exact instead of regex-loose, and path traversal in extracted values is rejected by default. Stricter templates fail at decoration time, not on the first request. **[URI templates](servers/uri-templates.md)**.
|
* **URI templates are real RFC 6570 now.** `{+path}`, `{?query}` and friends work, matching is exact instead of regex-loose, and path traversal in extracted values is rejected by default. Stricter templates fail at decoration time, not on the first request. **[URI templates](servers/uri-templates.md)**.
|
||||||
* **The streamable HTTP lifespan runs once**, at startup, and its state is shared by every session and request. In v1 it ran once per session, and once per request under `stateless_http=True`. Pools and caches built in a lifespan get dramatically cheaper; anything that acquired a per-connection resource there belongs in the handler body now. **[Lifespan](handlers/lifespan.md)**.
|
* **The streamable HTTP lifespan runs once**, at startup, and its state is shared by every session and request. In v1 it ran once per session, and once per request under `stateless_http=True`. Pools and caches built in a lifespan get dramatically cheaper; anything that acquired a per-connection resource there belongs in the handler body now. **[Lifespan](handlers/lifespan.md)**.
|
||||||
* **`mcp dev` and `mcp install` pin the environment they spawn** to your installed SDK version. Both commands run your server in a fresh `uv run --with ...` environment, which used to resolve `mcp` to the newest stable release rather than the version you are developing against. **[Migration Guide](migration.md#mcp-dev-and-mcp-install-pin-the-spawned-environment-to-your-sdk-version)**.
|
* **`mcp dev` and `mcp install` pin the environment they spawn** to your installed SDK version. Both commands run your server in a fresh `uv run --with ...` environment, which used to resolve `mcp` to the newest stable release rather than the version you are developing against. **[Migration Guide](migration.md#mcp-dev-and-mcp-install-pin-the-spawned-environment-to-your-sdk-version)**.
|
||||||
|
* **The HTTP client is now `httpx2`, not `httpx`.** The dependency swap changes what your code catches and passes (`httpx2.AsyncClient`, `httpx2.ConnectError`), and it changes how TLS certificates are verified: `httpx2` validates through `truststore` against the operating system trust store instead of certifi's bundled CA list. Most environments never notice; a minimal container with no system CA store, or a private CA that only certifi's bundle knew about, starts failing the TLS handshake. Set `SSL_CERT_FILE`/`SSL_CERT_DIR` or pass `verify=ssl_context` to your client. **[Migration Guide](migration.md#httpx-and-httpx-sse-replaced-by-httpx2)**.
|
||||||
|
|
||||||
### Removed outright
|
### Removed outright
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import httpx
|
import httpx2
|
||||||
|
|
||||||
from mcp import Client
|
from mcp import Client
|
||||||
from mcp.client.streamable_http import streamable_http_client
|
from mcp.client.streamable_http import streamable_http_client
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with httpx.AsyncClient(
|
async with httpx2.AsyncClient(
|
||||||
headers={"Authorization": "Bearer ..."},
|
headers={"Authorization": "Bearer ..."},
|
||||||
timeout=httpx.Timeout(30.0, read=300.0),
|
timeout=httpx2.Timeout(30.0, read=300.0),
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
) as http_client:
|
) as http_client:
|
||||||
transport = streamable_http_client("http://localhost:8000/mcp", http_client=http_client)
|
transport = streamable_http_client("http://localhost:8000/mcp", http_client=http_client)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import httpx
|
import httpx2
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from mcp import Client
|
from mcp import Client
|
||||||
|
|
@ -62,7 +62,7 @@ oauth = IdentityAssertionOAuthProvider(
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with httpx.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
|
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
|
||||||
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
|
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
|
||||||
async with Client(transport) as client:
|
async with Client(transport) as client:
|
||||||
result = await client.list_tools()
|
result = await client.list_tools()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx2
|
||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
|
|
||||||
from mcp import Client
|
from mcp import Client
|
||||||
|
|
@ -55,7 +55,7 @@ oauth = OAuthClientProvider(
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with httpx.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
|
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
|
||||||
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
|
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
|
||||||
async with Client(transport) as client:
|
async with Client(transport) as client:
|
||||||
result = await client.list_tools()
|
result = await client.list_tools()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import httpx
|
import httpx2
|
||||||
|
|
||||||
from mcp import Client
|
from mcp import Client
|
||||||
from mcp.client.auth.extensions.client_credentials import ClientCredentialsOAuthProvider
|
from mcp.client.auth.extensions.client_credentials import ClientCredentialsOAuthProvider
|
||||||
|
|
@ -34,7 +34,7 @@ oauth = ClientCredentialsOAuthProvider(
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with httpx.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
|
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
|
||||||
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
|
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
|
||||||
async with Client(transport) as client:
|
async with Client(transport) as client:
|
||||||
result = await client.list_tools()
|
result = await client.list_tools()
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx2
|
||||||
from mcp.client._transport import ReadStream, WriteStream
|
from mcp.client._transport import ReadStream, WriteStream
|
||||||
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider, TokenStorage
|
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider, TokenStorage
|
||||||
from mcp.client.session import ClientSession
|
from mcp.client.session import ClientSession
|
||||||
|
|
@ -233,7 +233,7 @@ class SimpleAuthClient:
|
||||||
await self._run_session(read_stream, write_stream)
|
await self._run_session(read_stream, write_stream)
|
||||||
else:
|
else:
|
||||||
print("📡 Opening StreamableHTTP transport connection with auth...")
|
print("📡 Opening StreamableHTTP transport connection with auth...")
|
||||||
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
|
async with httpx2.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
|
||||||
async with streamable_http_client(url=self.server_url, http_client=custom_client) as (
|
async with streamable_http_client(url=self.server_url, http_client=custom_client) as (
|
||||||
read_stream,
|
read_stream,
|
||||||
write_stream,
|
write_stream,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import shutil
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx2
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from mcp import ClientSession, StdioServerParameters
|
from mcp import ClientSession, StdioServerParameters
|
||||||
from mcp.client.stdio import stdio_client
|
from mcp.client.stdio import stdio_client
|
||||||
|
|
@ -227,10 +227,7 @@ class LLMClient:
|
||||||
messages: A list of message dictionaries.
|
messages: A list of message dictionaries.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The LLM's response as a string.
|
The LLM's response as a string, or an error message if the request fails.
|
||||||
|
|
||||||
Raises:
|
|
||||||
httpx.RequestError: If the request to the LLM fails.
|
|
||||||
"""
|
"""
|
||||||
url = "https://api.groq.com/openai/v1/chat/completions"
|
url = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
|
|
||||||
|
|
@ -249,17 +246,17 @@ class LLMClient:
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with httpx.Client() as client:
|
with httpx2.Client() as client:
|
||||||
response = client.post(url, headers=headers, json=payload)
|
response = client.post(url, headers=headers, json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return data["choices"][0]["message"]["content"]
|
return data["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
except httpx.RequestError as e:
|
except httpx2.HTTPError as e:
|
||||||
error_message = f"Error getting LLM response: {str(e)}"
|
error_message = f"Error getting LLM response: {str(e)}"
|
||||||
logging.error(error_message)
|
logging.error(error_message)
|
||||||
|
|
||||||
if isinstance(e, httpx.HTTPStatusError):
|
if isinstance(e, httpx2.HTTPStatusError):
|
||||||
status_code = e.response.status_code
|
status_code = e.response.status_code
|
||||||
logging.error(f"Status code: {status_code}")
|
logging.error(f"Status code: {status_code}")
|
||||||
logging.error(f"Response details: {e.response.text}")
|
logging.error(f"Response details: {e.response.text}")
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,8 @@ def main(url: str, items: int, checkpoint_every: int, log_level: str) -> None:
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
)
|
)
|
||||||
# Suppress noisy HTTP client logging
|
# Suppress noisy HTTP client logging
|
||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx2").setLevel(logging.WARNING)
|
||||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
logging.getLogger("httpcore2").setLevel(logging.WARNING)
|
||||||
|
|
||||||
asyncio.run(run_demo(url, items, checkpoint_every))
|
asyncio.run(run_demo(url, items, checkpoint_every))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Visit https://surgemsg.com/ and click "Get Started" to obtain these values.
|
||||||
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
import httpx
|
import httpx2
|
||||||
from pydantic import BeforeValidator
|
from pydantic import BeforeValidator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ surge_settings = SurgeSettings() # type: ignore
|
||||||
@mcp.tool(name="textme", description="Send a text message to me")
|
@mcp.tool(name="textme", description="Send a text message to me")
|
||||||
def text_me(text_content: str) -> str:
|
def text_me(text_content: str) -> str:
|
||||||
"""Send a text message to a phone number via https://surgemsg.com/"""
|
"""Send a text message to a phone number via https://surgemsg.com/"""
|
||||||
with httpx.Client() as client:
|
with httpx2.Client() as client:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"https://api.surgemsg.com/messages",
|
"https://api.surgemsg.com/messages",
|
||||||
headers={
|
headers={
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ requires-python = ">=3.10"
|
||||||
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
|
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
|
||||||
keywords = ["mcp", "llm", "automation", "conformance", "testing"]
|
keywords = ["mcp", "llm", "automation", "conformance", "testing"]
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp", "starlette", "uvicorn"]
|
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx2>=2.5.0", "mcp", "starlette", "uvicorn"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mcp-everything-server = "mcp_everything_server.server:main"
|
mcp-everything-server = "mcp_everything_server.server:main"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class IntrospectionTokenVerifier(TokenVerifier):
|
||||||
|
|
||||||
async def verify_token(self, token: str) -> AccessToken | None:
|
async def verify_token(self, token: str) -> AccessToken | None:
|
||||||
"""Verify token via introspection endpoint."""
|
"""Verify token via introspection endpoint."""
|
||||||
import httpx
|
import httpx2
|
||||||
|
|
||||||
# Validate URL to prevent SSRF attacks
|
# Validate URL to prevent SSRF attacks
|
||||||
if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
|
if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
|
||||||
|
|
@ -41,10 +41,10 @@ class IntrospectionTokenVerifier(TokenVerifier):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Configure secure HTTP client
|
# Configure secure HTTP client
|
||||||
timeout = httpx.Timeout(10.0, connect=5.0)
|
timeout = httpx2.Timeout(10.0, connect=5.0)
|
||||||
limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
|
limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
|
||||||
|
|
||||||
async with httpx.AsyncClient(
|
async with httpx2.AsyncClient(
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
limits=limits,
|
limits=limits,
|
||||||
verify=True, # Enforce SSL verification
|
verify=True, # Enforce SSL verification
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ license = { text = "MIT" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyio>=4.5",
|
"anyio>=4.5",
|
||||||
"click>=8.2.0",
|
"click>=8.2.0",
|
||||||
"httpx>=0.27",
|
"httpx2>=2.5.0",
|
||||||
"mcp",
|
"mcp",
|
||||||
"pydantic>=2.0",
|
"pydantic>=2.0",
|
||||||
"pydantic-settings>=2.5.2",
|
"pydantic-settings>=2.5.2",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
]
|
]
|
||||||
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp"]
|
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx2>=2.5.0", "mcp"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mcp-simple-pagination = "mcp_simple_pagination.server:main"
|
mcp-simple-pagination = "mcp_simple_pagination.server:main"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
]
|
]
|
||||||
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx>=0.27", "mcp"]
|
dependencies = ["anyio>=4.5", "click>=8.2.0", "httpx2>=2.5.0", "mcp"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mcp-simple-prompt = "mcp_simple_prompt.server:main"
|
mcp-simple-prompt = "mcp_simple_prompt.server:main"
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue