Serve subscriptions/listen with a pluggable event bus (SEP-2575) (#3035)
This commit is contained in:
parent
48ef569f7e
commit
ca10dade2c
1 changed files with 1722 additions and 66 deletions
|
|
@ -88,6 +88,7 @@ from mcp.server.sse import SseServerTransport
|
|||
from mcp.server.stdio import stdio_server
|
||||
from mcp.server.streamable_http import EventStore
|
||||
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
||||
from mcp.server.subscriptions import InMemorySubscriptionBus, ListenHandler, SubscriptionBus
|
||||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.shared.uri_template import UriTemplate
|
||||
|
|
@ -182,6 +183,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
resource_security: ResourceSecurity = DEFAULT_RESOURCE_SECURITY,
|
||||
request_state_security: RequestStateSecurity | None = None,
|
||||
cache_hints: Mapping[CacheableMethod, CacheHint] | None = None,
|
||||
subscriptions: SubscriptionBus | None = None,
|
||||
):
|
||||
self._resource_security = resource_security
|
||||
self.settings = Settings(
|
||||
|
|
@ -201,6 +203,10 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
resources=resources, warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
|
||||
)
|
||||
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
|
||||
# The subscriptions/listen fan-out seam (2026-07-28). The default bus is
|
||||
# in-process; pass an `SubscriptionBus` implementation over an external pub/sub
|
||||
# backend to fan events out across replicas.
|
||||
self._subscriptions: SubscriptionBus = subscriptions if subscriptions is not None else InMemorySubscriptionBus()
|
||||
self._lowlevel_server = Server(
|
||||
name=name or "mcp-server",
|
||||
title=title,
|
||||
|
|
@ -217,6 +223,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
on_list_resource_templates=self._handle_list_resource_templates,
|
||||
on_list_prompts=self._handle_list_prompts,
|
||||
on_get_prompt=self._handle_get_prompt,
|
||||
on_subscriptions_listen=ListenHandler(self._subscriptions),
|
||||
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an MCPServer and Server.
|
||||
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
|
||||
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
|
||||
|
|
@ -396,7 +403,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
async def _handle_call_tool(
|
||||
self, ctx: ServerRequestContext[LifespanResultT], params: CallToolRequestParams
|
||||
) -> CallToolResult | InputRequiredResult:
|
||||
context = Context(request_context=ctx, mcp_server=self, input_params=params)
|
||||
context = Context(request_context=ctx, mcp_server=self, input_params=params, subscriptions=self._subscriptions)
|
||||
try:
|
||||
return await self.call_tool(params.name, params.arguments or {}, context)
|
||||
except MCPError:
|
||||
|
|
@ -412,7 +419,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
async def _handle_read_resource(
|
||||
self, ctx: ServerRequestContext[LifespanResultT], params: ReadResourceRequestParams
|
||||
) -> ReadResourceResult | InputRequiredResult:
|
||||
context = Context(request_context=ctx, mcp_server=self, input_params=params)
|
||||
context = Context(request_context=ctx, mcp_server=self, input_params=params, subscriptions=self._subscriptions)
|
||||
try:
|
||||
results = await self.read_resource(params.uri, context)
|
||||
except ResourceNotFoundError as err:
|
||||
|
|
@ -456,7 +463,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
async def _handle_get_prompt(
|
||||
self, ctx: ServerRequestContext[LifespanResultT], params: GetPromptRequestParams
|
||||
) -> GetPromptResult | InputRequiredResult:
|
||||
context = Context(request_context=ctx, mcp_server=self, input_params=params)
|
||||
context = Context(request_context=ctx, mcp_server=self, input_params=params, subscriptions=self._subscriptions)
|
||||
return await self.get_prompt(params.name, params.arguments, context)
|
||||
|
||||
async def list_tools(self) -> list[MCPTool]:
|
||||
|
|
@ -481,7 +488,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
) -> CallToolResult | InputRequiredResult:
|
||||
"""Call a tool by name with arguments."""
|
||||
if context is None:
|
||||
context = Context(mcp_server=self)
|
||||
context = Context(mcp_server=self, subscriptions=self._subscriptions)
|
||||
return await self._tool_manager.call_tool(name, arguments, context, convert_result=True)
|
||||
|
||||
async def list_resources(self) -> list[MCPResource]:
|
||||
|
|
@ -533,7 +540,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
ResourceError: If template creation or resource reading fails.
|
||||
"""
|
||||
if context is None:
|
||||
context = Context(mcp_server=self)
|
||||
context = Context(mcp_server=self, subscriptions=self._subscriptions)
|
||||
resource = await self._resource_manager.get_resource(uri, context)
|
||||
if isinstance(resource, InputRequiredResult):
|
||||
return resource
|
||||
|
|
@ -879,6 +886,17 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
"""
|
||||
self._prompt_manager.add_prompt(prompt)
|
||||
|
||||
def remove_prompt(self, name: str) -> None:
|
||||
"""Remove a prompt from the server by name.
|
||||
|
||||
Args:
|
||||
name: The name of the prompt to remove
|
||||
|
||||
Raises:
|
||||
ValueError: If the prompt does not exist
|
||||
"""
|
||||
self._prompt_manager.remove_prompt(name)
|
||||
|
||||
def prompt(
|
||||
self,
|
||||
name: str | None = None,
|
||||
|
|
@ -1242,7 +1260,7 @@ class MCPServer(Generic[LifespanResultT]):
|
|||
carrying the echoed opaque state.
|
||||
"""
|
||||
if context is None:
|
||||
context = Context(mcp_server=self)
|
||||
context = Context(mcp_server=self, subscriptions=self._subscriptions)
|
||||
try:
|
||||
prompt = self._prompt_manager.get_prompt(name)
|
||||
if not prompt:
|
||||
|
|
|
|||
Loading…
Reference in a new issue