Client half of an MCP connection, running on a `Dispatcher`. Construct it over a transport's stream pair (or pass a pre-built `dispatcher=`), enter as an async context manager, then call `initialize()`. The dispatcher owns the receive loop and request correlation; this class owns th
| 181 | |
| 182 | |
| 183 | class ClientSession: |
| 184 | """Client half of an MCP connection, running on a `Dispatcher`. |
| 185 | |
| 186 | Construct it over a transport's stream pair (or pass a pre-built |
| 187 | `dispatcher=`), enter as an async context manager, then call |
| 188 | `initialize()`. The dispatcher owns the receive loop and request |
| 189 | correlation; this class owns the typed MCP layer and the constructor |
| 190 | callbacks. Transport `Exception` items reach `message_handler` only when |
| 191 | the session builds its own dispatcher from a stream pair. |
| 192 | """ |
| 193 | |
| 194 | def __init__( |
| 195 | self, |
| 196 | read_stream: ReadStream[SessionMessage | Exception] | None = None, |
| 197 | write_stream: WriteStream[SessionMessage] | None = None, |
| 198 | read_timeout_seconds: float | None = None, |
| 199 | sampling_callback: SamplingFnT | None = None, |
| 200 | elicitation_callback: ElicitationFnT | None = None, |
| 201 | list_roots_callback: ListRootsFnT | None = None, |
| 202 | logging_callback: LoggingFnT | None = None, |
| 203 | message_handler: MessageHandlerFnT | None = None, |
| 204 | client_info: types.Implementation | None = None, |
| 205 | *, |
| 206 | sampling_capabilities: types.SamplingCapability | None = None, |
| 207 | dispatcher: Dispatcher[Any] | None = None, |
| 208 | ) -> None: |
| 209 | self._session_read_timeout_seconds = read_timeout_seconds |
| 210 | self._client_info = client_info or DEFAULT_CLIENT_INFO |
| 211 | self._sampling_callback = sampling_callback or _default_sampling_callback |
| 212 | self._sampling_capabilities = sampling_capabilities |
| 213 | self._elicitation_callback = elicitation_callback or _default_elicitation_callback |
| 214 | self._list_roots_callback = list_roots_callback or _default_list_roots_callback |
| 215 | self._logging_callback = logging_callback or _default_logging_callback |
| 216 | self._message_handler = message_handler or _default_message_handler |
| 217 | self._tool_output_schemas: dict[str, dict[str, Any] | None] = {} |
| 218 | self._initialize_result: types.InitializeResult | None = None |
| 219 | self._discover_result: types.DiscoverResult | None = None |
| 220 | self._negotiated_version: str | None = None |
| 221 | self._stamp: Callable[[dict[str, Any], CallOptions], None] = _preconnect_stamp |
| 222 | self._task_group: anyio.abc.TaskGroup | None = None |
| 223 | if dispatcher is not None: |
| 224 | if read_stream is not None or write_stream is not None: |
| 225 | raise ValueError("pass read_stream/write_stream or dispatcher, not both") |
| 226 | self._dispatcher: Dispatcher[Any] = dispatcher |
| 227 | if isinstance(dispatcher, JSONRPCDispatcher) and dispatcher.on_stream_exception is None: |
| 228 | # Route transport-level Exception items into message_handler — only |
| 229 | # stream-backed dispatchers carry these; DirectDispatcher has none. |
| 230 | # Don't clobber a caller-supplied hook. |
| 231 | # TODO(L78): this leaves a bound-method ref on the dispatcher after the |
| 232 | # session exits (memory pin) and a second wrap of the same dispatcher would |
| 233 | # skip install. The Transport-as-Dispatcher rework (L77) removes this seam. |
| 234 | dispatcher.on_stream_exception = self._on_stream_exception |
| 235 | else: |
| 236 | if read_stream is None or write_stream is None: |
| 237 | raise ValueError("read_stream and write_stream are required when no dispatcher is given") |
| 238 | # Built eagerly so notifications can be sent before entering the context manager. |
| 239 | self._dispatcher = JSONRPCDispatcher( |
| 240 | read_stream, write_stream, on_stream_exception=self._on_stream_exception |
no outgoing calls