Skip to content

Commit 954b51f

Browse files
committed
Merge kziemski transport abstractions review suggestions with test fixes
2 parents 0163c6d + 9b06bce commit 954b51f

File tree

8 files changed

+513
-137
lines changed

8 files changed

+513
-137
lines changed

docs/migration.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,193 @@ await client.read_resource("test://resource")
471471
await client.read_resource(str(my_any_url))
472472
```
473473

474+
### Transport Abstractions Refactored
475+
476+
The session hierarchy has been refactored to support pluggable transport implementations. This introduces several breaking changes:
477+
478+
#### `ClientRequestContext` type changed
479+
480+
`ClientRequestContext` is now `RequestContext[BaseClientSession]` instead of `RequestContext[ClientSession]`. This means callbacks receive the more general `BaseClientSession` type, which may not have all methods available on `ClientSession`.
481+
482+
**Before:**
483+
484+
```python
485+
from mcp.client.context import ClientRequestContext
486+
from mcp.client.session import ClientSession
487+
488+
async def my_callback(context: ClientRequestContext) -> None:
489+
# Could access ClientSession-specific methods
490+
caps = context.session.get_server_capabilities()
491+
```
492+
493+
**After:**
494+
495+
```python
496+
from mcp.client.context import ClientRequestContext
497+
from mcp.client.session import ClientSession
498+
499+
async def my_callback(context: ClientRequestContext) -> None:
500+
# context.session is BaseClientSession - narrow the type if needed
501+
if isinstance(context.session, ClientSession):
502+
caps = context.session.get_server_capabilities()
503+
```
504+
505+
#### Callback protocols are now generic
506+
507+
`sampling_callback`, `elicitation_callback`, and `list_roots_callback` protocols now require explicit type parameters.
508+
509+
**Before:**
510+
511+
```python
512+
from mcp.client.session import SamplingFnT
513+
514+
async def my_sampling(context, params) -> CreateMessageResult:
515+
...
516+
517+
# Type inferred as SamplingFnT
518+
session = ClientSession(..., sampling_callback=my_sampling)
519+
```
520+
521+
**After:**
522+
523+
```python
524+
from mcp.client.session import SamplingFnT, ClientSession
525+
526+
async def my_sampling(
527+
context: RequestContext[ClientSession],
528+
params: CreateMessageRequestParams
529+
) -> CreateMessageResult:
530+
...
531+
532+
# Explicit type annotation recommended
533+
my_sampling_typed: SamplingFnT[ClientSession] = my_sampling
534+
session = ClientSession(..., sampling_callback=my_sampling_typed)
535+
```
536+
537+
#### `SessionT` renamed to `SessionT_co`
538+
539+
In `mcp.shared._context` and `mcp.shared.progress`, the `SessionT` TypeVar has been renamed to `SessionT_co` to follow naming conventions for covariant type variables.
540+
541+
**Before:**
542+
543+
```python
544+
from mcp.shared._context import SessionT
545+
```
546+
547+
**After:**
548+
549+
```python
550+
from mcp.shared._context import SessionT_co
551+
```
552+
553+
#### New `AbstractBaseSession` structural interface
554+
555+
The session hierarchy now uses a new **runtime-checkable Protocol** called `AbstractBaseSession` to define the shared contract for all MCP sessions. This protocol enables structural subtyping, allowing different transport implementations to be used interchangeably without requiring rigid inheritance.
556+
557+
Key characteristics of `AbstractBaseSession`:
558+
1. **Pure Interface**: It is a structural protocol with no implementation state or `__init__` method.
559+
2. **Simplified Type Parameters**: It takes two parameters: `AbstractBaseSession[SendRequestT, SendNotificationT]`. Contravariant variance is used for these parameters to ensure that sessions can be used safely in generic contexts (like `RequestContext`).
560+
3. **BaseSession Implementation**: The concrete implementation logic (state management, response routing) is provided by the `BaseSession` class, which satisfies the protocol.
561+
562+
**Before:**
563+
564+
```python
565+
from mcp.shared.session import AbstractBaseSession
566+
567+
class MySession(AbstractBaseSession[MyMessage, ...]):
568+
def __init__(self):
569+
super().__init__() # Would set up _response_streams, _task_group
570+
```
571+
572+
**After:**
573+
574+
```python
575+
from mcp.shared.session import AbstractBaseSession
576+
577+
class MySession(AbstractBaseSession[...]):
578+
def __init__(self):
579+
# Manage your own state - no super().__init__() to call
580+
self._my_state = {}
581+
```
582+
583+
#### `SendRequestT` changed to contravariant
584+
585+
The `SendRequestT` TypeVar is now defined as **contravariant** to support its use in the `AbstractBaseSession` Protocol.
586+
587+
**Before:**
588+
589+
```python
590+
SendRequestT = TypeVar("SendRequestT", ClientRequest, ServerRequest)
591+
```
592+
593+
**After:**
594+
595+
```python
596+
SendRequestT = TypeVar("SendRequestT", ClientRequest, ServerRequest, contravariant=True)
597+
```
598+
599+
#### `SendNotificationT` changed to contravariant
600+
601+
The `SendNotificationT` TypeVar is now defined as **contravariant** to support its use in the `AbstractBaseSession` Protocol.
602+
603+
**Before:**
604+
605+
```python
606+
SendNotificationT = TypeVar("SendNotificationT", ClientNotification, ServerNotification)
607+
```
608+
609+
**After:**
610+
611+
```python
612+
SendNotificationT = TypeVar(
613+
"SendNotificationT", ClientNotification, ServerNotification, contravariant=True
614+
)
615+
```
616+
617+
#### `ReceiveResultT` changed to covariant
618+
619+
The `ReceiveResultT` TypeVar is now defined as **covariant** to support its use in the `AbstractBaseSession` Protocol.
620+
621+
**Before:**
622+
623+
```python
624+
ReceiveResultT = TypeVar("ReceiveResultT", bound=BaseModel)
625+
```
626+
627+
**After:**
628+
629+
```python
630+
ReceiveResultT = TypeVar("ReceiveResultT", bound=BaseModel, covariant=True)
631+
```
632+
633+
#### `BaseClientSession` is now a Protocol
634+
635+
`BaseClientSession` is now a `typing.Protocol` (structural subtyping) instead of an abstract base class. It no longer inherits from `AbstractBaseSession` and requires no inheritance to satisfy.
636+
637+
**Before:**
638+
639+
```python
640+
from mcp.client.base_client_session import BaseClientSession
641+
642+
class MyClientSession(BaseClientSession):
643+
async def initialize(self) -> InitializeResult:
644+
...
645+
```
646+
647+
**After:**
648+
649+
```python
650+
from mcp.client.base_client_session import BaseClientSession
651+
652+
class MyClientSession:
653+
# Just implement the methods - no inheritance needed
654+
async def initialize(self) -> InitializeResult:
655+
...
656+
657+
# Verify protocol satisfaction at runtime
658+
assert isinstance(MyClientSession(), BaseClientSession)
659+
```
660+
474661
## Deprecations
475662

476663
<!-- Add deprecations below -->
Lines changed: 71 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,68 @@
1-
from abc import abstractmethod
21
from typing import Any, TypeVar
32

3+
from typing_extensions import Protocol, runtime_checkable
4+
45
from mcp import types
5-
from mcp.shared.session import AbstractBaseSession, ProgressFnT
6+
from mcp.shared.session import ProgressFnT
67
from mcp.types._types import RequestParamsMeta
78

89
ClientSessionT_contra = TypeVar("ClientSessionT_contra", bound="BaseClientSession", contravariant=True)
910

1011

11-
class BaseClientSession(
12-
AbstractBaseSession[
13-
Any,
14-
types.ClientRequest,
15-
types.ClientNotification,
16-
types.ClientResult,
17-
types.ServerRequest,
18-
types.ServerNotification,
19-
]
20-
):
21-
"""Base class for client transport sessions.
22-
23-
The class provides all the methods that a client session should implement,
24-
irrespective of the transport used.
12+
@runtime_checkable
13+
class BaseClientSession(Protocol):
14+
"""Protocol defining the interface for MCP client sessions.
15+
16+
This protocol specifies all methods that a client session must implement,
17+
irrespective of the transport used. Implementations satisfy this protocol
18+
through structural subtyping — no inheritance required.
2519
"""
2620

27-
@abstractmethod
28-
async def initialize(self) -> types.InitializeResult:
29-
"""Initialize the client session."""
30-
raise NotImplementedError
21+
# Methods from AbstractBaseSession (must be explicitly declared in Protocol)
22+
async def send_request(
23+
self,
24+
request: types.ClientRequest,
25+
result_type: type,
26+
request_read_timeout_seconds: float | None = None,
27+
metadata: Any = None,
28+
progress_callback: ProgressFnT | None = None,
29+
) -> Any: ...
30+
31+
async def send_notification(
32+
self,
33+
notification: types.ClientNotification,
34+
related_request_id: Any = None,
35+
) -> None: ...
36+
37+
async def send_progress_notification(
38+
self,
39+
progress_token: types.ProgressToken,
40+
progress: float,
41+
total: float | None = None,
42+
message: str | None = None,
43+
*,
44+
meta: RequestParamsMeta | None = None,
45+
) -> None: ...
3146

32-
@abstractmethod
33-
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
34-
"""Send a ping request."""
35-
raise NotImplementedError
47+
# Client-specific methods
48+
async def initialize(self) -> types.InitializeResult: ...
3649

37-
@abstractmethod
38-
async def list_resources(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListResourcesResult:
39-
"""Send a resources/list request.
50+
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult: ...
4051

41-
Args:
42-
params: Full pagination parameters including cursor and any future fields
43-
"""
44-
raise NotImplementedError
52+
async def list_resources(
53+
self, *, params: types.PaginatedRequestParams | None = None
54+
) -> types.ListResourcesResult: ...
4555

46-
@abstractmethod
4756
async def list_resource_templates(
4857
self, *, params: types.PaginatedRequestParams | None = None
49-
) -> types.ListResourceTemplatesResult:
50-
"""Send a resources/templates/list request.
51-
52-
Args:
53-
params: Full pagination parameters including cursor and any future fields
54-
"""
55-
raise NotImplementedError
56-
57-
@abstractmethod
58-
async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.ReadResourceResult:
59-
"""Send a resources/read request."""
60-
raise NotImplementedError
61-
62-
@abstractmethod
63-
async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
64-
"""Send a resources/subscribe request."""
65-
raise NotImplementedError
66-
67-
@abstractmethod
68-
async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
69-
"""Send a resources/unsubscribe request."""
70-
raise NotImplementedError
71-
72-
@abstractmethod
58+
) -> types.ListResourceTemplatesResult: ...
59+
60+
async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.ReadResourceResult: ...
61+
62+
async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult: ...
63+
64+
async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult: ...
65+
7366
async def call_tool(
7467
self,
7568
name: str,
@@ -78,40 +71,33 @@ async def call_tool(
7871
progress_callback: ProgressFnT | None = None,
7972
*,
8073
meta: RequestParamsMeta | None = None,
81-
) -> types.CallToolResult:
82-
"""Send a tools/call request with optional progress callback support."""
83-
raise NotImplementedError
74+
) -> types.CallToolResult: ...
8475

85-
@abstractmethod
86-
async def list_prompts(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListPromptsResult:
87-
"""Send a prompts/list request.
76+
async def list_prompts(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListPromptsResult: ...
8877

89-
Args:
90-
params: Full pagination parameters including cursor and any future fields
91-
"""
92-
raise NotImplementedError
93-
94-
@abstractmethod
9578
async def get_prompt(
9679
self,
9780
name: str,
9881
arguments: dict[str, str] | None = None,
9982
*,
10083
meta: RequestParamsMeta | None = None,
101-
) -> types.GetPromptResult:
102-
"""Send a prompts/get request."""
103-
raise NotImplementedError
104-
105-
@abstractmethod
106-
async def list_tools(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListToolsResult:
107-
"""Send a tools/list request.
108-
109-
Args:
110-
params: Full pagination parameters including cursor and any future fields
111-
"""
112-
raise NotImplementedError
113-
114-
@abstractmethod
115-
async def send_roots_list_changed(self) -> None: # pragma: no cover
116-
"""Send a roots/list_changed notification."""
117-
raise NotImplementedError
84+
) -> types.GetPromptResult: ...
85+
86+
async def list_tools(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListToolsResult: ...
87+
88+
# Missing methods added per review
89+
async def complete(
90+
self,
91+
ref: types.ResourceTemplateReference | types.PromptReference,
92+
argument: dict[str, str],
93+
context_arguments: dict[str, str] | None = None,
94+
) -> types.CompleteResult: ...
95+
96+
async def set_logging_level(
97+
self,
98+
level: types.LoggingLevel,
99+
*,
100+
meta: RequestParamsMeta | None = None,
101+
) -> types.EmptyResult: ...
102+
103+
async def send_roots_list_changed(self) -> None: ...

src/mcp/client/context.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""Request context for MCP client handlers."""
22

33
from mcp.client import BaseClientSession
4-
5-
# from mcp.client.session import ClientSession
64
from mcp.shared._context import RequestContext
75

86
ClientRequestContext = RequestContext[BaseClientSession]

src/mcp/client/session.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ class ClientSession(
107107
types.ServerRequest,
108108
types.ServerNotification,
109109
],
110-
BaseClientSession,
111110
):
112111
def __init__(
113112
self,

0 commit comments

Comments
 (0)