| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 | import logging
import queue
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from contextlib import ExitStack
from datetime import timedelta
from types import TracebackType
from typing import Any, Generic, Self, TypeVar
from httpx import HTTPStatusError
from pydantic import BaseModel
from core.mcp.error import MCPAuthError, MCPConnectionError
from core.mcp.types import (
    CancelledNotification,
    ClientNotification,
    ClientRequest,
    ClientResult,
    ErrorData,
    JSONRPCError,
    JSONRPCMessage,
    JSONRPCNotification,
    JSONRPCRequest,
    JSONRPCResponse,
    MessageMetadata,
    RequestId,
    RequestParams,
    ServerMessageMetadata,
    ServerNotification,
    ServerRequest,
    ServerResult,
    SessionMessage,
)
SendRequestT = TypeVar("SendRequestT", ClientRequest, ServerRequest)
SendResultT = TypeVar("SendResultT", ClientResult, ServerResult)
SendNotificationT = TypeVar("SendNotificationT", ClientNotification, ServerNotification)
ReceiveRequestT = TypeVar("ReceiveRequestT", ClientRequest, ServerRequest)
ReceiveResultT = TypeVar("ReceiveResultT", bound=BaseModel)
ReceiveNotificationT = TypeVar("ReceiveNotificationT", ClientNotification, ServerNotification)
DEFAULT_RESPONSE_READ_TIMEOUT = 1.0
class RequestResponder(Generic[ReceiveRequestT, SendResultT]):
    """Handles responding to MCP requests and manages request lifecycle.
    This class MUST be used as a context manager to ensure proper cleanup and
    cancellation handling:
    Example:
        with request_responder as resp:
            resp.respond(result)
    The context manager ensures:
    1. Proper cancellation scope setup and cleanup
    2. Request completion tracking
    3. Cleanup of in-flight requests
    """
    request: ReceiveRequestT
    _session: Any
    _on_complete: Callable[["RequestResponder[ReceiveRequestT, SendResultT]"], Any]
    def __init__(
        self,
        request_id: RequestId,
        request_meta: RequestParams.Meta | None,
        request: ReceiveRequestT,
        session: """BaseSession[
            SendRequestT,
            SendNotificationT,
            SendResultT,
            ReceiveRequestT,
            ReceiveNotificationT
        ]""",
        on_complete: Callable[["RequestResponder[ReceiveRequestT, SendResultT]"], Any],
    ) -> None:
        self.request_id = request_id
        self.request_meta = request_meta
        self.request = request
        self._session = session
        self._completed = False
        self._on_complete = on_complete
        self._entered = False  # Track if we're in a context manager
    def __enter__(self) -> "RequestResponder[ReceiveRequestT, SendResultT]":
        """Enter the context manager, enabling request cancellation tracking."""
        self._entered = True
        return self
    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        """Exit the context manager, performing cleanup and notifying completion."""
        try:
            if self._completed:
                self._on_complete(self)
        finally:
            self._entered = False
    def respond(self, response: SendResultT | ErrorData) -> None:
        """Send a response for this request.
        Must be called within a context manager block.
        Raises:
            RuntimeError: If not used within a context manager
            AssertionError: If request was already responded to
        """
        if not self._entered:
            raise RuntimeError("RequestResponder must be used as a context manager")
        assert not self._completed, "Request already responded to"
        self._completed = True
        self._session._send_response(request_id=self.request_id, response=response)
    def cancel(self) -> None:
        """Cancel this request and mark it as completed."""
        if not self._entered:
            raise RuntimeError("RequestResponder must be used as a context manager")
        self._completed = True  # Mark as completed so it's removed from in_flight
        # Send an error response to indicate cancellation
        self._session._send_response(
            request_id=self.request_id,
            response=ErrorData(code=0, message="Request cancelled", data=None),
        )
class BaseSession(
    Generic[
        SendRequestT,
        SendNotificationT,
        SendResultT,
        ReceiveRequestT,
        ReceiveNotificationT,
    ],
):
    """
    Implements an MCP "session" on top of read/write streams, including features
    like request/response linking, notifications, and progress.
    This class is a context manager that automatically starts processing
    messages when entered.
    """
    _response_streams: dict[RequestId, queue.Queue[JSONRPCResponse | JSONRPCError]]
    _request_id: int
    _in_flight: dict[RequestId, RequestResponder[ReceiveRequestT, SendResultT]]
    _receive_request_type: type[ReceiveRequestT]
    _receive_notification_type: type[ReceiveNotificationT]
    def __init__(
        self,
        read_stream: queue.Queue,
        write_stream: queue.Queue,
        receive_request_type: type[ReceiveRequestT],
        receive_notification_type: type[ReceiveNotificationT],
        # If none, reading will never time out
        read_timeout_seconds: timedelta | None = None,
    ) -> None:
        self._read_stream = read_stream
        self._write_stream = write_stream
        self._response_streams = {}
        self._request_id = 0
        self._receive_request_type = receive_request_type
        self._receive_notification_type = receive_notification_type
        self._session_read_timeout_seconds = read_timeout_seconds
        self._in_flight = {}
        self._exit_stack = ExitStack()
    def __enter__(self) -> Self:
        self._executor = ThreadPoolExecutor()
        self._receiver_future = self._executor.submit(self._receive_loop)
        return self
    def check_receiver_status(self) -> None:
        if self._receiver_future.done():
            self._receiver_future.result()
    def __exit__(
        self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
    ) -> None:
        self._exit_stack.close()
        self._read_stream.put(None)
        self._write_stream.put(None)
    def send_request(
        self,
        request: SendRequestT,
        result_type: type[ReceiveResultT],
        request_read_timeout_seconds: timedelta | None = None,
        metadata: MessageMetadata = None,
    ) -> ReceiveResultT:
        """
        Sends a request and wait for a response. Raises an McpError if the
        response contains an error. If a request read timeout is provided, it
        will take precedence over the session read timeout.
        Do not use this method to emit notifications! Use send_notification()
        instead.
        """
        self.check_receiver_status()
        request_id = self._request_id
        self._request_id = request_id + 1
        response_queue: queue.Queue[JSONRPCResponse | JSONRPCError] = queue.Queue()
        self._response_streams[request_id] = response_queue
        try:
            jsonrpc_request = JSONRPCRequest(
                jsonrpc="2.0",
                id=request_id,
                **request.model_dump(by_alias=True, mode="json", exclude_none=True),
            )
            self._write_stream.put(SessionMessage(message=JSONRPCMessage(jsonrpc_request), metadata=metadata))
            timeout = DEFAULT_RESPONSE_READ_TIMEOUT
            if request_read_timeout_seconds is not None:
                timeout = float(request_read_timeout_seconds.total_seconds())
            elif self._session_read_timeout_seconds is not None:
                timeout = float(self._session_read_timeout_seconds.total_seconds())
            while True:
                try:
                    response_or_error = response_queue.get(timeout=timeout)
                    break
                except queue.Empty:
                    self.check_receiver_status()
                    continue
            if response_or_error is None:
                raise MCPConnectionError(
                    ErrorData(
                        code=500,
                        message="No response received",
                    )
                )
            elif isinstance(response_or_error, JSONRPCError):
                if response_or_error.error.code == 401:
                    raise MCPAuthError(
                        ErrorData(code=response_or_error.error.code, message=response_or_error.error.message)
                    )
                else:
                    raise MCPConnectionError(
                        ErrorData(code=response_or_error.error.code, message=response_or_error.error.message)
                    )
            else:
                return result_type.model_validate(response_or_error.result)
        finally:
            self._response_streams.pop(request_id, None)
    def send_notification(
        self,
        notification: SendNotificationT,
        related_request_id: RequestId | None = None,
    ) -> None:
        """
        Emits a notification, which is a one-way message that does not expect
        a response.
        """
        self.check_receiver_status()
        # Some transport implementations may need to set the related_request_id
        # to attribute to the notifications to the request that triggered them.
        jsonrpc_notification = JSONRPCNotification(
            jsonrpc="2.0",
            **notification.model_dump(by_alias=True, mode="json", exclude_none=True),
        )
        session_message = SessionMessage(
            message=JSONRPCMessage(jsonrpc_notification),
            metadata=ServerMessageMetadata(related_request_id=related_request_id) if related_request_id else None,
        )
        self._write_stream.put(session_message)
    def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:
        if isinstance(response, ErrorData):
            jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response)
            session_message = SessionMessage(message=JSONRPCMessage(jsonrpc_error))
            self._write_stream.put(session_message)
        else:
            jsonrpc_response = JSONRPCResponse(
                jsonrpc="2.0",
                id=request_id,
                result=response.model_dump(by_alias=True, mode="json", exclude_none=True),
            )
            session_message = SessionMessage(message=JSONRPCMessage(jsonrpc_response))
            self._write_stream.put(session_message)
    def _receive_loop(self) -> None:
        """
        Main message processing loop.
        In a real synchronous implementation, this would likely run in a separate thread.
        """
        while True:
            try:
                # Attempt to receive a message (this would be blocking in a synchronous context)
                message = self._read_stream.get(timeout=DEFAULT_RESPONSE_READ_TIMEOUT)
                if message is None:
                    break
                if isinstance(message, HTTPStatusError):
                    response_queue = self._response_streams.get(self._request_id - 1)
                    if response_queue is not None:
                        response_queue.put(
                            JSONRPCError(
                                jsonrpc="2.0",
                                id=self._request_id - 1,
                                error=ErrorData(code=message.response.status_code, message=message.args[0]),
                            )
                        )
                    else:
                        self._handle_incoming(RuntimeError(f"Received response with an unknown request ID: {message}"))
                elif isinstance(message, Exception):
                    self._handle_incoming(message)
                elif isinstance(message.message.root, JSONRPCRequest):
                    validated_request = self._receive_request_type.model_validate(
                        message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
                    )
                    responder = RequestResponder(
                        request_id=message.message.root.id,
                        request_meta=validated_request.root.params.meta if validated_request.root.params else None,
                        request=validated_request,
                        session=self,
                        on_complete=lambda r: self._in_flight.pop(r.request_id, None),
                    )
                    self._in_flight[responder.request_id] = responder
                    self._received_request(responder)
                    if not responder._completed:
                        self._handle_incoming(responder)
                elif isinstance(message.message.root, JSONRPCNotification):
                    try:
                        notification = self._receive_notification_type.model_validate(
                            message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
                        )
                        # Handle cancellation notifications
                        if isinstance(notification.root, CancelledNotification):
                            cancelled_id = notification.root.params.requestId
                            if cancelled_id in self._in_flight:
                                self._in_flight[cancelled_id].cancel()
                        else:
                            self._received_notification(notification)
                            self._handle_incoming(notification)
                    except Exception as e:
                        # For other validation errors, log and continue
                        logging.warning(f"Failed to validate notification: {e}. Message was: {message.message.root}")
                else:  # Response or error
                    response_queue = self._response_streams.get(message.message.root.id)
                    if response_queue is not None:
                        response_queue.put(message.message.root)
                    else:
                        self._handle_incoming(RuntimeError(f"Server Error: {message}"))
            except queue.Empty:
                continue
            except Exception as e:
                logging.exception("Error in message processing loop")
                raise
    def _received_request(self, responder: RequestResponder[ReceiveRequestT, SendResultT]) -> None:
        """
        Can be overridden by subclasses to handle a request without needing to
        listen on the message stream.
        If the request is responded to within this method, it will not be
        forwarded on to the message stream.
        """
        pass
    def _received_notification(self, notification: ReceiveNotificationT) -> None:
        """
        Can be overridden by subclasses to handle a notification without needing
        to listen on the message stream.
        """
        pass
    def send_progress_notification(
        self, progress_token: str | int, progress: float, total: float | None = None
    ) -> None:
        """
        Sends a progress notification for a request that is currently being
        processed.
        """
        pass
    def _handle_incoming(
        self,
        req: RequestResponder[ReceiveRequestT, SendResultT] | ReceiveNotificationT | Exception,
    ) -> None:
        """A generic handler for incoming messages. Overwritten by subclasses."""
        pass
 |