Skip to content

Commit ee177aa

Browse files
authored
Merge branch 'main' into fix/prevent-session-hang
2 parents b2d6b0d + 62eb08e commit ee177aa

File tree

26 files changed

+251
-140
lines changed

26 files changed

+251
-140
lines changed

.github/workflows/shared.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ jobs:
7070
- name: Run pytest with coverage
7171
shell: bash
7272
run: |
73+
uv run --frozen --no-sync coverage erase
7374
uv run --frozen --no-sync coverage run -m pytest -n auto
7475
uv run --frozen --no-sync coverage combine
7576
uv run --frozen --no-sync coverage report

CLAUDE.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,19 @@ This document contains critical information about working with this codebase. Fo
2828
- Bug fixes require regression tests
2929
- IMPORTANT: The `tests/client/test_client.py` is the most well designed test file. Follow its patterns.
3030
- IMPORTANT: Be minimal, and focus on E2E tests: Use the `mcp.client.Client` whenever possible.
31-
- IMPORTANT: Before pushing, verify 100% branch coverage on changed files by running
32-
`uv run --frozen pytest -x` (coverage is configured in `pyproject.toml` with `fail_under = 100`
33-
and `branch = true`). If any branch is uncovered, add a test for it before pushing.
31+
- Coverage: CI requires 100% (`fail_under = 100`, `branch = true`).
32+
- Full check: `./scripts/test` (~20s, matches CI exactly)
33+
- Targeted check while iterating:
34+
35+
```bash
36+
uv run --frozen coverage erase
37+
uv run --frozen coverage run -m pytest tests/path/test_foo.py
38+
uv run --frozen coverage combine
39+
uv run --frozen coverage report --include='src/mcp/path/foo.py' --fail-under=0
40+
```
41+
42+
Partial runs can't hit 100% (coverage tracks `tests/` too), so `--fail-under=0`
43+
and `--include` scope the report to what you actually changed.
3444
- Avoid `anyio.sleep()` with a fixed duration to wait for async operations. Instead:
3545
- Use `anyio.Event` — set it in the callback/handler, `await event.wait()` in the test
3646
- For stream messages, use `await stream.receive()` instead of `sleep()` + `receive_nowait()`

README.v2.md

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,12 @@ Tools can optionally receive a Context object by including a parameter with the
346346
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
347347
```python
348348
from mcp.server.mcpserver import Context, MCPServer
349-
from mcp.server.session import ServerSession
350349

351350
mcp = MCPServer(name="Progress Example")
352351

353352

354353
@mcp.tool()
355-
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
354+
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
356355
"""Execute a task with progress updates."""
357356
await ctx.info(f"Starting: {task_name}")
358357

@@ -694,13 +693,12 @@ The Context object provides the following capabilities:
694693
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
695694
```python
696695
from mcp.server.mcpserver import Context, MCPServer
697-
from mcp.server.session import ServerSession
698696

699697
mcp = MCPServer(name="Progress Example")
700698

701699

702700
@mcp.tool()
703-
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
701+
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
704702
"""Execute a task with progress updates."""
705703
await ctx.info(f"Starting: {task_name}")
706704

@@ -826,7 +824,6 @@ import uuid
826824
from pydantic import BaseModel, Field
827825

828826
from mcp.server.mcpserver import Context, MCPServer
829-
from mcp.server.session import ServerSession
830827
from mcp.shared.exceptions import UrlElicitationRequiredError
831828
from mcp.types import ElicitRequestURLParams
832829

@@ -844,7 +841,7 @@ class BookingPreferences(BaseModel):
844841

845842

846843
@mcp.tool()
847-
async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str:
844+
async def book_table(date: str, time: str, party_size: int, ctx: Context) -> str:
848845
"""Book a table with date availability check.
849846
850847
This demonstrates form mode elicitation for collecting non-sensitive user input.
@@ -868,7 +865,7 @@ async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerS
868865

869866

870867
@mcp.tool()
871-
async def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> str:
868+
async def secure_payment(amount: float, ctx: Context) -> str:
872869
"""Process a secure payment requiring URL confirmation.
873870
874871
This demonstrates URL mode elicitation using ctx.elicit_url() for
@@ -892,7 +889,7 @@ async def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> st
892889

893890

894891
@mcp.tool()
895-
async def connect_service(service_name: str, ctx: Context[ServerSession, None]) -> str:
892+
async def connect_service(service_name: str, ctx: Context) -> str:
896893
"""Connect to a third-party service requiring OAuth authorization.
897894
898895
This demonstrates the "throw error" pattern using UrlElicitationRequiredError.
@@ -933,14 +930,13 @@ Tools can interact with LLMs through sampling (generating text):
933930
<!-- snippet-source examples/snippets/servers/sampling.py -->
934931
```python
935932
from mcp.server.mcpserver import Context, MCPServer
936-
from mcp.server.session import ServerSession
937933
from mcp.types import SamplingMessage, TextContent
938934

939935
mcp = MCPServer(name="Sampling Example")
940936

941937

942938
@mcp.tool()
943-
async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:
939+
async def generate_poem(topic: str, ctx: Context) -> str:
944940
"""Generate a poem using LLM sampling."""
945941
prompt = f"Write a short poem about {topic}"
946942

@@ -970,13 +966,12 @@ Tools can send logs and notifications through the context:
970966
<!-- snippet-source examples/snippets/servers/notifications.py -->
971967
```python
972968
from mcp.server.mcpserver import Context, MCPServer
973-
from mcp.server.session import ServerSession
974969

975970
mcp = MCPServer(name="Notifications Example")
976971

977972

978973
@mcp.tool()
979-
async def process_data(data: str, ctx: Context[ServerSession, None]) -> str:
974+
async def process_data(data: str, ctx: Context) -> str:
980975
"""Process data with logging."""
981976
# Different log levels
982977
await ctx.debug(f"Debug: Processing '{data}'")

examples/servers/everything-server/mcp_everything_server/server.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from mcp.server import ServerRequestContext
1414
from mcp.server.mcpserver import Context, MCPServer
1515
from mcp.server.mcpserver.prompts.base import UserMessage
16-
from mcp.server.session import ServerSession
1716
from mcp.server.streamable_http import EventCallback, EventMessage, EventStore
1817
from mcp.types import (
1918
AudioContent,
@@ -142,7 +141,7 @@ def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedR
142141

143142

144143
@mcp.tool()
145-
async def test_tool_with_logging(ctx: Context[ServerSession, None]) -> str:
144+
async def test_tool_with_logging(ctx: Context) -> str:
146145
"""Tests tool that emits log messages during execution"""
147146
await ctx.info("Tool execution started")
148147
await asyncio.sleep(0.05)
@@ -155,7 +154,7 @@ async def test_tool_with_logging(ctx: Context[ServerSession, None]) -> str:
155154

156155

157156
@mcp.tool()
158-
async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> str:
157+
async def test_tool_with_progress(ctx: Context) -> str:
159158
"""Tests tool that reports progress notifications"""
160159
await ctx.report_progress(progress=0, total=100, message="Completed step 0 of 100")
161160
await asyncio.sleep(0.05)
@@ -173,7 +172,7 @@ async def test_tool_with_progress(ctx: Context[ServerSession, None]) -> str:
173172

174173

175174
@mcp.tool()
176-
async def test_sampling(prompt: str, ctx: Context[ServerSession, None]) -> str:
175+
async def test_sampling(prompt: str, ctx: Context) -> str:
177176
"""Tests server-initiated sampling (LLM completion request)"""
178177
try:
179178
# Request sampling from client
@@ -198,7 +197,7 @@ class UserResponse(BaseModel):
198197

199198

200199
@mcp.tool()
201-
async def test_elicitation(message: str, ctx: Context[ServerSession, None]) -> str:
200+
async def test_elicitation(message: str, ctx: Context) -> str:
202201
"""Tests server-initiated elicitation (user input request)"""
203202
try:
204203
# Request user input from client
@@ -230,7 +229,7 @@ class SEP1034DefaultsSchema(BaseModel):
230229

231230

232231
@mcp.tool()
233-
async def test_elicitation_sep1034_defaults(ctx: Context[ServerSession, None]) -> str:
232+
async def test_elicitation_sep1034_defaults(ctx: Context) -> str:
234233
"""Tests elicitation with default values for all primitive types (SEP-1034)"""
235234
try:
236235
# Request user input with defaults for all primitive types
@@ -289,7 +288,7 @@ class EnumSchemasTestSchema(BaseModel):
289288

290289

291290
@mcp.tool()
292-
async def test_elicitation_sep1330_enums(ctx: Context[ServerSession, None]) -> str:
291+
async def test_elicitation_sep1330_enums(ctx: Context) -> str:
293292
"""Tests elicitation with enum schema variations per SEP-1330"""
294293
try:
295294
result = await ctx.elicit(
@@ -313,7 +312,7 @@ def test_error_handling() -> str:
313312

314313

315314
@mcp.tool()
316-
async def test_reconnection(ctx: Context[ServerSession, None]) -> str:
315+
async def test_reconnection(ctx: Context) -> str:
317316
"""Tests SSE polling by closing stream mid-call (SEP-1699)"""
318317
await ctx.info("Before disconnect")
319318

examples/snippets/servers/elicitation.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from pydantic import BaseModel, Field
1111

1212
from mcp.server.mcpserver import Context, MCPServer
13-
from mcp.server.session import ServerSession
1413
from mcp.shared.exceptions import UrlElicitationRequiredError
1514
from mcp.types import ElicitRequestURLParams
1615

@@ -28,7 +27,7 @@ class BookingPreferences(BaseModel):
2827

2928

3029
@mcp.tool()
31-
async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str:
30+
async def book_table(date: str, time: str, party_size: int, ctx: Context) -> str:
3231
"""Book a table with date availability check.
3332
3433
This demonstrates form mode elicitation for collecting non-sensitive user input.
@@ -52,7 +51,7 @@ async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerS
5251

5352

5453
@mcp.tool()
55-
async def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> str:
54+
async def secure_payment(amount: float, ctx: Context) -> str:
5655
"""Process a secure payment requiring URL confirmation.
5756
5857
This demonstrates URL mode elicitation using ctx.elicit_url() for
@@ -76,7 +75,7 @@ async def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> st
7675

7776

7877
@mcp.tool()
79-
async def connect_service(service_name: str, ctx: Context[ServerSession, None]) -> str:
78+
async def connect_service(service_name: str, ctx: Context) -> str:
8079
"""Connect to a third-party service requiring OAuth authorization.
8180
8281
This demonstrates the "throw error" pattern using UrlElicitationRequiredError.

examples/snippets/servers/notifications.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from mcp.server.mcpserver import Context, MCPServer
2-
from mcp.server.session import ServerSession
32

43
mcp = MCPServer(name="Notifications Example")
54

65

76
@mcp.tool()
8-
async def process_data(data: str, ctx: Context[ServerSession, None]) -> str:
7+
async def process_data(data: str, ctx: Context) -> str:
98
"""Process data with logging."""
109
# Different log levels
1110
await ctx.debug(f"Debug: Processing '{data}'")

examples/snippets/servers/sampling.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from mcp.server.mcpserver import Context, MCPServer
2-
from mcp.server.session import ServerSession
32
from mcp.types import SamplingMessage, TextContent
43

54
mcp = MCPServer(name="Sampling Example")
65

76

87
@mcp.tool()
9-
async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:
8+
async def generate_poem(topic: str, ctx: Context) -> str:
109
"""Generate a poem using LLM sampling."""
1110
prompt = f"Write a short poem about {topic}"
1211

examples/snippets/servers/tool_progress.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from mcp.server.mcpserver import Context, MCPServer
2-
from mcp.server.session import ServerSession
32

43
mcp = MCPServer(name="Progress Example")
54

65

76
@mcp.tool()
8-
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
7+
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
98
"""Execute a task with progress updates."""
109
await ctx.info(f"Starting: {task_name}")
1110

scripts/test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
set -ex
44

5+
uv run --frozen coverage erase
56
uv run --frozen coverage run -m pytest -n auto $@
67
uv run --frozen coverage combine
78
uv run --frozen coverage report

src/mcp/server/lowlevel/server.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,6 @@ async def _handle_message(
414414
)
415415
case Exception():
416416
logger.error(f"Received exception from stream: {message}")
417-
await session.send_log_message(
418-
level="error",
419-
data="Internal Server Error",
420-
logger="mcp.server.exception_handler",
421-
)
422417
if raise_exceptions:
423418
raise message
424419
case _:

0 commit comments

Comments
 (0)