|
22 | 22 | import uuid |
23 | 23 | from concurrent.futures import ThreadPoolExecutor |
24 | 24 | from contextlib import AsyncExitStack |
25 | | -from typing import Any, AsyncContextManager, Callable, cast, Dict, Optional, Type |
| 25 | +from typing import ( |
| 26 | + Any, |
| 27 | + AsyncContextManager, |
| 28 | + Awaitable, |
| 29 | + Callable, |
| 30 | + cast, |
| 31 | + Dict, |
| 32 | + Optional, |
| 33 | + Type, |
| 34 | + TypeVar, |
| 35 | +) |
26 | 36 |
|
27 | 37 | _MISSING = object() |
| 38 | +_MCPResult = TypeVar("_MCPResult") |
28 | 39 |
|
29 | 40 | from fastapi import ( |
30 | 41 | Body, |
@@ -542,6 +553,25 @@ async def _run_in_session_executor( |
542 | 553 | loop = asyncio.get_event_loop() |
543 | 554 | return await loop.run_in_executor(executor, lambda: func(*args, **kwargs)) |
544 | 555 |
|
| 556 | + async def _run_mcp_client_operation( |
| 557 | + self, |
| 558 | + mcp_client: Any, |
| 559 | + mcp_session_factory: Any, |
| 560 | + managed_session_id: Optional[str], |
| 561 | + operation: Callable[[], Awaitable[_MCPResult]], |
| 562 | + ) -> _MCPResult: |
| 563 | + """Run an MCP client operation with the appropriate transport lifecycle.""" |
| 564 | + if managed_session_id and mcp_client.is_connected(): |
| 565 | + return await operation() |
| 566 | + |
| 567 | + if callable(mcp_session_factory): |
| 568 | + mcp_session_cm = cast(AsyncContextManager[Any], mcp_session_factory()) |
| 569 | + async with mcp_session_cm: |
| 570 | + return await operation() |
| 571 | + |
| 572 | + async with mcp_client: |
| 573 | + return await operation() |
| 574 | + |
545 | 575 | @property |
546 | 576 | def active_sessions(self) -> int: |
547 | 577 | """Return the number of active WebSocket sessions.""" |
@@ -835,21 +865,12 @@ async def mcp_handler( |
835 | 865 | ) |
836 | 866 |
|
837 | 867 | if mcp_client: |
838 | | - if managed_session_id and mcp_client.is_connected(): |
839 | | - # Session-managed with live transport — call |
840 | | - # directly, no redundant re-entry. |
841 | | - tools = await mcp_client.list_tools() |
842 | | - elif callable(mcp_session_factory): |
843 | | - # Stateless request, or session-managed but the |
844 | | - # background transport was lost: (re-)open. |
845 | | - mcp_session_cm = cast( |
846 | | - AsyncContextManager[Any], mcp_session_factory() |
847 | | - ) |
848 | | - async with mcp_session_cm: |
849 | | - tools = await mcp_client.list_tools() |
850 | | - else: |
851 | | - async with mcp_client: |
852 | | - tools = await mcp_client.list_tools() |
| 868 | + tools = await self._run_mcp_client_operation( |
| 869 | + mcp_client, |
| 870 | + mcp_session_factory, |
| 871 | + managed_session_id, |
| 872 | + mcp_client.list_tools, |
| 873 | + ) |
853 | 874 |
|
854 | 875 | return JsonRpcResponse.success( |
855 | 876 | result={ |
@@ -903,26 +924,14 @@ async def mcp_handler( |
903 | 924 | ) |
904 | 925 |
|
905 | 926 | if mcp_client: |
906 | | - if managed_session_id and mcp_client.is_connected(): |
907 | | - # Session-managed with live transport. |
908 | | - result = await mcp_client.call_tool( |
| 927 | + result = await self._run_mcp_client_operation( |
| 928 | + mcp_client, |
| 929 | + mcp_session_factory, |
| 930 | + managed_session_id, |
| 931 | + lambda: mcp_client.call_tool( |
909 | 932 | name=tool_name, arguments=arguments |
910 | | - ) |
911 | | - elif callable(mcp_session_factory): |
912 | | - # Stateless request, or session-managed but the |
913 | | - # background transport was lost: (re-)open. |
914 | | - mcp_session_cm = cast( |
915 | | - AsyncContextManager[Any], mcp_session_factory() |
916 | | - ) |
917 | | - async with mcp_session_cm: |
918 | | - result = await mcp_client.call_tool( |
919 | | - name=tool_name, arguments=arguments |
920 | | - ) |
921 | | - else: |
922 | | - async with mcp_client: |
923 | | - result = await mcp_client.call_tool( |
924 | | - name=tool_name, arguments=arguments |
925 | | - ) |
| 933 | + ), |
| 934 | + ) |
926 | 935 | elif mcp_server: |
927 | 936 | server_tools = get_server_tools(mcp_server) |
928 | 937 | if tool_name in server_tools: |
|
0 commit comments