Files
kefu/wechat_rpa/.venv/Lib/site-packages/mcp/server/session.py
T
2026-09-21 10:34:06 +08:00

490 lines
20 KiB
Python

"""`ServerSession`: server-to-client requests and notifications.
A per-request proxy built by the kernel for each inbound request. Exposes the
request-scoped outbound channel and the connection's standalone channel.
Handlers reach it as `ctx.session` and use the typed helpers (`elicit_form`,
`send_log_message`, ...) to call back to the client.
"""
import logging
from typing import Any, TypeVar, overload
import mcp_types as types
from mcp_types import methods as _methods
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
from pydantic import AnyUrl, BaseModel
from typing_extensions import deprecated
from mcp.server.connection import Connection, allowed_log_levels
from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages, wants_sampling_tools
from mcp.shared.dispatcher import CallOptions, DispatchContext, ProgressFnT
from mcp.shared.exceptions import MCPDeprecationWarning
from mcp.shared.message import ServerMessageMetadata
__all__ = ["ServerSession"]
logger = logging.getLogger(__name__)
# `send_log_message`'s `logger` parameter (public API, the spec's logger-name
# field) shadows the module logger inside that method; this alias keeps it
# reachable there.
_logger = logger
ResultT = TypeVar("ResultT", bound=BaseModel)
class ServerSession:
"""Per-request proxy for server-to-client requests and notifications.
Built once per inbound request by the kernel's `_make_context`. Holds two
`Outbound` channels: the request-scoped one (the per-request
`DispatchContext`, which on streamable HTTP routes onto the originating
POST's response stream) and the connection's standalone channel
(`connection.outbound`). `related_request_id` on the public methods is the
selector — present means request-scoped, absent means standalone — and
never crosses the `Outbound` Protocol.
"""
def __init__(
self,
request_outbound: DispatchContext[Any],
connection: Connection,
*,
request_meta: types.RequestParamsMeta | None = None,
) -> None:
self._request_outbound = request_outbound
self._connection = connection
# The per-request log-delivery contract, fixed at construction: on
# 2026-07-28+ the inbound request's `_meta` log-level opt-in decides
# which `notifications/message` levels may be sent for this request
# (and they ride this request's stream only); on handshake versions
# every level may be sent (`logging/setLevel`-era semantics).
self._log_is_request_scoped = connection.protocol_version in MODERN_PROTOCOL_VERSIONS
self._allowed_log_levels = allowed_log_levels(connection.protocol_version, request_meta)
@property
def client_params(self) -> types.InitializeRequestParams | None:
"""The client's `initialize` request params; `None` when no client info was supplied."""
return self._connection.client_params
@property
def client_capabilities(self) -> types.ClientCapabilities | None:
"""The capabilities the client declared; `None` when none were declared.
Prefer this over `client_params.capabilities`: on 2026-07-28+ the
request envelope declares capabilities while client info stays
optional, so capabilities can be present without `client_params`.
"""
return self._connection.client_capabilities
@property
def can_send_request(self) -> bool:
"""Whether this request's channel can currently deliver a server-initiated request."""
return self._request_outbound.can_send_request
@property
def protocol_version(self) -> str:
"""The protocol version this connection speaks.
Populated at `Connection` construction and overwritten once the
handshake commits on the loop path; never `None`.
"""
return self._connection.protocol_version
async def send_request(
self,
request: types.ServerRequest,
result_type: type[ResultT],
request_read_timeout_seconds: float | None = None,
metadata: ServerMessageMetadata | None = None,
progress_callback: ProgressFnT | None = None,
) -> ResultT:
"""Send a typed server-to-client request and validate the result.
Raises:
MCPError: The peer responded with an error.
NoBackChannelError: The connection has no back-channel for
server-initiated requests (raised by the held `Outbound`).
pydantic.ValidationError: The peer's result does not match `result_type`.
"""
related = metadata.related_request_id if metadata is not None else None
channel = self._request_outbound if related is not None else self._connection.outbound
data = request.model_dump(by_alias=True, mode="json", exclude_none=True)
opts: CallOptions = {}
if request_read_timeout_seconds is not None:
opts["timeout"] = request_read_timeout_seconds
if progress_callback is not None:
opts["on_progress"] = progress_callback
result = await channel.send_raw_request(data["method"], data.get("params"), opts or None)
try:
_methods.validate_client_result(request.method, self.protocol_version, result)
except KeyError:
pass
return result_type.model_validate(result, by_name=False)
async def send_notification(
self,
notification: types.ServerNotification,
related_request_id: types.RequestId | None = None,
) -> None:
"""Send a typed server-to-client notification."""
await self._notify(notification, request_scoped=related_request_id is not None)
async def _notify(self, notification: types.ServerNotification, *, request_scoped: bool) -> None:
channel = self._request_outbound if request_scoped else self._connection.outbound
data = notification.model_dump(by_alias=True, mode="json", exclude_none=True)
await channel.notify(data["method"], data.get("params"))
def check_client_capability(self, capability: types.ClientCapabilities) -> bool:
"""Check if the client supports a specific capability."""
return self._connection.check_capability(capability)
@deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
async def send_log_message(
self,
level: types.LoggingLevel,
data: Any,
logger: str | None = None,
related_request_id: types.RequestId | None = None,
) -> None:
"""Send a log message notification.
On 2026-07-28+ delivery is a per-request opt-in: nothing is sent
unless this request's `_meta` carried the reserved log-level key, and
entries below the requested level are dropped (debug-logged). What is
sent rides this request's stream regardless of `related_request_id` -
the spec forbids `notifications/message` on any stream but the one
carrying the response. Handshake versions send unconditionally on the
channel `related_request_id` selects, as before.
"""
if level not in self._allowed_log_levels:
_logger.debug("dropped notifications/message at %r: not opted in at that level on this request", level)
return
await self._notify(
types.LoggingMessageNotification(
params=types.LoggingMessageNotificationParams(
level=level,
data=data,
logger=logger,
),
),
request_scoped=self._log_is_request_scoped or related_request_id is not None,
)
async def send_resource_updated(self, uri: str | AnyUrl) -> None:
"""Send a resource updated notification."""
await self.send_notification(
types.ResourceUpdatedNotification(
params=types.ResourceUpdatedNotificationParams(uri=str(uri)),
)
)
@overload
@deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
async def create_message(
self,
messages: list[types.SamplingMessage],
*,
max_tokens: int,
system_prompt: str | None = None,
include_context: types.IncludeContext | None = None,
temperature: float | None = None,
stop_sequences: list[str] | None = None,
metadata: dict[str, Any] | None = None,
model_preferences: types.ModelPreferences | None = None,
tools: None = None,
tool_choice: None = None,
related_request_id: types.RequestId | None = None,
) -> types.CreateMessageResult:
"""Overload: Without tools or tool_choice, returns single content."""
...
@overload
@deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
async def create_message(
self,
messages: list[types.SamplingMessage],
*,
max_tokens: int,
system_prompt: str | None = None,
include_context: types.IncludeContext | None = None,
temperature: float | None = None,
stop_sequences: list[str] | None = None,
metadata: dict[str, Any] | None = None,
model_preferences: types.ModelPreferences | None = None,
tools: list[types.Tool],
tool_choice: types.ToolChoice | None = None,
related_request_id: types.RequestId | None = None,
) -> types.CreateMessageResultWithTools:
"""Overload: With tools, returns array-capable content."""
...
@overload
@deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
async def create_message(
self,
messages: list[types.SamplingMessage],
*,
max_tokens: int,
system_prompt: str | None = None,
include_context: types.IncludeContext | None = None,
temperature: float | None = None,
stop_sequences: list[str] | None = None,
metadata: dict[str, Any] | None = None,
model_preferences: types.ModelPreferences | None = None,
tools: list[types.Tool] | None = None,
tool_choice: types.ToolChoice,
related_request_id: types.RequestId | None = None,
) -> types.CreateMessageResultWithTools:
"""Overload: With tool_choice, returns array-capable content."""
...
@deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
async def create_message(
self,
messages: list[types.SamplingMessage],
*,
max_tokens: int,
system_prompt: str | None = None,
include_context: types.IncludeContext | None = None,
temperature: float | None = None,
stop_sequences: list[str] | None = None,
metadata: dict[str, Any] | None = None,
model_preferences: types.ModelPreferences | None = None,
tools: list[types.Tool] | None = None,
tool_choice: types.ToolChoice | None = None,
related_request_id: types.RequestId | None = None,
) -> types.CreateMessageResult | types.CreateMessageResultWithTools:
"""Send a sampling/create_message request.
Args:
messages: The conversation messages to send.
max_tokens: Maximum number of tokens to generate.
system_prompt: Optional system prompt.
include_context: Optional context inclusion setting.
Should only be set to "thisServer" or "allServers"
if the client has sampling.context capability.
temperature: Optional sampling temperature.
stop_sequences: Optional stop sequences.
metadata: Optional metadata to pass through to the LLM provider.
model_preferences: Optional model selection preferences.
tools: Optional list of tools the LLM can use during sampling.
Requires client to have sampling.tools capability.
tool_choice: Optional control over tool usage behavior.
Requires client to have sampling.tools capability.
related_request_id: Optional ID of a related request.
Returns:
The sampling result from the client.
Raises:
MCPError: If tools are provided but client doesn't support them.
ValueError: If tool_use or tool_result message structure is invalid.
NoBackChannelError: The connection has no back-channel for
server-initiated requests.
"""
validate_sampling_tools(self.client_capabilities, tools, tool_choice)
validate_tool_use_result_messages(messages)
request = types.CreateMessageRequest(
params=types.CreateMessageRequestParams(
messages=messages,
system_prompt=system_prompt,
include_context=include_context,
temperature=temperature,
max_tokens=max_tokens,
stop_sequences=stop_sequences,
metadata=metadata,
model_preferences=model_preferences,
tools=tools,
tool_choice=tool_choice,
),
)
metadata_obj = ServerMessageMetadata(related_request_id=related_request_id)
if wants_sampling_tools(tools, tool_choice):
return await self.send_request(
request=request,
result_type=types.CreateMessageResultWithTools,
metadata=metadata_obj,
)
return await self.send_request(
request=request,
result_type=types.CreateMessageResult,
metadata=metadata_obj,
)
@deprecated("The roots capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
async def list_roots(self) -> types.ListRootsResult:
"""Send a roots/list request.
Raises:
NoBackChannelError: The connection has no back-channel for
server-initiated requests.
"""
return await self.send_request(
types.ListRootsRequest(),
types.ListRootsResult,
)
async def elicit(
self,
message: str,
requested_schema: types.ElicitRequestedSchema,
related_request_id: types.RequestId | None = None,
) -> types.ElicitResult:
"""Send a form mode elicitation/create request.
Args:
message: The message to present to the user.
requested_schema: Schema defining the expected response structure.
related_request_id: Optional ID of the request that triggered this elicitation.
Returns:
The client's response.
Note:
This method is deprecated in favor of elicit_form(). It remains for
backward compatibility but new code should use elicit_form().
"""
return await self.elicit_form(message, requested_schema, related_request_id)
async def elicit_form(
self,
message: str,
requested_schema: types.ElicitRequestedSchema,
related_request_id: types.RequestId | None = None,
) -> types.ElicitResult:
"""Send a form mode elicitation/create request.
Args:
message: The message to present to the user.
requested_schema: Schema defining the expected response structure.
related_request_id: Optional ID of the request that triggered this elicitation.
Returns:
The client's response with form data.
Raises:
NoBackChannelError: The connection has no back-channel for
server-initiated requests.
"""
return await self.send_request(
types.ElicitRequest(
params=types.ElicitRequestFormParams(
message=message,
requested_schema=requested_schema,
),
),
types.ElicitResult,
metadata=ServerMessageMetadata(related_request_id=related_request_id),
)
async def elicit_url(
self,
message: str,
url: str,
elicitation_id: str,
related_request_id: types.RequestId | None = None,
) -> types.ElicitResult:
"""Send a URL mode elicitation/create request.
This directs the user to an external URL for out-of-band interactions
like OAuth flows, credential collection, or payment processing.
Args:
message: Human-readable explanation of why the interaction is needed.
url: The URL the user should navigate to.
elicitation_id: Unique identifier for tracking this elicitation.
related_request_id: Optional ID of the request that triggered this elicitation.
Returns:
The client's response indicating acceptance, decline, or cancellation.
Raises:
NoBackChannelError: The connection has no back-channel for
server-initiated requests.
"""
return await self.send_request(
types.ElicitRequest(
params=types.ElicitRequestURLParams(
message=message,
url=url,
elicitation_id=elicitation_id,
),
),
types.ElicitResult,
metadata=ServerMessageMetadata(related_request_id=related_request_id),
)
async def send_ping(self) -> types.EmptyResult:
"""Send a ping request."""
return await self.send_request(
types.PingRequest(),
types.EmptyResult,
)
async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
"""Report progress for the inbound request this session is scoped to.
A no-op when the caller did not request progress. Dispatcher-agnostic:
on JSON-RPC the held `DispatchContext` emits ``notifications/progress``
against the caller's token; on the in-process direct dispatcher it
invokes the caller's callback directly.
"""
await self._request_outbound.progress(progress, total, message)
async def send_progress_notification(
self,
progress_token: str | int,
progress: float,
total: float | None = None,
message: str | None = None,
related_request_id: str | None = None,
) -> None:
"""Send a progress notification."""
await self.send_notification(
types.ProgressNotification(
params=types.ProgressNotificationParams(
progress_token=progress_token,
progress=progress,
total=total,
message=message,
),
),
related_request_id,
)
async def send_resource_list_changed(self) -> None:
"""Send a resource list changed notification."""
await self.send_notification(types.ResourceListChangedNotification())
async def send_tool_list_changed(self) -> None:
"""Send a tool list changed notification."""
await self.send_notification(types.ToolListChangedNotification())
async def send_prompt_list_changed(self) -> None:
"""Send a prompt list changed notification."""
await self.send_notification(types.PromptListChangedNotification())
async def send_elicit_complete(
self,
elicitation_id: str,
related_request_id: types.RequestId | None = None,
) -> None:
"""Send an elicitation completion notification.
This should be sent when a URL mode elicitation has been completed
out-of-band to inform the client that it may retry any requests
that were waiting for this elicitation.
Args:
elicitation_id: The unique identifier of the completed elicitation
related_request_id: Optional ID of the request that triggered this notification
"""
await self.send_notification(
types.ElicitCompleteNotification(
params=types.ElicitCompleteNotificationParams(elicitation_id=elicitation_id)
),
related_request_id,
)