Skip to content

Commit b3b9aff

Browse files
Merge branch 'main' into rfc-7591-grant-type
2 parents 91f3533 + 65b36de commit b3b9aff

29 files changed

+1450
-387
lines changed

README.md

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,8 @@ Run from the repository root:
13941394
uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload
13951395
"""
13961396

1397+
import contextlib
1398+
13971399
from starlette.applications import Starlette
13981400
from starlette.routing import Mount
13991401

@@ -1409,11 +1411,19 @@ def hello() -> str:
14091411
return "Hello from MCP!"
14101412

14111413

1414+
# Create a lifespan context manager to run the session manager
1415+
@contextlib.asynccontextmanager
1416+
async def lifespan(app: Starlette):
1417+
async with mcp.session_manager.run():
1418+
yield
1419+
1420+
14121421
# Mount the StreamableHTTP server to the existing ASGI server
14131422
app = Starlette(
14141423
routes=[
14151424
Mount("/", app=mcp.streamable_http_app()),
1416-
]
1425+
],
1426+
lifespan=lifespan,
14171427
)
14181428
```
14191429

@@ -1431,6 +1441,8 @@ Run from the repository root:
14311441
uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload
14321442
"""
14331443

1444+
import contextlib
1445+
14341446
from starlette.applications import Starlette
14351447
from starlette.routing import Host
14361448

@@ -1446,11 +1458,19 @@ def domain_info() -> str:
14461458
return "This is served from mcp.acme.corp"
14471459

14481460

1461+
# Create a lifespan context manager to run the session manager
1462+
@contextlib.asynccontextmanager
1463+
async def lifespan(app: Starlette):
1464+
async with mcp.session_manager.run():
1465+
yield
1466+
1467+
14491468
# Mount using Host-based routing
14501469
app = Starlette(
14511470
routes=[
14521471
Host("mcp.acme.corp", app=mcp.streamable_http_app()),
1453-
]
1472+
],
1473+
lifespan=lifespan,
14541474
)
14551475
```
14561476

@@ -1468,6 +1488,8 @@ Run from the repository root:
14681488
uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload
14691489
"""
14701490

1491+
import contextlib
1492+
14711493
from starlette.applications import Starlette
14721494
from starlette.routing import Mount
14731495

@@ -1495,12 +1517,23 @@ def send_message(message: str) -> str:
14951517
api_mcp.settings.streamable_http_path = "/"
14961518
chat_mcp.settings.streamable_http_path = "/"
14971519

1520+
1521+
# Create a combined lifespan to manage both session managers
1522+
@contextlib.asynccontextmanager
1523+
async def lifespan(app: Starlette):
1524+
async with contextlib.AsyncExitStack() as stack:
1525+
await stack.enter_async_context(api_mcp.session_manager.run())
1526+
await stack.enter_async_context(chat_mcp.session_manager.run())
1527+
yield
1528+
1529+
14981530
# Mount the servers
14991531
app = Starlette(
15001532
routes=[
15011533
Mount("/api", app=api_mcp.streamable_http_app()),
15021534
Mount("/chat", app=chat_mcp.streamable_http_app()),
1503-
]
1535+
],
1536+
lifespan=lifespan,
15041537
)
15051538
```
15061539

@@ -2208,12 +2241,12 @@ Run from the repository root:
22082241
import asyncio
22092242

22102243
from mcp import ClientSession
2211-
from mcp.client.streamable_http import streamablehttp_client
2244+
from mcp.client.streamable_http import streamable_http_client
22122245

22132246

22142247
async def main():
22152248
# Connect to a streamable HTTP server
2216-
async with streamablehttp_client("http://localhost:8000/mcp") as (
2249+
async with streamable_http_client("http://localhost:8000/mcp") as (
22172250
read_stream,
22182251
write_stream,
22192252
_,
@@ -2337,11 +2370,12 @@ cd to the `examples/snippets` directory and run:
23372370
import asyncio
23382371
from urllib.parse import parse_qs, urlparse
23392372

2373+
import httpx
23402374
from pydantic import AnyUrl
23412375

23422376
from mcp import ClientSession
23432377
from mcp.client.auth import OAuthClientProvider, TokenStorage
2344-
from mcp.client.streamable_http import streamablehttp_client
2378+
from mcp.client.streamable_http import streamable_http_client
23452379
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
23462380

23472381

@@ -2395,15 +2429,16 @@ async def main():
23952429
callback_handler=handle_callback,
23962430
)
23972431

2398-
async with streamablehttp_client("http://localhost:8001/mcp", auth=oauth_auth) as (read, write, _):
2399-
async with ClientSession(read, write) as session:
2400-
await session.initialize()
2432+
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
2433+
async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write, _):
2434+
async with ClientSession(read, write) as session:
2435+
await session.initialize()
24012436

2402-
tools = await session.list_tools()
2403-
print(f"Available tools: {[tool.name for tool in tools.tools]}")
2437+
tools = await session.list_tools()
2438+
print(f"Available tools: {[tool.name for tool in tools.tools]}")
24042439

2405-
resources = await session.list_resources()
2406-
print(f"Available resources: {[r.uri for r in resources.resources]}")
2440+
resources = await session.list_resources()
2441+
print(f"Available resources: {[r.uri for r in resources.resources]}")
24072442

24082443

24092444
def run():

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
import threading
1212
import time
1313
import webbrowser
14-
from datetime import timedelta
1514
from http.server import BaseHTTPRequestHandler, HTTPServer
1615
from typing import Any
1716
from urllib.parse import parse_qs, urlparse
1817

18+
import httpx
1919
from mcp.client.auth import OAuthClientProvider, TokenStorage
2020
from mcp.client.session import ClientSession
2121
from mcp.client.sse import sse_client
22-
from mcp.client.streamable_http import streamablehttp_client
22+
from mcp.client.streamable_http import streamable_http_client
2323
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
2424

2525

@@ -193,7 +193,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
193193
# Create OAuth authentication handler using the new interface
194194
# Use client_metadata_url to enable CIMD when the server supports it
195195
oauth_auth = OAuthClientProvider(
196-
server_url=self.server_url,
196+
server_url=self.server_url.replace("/mcp", ""),
197197
client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),
198198
storage=InMemoryTokenStorage(),
199199
redirect_handler=_default_redirect_handler,
@@ -212,12 +212,12 @@ async def _default_redirect_handler(authorization_url: str) -> None:
212212
await self._run_session(read_stream, write_stream, None)
213213
else:
214214
print("📡 Opening StreamableHTTP transport connection with auth...")
215-
async with streamablehttp_client(
216-
url=self.server_url,
217-
auth=oauth_auth,
218-
timeout=timedelta(seconds=60),
219-
) as (read_stream, write_stream, get_session_id):
220-
await self._run_session(read_stream, write_stream, get_session_id)
215+
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
216+
async with streamable_http_client(
217+
url=self.server_url,
218+
http_client=custom_client,
219+
) as (read_stream, write_stream, get_session_id):
220+
await self._run_session(read_stream, write_stream, get_session_id)
221221

222222
except Exception as e:
223223
print(f"❌ Failed to connect: {e}")

examples/snippets/clients/oauth_client.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
import asyncio
1111
from urllib.parse import parse_qs, urlparse
1212

13+
import httpx
1314
from pydantic import AnyUrl
1415

1516
from mcp import ClientSession
1617
from mcp.client.auth import OAuthClientProvider, TokenStorage
17-
from mcp.client.streamable_http import streamablehttp_client
18+
from mcp.client.streamable_http import streamable_http_client
1819
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
1920

2021

@@ -68,15 +69,16 @@ async def main():
6869
callback_handler=handle_callback,
6970
)
7071

71-
async with streamablehttp_client("http://localhost:8001/mcp", auth=oauth_auth) as (read, write, _):
72-
async with ClientSession(read, write) as session:
73-
await session.initialize()
72+
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
73+
async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write, _):
74+
async with ClientSession(read, write) as session:
75+
await session.initialize()
7476

75-
tools = await session.list_tools()
76-
print(f"Available tools: {[tool.name for tool in tools.tools]}")
77+
tools = await session.list_tools()
78+
print(f"Available tools: {[tool.name for tool in tools.tools]}")
7779

78-
resources = await session.list_resources()
79-
print(f"Available resources: {[r.uri for r in resources.resources]}")
80+
resources = await session.list_resources()
81+
print(f"Available resources: {[r.uri for r in resources.resources]}")
8082

8183

8284
def run():

examples/snippets/clients/streamable_basic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import asyncio
77

88
from mcp import ClientSession
9-
from mcp.client.streamable_http import streamablehttp_client
9+
from mcp.client.streamable_http import streamable_http_client
1010

1111

1212
async def main():
1313
# Connect to a streamable HTTP server
14-
async with streamablehttp_client("http://localhost:8000/mcp") as (
14+
async with streamable_http_client("http://localhost:8000/mcp") as (
1515
read_stream,
1616
write_stream,
1717
_,

examples/snippets/servers/streamable_http_basic_mounting.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload
66
"""
77

8+
import contextlib
9+
810
from starlette.applications import Starlette
911
from starlette.routing import Mount
1012

@@ -20,9 +22,17 @@ def hello() -> str:
2022
return "Hello from MCP!"
2123

2224

25+
# Create a lifespan context manager to run the session manager
26+
@contextlib.asynccontextmanager
27+
async def lifespan(app: Starlette):
28+
async with mcp.session_manager.run():
29+
yield
30+
31+
2332
# Mount the StreamableHTTP server to the existing ASGI server
2433
app = Starlette(
2534
routes=[
2635
Mount("/", app=mcp.streamable_http_app()),
27-
]
36+
],
37+
lifespan=lifespan,
2838
)

examples/snippets/servers/streamable_http_host_mounting.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload
66
"""
77

8+
import contextlib
9+
810
from starlette.applications import Starlette
911
from starlette.routing import Host
1012

@@ -20,9 +22,17 @@ def domain_info() -> str:
2022
return "This is served from mcp.acme.corp"
2123

2224

25+
# Create a lifespan context manager to run the session manager
26+
@contextlib.asynccontextmanager
27+
async def lifespan(app: Starlette):
28+
async with mcp.session_manager.run():
29+
yield
30+
31+
2332
# Mount using Host-based routing
2433
app = Starlette(
2534
routes=[
2635
Host("mcp.acme.corp", app=mcp.streamable_http_app()),
27-
]
36+
],
37+
lifespan=lifespan,
2838
)

examples/snippets/servers/streamable_http_multiple_servers.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload
66
"""
77

8+
import contextlib
9+
810
from starlette.applications import Starlette
911
from starlette.routing import Mount
1012

@@ -32,10 +34,21 @@ def send_message(message: str) -> str:
3234
api_mcp.settings.streamable_http_path = "/"
3335
chat_mcp.settings.streamable_http_path = "/"
3436

37+
38+
# Create a combined lifespan to manage both session managers
39+
@contextlib.asynccontextmanager
40+
async def lifespan(app: Starlette):
41+
async with contextlib.AsyncExitStack() as stack:
42+
await stack.enter_async_context(api_mcp.session_manager.run())
43+
await stack.enter_async_context(chat_mcp.session_manager.run())
44+
yield
45+
46+
3547
# Mount the servers
3648
app = Starlette(
3749
routes=[
3850
Mount("/api", app=api_mcp.streamable_http_app()),
3951
Mount("/chat", app=chat_mcp.streamable_http_app()),
40-
]
52+
],
53+
lifespan=lifespan,
4154
)

src/mcp/client/auth/oauth2.py

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import httpx
2020
from pydantic import BaseModel, Field, ValidationError
2121

22-
from mcp.client.auth.exceptions import OAuthFlowError, OAuthRegistrationError, OAuthTokenError
22+
from mcp.client.auth.exceptions import OAuthFlowError, OAuthTokenError
2323
from mcp.client.auth.utils import (
2424
build_oauth_authorization_server_metadata_discovery_urls,
2525
build_protected_resource_metadata_discovery_urls,
@@ -299,44 +299,6 @@ async def _handle_protected_resource_response(self, response: httpx.Response) ->
299299
f"Protected Resource Metadata request failed: {response.status_code}"
300300
) # pragma: no cover
301301

302-
async def _register_client(self) -> httpx.Request | None:
303-
"""Build registration request or skip if already registered."""
304-
if self.context.client_info:
305-
return None
306-
307-
if self.context.oauth_metadata and self.context.oauth_metadata.registration_endpoint:
308-
registration_url = str(self.context.oauth_metadata.registration_endpoint) # pragma: no cover
309-
else:
310-
auth_base_url = self.context.get_authorization_base_url(self.context.server_url)
311-
registration_url = urljoin(auth_base_url, "/register")
312-
313-
registration_data = self.context.client_metadata.model_dump(by_alias=True, mode="json", exclude_none=True)
314-
315-
# If token_endpoint_auth_method is None, auto-select based on server support
316-
if self.context.client_metadata.token_endpoint_auth_method is None:
317-
preference_order = ["client_secret_basic", "client_secret_post", "none"]
318-
319-
if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint_auth_methods_supported:
320-
supported = self.context.oauth_metadata.token_endpoint_auth_methods_supported
321-
for method in preference_order:
322-
if method in supported:
323-
registration_data["token_endpoint_auth_method"] = method
324-
break
325-
else:
326-
# No compatible methods between client and server
327-
raise OAuthRegistrationError(
328-
f"No compatible authentication methods. "
329-
f"Server supports: {supported}, "
330-
f"Client supports: {preference_order}"
331-
)
332-
else:
333-
# No server metadata available, use our default preference
334-
registration_data["token_endpoint_auth_method"] = preference_order[0]
335-
336-
return httpx.Request(
337-
"POST", registration_url, json=registration_data, headers={"Content-Type": "application/json"}
338-
)
339-
340302
async def _perform_authorization(self) -> httpx.Request:
341303
"""Perform the authorization flow."""
342304
auth_code, code_verifier = await self._perform_authorization_code_grant()

0 commit comments

Comments
 (0)