fast-agent-mcp 0.2.23__py3-none-any.whl → 0.2.24__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-agent-mcp
3
- Version: 0.2.23
3
+ Version: 0.2.24
4
4
  Summary: Define, Prompt and Test MCP enabled Agents and Workflows
5
5
  Author-email: Shaun Smith <fastagent@llmindset.co.uk>
6
6
  License: Apache License
@@ -218,6 +218,7 @@ Requires-Dist: openai>=1.63.2
218
218
  Requires-Dist: opentelemetry-distro>=0.50b0
219
219
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.29.0
220
220
  Requires-Dist: opentelemetry-instrumentation-anthropic>=0.39.3
221
+ Requires-Dist: opentelemetry-instrumentation-mcp>=0.40.3
221
222
  Requires-Dist: opentelemetry-instrumentation-openai>=0.39.3
222
223
  Requires-Dist: prompt-toolkit>=3.0.50
223
224
  Requires-Dist: pydantic-settings>=2.7.0
@@ -2,7 +2,7 @@ mcp_agent/__init__.py,sha256=18T0AG0W9sJhTY38O9GFFOzliDhxx9p87CvRyti9zbw,1620
2
2
  mcp_agent/app.py,sha256=WRsiUdwy_9IAnaGRDwuLm7pzgQpt2wgsg10vBOpfcwM,5539
3
3
  mcp_agent/config.py,sha256=L_wUWTdqFXaRTBA5tL_j2l_9dufWE_MHHPut5e89lBk,12773
4
4
  mcp_agent/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
5
- mcp_agent/context.py,sha256=Kb3s_0MolHx7AeTs1NVcY3ly-xFBd35o8LT7Srpx9is,7334
5
+ mcp_agent/context.py,sha256=5pnw78LgezCLeO5Os5dgmLDadwXqw_B4Ojib48XP1s4,7431
6
6
  mcp_agent/context_dependent.py,sha256=QXfhw3RaQCKfscEEBRGuZ3sdMWqkgShz2jJ1ivGGX1I,1455
7
7
  mcp_agent/event_progress.py,sha256=b1VKlQQF2AgPMb6XHjlJAVoPdx8GuxRTUk2g-4lBNm0,2749
8
8
  mcp_agent/mcp_server_registry.py,sha256=QTzu0elBWzqXks6u5nI5n8uN5CX8CpyV6ybxnyt5LZM,11531
@@ -65,7 +65,7 @@ mcp_agent/llm/providers/augmented_llm_anthropic.py,sha256=gK_IvllVBNJUUrSfpgFpdh
65
65
  mcp_agent/llm/providers/augmented_llm_deepseek.py,sha256=NiZK5nv91ZS2VgVFXpbsFNFYLsLcppcbo_RstlRMd7I,1145
66
66
  mcp_agent/llm/providers/augmented_llm_generic.py,sha256=5Uq8ZBhcFuQTt7koP_5ykolREh2iWu8zKhNbh3pM9lQ,1210
67
67
  mcp_agent/llm/providers/augmented_llm_google.py,sha256=N0a2fphVtkvNYxKQpEX6J4tlO1C_mRw4sw3LBXnrOeI,1130
68
- mcp_agent/llm/providers/augmented_llm_openai.py,sha256=0C7BOB7i3xo0HsMCTagRSQ8Hsywb-31mot26OfohzCU,14478
68
+ mcp_agent/llm/providers/augmented_llm_openai.py,sha256=jbLG9t0iuneRPX0Cscim6K48SJEB5vPopDE3IBmJ708,14515
69
69
  mcp_agent/llm/providers/augmented_llm_openrouter.py,sha256=V_TlVKm92GHBxYIo6gpvH_6cAaIdppS25Tz6x5T7LW0,2341
70
70
  mcp_agent/llm/providers/augmented_llm_tensorzero.py,sha256=Mol_Wzj_ZtccW-LMw0oFwWUt1m1yfofloay9QYNP23c,20729
71
71
  mcp_agent/llm/providers/multipart_converter_anthropic.py,sha256=t5lHYGfFUacJldnrVtMNW-8gEMoto8Y7hJkDrnyZR-Y,16650
@@ -86,8 +86,8 @@ mcp_agent/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
86
  mcp_agent/mcp/gen_client.py,sha256=fAVwFVCgSamw4PwoWOV4wrK9TABx1S_zZv8BctRyF2k,3030
87
87
  mcp_agent/mcp/interfaces.py,sha256=PAou8znAl2HgtvfCpLQOZFbKra9F72OcVRfBJbboNX8,6965
88
88
  mcp_agent/mcp/logger_textio.py,sha256=vljC1BtNTCxBAda9ExqNB-FwVNUZIuJT3h1nWmCjMws,3172
89
- mcp_agent/mcp/mcp_agent_client_session.py,sha256=Ng7epBXq8BEA_3m1GX5LqwafgNUAMSzBugwN6N0VUWQ,4364
90
- mcp_agent/mcp/mcp_aggregator.py,sha256=lVSt0yp0CnaYjcHCWmluwBeFgl8JXHYEZk0MzXgrQzA,40110
89
+ mcp_agent/mcp/mcp_agent_client_session.py,sha256=4597ww1ihSKh-zKc9xMF3ODqosVPU_A4xVmUbk1DvcE,6002
90
+ mcp_agent/mcp/mcp_aggregator.py,sha256=_zqSuWGwRTLleXldQjqPSNqV0RRRr1luJIZOvB2AdRg,46011
91
91
  mcp_agent/mcp/mcp_connection_manager.py,sha256=jlqaAdS4zc1UfVBHQU0TkTbVr0-rOkbN9bkrLPrZVLk,17159
92
92
  mcp_agent/mcp/mime_utils.py,sha256=difepNR_gpb4MpMLkBRAoyhDk-AjXUHTiqKvT_VwS1o,1805
93
93
  mcp_agent/mcp/prompt_message_multipart.py,sha256=BDwRdNwyWHb2q2bccDb2iR2VlORqVvkvoG3xYzcMpCE,4403
@@ -146,8 +146,8 @@ mcp_agent/resources/examples/workflows/parallel.py,sha256=DQ5vY5-h8Qa5QHcYjsWXhZ
146
146
  mcp_agent/resources/examples/workflows/router.py,sha256=E4x_-c3l4YW9w1i4ARcDtkdeqIdbWEGfsMzwLYpdbVc,1677
147
147
  mcp_agent/resources/examples/workflows/short_story.txt,sha256=X3y_1AyhLFN2AKzCKvucJtDgAFIJfnlbsbGZO5bBWu0,1187
148
148
  mcp_agent/ui/console_display.py,sha256=EUeMJ7yqtxJ0-hAjXNZFJFTlmfyKlENdfzTlw0e5ETg,9949
149
- fast_agent_mcp-0.2.23.dist-info/METADATA,sha256=Vtk96ocWT3Xk_Y8f3ZLUILhh2aybr8OebCPn5jZeZOY,30156
150
- fast_agent_mcp-0.2.23.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
- fast_agent_mcp-0.2.23.dist-info/entry_points.txt,sha256=bRniFM5zk3Kix5z7scX0gf9VnmGQ2Cz_Q1Gh7Ir4W00,186
152
- fast_agent_mcp-0.2.23.dist-info/licenses/LICENSE,sha256=cN3FxDURL9XuzE5mhK9L2paZo82LTfjwCYVT7e3j0e4,10939
153
- fast_agent_mcp-0.2.23.dist-info/RECORD,,
149
+ fast_agent_mcp-0.2.24.dist-info/METADATA,sha256=BxBQ5fsAWpeZ2UhH_zChQb816cZp6_ipY05ea60lKJA,30213
150
+ fast_agent_mcp-0.2.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
+ fast_agent_mcp-0.2.24.dist-info/entry_points.txt,sha256=bRniFM5zk3Kix5z7scX0gf9VnmGQ2Cz_Q1Gh7Ir4W00,186
152
+ fast_agent_mcp-0.2.24.dist-info/licenses/LICENSE,sha256=cN3FxDURL9XuzE5mhK9L2paZo82LTfjwCYVT7e3j0e4,10939
153
+ fast_agent_mcp-0.2.24.dist-info/RECORD,,
mcp_agent/context.py CHANGED
@@ -11,6 +11,7 @@ from mcp import ServerSession
11
11
  from opentelemetry import trace
12
12
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
13
13
  from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
14
+ from opentelemetry.instrumentation.mcp import McpInstrumentor
14
15
  from opentelemetry.instrumentation.openai import OpenAIInstrumentor
15
16
  from opentelemetry.propagate import set_global_textmap
16
17
  from opentelemetry.sdk.resources import Resource
@@ -111,6 +112,7 @@ async def configure_otel(config: "Settings") -> None:
111
112
  trace.set_tracer_provider(tracer_provider)
112
113
  AnthropicInstrumentor().instrument()
113
114
  OpenAIInstrumentor().instrument()
115
+ McpInstrumentor().instrument()
114
116
 
115
117
 
116
118
  async def configure_logger(config: "Settings") -> None:
@@ -274,7 +274,9 @@ class OpenAIAugmentedLLM(AugmentedLLM[ChatCompletionMessageParam, ChatCompletion
274
274
  # Calculate new conversation messages (excluding prompts)
275
275
  new_messages = messages[len(prompt_messages) :]
276
276
 
277
- # Update conversation history
277
+ if system_prompt:
278
+ new_messages = new_messages[1:]
279
+
278
280
  self.history.set(new_messages)
279
281
 
280
282
  self._log_chat_finished(model=self.default_request_params.model)
@@ -6,21 +6,16 @@ It adds logging and supports sampling requests.
6
6
  from datetime import timedelta
7
7
  from typing import TYPE_CHECKING, Optional
8
8
 
9
- from mcp import ClientSession
9
+ from mcp import ClientSession, ServerNotification
10
10
  from mcp.shared.session import (
11
- ReceiveNotificationT,
12
11
  ReceiveResultT,
13
12
  RequestId,
14
13
  SendNotificationT,
15
14
  SendRequestT,
16
15
  SendResultT,
17
16
  )
18
- from mcp.types import (
19
- ErrorData,
20
- ListRootsResult,
21
- Root,
22
- )
23
- from pydantic import AnyUrl
17
+ from mcp.types import ErrorData, ListRootsResult, Root, ToolListChangedNotification
18
+ from pydantic import FileUrl
24
19
 
25
20
  from mcp_agent.context_dependent import ContextDependent
26
21
  from mcp_agent.logging.logger import get_logger
@@ -45,7 +40,7 @@ async def list_roots(ctx: ClientSession) -> ListRootsResult:
45
40
  ):
46
41
  roots = [
47
42
  Root(
48
- uri=AnyUrl(
43
+ uri=FileUrl(
49
44
  root.server_uri_alias or root.uri,
50
45
  ),
51
46
  name=root.name,
@@ -67,6 +62,11 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
67
62
  """
68
63
 
69
64
  def __init__(self, *args, **kwargs) -> None:
65
+ # Extract server_name if provided in kwargs
66
+ self.session_server_name = kwargs.pop("server_name", None)
67
+ # Extract the notification callbacks if provided
68
+ self._tool_list_changed_callback = kwargs.pop("tool_list_changed_callback", None)
69
+
70
70
  super().__init__(*args, **kwargs, list_roots_callback=list_roots, sampling_callback=sample)
71
71
  self.server_config: Optional[MCPServerSettings] = None
72
72
 
@@ -104,7 +104,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
104
104
  )
105
105
  return await super()._send_response(request_id, response)
106
106
 
107
- async def _received_notification(self, notification: ReceiveNotificationT) -> None:
107
+ async def _received_notification(self, notification: ServerNotification) -> None:
108
108
  """
109
109
  Can be overridden by subclasses to handle a notification without needing
110
110
  to listen on the message stream.
@@ -113,7 +113,37 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
113
113
  "_received_notification: notification=",
114
114
  data=notification.model_dump(),
115
115
  )
116
- return await super()._received_notification(notification)
116
+
117
+ # Call parent notification handler first
118
+ await super()._received_notification(notification)
119
+
120
+ # Then process our specific notification types
121
+ match notification.root:
122
+ case ToolListChangedNotification():
123
+ # Simple notification handling - just call the callback if it exists
124
+ if self._tool_list_changed_callback and self.session_server_name:
125
+ logger.info(
126
+ f"Tool list changed for server '{self.session_server_name}', triggering callback"
127
+ )
128
+ # Use asyncio.create_task to prevent blocking the notification handler
129
+ import asyncio
130
+ asyncio.create_task(self._handle_tool_list_change_callback(self.session_server_name))
131
+ else:
132
+ logger.debug(
133
+ f"Tool list changed for server '{self.session_server_name}' but no callback registered"
134
+ )
135
+
136
+ return None
137
+
138
+ async def _handle_tool_list_change_callback(self, server_name: str) -> None:
139
+ """
140
+ Helper method to handle tool list change callback in a separate task
141
+ to prevent blocking the notification handler
142
+ """
143
+ try:
144
+ await self._tool_list_changed_callback(server_name)
145
+ except Exception as e:
146
+ logger.error(f"Error in tool list changed callback: {e}")
117
147
 
118
148
  async def send_progress_notification(
119
149
  self, progress_token: str | int, progress: float, total: float | None = None
@@ -138,6 +138,9 @@ class MCPAggregator(ContextDependent):
138
138
  self._prompt_cache: Dict[str, List[Prompt]] = {}
139
139
  self._prompt_cache_lock = Lock()
140
140
 
141
+ # Lock for refreshing tools from a server
142
+ self._refresh_lock = Lock()
143
+
141
144
  async def close(self) -> None:
142
145
  """
143
146
  Close all persistent connections when the aggregator is deleted.
@@ -217,8 +220,19 @@ class MCPAggregator(ContextDependent):
217
220
  },
218
221
  )
219
222
 
223
+ # Create a wrapper to capture the parameters for the client session
224
+ def session_factory(read_stream, write_stream, read_timeout):
225
+ return MCPAgentClientSession(
226
+ read_stream,
227
+ write_stream,
228
+ read_timeout,
229
+ server_name=server_name,
230
+ tool_list_changed_callback=self._handle_tool_list_changed
231
+ )
232
+
220
233
  await self._persistent_connection_manager.get_server(
221
- server_name, client_session_factory=MCPAgentClientSession
234
+ server_name,
235
+ client_session_factory=session_factory
222
236
  )
223
237
 
224
238
  logger.info(
@@ -261,8 +275,20 @@ class MCPAggregator(ContextDependent):
261
275
  tools = await fetch_tools(server_connection.session)
262
276
  prompts = await fetch_prompts(server_connection.session, server_name)
263
277
  else:
278
+ # Create a factory function for the client session
279
+ def create_session(read_stream, write_stream, read_timeout):
280
+ return MCPAgentClientSession(
281
+ read_stream,
282
+ write_stream,
283
+ read_timeout,
284
+ server_name=server_name,
285
+ tool_list_changed_callback=self._handle_tool_list_changed
286
+ )
287
+
264
288
  async with gen_client(
265
- server_name, server_registry=self.context.server_registry
289
+ server_name,
290
+ server_registry=self.context.server_registry,
291
+ client_session_factory=create_session
266
292
  ) as client:
267
293
  tools = await fetch_tools(client)
268
294
  prompts = await fetch_prompts(client, server_name)
@@ -384,6 +410,15 @@ class MCPAggregator(ContextDependent):
384
410
  ]
385
411
  )
386
412
 
413
+ async def refresh_all_tools(self) -> None:
414
+ """
415
+ Refresh the tools for all servers.
416
+ This is useful when you know tools have changed but haven't received notifications.
417
+ """
418
+ logger.info("Refreshing tools for all servers")
419
+ for server_name in self.server_names:
420
+ await self._refresh_server_tools(server_name)
421
+
387
422
  async def _execute_on_server(
388
423
  self,
389
424
  server_name: str,
@@ -864,6 +899,102 @@ class MCPAggregator(ContextDependent):
864
899
  logger.debug(f"Available prompts across servers: {results}")
865
900
  return results
866
901
 
902
+ async def _handle_tool_list_changed(self, server_name: str) -> None:
903
+ """
904
+ Callback handler for ToolListChangedNotification.
905
+ This will refresh the tools for the specified server.
906
+
907
+ Args:
908
+ server_name: The name of the server whose tools have changed
909
+ """
910
+ logger.info(f"Tool list changed for server '{server_name}', refreshing tools")
911
+
912
+ # Refresh the tools for this server
913
+ await self._refresh_server_tools(server_name)
914
+
915
+ async def _refresh_server_tools(self, server_name: str) -> None:
916
+ """
917
+ Refresh the tools for a specific server.
918
+
919
+ Args:
920
+ server_name: The name of the server to refresh tools for
921
+ """
922
+ if not await self.validate_server(server_name):
923
+ logger.error(f"Cannot refresh tools for unknown server '{server_name}'")
924
+ return
925
+
926
+ async with self._refresh_lock:
927
+ try:
928
+ # Fetch new tools from the server
929
+ if self.connection_persistence:
930
+ # Create a factory function that will include our parameters
931
+ def create_session(read_stream, write_stream, read_timeout):
932
+ return MCPAgentClientSession(
933
+ read_stream,
934
+ write_stream,
935
+ read_timeout,
936
+ server_name=server_name,
937
+ tool_list_changed_callback=self._handle_tool_list_changed
938
+ )
939
+
940
+ server_connection = await self._persistent_connection_manager.get_server(
941
+ server_name,
942
+ client_session_factory=create_session
943
+ )
944
+ tools_result = await server_connection.session.list_tools()
945
+ new_tools = tools_result.tools or []
946
+ else:
947
+ # Create a factory function for the client session
948
+ def create_session(read_stream, write_stream, read_timeout):
949
+ return MCPAgentClientSession(
950
+ read_stream,
951
+ write_stream,
952
+ read_timeout,
953
+ server_name=server_name,
954
+ tool_list_changed_callback=self._handle_tool_list_changed
955
+ )
956
+
957
+ async with gen_client(
958
+ server_name,
959
+ server_registry=self.context.server_registry,
960
+ client_session_factory=create_session
961
+ ) as client:
962
+ tools_result = await client.list_tools()
963
+ new_tools = tools_result.tools or []
964
+
965
+ # Update tool maps
966
+ async with self._tool_map_lock:
967
+ # Remove old tools for this server
968
+ old_tools = self._server_to_tool_map.get(server_name, [])
969
+ for old_tool in old_tools:
970
+ if old_tool.namespaced_tool_name in self._namespaced_tool_map:
971
+ del self._namespaced_tool_map[old_tool.namespaced_tool_name]
972
+
973
+ # Add new tools
974
+ self._server_to_tool_map[server_name] = []
975
+ for tool in new_tools:
976
+ namespaced_tool_name = create_namespaced_name(server_name, tool.name)
977
+ namespaced_tool = NamespacedTool(
978
+ tool=tool,
979
+ server_name=server_name,
980
+ namespaced_tool_name=namespaced_tool_name,
981
+ )
982
+
983
+ self._namespaced_tool_map[namespaced_tool_name] = namespaced_tool
984
+ self._server_to_tool_map[server_name].append(namespaced_tool)
985
+
986
+ logger.info(
987
+ f"Successfully refreshed tools for server '{server_name}'",
988
+ data={
989
+ "progress_action": ProgressAction.UPDATED,
990
+ "server_name": server_name,
991
+ "agent_name": self.agent_name,
992
+ "tool_count": len(new_tools),
993
+ },
994
+ )
995
+ except Exception as e:
996
+ logger.error(f"Failed to refresh tools for server '{server_name}': {e}")
997
+
867
998
  async def get_resource(
868
999
  self, resource_uri: str, server_name: str | None = None
869
1000
  ) -> ReadResourceResult: