3030from mcp .server .fastmcp .prompts import Prompt , PromptManager
3131from mcp .server .fastmcp .resources import FunctionResource , Resource , ResourceManager
3232from mcp .server .fastmcp .tools import Tool , ToolManager
33+ from mcp .server .fastmcp .tools .base import InvocationMode
3334from mcp .server .fastmcp .utilities .context_injection import find_context_parameter
3435from mcp .server .fastmcp .utilities .logging import configure_logging , get_logger
3536from mcp .server .lowlevel .helper_types import ReadResourceContents
4344from mcp .server .streamable_http_manager import StreamableHTTPSessionManager
4445from mcp .server .transport_security import TransportSecuritySettings
4546from mcp .shared .context import LifespanContextT , RequestContext , RequestT
46- from mcp .types import AnyFunction , ContentBlock , GetPromptResult , ToolAnnotations
47+ from mcp .types import NEXT_PROTOCOL_VERSION , AnyFunction , ContentBlock , GetPromptResult , ToolAnnotations
4748from mcp .types import Prompt as MCPPrompt
4849from mcp .types import PromptArgument as MCPPromptArgument
4950from mcp .types import Resource as MCPResource
@@ -266,9 +267,39 @@ def _setup_handlers(self) -> None:
266267 self ._mcp_server .get_prompt ()(self .get_prompt )
267268 self ._mcp_server .list_resource_templates ()(self .list_resource_templates )
268269
270+ def _client_supports_async (self ) -> bool :
271+ """Check if the current client supports async tools based on protocol version."""
272+ try :
273+ context = self .get_context ()
274+ if context .request_context and context .request_context .session .client_params :
275+ client_version = str (context .request_context .session .client_params .protocolVersion )
276+ # Only "next" version supports async tools for now
277+ return client_version == NEXT_PROTOCOL_VERSION
278+ except ValueError :
279+ # Context not available (outside of request), assume no async support
280+ pass
281+ return False
282+
283+ def _get_invocation_mode (self , info : Tool , client_supports_async : bool ) -> Literal ["sync" , "async" ] | None :
284+ """Determine invocationMode field based on client support."""
285+ if not client_supports_async :
286+ return None # Old clients don't see invocationMode field
287+
288+ # New clients see the invocationMode field
289+ if "async" in info .invocation_modes and len (info .invocation_modes ) == 1 :
290+ return "async" # Async-only
291+ elif len (info .invocation_modes ) > 1 or info .invocation_modes == ["sync" ]:
292+ return "sync" # Hybrid or explicit sync
293+ return None
294+
269295 async def list_tools (self ) -> list [MCPTool ]:
270296 """List all available tools."""
271297 tools = self ._tool_manager .list_tools ()
298+
299+ # Check if client supports async tools based on protocol version
300+ client_supports_async = self ._client_supports_async ()
301+
302+ # Filter out async-only tools for old clients and set invocationMode based on client support
272303 return [
273304 MCPTool (
274305 name = info .name ,
@@ -277,8 +308,10 @@ async def list_tools(self) -> list[MCPTool]:
277308 inputSchema = info .parameters ,
278309 outputSchema = info .output_schema ,
279310 annotations = info .annotations ,
311+ invocationMode = self ._get_invocation_mode (info , client_supports_async ),
280312 )
281313 for info in tools
314+ if client_supports_async or info .invocation_modes != ["async" ]
282315 ]
283316
284317 def get_context (self ) -> Context [ServerSession , LifespanResultT , Request ]:
@@ -348,6 +381,7 @@ def add_tool(
348381 description : str | None = None ,
349382 annotations : ToolAnnotations | None = None ,
350383 structured_output : bool | None = None ,
384+ invocation_modes : list [InvocationMode ] | None = None ,
351385 ) -> None :
352386 """Add a tool to the server.
353387
@@ -364,6 +398,8 @@ def add_tool(
364398 - If None, auto-detects based on the function's return type annotation
365399 - If True, unconditionally creates a structured tool (return type annotation permitting)
366400 - If False, unconditionally creates an unstructured tool
401+ invocation_modes: List of supported invocation modes (e.g., ["sync", "async"])
402+ - If None, defaults to ["sync"] for backwards compatibility
367403 """
368404 self ._tool_manager .add_tool (
369405 fn ,
@@ -372,6 +408,7 @@ def add_tool(
372408 description = description ,
373409 annotations = annotations ,
374410 structured_output = structured_output ,
411+ invocation_modes = invocation_modes ,
375412 )
376413
377414 def tool (
@@ -381,6 +418,7 @@ def tool(
381418 description : str | None = None ,
382419 annotations : ToolAnnotations | None = None ,
383420 structured_output : bool | None = None ,
421+ invocation_modes : list [InvocationMode ] | None = None ,
384422 ) -> Callable [[AnyFunction ], AnyFunction ]:
385423 """Decorator to register a tool.
386424
@@ -397,6 +435,10 @@ def tool(
397435 - If None, auto-detects based on the function's return type annotation
398436 - If True, unconditionally creates a structured tool (return type annotation permitting)
399437 - If False, unconditionally creates an unstructured tool
438+ invocation_modes: List of supported invocation modes (e.g., ["sync", "async"])
439+ - If None, defaults to ["sync"] for backwards compatibility
440+ - Supports "sync" for synchronous execution and "async" for asynchronous execution
441+ - Tools with "async" mode will be hidden from clients that don't support async execution
400442
401443 Example:
402444 @server.tool()
@@ -412,6 +454,17 @@ def tool_with_context(x: int, ctx: Context) -> str:
412454 async def async_tool(x: int, context: Context) -> str:
413455 await context.report_progress(50, 100)
414456 return str(x)
457+
458+ @server.tool(invocation_modes=["async"])
459+ async def async_only_tool(data: str, ctx: Context) -> str:
460+ # This tool only supports async execution
461+ await ctx.info("Starting long-running analysis...")
462+ return await analyze_data(data)
463+
464+ @server.tool(invocation_modes=["sync", "async"])
465+ def hybrid_tool(x: int) -> str:
466+ # This tool supports both sync and async execution
467+ return str(x)
415468 """
416469 # Check if user passed function directly instead of calling decorator
417470 if callable (name ):
@@ -427,6 +480,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
427480 description = description ,
428481 annotations = annotations ,
429482 structured_output = structured_output ,
483+ invocation_modes = invocation_modes ,
430484 )
431485 return fn
432486
0 commit comments