mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-26 23:44:17 +02:00
Add automatic MCP list change notifications and client message handling
Implements comprehensive notification system for tools, resources, and prompts with automatic client updates and flexible message handlers. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c8da2432c3
commit
38036b1f42
22 changed files with 1013 additions and 58 deletions
|
|
@ -15,10 +15,10 @@ from pydantic import AnyUrl
|
|||
import fastmcp
|
||||
from fastmcp.client.logging import (
|
||||
LogHandler,
|
||||
MessageHandler,
|
||||
create_log_callback,
|
||||
default_log_handler,
|
||||
)
|
||||
from fastmcp.client.messages import MessageHandler, MessageHandlerFnT
|
||||
from fastmcp.client.progress import ProgressHandler, default_progress_handler
|
||||
from fastmcp.client.roots import (
|
||||
RootsHandler,
|
||||
|
|
@ -143,7 +143,7 @@ class Client(Generic[ClientTransportT]):
|
|||
roots: RootsList | RootsHandler | None = None,
|
||||
sampling_handler: SamplingHandler | None = None,
|
||||
log_handler: LogHandler | None = None,
|
||||
message_handler: MessageHandler | None = None,
|
||||
message_handler: MessageHandlerFnT | MessageHandler | None = None,
|
||||
progress_handler: ProgressHandler | None = None,
|
||||
timeout: datetime.timedelta | float | int | None = None,
|
||||
init_timeout: datetime.timedelta | float | int | None = None,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from collections.abc import Awaitable, Callable
|
||||
from typing import TypeAlias
|
||||
|
||||
from mcp.client.session import LoggingFnT, MessageHandlerFnT
|
||||
from mcp.client.session import LoggingFnT
|
||||
from mcp.types import LoggingMessageNotificationParams
|
||||
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
|
@ -10,7 +10,6 @@ logger = get_logger(__name__)
|
|||
|
||||
LogMessage: TypeAlias = LoggingMessageNotificationParams
|
||||
LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
|
||||
MessageHandler: TypeAlias = MessageHandlerFnT
|
||||
|
||||
|
||||
async def default_log_handler(message: LogMessage) -> None:
|
||||
|
|
|
|||
122
src/fastmcp/client/messages.py
Normal file
122
src/fastmcp/client/messages.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
from collections.abc import Awaitable, Coroutine
|
||||
from typing import Any, TypeAlias, Union
|
||||
|
||||
import mcp.types
|
||||
from mcp.client.session import MessageHandlerFnT
|
||||
from mcp.shared.session import RequestResponder
|
||||
|
||||
Message: TypeAlias = (
|
||||
RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]
|
||||
| mcp.types.ServerNotification
|
||||
| Exception
|
||||
)
|
||||
|
||||
MessageHandlerFn: TypeAlias = MessageHandlerFnT
|
||||
|
||||
|
||||
class MessageHandler:
|
||||
"""
|
||||
This class is used to handle MCP messages sent to the client. It is used to handle all messages,
|
||||
requests, notifications, and exceptions. Users can override any of the hooks
|
||||
"""
|
||||
|
||||
def __call__(self, message: Message) -> Coroutine[Any, Any, None]:
|
||||
return self.dispatch(message)
|
||||
|
||||
async def dispatch(self, message: Message) -> None:
|
||||
# handle all messages
|
||||
await self.on_message(message)
|
||||
|
||||
match message:
|
||||
# requests
|
||||
case RequestResponder():
|
||||
# handle all requests
|
||||
await self.on_request(message)
|
||||
|
||||
# handle specific requests
|
||||
match message.request.root:
|
||||
case mcp.types.PingRequest():
|
||||
await self.on_ping(message.request.root)
|
||||
case mcp.types.ListRootsRequest():
|
||||
await self.on_list_roots(message.request.root)
|
||||
case mcp.types.CreateMessageRequest():
|
||||
await self.on_create_message(message.request.root)
|
||||
|
||||
# notifications
|
||||
case mcp.types.ServerNotification():
|
||||
# handle all notifications
|
||||
await self.on_notification(message)
|
||||
|
||||
# handle specific notifications
|
||||
match message.root:
|
||||
case mcp.types.CancelledNotification():
|
||||
await self.on_cancelled(message.root)
|
||||
case mcp.types.ProgressNotification():
|
||||
await self.on_progress(message.root)
|
||||
case mcp.types.LoggingMessageNotification():
|
||||
await self.on_logging_message(message.root)
|
||||
case mcp.types.ToolListChangedNotification():
|
||||
await self.on_tool_list_changed(message.root)
|
||||
case mcp.types.ResourceListChangedNotification():
|
||||
await self.on_resource_list_changed(message.root)
|
||||
case mcp.types.PromptListChangedNotification():
|
||||
await self.on_prompt_list_changed(message.root)
|
||||
case mcp.types.ResourceUpdatedNotification():
|
||||
await self.on_resource_updated(message.root)
|
||||
|
||||
case Exception():
|
||||
await self.on_exception(message)
|
||||
|
||||
async def on_message(self, message: Message) -> None:
|
||||
pass
|
||||
|
||||
async def on_request(
|
||||
self, message: RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
async def on_ping(self, message: mcp.types.PingRequest) -> None:
|
||||
pass
|
||||
|
||||
async def on_list_roots(self, message: mcp.types.ListRootsRequest) -> None:
|
||||
pass
|
||||
|
||||
async def on_create_message(self, message: mcp.types.CreateMessageRequest) -> None:
|
||||
pass
|
||||
|
||||
async def on_notification(self, message: mcp.types.ServerNotification) -> None:
|
||||
pass
|
||||
|
||||
async def on_exception(self, message: Exception) -> None:
|
||||
pass
|
||||
|
||||
async def on_progress(self, message: mcp.types.ProgressNotification) -> None:
|
||||
pass
|
||||
|
||||
async def on_logging_message(
|
||||
self, message: mcp.types.LoggingMessageNotification
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
async def on_tool_list_changed(
|
||||
self, message: mcp.types.ToolListChangedNotification
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
async def on_resource_list_changed(
|
||||
self, message: mcp.types.ResourceListChangedNotification
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
async def on_prompt_list_changed(
|
||||
self, message: mcp.types.PromptListChangedNotification
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
async def on_resource_updated(
|
||||
self, message: mcp.types.ResourceUpdatedNotification
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
async def on_cancelled(self, message: mcp.types.CancelledNotification) -> None:
|
||||
pass
|
||||
|
|
@ -24,6 +24,7 @@ from typing_extensions import Unpack
|
|||
import fastmcp
|
||||
from fastmcp.client.auth.bearer import BearerAuth
|
||||
from fastmcp.client.auth.oauth import OAuth
|
||||
from fastmcp.client.messages import MessageHandler
|
||||
from fastmcp.server.dependencies import get_http_headers
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
|
@ -56,7 +57,7 @@ class SessionKwargs(TypedDict, total=False):
|
|||
sampling_callback: SamplingFnT | None
|
||||
list_roots_callback: ListRootsFnT | None
|
||||
logging_callback: LoggingFnT | None
|
||||
message_handler: MessageHandlerFnT | None
|
||||
message_handler: MessageHandlerFnT | MessageHandler | None
|
||||
client_info: mcp.types.Implementation | None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,22 @@ class Prompt(FastMCPComponent, ABC):
|
|||
default=None, description="Arguments that can be passed to the prompt"
|
||||
)
|
||||
|
||||
def enable(self) -> None:
|
||||
super().enable()
|
||||
try:
|
||||
context = get_context()
|
||||
context._queue_prompt_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def disable(self) -> None:
|
||||
super().disable()
|
||||
try:
|
||||
context = get_context()
|
||||
context._queue_prompt_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
|
||||
"""Convert the prompt to an MCP prompt."""
|
||||
arguments = [
|
||||
|
|
|
|||
|
|
@ -44,6 +44,22 @@ class Resource(FastMCPComponent, abc.ABC):
|
|||
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
||||
)
|
||||
|
||||
def enable(self) -> None:
|
||||
super().enable()
|
||||
try:
|
||||
context = get_context()
|
||||
context._queue_resource_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def disable(self) -> None:
|
||||
super().disable()
|
||||
try:
|
||||
context = get_context()
|
||||
context._queue_resource_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
@staticmethod
|
||||
def from_function(
|
||||
fn: Callable[[], Any],
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from pydantic import (
|
|||
validate_call,
|
||||
)
|
||||
|
||||
from fastmcp.resources.types import Resource
|
||||
from fastmcp.resources.resource import Resource
|
||||
from fastmcp.server.dependencies import get_context
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.json_schema import compress_schema
|
||||
|
|
@ -65,6 +65,22 @@ class ResourceTemplate(FastMCPComponent):
|
|||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(uri_template={self.uri_template!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
|
||||
|
||||
def enable(self) -> None:
|
||||
super().enable()
|
||||
try:
|
||||
context = get_context()
|
||||
context._queue_resource_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def disable(self) -> None:
|
||||
super().disable()
|
||||
try:
|
||||
context = get_context()
|
||||
context._queue_resource_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
@staticmethod
|
||||
def from_function(
|
||||
fn: Callable[..., Any],
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
from __future__ import annotations as _annotations
|
||||
|
||||
import asyncio
|
||||
import warnings
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar, Token
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mcp import LoggingLevel
|
||||
from mcp import LoggingLevel, ServerSession
|
||||
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
||||
from mcp.server.lowlevel.server import request_ctx
|
||||
from mcp.shared.context import RequestContext
|
||||
|
|
@ -30,6 +31,7 @@ from fastmcp.utilities.types import MCPContent
|
|||
logger = get_logger(__name__)
|
||||
|
||||
_current_context: ContextVar[Context | None] = ContextVar("context", default=None)
|
||||
_flush_lock = asyncio.Lock()
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -80,16 +82,20 @@ class Context:
|
|||
def __init__(self, fastmcp: FastMCP):
|
||||
self.fastmcp = fastmcp
|
||||
self._tokens: list[Token] = []
|
||||
self._notification_queue: set[str] = set() # Dedupe notifications
|
||||
|
||||
def __enter__(self) -> Context:
|
||||
async def __aenter__(self) -> Context:
|
||||
"""Enter the context manager and set this context as the current context."""
|
||||
# Always set this context and save the token
|
||||
token = _current_context.set(self)
|
||||
self._tokens.append(token)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
"""Exit the context manager and reset the most recent token."""
|
||||
# Flush any remaining notifications before exiting
|
||||
await self._flush_notifications()
|
||||
|
||||
if self._tokens:
|
||||
token = self._tokens.pop()
|
||||
_current_context.reset(token)
|
||||
|
|
@ -124,7 +130,7 @@ class Context:
|
|||
if progress_token is None:
|
||||
return
|
||||
|
||||
await self.request_context.session.send_progress_notification(
|
||||
await self.session.send_progress_notification(
|
||||
progress_token=progress_token,
|
||||
progress=progress,
|
||||
total=total,
|
||||
|
|
@ -160,7 +166,7 @@ class Context:
|
|||
"""
|
||||
if level is None:
|
||||
level = "info"
|
||||
await self.request_context.session.send_log_message(
|
||||
await self.session.send_log_message(
|
||||
level=level, data=message, logger=logger_name
|
||||
)
|
||||
|
||||
|
|
@ -210,7 +216,7 @@ class Context:
|
|||
return None
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
def session(self) -> ServerSession:
|
||||
"""Access to the underlying session for advanced usage."""
|
||||
return self.request_context.session
|
||||
|
||||
|
|
@ -233,9 +239,21 @@ class Context:
|
|||
|
||||
async def list_roots(self) -> list[Root]:
|
||||
"""List the roots available to the server, as indicated by the client."""
|
||||
result = await self.request_context.session.list_roots()
|
||||
result = await self.session.list_roots()
|
||||
return result.roots
|
||||
|
||||
async def send_tool_list_changed(self) -> None:
|
||||
"""Send a tool list changed notification to the client."""
|
||||
await self.session.send_tool_list_changed()
|
||||
|
||||
async def send_resource_list_changed(self) -> None:
|
||||
"""Send a resource list changed notification to the client."""
|
||||
await self.session.send_resource_list_changed()
|
||||
|
||||
async def send_prompt_list_changed(self) -> None:
|
||||
"""Send a prompt list changed notification to the client."""
|
||||
await self.session.send_prompt_list_changed()
|
||||
|
||||
async def sample(
|
||||
self,
|
||||
messages: str | list[str | SamplingMessage],
|
||||
|
|
@ -269,7 +287,7 @@ class Context:
|
|||
for m in messages
|
||||
]
|
||||
|
||||
result: CreateMessageResult = await self.request_context.session.create_message(
|
||||
result: CreateMessageResult = await self.session.create_message(
|
||||
messages=sampling_messages,
|
||||
system_prompt=system_prompt,
|
||||
temperature=temperature,
|
||||
|
|
@ -294,6 +312,52 @@ class Context:
|
|||
|
||||
return fastmcp.server.dependencies.get_http_request()
|
||||
|
||||
def _queue_tool_list_changed(self) -> None:
|
||||
"""Queue a tool list changed notification."""
|
||||
self._notification_queue.add("notifications/tools/list_changed")
|
||||
self._try_flush_notifications()
|
||||
|
||||
def _queue_resource_list_changed(self) -> None:
|
||||
"""Queue a resource list changed notification."""
|
||||
self._notification_queue.add("notifications/resources/list_changed")
|
||||
self._try_flush_notifications()
|
||||
|
||||
def _queue_prompt_list_changed(self) -> None:
|
||||
"""Queue a prompt list changed notification."""
|
||||
self._notification_queue.add("notifications/prompts/list_changed")
|
||||
self._try_flush_notifications()
|
||||
|
||||
def _try_flush_notifications(self) -> None:
|
||||
"""Synchronous method that attempts to flush notifications if we're in an async context."""
|
||||
try:
|
||||
# Check if we're in an async context
|
||||
loop = asyncio.get_running_loop()
|
||||
if loop and not loop.is_running():
|
||||
return
|
||||
# Schedule flush as a task (fire-and-forget)
|
||||
asyncio.create_task(self._flush_notifications())
|
||||
except RuntimeError:
|
||||
# No event loop - will flush later
|
||||
pass
|
||||
|
||||
async def _flush_notifications(self) -> None:
|
||||
"""Send all queued notifications."""
|
||||
async with _flush_lock:
|
||||
if not self._notification_queue:
|
||||
return
|
||||
|
||||
try:
|
||||
if "notifications/tools/list_changed" in self._notification_queue:
|
||||
await self.session.send_tool_list_changed()
|
||||
if "notifications/resources/list_changed" in self._notification_queue:
|
||||
await self.session.send_resource_list_changed()
|
||||
if "notifications/prompts/list_changed" in self._notification_queue:
|
||||
await self.session.send_prompt_list_changed()
|
||||
self._notification_queue.clear()
|
||||
except Exception:
|
||||
# Don't let notification failures break the request
|
||||
pass
|
||||
|
||||
def _parse_model_preferences(
|
||||
self, model_preferences: ModelPreferences | str | list[str] | None
|
||||
) -> ModelPreferences | None:
|
||||
|
|
|
|||
35
src/fastmcp/server/low_level.py
Normal file
35
src/fastmcp/server/low_level.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from typing import Any
|
||||
|
||||
from mcp.server.lowlevel.server import (
|
||||
LifespanResultT,
|
||||
NotificationOptions,
|
||||
RequestT,
|
||||
Server,
|
||||
)
|
||||
from mcp.server.models import InitializationOptions
|
||||
|
||||
|
||||
class LowLevelServer(Server[LifespanResultT, RequestT]):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# FastMCP servers support notifications for all components
|
||||
self.notification_options = NotificationOptions(
|
||||
prompts_changed=True,
|
||||
resources_changed=True,
|
||||
tools_changed=True,
|
||||
)
|
||||
|
||||
def create_initialization_options(
|
||||
self,
|
||||
notification_options: NotificationOptions | None = None,
|
||||
experimental_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> InitializationOptions:
|
||||
# ensure we use the FastMCP notification options
|
||||
if notification_options is None:
|
||||
notification_options = self.notification_options
|
||||
return super().create_initialization_options(
|
||||
notification_options=notification_options,
|
||||
experimental_capabilities=experimental_capabilities,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -23,7 +23,6 @@ import mcp.types
|
|||
import uvicorn
|
||||
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
||||
from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
||||
from mcp.server.lowlevel.server import Server as MCPServer
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import (
|
||||
AnyFunction,
|
||||
|
|
@ -54,6 +53,7 @@ from fastmcp.server.http import (
|
|||
create_sse_app,
|
||||
create_streamable_http_app,
|
||||
)
|
||||
from fastmcp.server.low_level import LowLevelServer
|
||||
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
||||
from fastmcp.settings import Settings
|
||||
from fastmcp.tools import ToolManager
|
||||
|
|
@ -99,10 +99,12 @@ def _lifespan_wrapper(
|
|||
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
||||
],
|
||||
) -> Callable[
|
||||
[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
||||
[LowLevelServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
||||
]:
|
||||
@asynccontextmanager
|
||||
async def wrap(s: MCPServer[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
|
||||
async def wrap(
|
||||
s: LowLevelServer[LifespanResultT],
|
||||
) -> AsyncIterator[LifespanResultT]:
|
||||
async with AsyncExitStack() as stack:
|
||||
context = await stack.enter_async_context(lifespan(app))
|
||||
yield context
|
||||
|
|
@ -179,7 +181,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
lifespan = default_lifespan
|
||||
else:
|
||||
self._has_lifespan = True
|
||||
self._mcp_server = MCPServer[LifespanResultT](
|
||||
self._mcp_server = LowLevelServer[LifespanResultT](
|
||||
name=name or "FastMCP",
|
||||
version=version,
|
||||
instructions=instructions,
|
||||
|
|
@ -427,7 +429,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
async def _mcp_list_tools(self) -> list[MCPTool]:
|
||||
logger.debug("Handler called: list_tools")
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self):
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
tools = await self._list_tools()
|
||||
return [tool.to_mcp_tool(name=tool.key) for tool in tools]
|
||||
|
||||
|
|
@ -450,7 +452,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
return mcp_tools
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
# Create the middleware context.
|
||||
mw_context = MiddlewareContext(
|
||||
message=mcp.types.ListToolsRequest(method="tools/list"),
|
||||
|
|
@ -466,7 +468,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
async def _mcp_list_resources(self) -> list[MCPResource]:
|
||||
logger.debug("Handler called: list_resources")
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self):
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
resources = await self._list_resources()
|
||||
return [
|
||||
resource.to_mcp_resource(uri=resource.key) for resource in resources
|
||||
|
|
@ -491,7 +493,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
return mcp_resources
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
# Create the middleware context.
|
||||
mw_context = MiddlewareContext(
|
||||
message={}, # List resources doesn't have parameters
|
||||
|
|
@ -507,7 +509,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
||||
logger.debug("Handler called: list_resource_templates")
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self):
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
templates = await self._list_resource_templates()
|
||||
return [
|
||||
template.to_mcp_template(uriTemplate=template.key)
|
||||
|
|
@ -533,7 +535,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
return mcp_templates
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
# Create the middleware context.
|
||||
mw_context = MiddlewareContext(
|
||||
message={}, # List resource templates doesn't have parameters
|
||||
|
|
@ -549,7 +551,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
||||
logger.debug("Handler called: list_prompts")
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self):
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
prompts = await self._list_prompts()
|
||||
return [prompt.to_mcp_prompt(name=prompt.key) for prompt in prompts]
|
||||
|
||||
|
|
@ -572,7 +574,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
return mcp_prompts
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
# Create the middleware context.
|
||||
mw_context = MiddlewareContext(
|
||||
message=mcp.types.ListPromptsRequest(method="prompts/list"),
|
||||
|
|
@ -602,7 +604,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""
|
||||
logger.debug("Handler called: call_tool %s with %s", key, arguments)
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self):
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
try:
|
||||
return await self._call_tool(key, arguments)
|
||||
except DisabledError:
|
||||
|
|
@ -643,7 +645,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""
|
||||
logger.debug("Handler called: read_resource %s", uri)
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self):
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
try:
|
||||
return await self._read_resource(uri)
|
||||
except DisabledError:
|
||||
|
|
@ -698,7 +700,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""
|
||||
logger.debug("Handler called: get_prompt %s with %s", name, arguments)
|
||||
|
||||
with fastmcp.server.context.Context(fastmcp=self):
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
try:
|
||||
return await self._get_prompt(name, arguments)
|
||||
except DisabledError:
|
||||
|
|
@ -747,6 +749,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._tool_manager.add_tool(tool)
|
||||
self._cache.clear()
|
||||
|
||||
# Send notification if we're in a request context
|
||||
try:
|
||||
from fastmcp.server.dependencies import get_context
|
||||
|
||||
context = get_context()
|
||||
context._queue_tool_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def remove_tool(self, name: str) -> None:
|
||||
"""Remove a tool from the server.
|
||||
|
||||
|
|
@ -759,6 +770,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._tool_manager.remove_tool(name)
|
||||
self._cache.clear()
|
||||
|
||||
# Send notification if we're in a request context
|
||||
try:
|
||||
from fastmcp.server.dependencies import get_context
|
||||
|
||||
context = get_context()
|
||||
context._queue_tool_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
@overload
|
||||
def tool(
|
||||
self,
|
||||
|
|
@ -911,6 +931,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._resource_manager.add_resource(resource)
|
||||
self._cache.clear()
|
||||
|
||||
# Send notification if we're in a request context
|
||||
try:
|
||||
from fastmcp.server.dependencies import get_context
|
||||
|
||||
context = get_context()
|
||||
context._queue_resource_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def add_template(self, template: ResourceTemplate) -> None:
|
||||
"""Add a resource template to the server.
|
||||
|
||||
|
|
@ -919,6 +948,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
"""
|
||||
self._resource_manager.add_template(template)
|
||||
|
||||
# Send notification if we're in a request context
|
||||
try:
|
||||
from fastmcp.server.dependencies import get_context
|
||||
|
||||
context = get_context()
|
||||
context._queue_resource_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def add_resource_fn(
|
||||
self,
|
||||
fn: AnyFunction,
|
||||
|
|
@ -1087,6 +1125,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._prompt_manager.add_prompt(prompt)
|
||||
self._cache.clear()
|
||||
|
||||
# Send notification if we're in a request context
|
||||
try:
|
||||
from fastmcp.server.dependencies import get_context
|
||||
|
||||
context = get_context()
|
||||
context._queue_prompt_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
@overload
|
||||
def prompt(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,22 @@ class Tool(FastMCPComponent):
|
|||
default=None, description="Optional custom serializer for tool results"
|
||||
)
|
||||
|
||||
def enable(self) -> None:
|
||||
super().enable()
|
||||
try:
|
||||
context = get_context()
|
||||
context._queue_tool_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def disable(self) -> None:
|
||||
super().disable()
|
||||
try:
|
||||
context = get_context()
|
||||
context._queue_tool_list_changed() # type: ignore[private-use]
|
||||
except RuntimeError:
|
||||
pass # No context available
|
||||
|
||||
def to_mcp_tool(self, **overrides: Any) -> MCPTool:
|
||||
kwargs = {
|
||||
"name": self.name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue