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.
Files changed (91) hide show
  1. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/PKG-INFO +1 -1
  2. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/__init__.py +2 -0
  3. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_feature_stage.py +2 -0
  4. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_mcp.py +206 -30
  5. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_skills.py +668 -181
  6. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent.py +31 -16
  7. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent_executor.py +2 -2
  8. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_edge_runner.py +34 -6
  9. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_events.py +34 -18
  10. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_functional.py +2 -1
  11. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_runner_context.py +21 -1
  12. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_validation.py +29 -3
  13. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow.py +175 -27
  14. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_builder.py +217 -13
  15. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_context.py +28 -2
  16. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_executor.py +17 -1
  17. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/observability.py +177 -27
  18. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/pyproject.toml +1 -1
  19. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/LICENSE +0 -0
  20. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/README.md +0 -0
  21. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_agents.py +0 -0
  22. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_clients.py +0 -0
  23. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_compaction.py +0 -0
  24. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_docstrings.py +0 -0
  25. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_evaluation.py +0 -0
  26. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_harness/__init__.py +0 -0
  27. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_memory.py +0 -0
  28. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_mode.py +0 -0
  29. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_todo.py +0 -0
  30. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_middleware.py +0 -0
  31. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_serialization.py +0 -0
  32. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_sessions.py +0 -0
  33. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_settings.py +0 -0
  34. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_telemetry.py +0 -0
  35. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_tools.py +0 -0
  36. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_types.py +0 -0
  37. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/__init__.py +0 -0
  38. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent_utils.py +0 -0
  39. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_checkpoint.py +0 -0
  40. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_checkpoint_encoding.py +0 -0
  41. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_const.py +0 -0
  42. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_conversation_history.py +0 -0
  43. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_edge.py +0 -0
  44. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_executor.py +0 -0
  45. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_function_executor.py +0 -0
  46. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_message_utils.py +0 -0
  47. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_model_utils.py +0 -0
  48. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_request_info_mixin.py +0 -0
  49. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_runner.py +0 -0
  50. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_state.py +0 -0
  51. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_typing_utils.py +0 -0
  52. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_viz.py +0 -0
  53. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/a2a/__init__.py +0 -0
  54. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/a2a/__init__.pyi +0 -0
  55. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/ag_ui/__init__.py +0 -0
  56. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/ag_ui/__init__.pyi +0 -0
  57. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/amazon/__init__.py +0 -0
  58. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/amazon/__init__.pyi +0 -0
  59. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/anthropic/__init__.py +0 -0
  60. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/anthropic/__init__.pyi +0 -0
  61. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/azure/__init__.py +0 -0
  62. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/azure/__init__.pyi +0 -0
  63. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/chatkit/__init__.py +0 -0
  64. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/chatkit/__init__.pyi +0 -0
  65. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/declarative/__init__.py +0 -0
  66. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/declarative/__init__.pyi +0 -0
  67. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/devui/__init__.py +0 -0
  68. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/devui/__init__.pyi +0 -0
  69. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/exceptions.py +0 -0
  70. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/foundry/__init__.py +0 -0
  71. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/foundry/__init__.pyi +0 -0
  72. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/github/__init__.py +0 -0
  73. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/github/__init__.pyi +0 -0
  74. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/google/__init__.py +0 -0
  75. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/google/__init__.pyi +0 -0
  76. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/hyperlight/__init__.py +0 -0
  77. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/lab/__init__.py +0 -0
  78. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/mem0/__init__.py +0 -0
  79. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/mem0/__init__.pyi +0 -0
  80. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/microsoft/__init__.py +0 -0
  81. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/microsoft/__init__.pyi +0 -0
  82. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/ollama/__init__.py +0 -0
  83. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/ollama/__init__.pyi +0 -0
  84. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/openai/__init__.py +0 -0
  85. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/openai/__init__.pyi +0 -0
  86. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/orchestrations/__init__.py +0 -0
  87. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/orchestrations/__init__.pyi +0 -0
  88. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/py.typed +0 -0
  89. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/redis/__init__.py +0 -0
  90. {agent_framework_core-1.3.0 → agent_framework_core-1.6.0}/agent_framework/redis/__init__.pyi +0 -0
  91. {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.0
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(self, action: str, *, reset: bool = False) -> None:
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
- await self.load_tools()
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
- await self.load_prompts()
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
- await self.load_tools()
955
+ self._schedule_reload(self.load_tools())
909
956
  case "notifications/prompts/list_changed":
910
- await self.load_prompts()
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
- # Ensure connection is still valid before each page request
947
- await self._ensure_connected()
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 = await self.session.list_prompts(params=params) # type: ignore[union-attr]
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 or not prompt_list.nextCursor:
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
- # Ensure connection is still valid before each page request
997
- await self._ensure_connected()
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 = await self.session.list_tools(params=params) # type: ignore[union-attr]
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 or not tool_list.nextCursor:
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.connect(reset=True)
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
- # Inject OpenTelemetry trace context into MCP _meta for distributed tracing.
1145
- otel_meta = _inject_otel_into_mcp_meta()
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=otel_meta) # type: ignore
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 = (