agent-framework-core 1.3.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/PKG-INFO +1 -1
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/__init__.py +2 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_feature_stage.py +2 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_mcp.py +206 -30
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_skills.py +668 -181
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent.py +31 -16
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent_executor.py +2 -2
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_edge_runner.py +34 -6
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_events.py +34 -18
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_functional.py +2 -1
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_runner_context.py +21 -1
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_validation.py +29 -3
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow.py +175 -27
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_builder.py +217 -13
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_context.py +28 -2
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_executor.py +17 -1
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/observability.py +177 -27
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/pyproject.toml +1 -1
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/LICENSE +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/README.md +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_agents.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_clients.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_compaction.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_docstrings.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_evaluation.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_harness/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_memory.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_mode.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_todo.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_middleware.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_serialization.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_sessions.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_settings.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_telemetry.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_tools.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_types.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent_utils.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_checkpoint.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_checkpoint_encoding.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_const.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_conversation_history.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_edge.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_executor.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_function_executor.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_message_utils.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_model_utils.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_request_info_mixin.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_runner.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_state.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_typing_utils.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_viz.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/a2a/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/a2a/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/ag_ui/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/ag_ui/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/amazon/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/amazon/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/anthropic/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/anthropic/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/azure/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/azure/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/chatkit/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/chatkit/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/declarative/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/declarative/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/devui/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/devui/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/exceptions.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/foundry/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/foundry/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/github/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/github/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/google/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/google/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/hyperlight/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/lab/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/mem0/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/mem0/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/microsoft/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/microsoft/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/ollama/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/ollama/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/openai/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/openai/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/orchestrations/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/orchestrations/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/py.typed +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/redis/__init__.py +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/redis/__init__.pyi +0 -0
- {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/security.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-framework-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Microsoft Agent Framework for building AI Agents with Python. This is the core package that has all the core abstractions and implementations.
|
|
5
5
|
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -147,6 +147,7 @@ from ._skills import (
|
|
|
147
147
|
InlineSkillScript,
|
|
148
148
|
InMemorySkillsSource,
|
|
149
149
|
Skill,
|
|
150
|
+
SkillFrontmatter,
|
|
150
151
|
SkillResource,
|
|
151
152
|
SkillScript,
|
|
152
153
|
SkillScriptRunner,
|
|
@@ -432,6 +433,7 @@ __all__ = [
|
|
|
432
433
|
"SessionContext",
|
|
433
434
|
"SingleEdgeGroup",
|
|
434
435
|
"Skill",
|
|
436
|
+
"SkillFrontmatter",
|
|
435
437
|
"SkillResource",
|
|
436
438
|
"SkillScript",
|
|
437
439
|
"SkillScriptRunner",
|
|
@@ -49,6 +49,8 @@ class ExperimentalFeature(str, Enum):
|
|
|
49
49
|
EVALS = "EVALS"
|
|
50
50
|
FILE_HISTORY = "FILE_HISTORY"
|
|
51
51
|
FIDES = "FIDES"
|
|
52
|
+
FOUNDRY_TOOLS = "FOUNDRY_TOOLS"
|
|
53
|
+
FOUNDRY_PREVIEW_TOOLS = "FOUNDRY_PREVIEW_TOOLS"
|
|
52
54
|
FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
|
|
53
55
|
HARNESS = "HARNESS"
|
|
54
56
|
SKILLS = "SKILLS"
|
|
@@ -10,7 +10,7 @@ import logging
|
|
|
10
10
|
import re
|
|
11
11
|
import sys
|
|
12
12
|
from abc import abstractmethod
|
|
13
|
-
from collections.abc import Callable, Collection, Sequence
|
|
13
|
+
from collections.abc import Callable, Collection, Coroutine, Sequence
|
|
14
14
|
from contextlib import AsyncExitStack, _AsyncGeneratorContextManager # type: ignore
|
|
15
15
|
from datetime import timedelta
|
|
16
16
|
from functools import partial
|
|
@@ -255,15 +255,22 @@ class MCPTool:
|
|
|
255
255
|
self._exit_stack = AsyncExitStack()
|
|
256
256
|
self._lifecycle_lock = asyncio.Lock()
|
|
257
257
|
self._lifecycle_request_lock = asyncio.Lock()
|
|
258
|
-
self._lifecycle_queue: asyncio.Queue[tuple[str, bool, asyncio.Future[None]]] | None = None
|
|
258
|
+
self._lifecycle_queue: asyncio.Queue[tuple[str, bool, bool, asyncio.Future[None]]] | None = None
|
|
259
259
|
self._lifecycle_owner_task: asyncio.Task[None] | None = None
|
|
260
260
|
self.session = session
|
|
261
261
|
self.request_timeout = request_timeout
|
|
262
262
|
self.client = client
|
|
263
263
|
self._functions: list[FunctionTool] = []
|
|
264
|
+
self._tool_call_meta_by_name: dict[str, dict[str, Any]] = {}
|
|
264
265
|
self.is_connected: bool = False
|
|
265
266
|
self._tools_loaded: bool = False
|
|
266
267
|
self._prompts_loaded: bool = False
|
|
268
|
+
self._server_capabilities: types.ServerCapabilities | None = None
|
|
269
|
+
self._supports_tools: bool = True
|
|
270
|
+
self._supports_prompts: bool = True
|
|
271
|
+
self._supports_logging: bool | None = None
|
|
272
|
+
self._ping_available: bool = True
|
|
273
|
+
self._pending_reload_tasks: set[asyncio.Task[None]] = set()
|
|
267
274
|
|
|
268
275
|
def __str__(self) -> str:
|
|
269
276
|
return f"MCPTool(name={self.name}, description={self.description})"
|
|
@@ -564,11 +571,11 @@ class MCPTool:
|
|
|
564
571
|
stop_error: BaseException | None = None
|
|
565
572
|
try:
|
|
566
573
|
while True:
|
|
567
|
-
action, reset, future = await queue.get()
|
|
574
|
+
action, reset, load_configured, future = await queue.get()
|
|
568
575
|
|
|
569
576
|
try:
|
|
570
577
|
if action == "connect":
|
|
571
|
-
await self._connect_on_owner(reset=reset)
|
|
578
|
+
await self._connect_on_owner(reset=reset, load_configured=load_configured)
|
|
572
579
|
elif action == "close":
|
|
573
580
|
await self._close_on_owner()
|
|
574
581
|
else:
|
|
@@ -593,7 +600,7 @@ class MCPTool:
|
|
|
593
600
|
finally:
|
|
594
601
|
while True:
|
|
595
602
|
try:
|
|
596
|
-
_, _, future = queue.get_nowait()
|
|
603
|
+
_, _, _, future = queue.get_nowait()
|
|
597
604
|
except asyncio.QueueEmpty:
|
|
598
605
|
break
|
|
599
606
|
if not future.done():
|
|
@@ -606,12 +613,18 @@ class MCPTool:
|
|
|
606
613
|
owner_task = self._lifecycle_owner_task
|
|
607
614
|
return owner_task is not None and asyncio.current_task() is owner_task
|
|
608
615
|
|
|
609
|
-
async def _run_on_lifecycle_owner(
|
|
616
|
+
async def _run_on_lifecycle_owner(
|
|
617
|
+
self,
|
|
618
|
+
action: str,
|
|
619
|
+
*,
|
|
620
|
+
reset: bool = False,
|
|
621
|
+
load_configured: bool = True,
|
|
622
|
+
) -> None:
|
|
610
623
|
await self._ensure_lifecycle_owner()
|
|
611
624
|
|
|
612
625
|
if self._is_lifecycle_owner_task():
|
|
613
626
|
if action == "connect":
|
|
614
|
-
await self._connect_on_owner(reset=reset)
|
|
627
|
+
await self._connect_on_owner(reset=reset, load_configured=load_configured)
|
|
615
628
|
elif action == "close":
|
|
616
629
|
await self._close_on_owner()
|
|
617
630
|
else:
|
|
@@ -623,7 +636,7 @@ class MCPTool:
|
|
|
623
636
|
raise RuntimeError("MCP lifecycle owner is not available.")
|
|
624
637
|
|
|
625
638
|
future = asyncio.get_running_loop().create_future()
|
|
626
|
-
await queue.put((action, reset, future))
|
|
639
|
+
await queue.put((action, reset, load_configured, future))
|
|
627
640
|
await future
|
|
628
641
|
|
|
629
642
|
async def _safe_close_exit_stack(self) -> None:
|
|
@@ -654,6 +667,32 @@ class MCPTool:
|
|
|
654
667
|
await self._safe_close_exit_stack()
|
|
655
668
|
return _should_propagate_cancelled_error(ex)
|
|
656
669
|
|
|
670
|
+
def _reset_session_state(self) -> None:
|
|
671
|
+
self._server_capabilities = None
|
|
672
|
+
self._supports_tools = True
|
|
673
|
+
self._supports_prompts = True
|
|
674
|
+
self._supports_logging = None
|
|
675
|
+
self._ping_available = True
|
|
676
|
+
|
|
677
|
+
def _set_server_capabilities(self, capabilities: types.ServerCapabilities | None) -> None:
|
|
678
|
+
self._server_capabilities = capabilities
|
|
679
|
+
if capabilities is None:
|
|
680
|
+
self._supports_tools = False
|
|
681
|
+
self._supports_prompts = False
|
|
682
|
+
self._supports_logging = False
|
|
683
|
+
return
|
|
684
|
+
|
|
685
|
+
self._supports_tools = getattr(capabilities, "tools", None) is not None
|
|
686
|
+
self._supports_prompts = getattr(capabilities, "prompts", None) is not None
|
|
687
|
+
self._supports_logging = getattr(capabilities, "logging", None) is not None
|
|
688
|
+
|
|
689
|
+
async def _reconnect_without_loading(self) -> None:
|
|
690
|
+
if self._is_lifecycle_owner_task():
|
|
691
|
+
await self._connect_on_owner(reset=True, load_configured=False)
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
await self._run_on_lifecycle_owner("connect", reset=True, load_configured=False)
|
|
695
|
+
|
|
657
696
|
async def connect(self, *, reset: bool = False) -> None:
|
|
658
697
|
if self._is_lifecycle_owner_task():
|
|
659
698
|
await self._connect_on_owner(reset=reset)
|
|
@@ -662,7 +701,7 @@ class MCPTool:
|
|
|
662
701
|
async with self._lifecycle_request_lock:
|
|
663
702
|
await self._run_on_lifecycle_owner("connect", reset=reset)
|
|
664
703
|
|
|
665
|
-
async def _connect_on_owner(self, *, reset: bool = False) -> None:
|
|
704
|
+
async def _connect_on_owner(self, *, reset: bool = False, load_configured: bool = True) -> None:
|
|
666
705
|
"""Connect to the MCP server.
|
|
667
706
|
|
|
668
707
|
Establishes a connection to the MCP server, initializes the session,
|
|
@@ -670,6 +709,7 @@ class MCPTool:
|
|
|
670
709
|
|
|
671
710
|
Keyword Args:
|
|
672
711
|
reset: If True, forces a reconnection even if already connected.
|
|
712
|
+
load_configured: If True, loads tools and prompts according to the constructor flags.
|
|
673
713
|
|
|
674
714
|
Raises:
|
|
675
715
|
ToolException: If connection or session initialization fails.
|
|
@@ -678,6 +718,7 @@ class MCPTool:
|
|
|
678
718
|
await self._safe_close_exit_stack()
|
|
679
719
|
self.session = None
|
|
680
720
|
self.is_connected = False
|
|
721
|
+
self._reset_session_state()
|
|
681
722
|
self._exit_stack = AsyncExitStack()
|
|
682
723
|
if not self.session:
|
|
683
724
|
try:
|
|
@@ -739,7 +780,8 @@ class MCPTool:
|
|
|
739
780
|
inner_exception=ex if isinstance(ex, Exception) else None,
|
|
740
781
|
) from ex
|
|
741
782
|
try:
|
|
742
|
-
await session.initialize()
|
|
783
|
+
initialize_result = await session.initialize()
|
|
784
|
+
self._set_server_capabilities(getattr(initialize_result, "capabilities", None))
|
|
743
785
|
except (Exception, asyncio.CancelledError) as ex:
|
|
744
786
|
if await self._close_and_check_cancelled(ex):
|
|
745
787
|
raise
|
|
@@ -757,17 +799,22 @@ class MCPTool:
|
|
|
757
799
|
self.session = session
|
|
758
800
|
elif self.session._request_id == 0: # type: ignore[attr-defined]
|
|
759
801
|
# If the session is not initialized, we need to reinitialize it
|
|
760
|
-
await self.session.initialize()
|
|
802
|
+
initialize_result = await self.session.initialize()
|
|
803
|
+
self._set_server_capabilities(getattr(initialize_result, "capabilities", None))
|
|
804
|
+
elif self._server_capabilities is None:
|
|
805
|
+
self._set_server_capabilities(getattr(self.session, "_server_capabilities", None))
|
|
761
806
|
logger.debug("Connected to MCP server: %s", self.session)
|
|
762
807
|
self.is_connected = True
|
|
763
|
-
if self.load_tools_flag:
|
|
764
|
-
|
|
808
|
+
if load_configured and self.load_tools_flag:
|
|
809
|
+
if self._supports_tools:
|
|
810
|
+
await self.load_tools()
|
|
765
811
|
self._tools_loaded = True
|
|
766
|
-
if self.load_prompts_flag:
|
|
767
|
-
|
|
812
|
+
if load_configured and self.load_prompts_flag:
|
|
813
|
+
if self._supports_prompts:
|
|
814
|
+
await self.load_prompts()
|
|
768
815
|
self._prompts_loaded = True
|
|
769
816
|
|
|
770
|
-
if logger.level != logging.NOTSET:
|
|
817
|
+
if logger.level != logging.NOTSET and self._supports_logging is not False:
|
|
771
818
|
try:
|
|
772
819
|
level_name = cast(
|
|
773
820
|
Any, next(level for level, value in LOG_LEVEL_MAPPING.items() if value == logger.level)
|
|
@@ -905,12 +952,47 @@ class MCPTool:
|
|
|
905
952
|
if isinstance(message, types.ServerNotification):
|
|
906
953
|
match message.root.method:
|
|
907
954
|
case "notifications/tools/list_changed":
|
|
908
|
-
|
|
955
|
+
self._schedule_reload(self.load_tools())
|
|
909
956
|
case "notifications/prompts/list_changed":
|
|
910
|
-
|
|
957
|
+
self._schedule_reload(self.load_prompts())
|
|
911
958
|
case _:
|
|
912
959
|
logger.debug("Unhandled notification: %s", message.root.method)
|
|
913
960
|
|
|
961
|
+
def _schedule_reload(self, coro: Coroutine[Any, Any, None]) -> None:
|
|
962
|
+
"""Schedule a reload coroutine as a background task.
|
|
963
|
+
|
|
964
|
+
Reloads (load_tools / load_prompts) triggered by MCP server
|
|
965
|
+
notifications must NOT be awaited inside the message handler because
|
|
966
|
+
the handler runs on the MCP SDK's single-threaded receive loop.
|
|
967
|
+
Awaiting a session request (e.g. ``list_tools``) from within that loop
|
|
968
|
+
deadlocks: the receive loop cannot read the response while it is
|
|
969
|
+
blocked waiting for the handler to return.
|
|
970
|
+
|
|
971
|
+
Instead we fire the reload as an independent ``asyncio.Task`` and keep
|
|
972
|
+
a strong reference in ``_pending_reload_tasks`` so it is not garbage-
|
|
973
|
+
collected before completion. Only one reload per kind (tools / prompts)
|
|
974
|
+
is kept in flight; a new notification cancels the previous pending task
|
|
975
|
+
for the same coroutine name to avoid unbounded growth.
|
|
976
|
+
"""
|
|
977
|
+
# Cancel-and-replace: only one reload per kind should be in flight.
|
|
978
|
+
reload_name = f"mcp-reload:{self.name}:{coro.__qualname__}"
|
|
979
|
+
for existing in list(self._pending_reload_tasks):
|
|
980
|
+
if existing.get_name() == reload_name and not existing.done():
|
|
981
|
+
logger.debug("Cancelling in-flight reload %s; superseded by new notification", reload_name)
|
|
982
|
+
existing.cancel()
|
|
983
|
+
|
|
984
|
+
async def _safe_reload() -> None:
|
|
985
|
+
try:
|
|
986
|
+
await coro
|
|
987
|
+
except asyncio.CancelledError:
|
|
988
|
+
raise
|
|
989
|
+
except Exception:
|
|
990
|
+
logger.warning("Background MCP reload failed", exc_info=True)
|
|
991
|
+
|
|
992
|
+
task = asyncio.create_task(_safe_reload(), name=reload_name)
|
|
993
|
+
self._pending_reload_tasks.add(task)
|
|
994
|
+
task.add_done_callback(self._pending_reload_tasks.discard)
|
|
995
|
+
|
|
914
996
|
def _determine_approval_mode(
|
|
915
997
|
self,
|
|
916
998
|
*candidate_names: str,
|
|
@@ -936,17 +1018,49 @@ class MCPTool:
|
|
|
936
1018
|
Raises:
|
|
937
1019
|
ToolExecutionException: If the MCP server is not connected.
|
|
938
1020
|
"""
|
|
1021
|
+
from anyio import ClosedResourceError
|
|
939
1022
|
from mcp import types
|
|
940
1023
|
|
|
1024
|
+
if not self._supports_prompts:
|
|
1025
|
+
logger.debug("Skipping MCP prompt loading because the server did not advertise prompts support.")
|
|
1026
|
+
return
|
|
1027
|
+
|
|
941
1028
|
# Track existing function names to prevent duplicates
|
|
942
1029
|
existing_names = {func.name for func in self._functions}
|
|
943
1030
|
|
|
944
1031
|
params: types.PaginatedRequestParams | None = None
|
|
945
1032
|
while True:
|
|
946
|
-
|
|
947
|
-
|
|
1033
|
+
prompt_list: types.ListPromptsResult | None = None
|
|
1034
|
+
for attempt in range(2):
|
|
1035
|
+
try:
|
|
1036
|
+
# Ensure connection is still valid before each page request
|
|
1037
|
+
await self._ensure_connected()
|
|
1038
|
+
if not self._supports_prompts:
|
|
1039
|
+
logger.debug(
|
|
1040
|
+
"Skipping MCP prompt loading because the server did not advertise prompts support."
|
|
1041
|
+
)
|
|
1042
|
+
return
|
|
1043
|
+
prompt_list = await self.session.list_prompts(params=params) # type: ignore[union-attr]
|
|
1044
|
+
break
|
|
1045
|
+
except ClosedResourceError as cl_ex:
|
|
1046
|
+
if attempt == 0:
|
|
1047
|
+
logger.info("MCP connection closed unexpectedly while loading prompts. Reconnecting...")
|
|
1048
|
+
try:
|
|
1049
|
+
await self._reconnect_without_loading()
|
|
1050
|
+
except Exception as reconn_ex:
|
|
1051
|
+
raise ToolExecutionException(
|
|
1052
|
+
"Failed to reconnect to MCP server.",
|
|
1053
|
+
inner_exception=reconn_ex,
|
|
1054
|
+
) from reconn_ex
|
|
1055
|
+
continue
|
|
1056
|
+
logger.error("MCP connection closed unexpectedly after reconnection: %s", cl_ex)
|
|
1057
|
+
raise ToolExecutionException(
|
|
1058
|
+
"Failed to load prompts - connection lost.",
|
|
1059
|
+
inner_exception=cl_ex,
|
|
1060
|
+
) from cl_ex
|
|
948
1061
|
|
|
949
|
-
prompt_list
|
|
1062
|
+
if prompt_list is None:
|
|
1063
|
+
raise ToolExecutionException("Failed to load prompts.")
|
|
950
1064
|
|
|
951
1065
|
for prompt in prompt_list.prompts:
|
|
952
1066
|
normalized_name = _normalize_mcp_name(prompt.name)
|
|
@@ -973,7 +1087,7 @@ class MCPTool:
|
|
|
973
1087
|
existing_names.add(local_name)
|
|
974
1088
|
|
|
975
1089
|
# Check if there are more pages
|
|
976
|
-
if not prompt_list
|
|
1090
|
+
if not prompt_list.nextCursor:
|
|
977
1091
|
break
|
|
978
1092
|
params = types.PaginatedRequestParams(cursor=prompt_list.nextCursor)
|
|
979
1093
|
|
|
@@ -986,19 +1100,53 @@ class MCPTool:
|
|
|
986
1100
|
Raises:
|
|
987
1101
|
ToolExecutionException: If the MCP server is not connected.
|
|
988
1102
|
"""
|
|
1103
|
+
from anyio import ClosedResourceError
|
|
989
1104
|
from mcp import types
|
|
990
1105
|
|
|
1106
|
+
if not self._supports_tools:
|
|
1107
|
+
logger.debug("Skipping MCP tool loading because the server did not advertise tools support.")
|
|
1108
|
+
return
|
|
1109
|
+
|
|
991
1110
|
# Track existing function names to prevent duplicates
|
|
992
1111
|
existing_names = {func.name for func in self._functions}
|
|
1112
|
+
self._tool_call_meta_by_name.clear()
|
|
993
1113
|
|
|
994
1114
|
params: types.PaginatedRequestParams | None = None
|
|
995
1115
|
while True:
|
|
996
|
-
|
|
997
|
-
|
|
1116
|
+
tool_list: types.ListToolsResult | None = None
|
|
1117
|
+
for attempt in range(2):
|
|
1118
|
+
try:
|
|
1119
|
+
# Ensure connection is still valid before each page request
|
|
1120
|
+
await self._ensure_connected()
|
|
1121
|
+
if not self._supports_tools:
|
|
1122
|
+
logger.debug("Skipping MCP tool loading because the server did not advertise tools support.")
|
|
1123
|
+
return
|
|
1124
|
+
tool_list = await self.session.list_tools(params=params) # type: ignore[union-attr]
|
|
1125
|
+
break
|
|
1126
|
+
except ClosedResourceError as cl_ex:
|
|
1127
|
+
if attempt == 0:
|
|
1128
|
+
logger.info("MCP connection closed unexpectedly while loading tools. Reconnecting...")
|
|
1129
|
+
try:
|
|
1130
|
+
await self._reconnect_without_loading()
|
|
1131
|
+
except Exception as reconn_ex:
|
|
1132
|
+
raise ToolExecutionException(
|
|
1133
|
+
"Failed to reconnect to MCP server.",
|
|
1134
|
+
inner_exception=reconn_ex,
|
|
1135
|
+
) from reconn_ex
|
|
1136
|
+
continue
|
|
1137
|
+
logger.error("MCP connection closed unexpectedly after reconnection: %s", cl_ex)
|
|
1138
|
+
raise ToolExecutionException(
|
|
1139
|
+
"Failed to load tools - connection lost.",
|
|
1140
|
+
inner_exception=cl_ex,
|
|
1141
|
+
) from cl_ex
|
|
998
1142
|
|
|
999
|
-
tool_list
|
|
1143
|
+
if tool_list is None:
|
|
1144
|
+
raise ToolExecutionException("Failed to load tools.")
|
|
1000
1145
|
|
|
1001
1146
|
for tool in tool_list.tools:
|
|
1147
|
+
if tool.meta is not None:
|
|
1148
|
+
self._tool_call_meta_by_name[tool.name] = dict(tool.meta)
|
|
1149
|
+
|
|
1002
1150
|
normalized_name = _normalize_mcp_name(tool.name)
|
|
1003
1151
|
local_name = _build_prefixed_mcp_name(normalized_name, self.tool_name_prefix)
|
|
1004
1152
|
|
|
@@ -1042,15 +1190,24 @@ class MCPTool:
|
|
|
1042
1190
|
existing_names.add(local_name)
|
|
1043
1191
|
|
|
1044
1192
|
# Check if there are more pages
|
|
1045
|
-
if not tool_list
|
|
1193
|
+
if not tool_list.nextCursor:
|
|
1046
1194
|
break
|
|
1047
1195
|
params = types.PaginatedRequestParams(cursor=tool_list.nextCursor)
|
|
1048
1196
|
|
|
1049
1197
|
async def _close_on_owner(self) -> None:
|
|
1198
|
+
# Cancel any pending reload tasks before tearing down the session.
|
|
1199
|
+
tasks = list(self._pending_reload_tasks)
|
|
1200
|
+
for task in tasks:
|
|
1201
|
+
task.cancel()
|
|
1202
|
+
self._pending_reload_tasks.clear()
|
|
1203
|
+
if tasks:
|
|
1204
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
1205
|
+
|
|
1050
1206
|
await self._safe_close_exit_stack()
|
|
1051
1207
|
self._exit_stack = AsyncExitStack()
|
|
1052
1208
|
self.session = None
|
|
1053
1209
|
self.is_connected = False
|
|
1210
|
+
self._reset_session_state()
|
|
1054
1211
|
|
|
1055
1212
|
async def close(self) -> None:
|
|
1056
1213
|
"""Disconnect from the MCP server.
|
|
@@ -1082,12 +1239,30 @@ class MCPTool:
|
|
|
1082
1239
|
Raises:
|
|
1083
1240
|
ToolExecutionException: If reconnection fails.
|
|
1084
1241
|
"""
|
|
1242
|
+
from mcp.shared.exceptions import McpError
|
|
1243
|
+
|
|
1244
|
+
if not self._ping_available:
|
|
1245
|
+
return
|
|
1246
|
+
|
|
1085
1247
|
try:
|
|
1086
1248
|
await self.session.send_ping() # type: ignore[union-attr]
|
|
1249
|
+
except McpError as mcp_exc:
|
|
1250
|
+
if mcp_exc.error.code == -32601:
|
|
1251
|
+
self._ping_available = False
|
|
1252
|
+
logger.debug("Skipping future MCP pings because the server does not support ping.")
|
|
1253
|
+
return
|
|
1254
|
+
logger.info("MCP connection invalid or closed. Reconnecting...")
|
|
1255
|
+
try:
|
|
1256
|
+
await self._reconnect_without_loading()
|
|
1257
|
+
except Exception as ex:
|
|
1258
|
+
raise ToolExecutionException(
|
|
1259
|
+
"Failed to establish MCP connection.",
|
|
1260
|
+
inner_exception=ex,
|
|
1261
|
+
) from ex
|
|
1087
1262
|
except Exception:
|
|
1088
1263
|
logger.info("MCP connection invalid or closed. Reconnecting...")
|
|
1089
1264
|
try:
|
|
1090
|
-
await self.
|
|
1265
|
+
await self._reconnect_without_loading()
|
|
1091
1266
|
except Exception as ex:
|
|
1092
1267
|
raise ToolExecutionException(
|
|
1093
1268
|
"Failed to establish MCP connection.",
|
|
@@ -1141,14 +1316,15 @@ class MCPTool:
|
|
|
1141
1316
|
}
|
|
1142
1317
|
}
|
|
1143
1318
|
|
|
1144
|
-
#
|
|
1145
|
-
|
|
1319
|
+
# Some MCP proxies require their tools/list metadata to be echoed on tools/call.
|
|
1320
|
+
tool_meta = self._tool_call_meta_by_name.get(tool_name)
|
|
1321
|
+
meta = _inject_otel_into_mcp_meta(dict(tool_meta) if tool_meta is not None else None)
|
|
1146
1322
|
|
|
1147
1323
|
parser = self.parse_tool_results or self._parse_tool_result_from_mcp
|
|
1148
1324
|
# Try the operation, reconnecting once if the connection is closed
|
|
1149
1325
|
for attempt in range(2):
|
|
1150
1326
|
try:
|
|
1151
|
-
result = await self.session.call_tool(tool_name, arguments=filtered_kwargs, meta=
|
|
1327
|
+
result = await self.session.call_tool(tool_name, arguments=filtered_kwargs, meta=meta) # type: ignore
|
|
1152
1328
|
if result.isError:
|
|
1153
1329
|
parsed = parser(result)
|
|
1154
1330
|
text = (
|