Skip to content

Commit 8d646e5

Browse files
Merge branch 'main' into lorenzocesconetto/fix-callback-exception-warning
2 parents 2544c9c + d55cb2b commit 8d646e5

34 files changed

+1819
-870
lines changed

.github/workflows/publish-docs-manually.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
uses: astral-sh/setup-uv@v3
2020
with:
2121
enable-cache: true
22+
version: 0.7.2
2223

2324
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
2425
- uses: actions/cache@v4

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
uses: astral-sh/setup-uv@v3
1717
with:
1818
enable-cache: true
19+
version: 0.7.2
1920

2021
- name: Set up Python 3.12
2122
run: uv python install 3.12
@@ -67,6 +68,7 @@ jobs:
6768
uses: astral-sh/setup-uv@v3
6869
with:
6970
enable-cache: true
71+
version: 0.7.2
7072

7173
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
7274
- uses: actions/cache@v4

.github/workflows/shared.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jobs:
1313
uses: astral-sh/setup-uv@v3
1414
with:
1515
enable-cache: true
16+
version: 0.7.2
1617

1718
- name: Install the project
1819
run: uv sync --frozen --all-extras --dev --python 3.12
@@ -29,6 +30,7 @@ jobs:
2930
uses: astral-sh/setup-uv@v3
3031
with:
3132
enable-cache: true
33+
version: 0.7.2
3234

3335
- name: Install the project
3436
run: uv sync --frozen --all-extras --dev --python 3.12
@@ -50,6 +52,7 @@ jobs:
5052
uses: astral-sh/setup-uv@v3
5153
with:
5254
enable-cache: true
55+
version: 0.7.2
5356

5457
- name: Install the project
5558
run: uv sync --frozen --all-extras --dev --python ${{ matrix.python-version }}

README.md

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ from dataclasses import dataclass
160160

161161
from fake_database import Database # Replace with your actual DB type
162162

163-
from mcp.server.fastmcp import Context, FastMCP
163+
from mcp.server.fastmcp import FastMCP
164164

165165
# Create a named server
166166
mcp = FastMCP("My App")
@@ -192,9 +192,10 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
192192

193193
# Access type-safe lifespan context in tools
194194
@mcp.tool()
195-
def query_db(ctx: Context) -> str:
195+
def query_db() -> str:
196196
"""Tool that uses initialized resources"""
197-
db = ctx.request_context.lifespan_context.db
197+
ctx = mcp.get_context()
198+
db = ctx.request_context.lifespan_context["db"]
198199
return db.query()
199200
```
200201

@@ -314,27 +315,42 @@ async def long_task(files: list[str], ctx: Context) -> str:
314315
Authentication can be used by servers that want to expose tools accessing protected resources.
315316

316317
`mcp.server.auth` implements an OAuth 2.0 server interface, which servers can use by
317-
providing an implementation of the `OAuthServerProvider` protocol.
318+
providing an implementation of the `OAuthAuthorizationServerProvider` protocol.
318319

319-
```
320-
mcp = FastMCP("My App",
321-
auth_server_provider=MyOAuthServerProvider(),
322-
auth=AuthSettings(
323-
issuer_url="https://myapp.com",
324-
revocation_options=RevocationOptions(
325-
enabled=True,
326-
),
327-
client_registration_options=ClientRegistrationOptions(
328-
enabled=True,
329-
valid_scopes=["myscope", "myotherscope"],
330-
default_scopes=["myscope"],
331-
),
332-
required_scopes=["myscope"],
320+
```python
321+
from mcp import FastMCP
322+
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
323+
from mcp.server.auth.settings import (
324+
AuthSettings,
325+
ClientRegistrationOptions,
326+
RevocationOptions,
327+
)
328+
329+
330+
class MyOAuthServerProvider(OAuthAuthorizationServerProvider):
331+
# See an example on how to implement at `examples/servers/simple-auth`
332+
...
333+
334+
335+
mcp = FastMCP(
336+
"My App",
337+
auth_server_provider=MyOAuthServerProvider(),
338+
auth=AuthSettings(
339+
issuer_url="https://myapp.com",
340+
revocation_options=RevocationOptions(
341+
enabled=True,
342+
),
343+
client_registration_options=ClientRegistrationOptions(
344+
enabled=True,
345+
valid_scopes=["myscope", "myotherscope"],
346+
default_scopes=["myscope"],
333347
),
348+
required_scopes=["myscope"],
349+
),
334350
)
335351
```
336352

337-
See [OAuthServerProvider](src/mcp/server/auth/provider.py) for more details.
353+
See [OAuthAuthorizationServerProvider](src/mcp/server/auth/provider.py) for more details.
338354

339355
## Running Your Server
340356

@@ -461,15 +477,12 @@ For low level server with Streamable HTTP implementations, see:
461477
- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)
462478
- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)
463479

464-
465-
466480
The streamable HTTP transport supports:
467481
- Stateful and stateless operation modes
468482
- Resumability with event stores
469-
- JSON or SSE response formats
483+
- JSON or SSE response formats
470484
- Better scalability for multi-node deployments
471485

472-
473486
### Mounting to an Existing ASGI Server
474487

475488
> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).
@@ -631,7 +644,7 @@ server = Server("example-server", lifespan=server_lifespan)
631644
# Access lifespan context in handlers
632645
@server.call_tool()
633646
async def query_db(name: str, arguments: dict) -> list:
634-
ctx = server.get_context()
647+
ctx = server.request_context
635648
db = ctx.lifespan_context["db"]
636649
return await db.query(arguments["query"])
637650
```

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ def get_response(self, messages: list[dict[str, str]]) -> str:
245245
}
246246
payload = {
247247
"messages": messages,
248-
"model": "llama-3.2-90b-vision-preview",
248+
"model": "meta-llama/llama-4-scout-17b-16e-instruct",
249249
"temperature": 0.7,
250250
"max_tokens": 4096,
251251
"top_p": 1,
@@ -284,12 +284,9 @@ def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:
284284

285285
async def cleanup_servers(self) -> None:
286286
"""Clean up all servers properly."""
287-
cleanup_tasks = [
288-
asyncio.create_task(server.cleanup()) for server in self.servers
289-
]
290-
if cleanup_tasks:
287+
for server in reversed(self.servers):
291288
try:
292-
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
289+
await server.cleanup()
293290
except Exception as e:
294291
logging.warning(f"Warning during final cleanup: {e}")
295292

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ mcp = "mcp.cli:app [cli]"
4444
[tool.uv]
4545
resolution = "lowest-direct"
4646
default-groups = ["dev", "docs"]
47+
required-version = ">=0.7.2"
4748

4849
[dependency-groups]
4950
dev = [
@@ -55,6 +56,7 @@ dev = [
5556
"pytest-xdist>=3.6.1",
5657
"pytest-examples>=0.0.14",
5758
"pytest-pretty>=1.2.0",
59+
"inline-snapshot>=0.23.0",
5860
]
5961
docs = [
6062
"mkdocs>=1.6.1",
@@ -63,7 +65,6 @@ docs = [
6365
"mkdocstrings-python>=1.12.2",
6466
]
6567

66-
6768
[build-system]
6869
requires = ["hatchling", "uv-dynamic-versioning"]
6970
build-backend = "hatchling.build"
@@ -109,7 +110,13 @@ members = ["examples/servers/*"]
109110
mcp = { workspace = true }
110111

111112
[tool.pytest.ini_options]
113+
log_cli = true
112114
xfail_strict = true
115+
addopts = """
116+
--color=yes
117+
--capture=fd
118+
--numprocesses auto
119+
"""
113120
filterwarnings = [
114121
"error",
115122
# This should be fixed on Uvicorn's side.

src/mcp/client/session.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,9 @@ async def list_resources(
209209
types.ClientRequest(
210210
types.ListResourcesRequest(
211211
method="resources/list",
212-
cursor=cursor,
212+
params=types.PaginatedRequestParams(cursor=cursor)
213+
if cursor is not None
214+
else None,
213215
)
214216
),
215217
types.ListResourcesResult,
@@ -223,7 +225,9 @@ async def list_resource_templates(
223225
types.ClientRequest(
224226
types.ListResourceTemplatesRequest(
225227
method="resources/templates/list",
226-
cursor=cursor,
228+
params=types.PaginatedRequestParams(cursor=cursor)
229+
if cursor is not None
230+
else None,
227231
)
228232
),
229233
types.ListResourceTemplatesResult,
@@ -295,7 +299,9 @@ async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResu
295299
types.ClientRequest(
296300
types.ListPromptsRequest(
297301
method="prompts/list",
298-
cursor=cursor,
302+
params=types.PaginatedRequestParams(cursor=cursor)
303+
if cursor is not None
304+
else None,
299305
)
300306
),
301307
types.ListPromptsResult,
@@ -340,7 +346,9 @@ async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
340346
types.ClientRequest(
341347
types.ListToolsRequest(
342348
method="tools/list",
343-
cursor=cursor,
349+
params=types.PaginatedRequestParams(cursor=cursor)
350+
if cursor is not None
351+
else None,
344352
)
345353
),
346354
types.ListToolsResult,

src/mcp/client/session_group.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ class ClientSessionGroup:
7777
the client and can be accessed via the session.
7878
7979
Example Usage:
80-
name_fn = lambda name, server_info: f"{(server_info.name)}-{name}"
80+
name_fn = lambda name, server_info: f"{(server_info.name)}_{name}"
8181
async with ClientSessionGroup(component_name_hook=name_fn) as group:
8282
for server_params in server_params:
83-
group.connect_to_server(server_param)
83+
await group.connect_to_server(server_param)
8484
...
8585
8686
"""
@@ -145,15 +145,15 @@ async def __aexit__(
145145
) -> bool | None:
146146
"""Closes session exit stacks and main exit stack upon completion."""
147147

148+
# Only close the main exit stack if we created it
149+
if self._owns_exit_stack:
150+
await self._exit_stack.aclose()
151+
148152
# Concurrently close session stacks.
149153
async with anyio.create_task_group() as tg:
150154
for exit_stack in self._session_exit_stacks.values():
151155
tg.start_soon(exit_stack.aclose)
152156

153-
# Only close the main exit stack if we created it
154-
if self._owns_exit_stack:
155-
await self._exit_stack.aclose()
156-
157157
@property
158158
def sessions(self) -> list[mcp.ClientSession]:
159159
"""Returns the list of sessions being managed."""

src/mcp/client/sse.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from httpx_sse import aconnect_sse
1111

1212
import mcp.types as types
13-
from mcp.shared._httpx_utils import create_mcp_http_client
13+
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
1414
from mcp.shared.message import SessionMessage
1515

1616
logger = logging.getLogger(__name__)
@@ -26,6 +26,7 @@ async def sse_client(
2626
headers: dict[str, Any] | None = None,
2727
timeout: float = 5,
2828
sse_read_timeout: float = 60 * 5,
29+
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
2930
auth: httpx.Auth | None = None,
3031
):
3132
"""
@@ -52,8 +53,8 @@ async def sse_client(
5253

5354
async with anyio.create_task_group() as tg:
5455
try:
55-
logger.info(f"Connecting to SSE endpoint: {remove_request_params(url)}")
56-
async with create_mcp_http_client(headers=headers, auth=auth) as client:
56+
logger.debug(f"Connecting to SSE endpoint: {remove_request_params(url)}")
57+
async with httpx_client_factory(headers=headers, auth=auth) as client:
5758
async with aconnect_sse(
5859
client,
5960
"GET",
@@ -72,7 +73,7 @@ async def sse_reader(
7273
match sse.event:
7374
case "endpoint":
7475
endpoint_url = urljoin(url, sse.data)
75-
logger.info(
76+
logger.debug(
7677
f"Received endpoint URL: {endpoint_url}"
7778
)
7879

@@ -145,7 +146,7 @@ async def post_writer(endpoint_url: str):
145146
await write_stream.aclose()
146147

147148
endpoint_url = await tg.start(sse_reader)
148-
logger.info(
149+
logger.debug(
149150
f"Starting post writer with endpoint URL: {endpoint_url}"
150151
)
151152
tg.start_soon(post_writer, endpoint_url)

0 commit comments

Comments
 (0)