fast-agent-mcp 0.3.9__py3-none-any.whl → 0.3.11__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.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

fast_agent/config.py CHANGED
@@ -43,8 +43,8 @@ class MCPSamplingSettings(BaseModel):
43
43
 
44
44
 
45
45
  class MCPElicitationSettings(BaseModel):
46
- mode: Literal["forms", "auto_cancel", "none"] = "none"
47
- """Elicitation mode: 'forms' (default UI), 'auto_cancel', 'none' (no capability)"""
46
+ mode: Literal["forms", "auto-cancel", "none"] = "none"
47
+ """Elicitation mode: 'forms' (default UI), 'auto-cancel', 'none' (no capability)"""
48
48
 
49
49
  model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
50
50
 
@@ -29,64 +29,81 @@ async def elicitation_input_callback(
29
29
  )
30
30
  effective_server_name = server_name or "Unknown Server"
31
31
 
32
- # Check if elicitation is disabled for this server
33
- if elicitation_state.is_disabled(effective_server_name):
32
+ # Start tracking elicitation operation
33
+ try:
34
+ from fast_agent.ui import notification_tracker
35
+ notification_tracker.start_elicitation(effective_server_name)
36
+ except Exception:
37
+ # Don't let notification tracking break elicitation
38
+ pass
39
+
40
+ try:
41
+ # Check if elicitation is disabled for this server
42
+ if elicitation_state.is_disabled(effective_server_name):
43
+ return HumanInputResponse(
44
+ request_id=request.request_id,
45
+ response="__CANCELLED__",
46
+ metadata={"auto_cancelled": True, "reason": "Server elicitation disabled by user"},
47
+ )
48
+
49
+ # Get the elicitation schema from metadata
50
+ schema: Optional[Dict[str, Any]] = None
51
+ if request.metadata and "requested_schema" in request.metadata:
52
+ schema = request.metadata["requested_schema"]
53
+
54
+ # Use the context manager to pause the progress display while getting input
55
+ with progress_display.paused():
56
+ try:
57
+ if schema:
58
+ form_action, form_data = await show_simple_elicitation_form(
59
+ schema=schema,
60
+ message=request.prompt,
61
+ agent_name=effective_agent_name,
62
+ server_name=effective_server_name,
63
+ )
64
+
65
+ if form_action == "accept" and form_data is not None:
66
+ # Convert form data to JSON string
67
+ import json
68
+
69
+ response = json.dumps(form_data)
70
+ elif form_action == "decline":
71
+ response = "__DECLINED__"
72
+ elif form_action == "disable":
73
+ response = "__DISABLE_SERVER__"
74
+ else: # cancel
75
+ response = "__CANCELLED__"
76
+ else:
77
+ # No schema, fall back to text input using prompt_toolkit only
78
+ from prompt_toolkit.shortcuts import input_dialog
79
+
80
+ response = await input_dialog(
81
+ title="Input Requested",
82
+ text=f"Agent: {effective_agent_name}\nServer: {effective_server_name}\n\n{request.prompt}",
83
+ style=ELICITATION_STYLE,
84
+ ).run_async()
85
+
86
+ if response is None:
87
+ response = "__CANCELLED__"
88
+
89
+ except KeyboardInterrupt:
90
+ response = "__CANCELLED__"
91
+ except EOFError:
92
+ response = "__CANCELLED__"
93
+
34
94
  return HumanInputResponse(
35
95
  request_id=request.request_id,
36
- response="__CANCELLED__",
37
- metadata={"auto_cancelled": True, "reason": "Server elicitation disabled by user"},
96
+ response=response.strip() if isinstance(response, str) else response,
97
+ metadata={"has_schema": schema is not None},
38
98
  )
39
-
40
- # Get the elicitation schema from metadata
41
- schema: Optional[Dict[str, Any]] = None
42
- if request.metadata and "requested_schema" in request.metadata:
43
- schema = request.metadata["requested_schema"]
44
-
45
- # Use the context manager to pause the progress display while getting input
46
- with progress_display.paused():
99
+ finally:
100
+ # End tracking elicitation operation
47
101
  try:
48
- if schema:
49
- form_action, form_data = await show_simple_elicitation_form(
50
- schema=schema,
51
- message=request.prompt,
52
- agent_name=effective_agent_name,
53
- server_name=effective_server_name,
54
- )
55
-
56
- if form_action == "accept" and form_data is not None:
57
- # Convert form data to JSON string
58
- import json
59
-
60
- response = json.dumps(form_data)
61
- elif form_action == "decline":
62
- response = "__DECLINED__"
63
- elif form_action == "disable":
64
- response = "__DISABLE_SERVER__"
65
- else: # cancel
66
- response = "__CANCELLED__"
67
- else:
68
- # No schema, fall back to text input using prompt_toolkit only
69
- from prompt_toolkit.shortcuts import input_dialog
70
-
71
- response = await input_dialog(
72
- title="Input Requested",
73
- text=f"Agent: {effective_agent_name}\nServer: {effective_server_name}\n\n{request.prompt}",
74
- style=ELICITATION_STYLE,
75
- ).run_async()
76
-
77
- if response is None:
78
- response = "__CANCELLED__"
79
-
80
- except KeyboardInterrupt:
81
- response = "__CANCELLED__"
82
- except EOFError:
83
- response = "__CANCELLED__"
84
-
85
- return HumanInputResponse(
86
- request_id=request.request_id,
87
- response=response.strip() if isinstance(response, str) else response,
88
- metadata={"has_schema": schema is not None},
89
- )
102
+ from fast_agent.ui import notification_tracker
103
+ notification_tracker.end_elicitation(effective_server_name)
104
+ except Exception:
105
+ # Don't let notification tracking break elicitation
106
+ pass
90
107
 
91
108
 
92
109
  # Register adapter with fast_agent tools so they can invoke this UI handler without importing types
@@ -53,7 +53,7 @@ def resolve_elicitation_handler(
53
53
  if mode == "none":
54
54
  logger.debug(f"Elicitation disabled by server config for agent {agent_config.name}")
55
55
  return None # Don't advertise elicitation capability
56
- elif mode == "auto_cancel":
56
+ elif mode == "auto-cancel":
57
57
  logger.debug(
58
58
  f"Using auto-cancel elicitation handler (server config) for agent {agent_config.name}"
59
59
  )
@@ -74,7 +74,7 @@ def resolve_elicitation_handler(
74
74
  if mode == "none":
75
75
  logger.debug(f"Elicitation disabled by global config for agent {agent_config.name}")
76
76
  return None # Don't advertise elicitation capability
77
- elif mode == "auto_cancel":
77
+ elif mode == "auto-cancel":
78
78
  logger.debug(
79
79
  f"Using auto-cancel elicitation handler (global config) for agent {agent_config.name}"
80
80
  )
@@ -334,6 +334,9 @@ class MCPAggregator(ContextDependent):
334
334
  server_name, client_session_factory=self._create_session_factory(server_name)
335
335
  )
336
336
 
337
+ # Record the initialize call that happened during connection setup
338
+ await self._record_server_call(server_name, "initialize", True)
339
+
337
340
  logger.info(
338
341
  f"MCP Servers initialized for agent '{self.agent_name}'",
339
342
  data={
@@ -342,27 +345,39 @@ class MCPAggregator(ContextDependent):
342
345
  },
343
346
  )
344
347
 
345
- async def fetch_tools(client: ClientSession, server_name: str) -> List[Tool]:
348
+ async def fetch_tools(server_name: str) -> List[Tool]:
346
349
  # Only fetch tools if the server supports them
347
350
  if not await self.server_supports_feature(server_name, "tools"):
348
351
  logger.debug(f"Server '{server_name}' does not support tools")
349
352
  return []
350
353
 
351
354
  try:
352
- result: ListToolsResult = await client.list_tools()
355
+ result: ListToolsResult = await self._execute_on_server(
356
+ server_name=server_name,
357
+ operation_type="tools/list",
358
+ operation_name="",
359
+ method_name="list_tools",
360
+ method_args={},
361
+ )
353
362
  return result.tools or []
354
363
  except Exception as e:
355
364
  logger.error(f"Error loading tools from server '{server_name}'", data=e)
356
365
  return []
357
366
 
358
- async def fetch_prompts(client: ClientSession, server_name: str) -> List[Prompt]:
367
+ async def fetch_prompts(server_name: str) -> List[Prompt]:
359
368
  # Only fetch prompts if the server supports them
360
369
  if not await self.server_supports_feature(server_name, "prompts"):
361
370
  logger.debug(f"Server '{server_name}' does not support prompts")
362
371
  return []
363
372
 
364
373
  try:
365
- result = await client.list_prompts()
374
+ result = await self._execute_on_server(
375
+ server_name=server_name,
376
+ operation_type="prompts/list",
377
+ operation_name="",
378
+ method_name="list_prompts",
379
+ method_args={},
380
+ )
366
381
  return getattr(result, "prompts", [])
367
382
  except Exception as e:
368
383
  logger.debug(f"Error loading prompts from server '{server_name}': {e}")
@@ -372,20 +387,9 @@ class MCPAggregator(ContextDependent):
372
387
  tools: List[Tool] = []
373
388
  prompts: List[Prompt] = []
374
389
 
375
- if self.connection_persistence:
376
- server_connection = await self._persistent_connection_manager.get_server(
377
- server_name, client_session_factory=self._create_session_factory(server_name)
378
- )
379
- tools = await fetch_tools(server_connection.session, server_name)
380
- prompts = await fetch_prompts(server_connection.session, server_name)
381
- else:
382
- async with gen_client(
383
- server_name,
384
- server_registry=self.context.server_registry,
385
- client_session_factory=self._create_session_factory(server_name),
386
- ) as client:
387
- tools = await fetch_tools(client, server_name)
388
- prompts = await fetch_prompts(client, server_name)
390
+ # Use _execute_on_server for consistent tracking regardless of connection mode
391
+ tools = await fetch_tools(server_name)
392
+ prompts = await fetch_prompts(server_name)
389
393
 
390
394
  return server_name, tools, prompts
391
395
 
@@ -978,7 +982,7 @@ class MCPAggregator(ContextDependent):
978
982
 
979
983
  return await self._execute_on_server(
980
984
  server_name=server_name,
981
- operation_type="tool",
985
+ operation_type="tools/call",
982
986
  operation_name=local_tool_name,
983
987
  method_name="call_tool",
984
988
  method_args={
@@ -1071,7 +1075,7 @@ class MCPAggregator(ContextDependent):
1071
1075
 
1072
1076
  result = await self._execute_on_server(
1073
1077
  server_name=server_name,
1074
- operation_type="prompt",
1078
+ operation_type="prompts/get",
1075
1079
  operation_name=local_prompt_name or "default",
1076
1080
  method_name="get_prompt",
1077
1081
  method_args=method_args,
@@ -1119,7 +1123,7 @@ class MCPAggregator(ContextDependent):
1119
1123
 
1120
1124
  result = await self._execute_on_server(
1121
1125
  server_name=s_name,
1122
- operation_type="prompt",
1126
+ operation_type="prompts/get",
1123
1127
  operation_name=local_prompt_name,
1124
1128
  method_name="get_prompt",
1125
1129
  method_args=method_args,
@@ -1167,7 +1171,7 @@ class MCPAggregator(ContextDependent):
1167
1171
 
1168
1172
  result = await self._execute_on_server(
1169
1173
  server_name=s_name,
1170
- operation_type="prompt",
1174
+ operation_type="prompts/get",
1171
1175
  operation_name=local_prompt_name,
1172
1176
  method_name="get_prompt",
1173
1177
  method_args=method_args,
@@ -1190,7 +1194,7 @@ class MCPAggregator(ContextDependent):
1190
1194
  try:
1191
1195
  prompt_list_result = await self._execute_on_server(
1192
1196
  server_name=s_name,
1193
- operation_type="prompts-list",
1197
+ operation_type="prompts/list",
1194
1198
  operation_name="",
1195
1199
  method_name="list_prompts",
1196
1200
  error_factory=lambda _: None,
@@ -1264,7 +1268,7 @@ class MCPAggregator(ContextDependent):
1264
1268
  # Fetch from server
1265
1269
  result = await self._execute_on_server(
1266
1270
  server_name=server_name,
1267
- operation_type="prompts-list",
1271
+ operation_type="prompts/list",
1268
1272
  operation_name="",
1269
1273
  method_name="list_prompts",
1270
1274
  error_factory=lambda _: None,
@@ -1303,7 +1307,7 @@ class MCPAggregator(ContextDependent):
1303
1307
  try:
1304
1308
  result = await self._execute_on_server(
1305
1309
  server_name=s_name,
1306
- operation_type="prompts-list",
1310
+ operation_type="prompts/list",
1307
1311
  operation_name="",
1308
1312
  method_name="list_prompts",
1309
1313
  error_factory=lambda _: None,
@@ -1358,22 +1362,15 @@ class MCPAggregator(ContextDependent):
1358
1362
 
1359
1363
  async with self._refresh_lock:
1360
1364
  try:
1361
- # Fetch new tools from the server
1362
- if self.connection_persistence:
1363
- server_connection = await self._persistent_connection_manager.get_server(
1364
- server_name,
1365
- client_session_factory=self._create_session_factory(server_name),
1366
- )
1367
- tools_result = await server_connection.session.list_tools()
1368
- new_tools = tools_result.tools or []
1369
- else:
1370
- async with gen_client(
1371
- server_name,
1372
- server_registry=self.context.server_registry,
1373
- client_session_factory=self._create_session_factory(server_name),
1374
- ) as client:
1375
- tools_result = await client.list_tools()
1376
- new_tools = tools_result.tools or []
1365
+ # Fetch new tools from the server using _execute_on_server to properly record stats
1366
+ tools_result = await self._execute_on_server(
1367
+ server_name=server_name,
1368
+ operation_type="tools/list",
1369
+ operation_name="",
1370
+ method_name="list_tools",
1371
+ method_args={},
1372
+ )
1373
+ new_tools = tools_result.tools or []
1377
1374
 
1378
1375
  # Update tool maps
1379
1376
  async with self._tool_map_lock:
@@ -1489,7 +1486,7 @@ class MCPAggregator(ContextDependent):
1489
1486
  # Use the _execute_on_server method to call read_resource on the server
1490
1487
  result = await self._execute_on_server(
1491
1488
  server_name=server_name,
1492
- operation_type="resource",
1489
+ operation_type="resources/read",
1493
1490
  operation_name=resource_uri,
1494
1491
  method_name="read_resource",
1495
1492
  method_args={"uri": uri},
@@ -1540,7 +1537,7 @@ class MCPAggregator(ContextDependent):
1540
1537
  # Use the _execute_on_server method to call list_resources on the server
1541
1538
  result = await self._execute_on_server(
1542
1539
  server_name=s_name,
1543
- operation_type="resources-list",
1540
+ operation_type="resources/list",
1544
1541
  operation_name="",
1545
1542
  method_name="list_resources",
1546
1543
  method_args={}, # Empty dictionary instead of None
@@ -1593,7 +1590,7 @@ class MCPAggregator(ContextDependent):
1593
1590
  # Use the _execute_on_server method to call list_tools on the server
1594
1591
  result = await self._execute_on_server(
1595
1592
  server_name=s_name,
1596
- operation_type="tools-list",
1593
+ operation_type="tools/list",
1597
1594
  operation_name="",
1598
1595
  method_name="list_tools",
1599
1596
  method_args={},
@@ -77,6 +77,19 @@ async def sample(mcp_ctx: ClientSession, params: CreateMessageRequestParams) ->
77
77
  Returns:
78
78
  A CreateMessageResult containing the LLM's response
79
79
  """
80
+ # Get server name for notification tracking
81
+ server_name = "unknown"
82
+ if hasattr(mcp_ctx, "session") and hasattr(mcp_ctx.session, "session_server_name"):
83
+ server_name = mcp_ctx.session.session_server_name or "unknown"
84
+
85
+ # Start tracking sampling operation
86
+ try:
87
+ from fast_agent.ui import notification_tracker
88
+ notification_tracker.start_sampling(server_name)
89
+ except Exception:
90
+ # Don't let notification tracking break sampling
91
+ pass
92
+
80
93
  model: str | None = None
81
94
  api_key: str | None = None
82
95
  try:
@@ -157,6 +170,14 @@ async def sample(mcp_ctx: ClientSession, params: CreateMessageRequestParams) ->
157
170
  return SamplingConverter.error_result(
158
171
  error_message=f"Error in sampling: {str(e)}", model=model
159
172
  )
173
+ finally:
174
+ # End tracking sampling operation
175
+ try:
176
+ from fast_agent.ui import notification_tracker
177
+ notification_tracker.end_sampling(server_name)
178
+ except Exception:
179
+ # Don't let notification tracking break sampling
180
+ pass
160
181
 
161
182
 
162
183
  def sampling_agent_config(
@@ -243,7 +243,9 @@ class TransportChannelMetrics:
243
243
  self._get_last_error = event.detail
244
244
  self._get_last_event = "error"
245
245
  self._get_last_event_at = now
246
- self._record_history("get", "error", now)
246
+ # Record 405 as "disabled" in timeline, not "error"
247
+ timeline_state = "disabled" if event.status_code == 405 else "error"
248
+ self._record_history("get", timeline_state, now)
247
249
 
248
250
  def _handle_resumption_event(self, event: ChannelEvent, now: datetime) -> None:
249
251
  if event.event_type == "message" and event.message is not None:
@@ -623,33 +623,38 @@ class ConsoleDisplay:
623
623
  if not self.config or not self.config.logger.show_tools:
624
624
  return
625
625
 
626
- # Combined separator and status line
627
- if agent_name:
628
- left = (
629
- f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
630
- )
631
- else:
632
- left = "[magenta]▎[/magenta][dim magenta]▶[/dim magenta]"
626
+ # Check if prompt_toolkit is active
627
+ try:
628
+ from prompt_toolkit.application.current import get_app
633
629
 
634
- right = f"[dim]{updated_server}[/dim]"
635
- self._create_combined_separator_status(left, right)
630
+ app = get_app()
631
+ # We're in interactive mode - add to notification tracker
632
+ from fast_agent.ui import notification_tracker
636
633
 
637
- # Display update message
638
- message = f"Updating tools for server {updated_server}"
639
- console.console.print(message, style="dim", markup=self._markup)
634
+ notification_tracker.add_tool_update(updated_server)
635
+ app.invalidate() # Force toolbar redraw
640
636
 
641
- # Bottom separator
642
- console.console.print()
643
- console.console.print("─" * console.console.size.width, style="dim")
644
- console.console.print()
637
+ except: # noqa: E722
638
+ # No active prompt_toolkit session - display with rich as before
639
+ # Combined separator and status line
640
+ if agent_name:
641
+ left = (
642
+ f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
643
+ )
644
+ else:
645
+ left = "[magenta]▎[/magenta][dim magenta]▶[/dim magenta]"
645
646
 
646
- # Force prompt_toolkit redraw if active
647
- try:
648
- from prompt_toolkit.application.current import get_app
647
+ right = f"[dim]{updated_server}[/dim]"
648
+ self._create_combined_separator_status(left, right)
649
649
 
650
- get_app().invalidate() # Forces prompt_toolkit to redraw
651
- except: # noqa: E722
652
- pass # No active prompt_toolkit session
650
+ # Display update message
651
+ message = f"Updating tools for server {updated_server}"
652
+ console.console.print(message, style="dim", markup=self._markup)
653
+
654
+ # Bottom separator
655
+ console.console.print()
656
+ console.console.print("─" * console.console.size.width, style="dim")
657
+ console.console.print()
653
658
 
654
659
  def _create_combined_separator_status(self, left_content: str, right_info: str = "") -> None:
655
660
  """
@@ -7,16 +7,16 @@ ELICITATION_STYLE = Style.from_dict(
7
7
  {
8
8
  # Dialog structure - use ansidefault for true black, remove problematic shadow
9
9
  "dialog": "bg:ansidefault", # True black dialog using ansidefault
10
- "dialog.body": "bg:ansidefault fg:ansiwhite", # True black dialog body
10
+ "dialog.body": "bg:ansidefault fg:ansidefault", # True black dialog body with default text color
11
11
  "dialog shadow": "bg:ansidefault", # Set shadow background to match application
12
12
  "dialog.border": "bg:ansidefault", # True black border background
13
13
  # Set application background to true black
14
14
  "application": "bg:ansidefault", # True black application background
15
15
  # Title styling with better contrast
16
- "title": "fg:ansibrightmagenta bold", # Bright magenta text
16
+ "title": "fg:ansidefault bold", # Default color title for terminal compatibility
17
17
  # Buttons - only define focused state to preserve focus highlighting
18
18
  "button.focused": "bg:ansibrightgreen fg:ansiblack bold", # Bright green with black text for contrast
19
- "button.arrow": "fg:ansiwhite bold", # White arrows for visibility
19
+ "button.arrow": "fg:ansidefault bold", # Default color arrows for terminal compatibility
20
20
  # Form elements with consistent green/yellow theme
21
21
  # Checkboxes - green when checked, yellow when focused
22
22
  "checkbox": "fg:ansidefault", # Default color unchecked checkbox (dimmer)
@@ -34,15 +34,15 @@ ELICITATION_STYLE = Style.from_dict(
34
34
  # Frame styling with ANSI colors - make borders visible
35
35
  "frame.border": "fg:ansibrightblack", # Bright black borders for subtlety
36
36
  "frame.label": "fg:ansigray", # Gray frame labels (less prominent)
37
- # Labels and text - use white for good visibility
38
- "label": "fg:ansiwhite", # White labels for good readability
37
+ # Labels and text - use default color for terminal compatibility
38
+ "label": "fg:ansidefault", # Default color labels for terminal compatibility
39
39
  "message": "fg:ansibrightcyan", # Bright cyan messages (no bold)
40
40
  # Agent and server names - make them match
41
41
  "agent-name": "fg:ansibrightblue bold",
42
42
  "server-name": "fg:ansibrightblue bold", # Same color as agent
43
43
  # Validation errors - better contrast
44
- "validation-toolbar": "bg:ansibrightred fg:ansiwhite bold",
45
- "validation-toolbar.text": "bg:ansibrightred fg:ansiwhite",
44
+ "validation-toolbar": "bg:ansibrightred fg:ansidefault bold",
45
+ "validation-toolbar.text": "bg:ansibrightred fg:ansidefault",
46
46
  "validation.border": "fg:ansibrightred",
47
47
  "validation-error": "fg:ansibrightred bold", # For status line errors
48
48
  # Separator styling
@@ -717,17 +717,33 @@ async def get_enhanced_input(
717
717
  # Version/app label in green (dynamic version)
718
718
  version_segment = f"fast-agent {app_version}"
719
719
 
720
+ # Add notifications - prioritize active events over completed ones
721
+ from fast_agent.ui import notification_tracker
722
+
723
+ notification_segment = ""
724
+
725
+ # Check for active events first (highest priority)
726
+ active_status = notification_tracker.get_active_status()
727
+ if active_status:
728
+ event_type = active_status['type'].upper()
729
+ server = active_status['server']
730
+ notification_segment = f" | <style fg='ansired' bg='ansiblack'>◀ {event_type} ({server})</style>"
731
+ elif notification_tracker.get_count() > 0:
732
+ # Show completed events summary when no active events
733
+ summary = notification_tracker.get_summary()
734
+ notification_segment = f" | ◀ {notification_tracker.get_count()} updates ({summary})"
735
+
720
736
  if middle:
721
737
  return HTML(
722
738
  f" <style fg='{toolbar_color}' bg='ansiblack'> {agent_name} </style> "
723
739
  f" {middle} | <style fg='{mode_style}' bg='ansiblack'> {mode_text} </style> | "
724
- f"{version_segment}"
740
+ f"{version_segment}{notification_segment}"
725
741
  )
726
742
  else:
727
743
  return HTML(
728
744
  f" <style fg='{toolbar_color}' bg='ansiblack'> {agent_name} </style> "
729
745
  f"Mode: <style fg='{mode_style}' bg='ansiblack'> {mode_text} </style> | "
730
- f"{version_segment}"
746
+ f"{version_segment}{notification_segment}"
731
747
  )
732
748
 
733
749
  # A more terminal-agnostic style that should work across themes
@@ -766,7 +782,7 @@ async def get_enhanced_input(
766
782
  session.app.key_bindings = bindings
767
783
 
768
784
  # Create formatted prompt text
769
- prompt_text = f"<ansibrightblue>{agent_name}</ansibrightblue> > "
785
+ prompt_text = f"<ansibrightblue>{agent_name}</ansibrightblue> "
770
786
 
771
787
  # Add default value display if requested
772
788
  if show_default and default and default != "STOP":
@@ -14,6 +14,67 @@ if TYPE_CHECKING:
14
14
  from fast_agent.mcp.transport_tracking import ChannelSnapshot
15
15
 
16
16
 
17
+ # Centralized color configuration
18
+ class Colours:
19
+ """Color constants for MCP status display elements."""
20
+
21
+ # Timeline activity colors (Option A: Mixed Intensity)
22
+ ERROR = "bright_red" # Keep error bright
23
+ DISABLED = "bright_blue" # Keep disabled bright
24
+ RESPONSE = "blue" # Normal blue instead of bright
25
+ REQUEST = "yellow" # Normal yellow instead of bright
26
+ NOTIFICATION = "cyan" # Normal cyan instead of bright
27
+ PING = "dim green" # Keep ping dim
28
+ IDLE = "white dim"
29
+ NONE = "dim"
30
+
31
+ # Channel arrow states
32
+ ARROW_ERROR = "bright_red"
33
+ ARROW_DISABLED = "bright_yellow" # For explicitly disabled/off
34
+ ARROW_METHOD_NOT_ALLOWED = "cyan" # For 405 method not allowed (notification color)
35
+ ARROW_OFF = "black dim"
36
+ ARROW_IDLE = "bright_cyan" # Connected but no activity
37
+ ARROW_ACTIVE = "bright_green" # Connected with activity
38
+
39
+ # Capability token states
40
+ TOKEN_ERROR = "bright_red"
41
+ TOKEN_WARNING = "bright_cyan"
42
+ TOKEN_DISABLED = "dim"
43
+ TOKEN_HIGHLIGHTED = "bright_yellow"
44
+ TOKEN_ENABLED = "bright_green"
45
+
46
+ # Text elements
47
+ TEXT_DIM = "dim"
48
+ TEXT_DEFAULT = "default" # Use terminal's default text color
49
+ TEXT_BRIGHT = "bright_white"
50
+ TEXT_ERROR = "bright_red"
51
+ TEXT_WARNING = "bright_yellow"
52
+ TEXT_SUCCESS = "bright_green"
53
+ TEXT_INFO = "bright_blue"
54
+ TEXT_CYAN = "cyan"
55
+
56
+
57
+ # Color mappings for different contexts
58
+ TIMELINE_COLORS = {
59
+ "error": Colours.ERROR,
60
+ "disabled": Colours.DISABLED,
61
+ "response": Colours.RESPONSE,
62
+ "request": Colours.REQUEST,
63
+ "notification": Colours.NOTIFICATION,
64
+ "ping": Colours.PING,
65
+ "none": Colours.IDLE,
66
+ }
67
+
68
+ TIMELINE_COLORS_STDIO = {
69
+ "error": Colours.ERROR,
70
+ "request": Colours.TOKEN_ENABLED, # All activity shows as bright green
71
+ "response": Colours.TOKEN_ENABLED,
72
+ "notification": Colours.TOKEN_ENABLED,
73
+ "ping": Colours.PING,
74
+ "none": Colours.IDLE,
75
+ }
76
+
77
+
17
78
  def _format_compact_duration(seconds: float | None) -> str | None:
18
79
  if seconds is None:
19
80
  return None
@@ -58,7 +119,7 @@ def _format_session_id(session_id: str | None) -> Text:
58
119
 
59
120
 
60
121
  def _build_aligned_field(
61
- label: str, value: Text | str, *, label_width: int = 9, value_style: str = "white"
122
+ label: str, value: Text | str, *, label_width: int = 9, value_style: str = Colours.TEXT_DEFAULT
62
123
  ) -> Text:
63
124
  field = Text()
64
125
  field.append(f"{label:<{label_width}}: ", style="dim")
@@ -134,7 +195,7 @@ def _format_capability_shorthand(
134
195
  entries.append(("Ro", False, False))
135
196
 
136
197
  mode = (status.elicitation_mode or "").lower()
137
- if mode == "auto_cancel":
198
+ if mode == "auto-cancel":
138
199
  entries.append(("El", "red", False))
139
200
  elif mode and mode != "none":
140
201
  entries.append(("El", True, False))
@@ -153,14 +214,14 @@ def _format_capability_shorthand(
153
214
 
154
215
  def token_style(supported, highlighted) -> str:
155
216
  if supported == "red":
156
- return "bright_red"
217
+ return Colours.TOKEN_ERROR
157
218
  if supported == "blue":
158
- return "bright_cyan"
219
+ return Colours.TOKEN_WARNING
159
220
  if not supported:
160
- return "dim"
221
+ return Colours.TOKEN_DISABLED
161
222
  if highlighted:
162
- return "bright_yellow"
163
- return "bright_green"
223
+ return Colours.TOKEN_HIGHLIGHTED
224
+ return Colours.TOKEN_ENABLED
164
225
 
165
226
  tokens = [
166
227
  (label, token_style(supported, highlighted)) for label, supported, highlighted in entries
@@ -198,19 +259,16 @@ def _format_label(label: str, width: int = 10) -> str:
198
259
 
199
260
  def _build_inline_timeline(buckets: Iterable[str]) -> str:
200
261
  """Build a compact timeline string for inline display."""
201
- color_map = {
202
- "error": "bright_red",
203
- "disabled": "bright_blue",
204
- "response": "bright_blue",
205
- "request": "bright_yellow",
206
- "notification": "bright_cyan",
207
- "ping": "bright_green",
208
- "none": "dim",
209
- }
210
262
  timeline = " [dim]10m[/dim] "
211
263
  for state in buckets:
212
- color = color_map.get(state, "dim")
213
- timeline += f"[bold {color}]●[/bold {color}]"
264
+ color = TIMELINE_COLORS.get(state, Colours.NONE)
265
+ if state in {"idle", "none"}:
266
+ symbol = "·"
267
+ elif state == "request":
268
+ symbol = "◆" # Diamond for requests - rare and important
269
+ else:
270
+ symbol = "●" # Circle for other activity
271
+ timeline += f"[bold {color}]{symbol}[/bold {color}]"
214
272
  timeline += " [dim]now[/dim]"
215
273
  return timeline
216
274
 
@@ -298,45 +356,25 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
298
356
  # Collect any errors to show at bottom
299
357
  errors = []
300
358
 
301
- # Build timeline color map
302
- if is_stdio:
303
- # Simplified color map for stdio: bright green for activity, dim for idle
304
- timeline_color_map = {
305
- "error": "bright_red", # Keep error as red
306
- "request": "bright_green", # All activity shows as bright green
307
- "response": "bright_green", # (not used in stdio but just in case)
308
- "notification": "bright_green", # (not used in stdio but just in case)
309
- "ping": "bright_green", # (not used in stdio but just in case)
310
- "none": "white dim",
311
- }
312
- else:
313
- # Full color map for HTTP channels
314
- timeline_color_map = {
315
- "error": "bright_red",
316
- "disabled": "bright_blue",
317
- "response": "bright_blue",
318
- "request": "bright_yellow",
319
- "notification": "bright_cyan",
320
- "ping": "bright_green",
321
- "none": "white dim",
322
- }
359
+ # Get appropriate timeline color map
360
+ timeline_color_map = TIMELINE_COLORS_STDIO if is_stdio else TIMELINE_COLORS
323
361
 
324
362
  for label, arrow, channel in entries:
325
363
  line = Text(indent)
326
364
  line.append("│ ", style="dim")
327
365
 
328
366
  # Determine arrow color based on state
329
- arrow_style = "black dim" # default no channel
367
+ arrow_style = Colours.ARROW_OFF # default no channel
330
368
  if channel:
331
369
  state = (channel.state or "open").lower()
332
370
 
333
- # Check for 405 status code (method not allowed = disabled endpoint)
371
+ # Check for 405 status code (method not allowed = not an error, just unsupported)
334
372
  if channel.last_status_code == 405:
335
- arrow_style = "bright_yellow"
336
- # Don't add 405 to errors list - it's just disabled, not an error
373
+ arrow_style = Colours.ARROW_METHOD_NOT_ALLOWED
374
+ # Don't add 405 to errors list - it's not an error, just method not supported
337
375
  # Error state (non-405 errors)
338
376
  elif state == "error":
339
- arrow_style = "bright_red"
377
+ arrow_style = Colours.ARROW_ERROR
340
378
  if channel.last_error and channel.last_status_code != 405:
341
379
  error_msg = channel.last_error
342
380
  if channel.last_status_code:
@@ -347,20 +385,41 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
347
385
  errors.append((label.split()[0], error_msg))
348
386
  # Explicitly disabled or off
349
387
  elif state in {"off", "disabled"}:
350
- arrow_style = "black dim"
388
+ arrow_style = Colours.ARROW_OFF
351
389
  # No activity (idle)
352
390
  elif channel.request_count == 0 and channel.response_count == 0:
353
- arrow_style = "bright_cyan"
391
+ arrow_style = Colours.ARROW_IDLE
354
392
  # Active/connected with activity
355
393
  elif state in {"open", "connected"}:
356
- arrow_style = "bright_green"
394
+ arrow_style = Colours.ARROW_ACTIVE
357
395
  # Fallback for other states
358
396
  else:
359
- arrow_style = "bright_cyan"
397
+ arrow_style = Colours.ARROW_IDLE
360
398
 
361
399
  # Arrow and label with better spacing
362
- line.append(arrow, style=arrow_style)
363
- line.append(f" {label:<13}", style="bright_white")
400
+ # Use hollow arrow for 405 Method Not Allowed
401
+ if channel and channel.last_status_code == 405:
402
+ # Convert solid arrows to hollow for 405
403
+ hollow_arrows = {"◀": "◁", "▶": "▷", "⇄": "⇄"} # bidirectional stays same
404
+ display_arrow = hollow_arrows.get(arrow, arrow)
405
+ else:
406
+ display_arrow = arrow
407
+ line.append(display_arrow, style=arrow_style)
408
+
409
+ # Determine label style based on activity and special cases
410
+ if not channel:
411
+ # No channel = dim
412
+ label_style = Colours.TEXT_DIM
413
+ elif channel.last_status_code == 405 and "GET" in label:
414
+ # Special case: GET (SSE) with 405 = dim (hollow arrow already handled above)
415
+ label_style = Colours.TEXT_DIM
416
+ elif channel.request_count == 0 and channel.response_count == 0:
417
+ # No activity = dim
418
+ label_style = Colours.TEXT_DIM
419
+ else:
420
+ # Has activity = normal
421
+ label_style = Colours.TEXT_DEFAULT
422
+ line.append(f" {label:<13}", style=label_style)
364
423
 
365
424
  # Always show timeline (dim black dots if no data)
366
425
  line.append("10m ", style="dim")
@@ -368,11 +427,20 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
368
427
  # Show actual activity
369
428
  for bucket_state in channel.activity_buckets:
370
429
  color = timeline_color_map.get(bucket_state, "dim")
371
- line.append("", style=f"bold {color}")
430
+ if bucket_state in {"idle", "none"}:
431
+ symbol = "·"
432
+ elif is_stdio:
433
+ # For STDIO, all activity shows as filled circles since types are combined
434
+ symbol = "●"
435
+ elif bucket_state == "request":
436
+ symbol = "◆" # Diamond for requests - rare and important
437
+ else:
438
+ symbol = "●" # Circle for other activity
439
+ line.append(symbol, style=f"bold {color}")
372
440
  else:
373
- # Show dim black dots for no activity
441
+ # Show dim dots for no activity
374
442
  for _ in range(20):
375
- line.append("", style="black dim")
443
+ line.append("·", style="black dim")
376
444
  line.append(" now", style="dim")
377
445
 
378
446
  # Metrics - different layouts for stdio vs HTTP
@@ -380,26 +448,41 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
380
448
  # Simplified activity column for stdio
381
449
  if channel and channel.message_count > 0:
382
450
  activity = str(channel.message_count).rjust(8)
383
- activity_style = "bright_white"
451
+ activity_style = Colours.TEXT_DEFAULT
384
452
  else:
385
453
  activity = "-".rjust(8)
386
- activity_style = "dim"
454
+ activity_style = Colours.TEXT_DIM
387
455
  line.append(f" {activity}", style=activity_style)
388
456
  else:
389
457
  # Original HTTP columns
390
458
  if channel:
391
- req = str(channel.request_count).rjust(5)
392
- resp = str(channel.response_count).rjust(5)
393
- notif = str(channel.notification_count).rjust(5)
394
- ping = str(channel.ping_count if channel.ping_count else "-").rjust(5)
459
+ # Show "-" for shut/disabled channels (405, off, disabled states)
460
+ channel_state = (channel.state or "open").lower()
461
+ is_shut = (
462
+ channel.last_status_code == 405
463
+ or channel_state in {"off", "disabled"}
464
+ or (channel_state == "error" and channel.last_status_code == 405)
465
+ )
466
+
467
+ if is_shut:
468
+ req = "-".rjust(5)
469
+ resp = "-".rjust(5)
470
+ notif = "-".rjust(5)
471
+ ping = "-".rjust(5)
472
+ metrics_style = Colours.TEXT_DIM
473
+ else:
474
+ req = str(channel.request_count).rjust(5)
475
+ resp = str(channel.response_count).rjust(5)
476
+ notif = str(channel.notification_count).rjust(5)
477
+ ping = str(channel.ping_count if channel.ping_count else "-").rjust(5)
478
+ metrics_style = Colours.TEXT_DEFAULT
395
479
  else:
396
480
  req = "-".rjust(5)
397
481
  resp = "-".rjust(5)
398
482
  notif = "-".rjust(5)
399
483
  ping = "-".rjust(5)
400
- line.append(
401
- f" {req} {resp} {notif} {ping}", style="bright_white" if channel else "dim"
402
- )
484
+ metrics_style = Colours.TEXT_DIM
485
+ line.append(f" {req} {resp} {notif} {ping}", style=metrics_style)
403
486
 
404
487
  console.console.print(line)
405
488
 
@@ -416,13 +499,13 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
416
499
 
417
500
  for channel_type, error_msg in errors:
418
501
  error_line = Text(indent)
419
- error_line.append("│ ", style="dim")
420
- error_line.append("⚠ ", style="bright_yellow")
421
- error_line.append(f"{channel_type}: ", style="bright_white")
502
+ error_line.append("│ ", style=Colours.TEXT_DIM)
503
+ error_line.append("⚠ ", style=Colours.TEXT_WARNING)
504
+ error_line.append(f"{channel_type}: ", style=Colours.TEXT_DEFAULT)
422
505
  # Truncate long error messages
423
506
  if len(error_msg) > 60:
424
507
  error_msg = error_msg[:57] + "..."
425
- error_line.append(error_msg, style="bright_red")
508
+ error_line.append(error_msg, style=Colours.TEXT_ERROR)
426
509
  console.console.print(error_line)
427
510
 
428
511
  # Legend if any timelines shown
@@ -444,24 +527,30 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
444
527
  if is_stdio:
445
528
  # Simplified legend for stdio: just activity vs idle
446
529
  legend_map = [
447
- ("activity", "bright_green"),
448
- ("idle", "white dim"),
530
+ ("activity", f"bold {Colours.TOKEN_ENABLED}"),
531
+ ("idle", Colours.IDLE),
449
532
  ]
450
533
  else:
451
534
  # Full legend for HTTP channels
452
535
  legend_map = [
453
- ("error", "bright_red"),
454
- ("response", "bright_blue"),
455
- ("request", "bright_yellow"),
456
- ("notification", "bright_cyan"),
457
- ("ping", "bright_green"),
458
- ("idle", "white dim"),
536
+ ("error", f"bold {Colours.ERROR}"),
537
+ ("response", f"bold {Colours.RESPONSE}"),
538
+ ("request", f"bold {Colours.REQUEST}"),
539
+ ("notification", f"bold {Colours.NOTIFICATION}"),
540
+ ("ping", Colours.PING),
541
+ ("idle", Colours.IDLE),
459
542
  ]
460
543
 
461
544
  for i, (name, color) in enumerate(legend_map):
462
545
  if i > 0:
463
546
  footer.append(" ", style="dim")
464
- footer.append("●", style=f"bold {color}")
547
+ if name == "idle":
548
+ symbol = "·"
549
+ elif name == "request":
550
+ symbol = "◆" # Diamond for requests
551
+ else:
552
+ symbol = "●"
553
+ footer.append(symbol, style=f"{color}")
465
554
  footer.append(f" {name}", style="dim")
466
555
 
467
556
  console.console.print(footer)
@@ -526,10 +615,10 @@ async def render_mcp_status(agent, indent: str = "") -> None:
526
615
  version_display = version_display[:9] + "..."
527
616
 
528
617
  header_label = Text(indent)
529
- header_label.append("▎", style="cyan")
530
- header_label.append("●", style="dim cyan")
531
- header_label.append(f" [{index:2}] ", style="cyan")
532
- header_label.append(server, style="bright_blue bold")
618
+ header_label.append("▎", style=Colours.TEXT_CYAN)
619
+ header_label.append("●", style=f"dim {Colours.TEXT_CYAN}")
620
+ header_label.append(f" [{index:2}] ", style=Colours.TEXT_CYAN)
621
+ header_label.append(server, style=f"{Colours.TEXT_INFO} bold")
533
622
  render_header(header_label)
534
623
 
535
624
  # First line: name and version
@@ -571,22 +660,22 @@ async def render_mcp_status(agent, indent: str = "") -> None:
571
660
 
572
661
  duration = _format_compact_duration(status.staleness_seconds)
573
662
  if duration:
574
- last_text = Text("last activity: ", style="dim")
575
- last_text.append(duration, style="bright_white")
576
- last_text.append(" ago", style="dim")
663
+ last_text = Text("last activity: ", style=Colours.TEXT_DIM)
664
+ last_text.append(duration, style=Colours.TEXT_DEFAULT)
665
+ last_text.append(" ago", style=Colours.TEXT_DIM)
577
666
  state_segments.append(last_text)
578
667
 
579
668
  if status.error_message and status.is_connected is False:
580
- state_segments.append(Text(status.error_message, style="bright_red"))
669
+ state_segments.append(Text(status.error_message, style=Colours.TEXT_ERROR))
581
670
 
582
671
  instr_available = bool(status.instructions_available)
583
672
  if instr_available and status.instructions_enabled is False:
584
- state_segments.append(Text("instructions disabled", style="bright_red"))
673
+ state_segments.append(Text("instructions disabled", style=Colours.TEXT_ERROR))
585
674
  elif instr_available and not template_expected:
586
- state_segments.append(Text("template missing", style="bright_yellow"))
675
+ state_segments.append(Text("template missing", style=Colours.TEXT_WARNING))
587
676
 
588
677
  if status.spoofing_enabled:
589
- state_segments.append(Text("client spoof", style="bright_yellow"))
678
+ state_segments.append(Text("client spoof", style=Colours.TEXT_WARNING))
590
679
 
591
680
  # Main status line (without transport and connected)
592
681
  if state_segments:
@@ -601,8 +690,8 @@ async def render_mcp_status(agent, indent: str = "") -> None:
601
690
  calls = _summarise_call_counts(status.call_counts)
602
691
  if calls:
603
692
  calls_line = Text(indent + " ")
604
- calls_line.append("mcp calls: ", style="dim")
605
- calls_line.append(calls, style="bright_white")
693
+ calls_line.append("mcp calls: ", style=Colours.TEXT_DIM)
694
+ calls_line.append(calls, style=Colours.TEXT_DEFAULT)
606
695
  console.console.print(calls_line)
607
696
  _render_channel_summary(status, indent, total_width)
608
697
 
@@ -0,0 +1,167 @@
1
+ """
2
+ Enhanced notification tracker for prompt_toolkit toolbar display.
3
+ Tracks both active events (sampling/elicitation) and completed notifications.
4
+ """
5
+
6
+ from datetime import datetime
7
+ from typing import Dict, List, Optional
8
+
9
+ # Active events currently in progress
10
+ active_events: Dict[str, Dict[str, str]] = {}
11
+
12
+ # Completed notifications history
13
+ notifications: List[Dict[str, str]] = []
14
+
15
+
16
+ def add_tool_update(server_name: str) -> None:
17
+ """Add a tool update notification.
18
+
19
+ Args:
20
+ server_name: Name of the server that had tools updated
21
+ """
22
+ notifications.append({
23
+ 'type': 'tool_update',
24
+ 'server': server_name
25
+ })
26
+
27
+
28
+ def start_sampling(server_name: str) -> None:
29
+ """Start tracking a sampling operation.
30
+
31
+ Args:
32
+ server_name: Name of the server making the sampling request
33
+ """
34
+ active_events['sampling'] = {
35
+ 'server': server_name,
36
+ 'start_time': datetime.now().isoformat()
37
+ }
38
+
39
+ # Force prompt_toolkit to redraw if active
40
+ try:
41
+ from prompt_toolkit.application.current import get_app
42
+ get_app().invalidate()
43
+ except Exception:
44
+ pass
45
+
46
+
47
+ def end_sampling(server_name: str) -> None:
48
+ """End tracking a sampling operation and add to completed notifications.
49
+
50
+ Args:
51
+ server_name: Name of the server that made the sampling request
52
+ """
53
+ if 'sampling' in active_events:
54
+ del active_events['sampling']
55
+
56
+ notifications.append({
57
+ 'type': 'sampling',
58
+ 'server': server_name
59
+ })
60
+
61
+ # Force prompt_toolkit to redraw if active
62
+ try:
63
+ from prompt_toolkit.application.current import get_app
64
+ get_app().invalidate()
65
+ except Exception:
66
+ pass
67
+
68
+
69
+ def start_elicitation(server_name: str) -> None:
70
+ """Start tracking an elicitation operation.
71
+
72
+ Args:
73
+ server_name: Name of the server making the elicitation request
74
+ """
75
+ active_events['elicitation'] = {
76
+ 'server': server_name,
77
+ 'start_time': datetime.now().isoformat()
78
+ }
79
+
80
+ # Force prompt_toolkit to redraw if active
81
+ try:
82
+ from prompt_toolkit.application.current import get_app
83
+ get_app().invalidate()
84
+ except Exception:
85
+ pass
86
+
87
+
88
+ def end_elicitation(server_name: str) -> None:
89
+ """End tracking an elicitation operation and add to completed notifications.
90
+
91
+ Args:
92
+ server_name: Name of the server that made the elicitation request
93
+ """
94
+ if 'elicitation' in active_events:
95
+ del active_events['elicitation']
96
+
97
+ notifications.append({
98
+ 'type': 'elicitation',
99
+ 'server': server_name
100
+ })
101
+
102
+ # Force prompt_toolkit to redraw if active
103
+ try:
104
+ from prompt_toolkit.application.current import get_app
105
+ get_app().invalidate()
106
+ except Exception:
107
+ pass
108
+
109
+
110
+ def get_active_status() -> Optional[Dict[str, str]]:
111
+ """Get currently active operation, if any.
112
+
113
+ Returns:
114
+ Dict with 'type' and 'server' keys, or None if nothing active
115
+ """
116
+ if 'sampling' in active_events:
117
+ return {'type': 'sampling', 'server': active_events['sampling']['server']}
118
+ if 'elicitation' in active_events:
119
+ return {'type': 'elicitation', 'server': active_events['elicitation']['server']}
120
+ return None
121
+
122
+
123
+ def clear() -> None:
124
+ """Clear all notifications and active events."""
125
+ notifications.clear()
126
+ active_events.clear()
127
+
128
+
129
+ def get_count() -> int:
130
+ """Get the current completed notification count."""
131
+ return len(notifications)
132
+
133
+
134
+ def get_latest() -> Dict[str, str] | None:
135
+ """Get the most recent completed notification."""
136
+ return notifications[-1] if notifications else None
137
+
138
+
139
+ def get_summary() -> str:
140
+ """Get a summary of completed notifications by type.
141
+
142
+ Returns:
143
+ String like "3 tools, 1 sampling, 2 elicitations" or "1 tool update"
144
+ """
145
+ if not notifications:
146
+ return ""
147
+
148
+ counts = {}
149
+ for notification in notifications:
150
+ event_type = notification['type']
151
+ if event_type == 'tool_update':
152
+ counts['tools'] = counts.get('tools', 0) + 1
153
+ else:
154
+ # For sampling/elicitation, use the type directly
155
+ counts[event_type] = counts.get(event_type, 0) + 1
156
+
157
+ # Build summary string
158
+ parts = []
159
+ for event_type, count in sorted(counts.items()):
160
+ if event_type == 'tools':
161
+ parts.append(f"{count} tool{'s' if count != 1 else ''}")
162
+ elif event_type == 'sampling':
163
+ parts.append(f"{count} sample{'s' if count != 1 else ''}")
164
+ else:
165
+ parts.append(f"{count} {event_type}{'s' if count != 1 else ''}")
166
+
167
+ return ", ".join(parts)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-agent-mcp
3
- Version: 0.3.9
3
+ Version: 0.3.11
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
@@ -1,5 +1,5 @@
1
1
  fast_agent/__init__.py,sha256=ns6CPmjOL5y7cyV4XgFTfMGcfLnuBJwVTbcjJ5Co3x4,4152
2
- fast_agent/config.py,sha256=5glgvbPcPZFGG6UG_P8hByryPrteqrYg4pUCep4x6s8,22509
2
+ fast_agent/config.py,sha256=tKnAhGAADpuwU7DggScG0VESs7el4R1Y1T3MUPzD5_U,22509
3
3
  fast_agent/constants.py,sha256=IoXL5m4L0iLlcRrKerMaK3ZPcS6KCJgK8b_bj1BAR60,345
4
4
  fast_agent/context.py,sha256=nBelOqehSH91z3aG2nYhwETP-biRzz-iuA2fqmKdHP8,7700
5
5
  fast_agent/context_dependent.py,sha256=KU1eydVBoIt4bYOZroqxDgE1AUexDaZi7hurE26QsF4,1584
@@ -54,7 +54,7 @@ fast_agent/core/logging/logger.py,sha256=L-hLfUGFCIABoNYDiUkNHWvFxL6j-6zn5Pc5E7a
54
54
  fast_agent/core/logging/transport.py,sha256=i_WYXk5mqyfetT72bCYrbdrMWcuL1HJCyeQKfQg7U2w,16994
55
55
  fast_agent/history/history_exporter.py,sha256=oqkw7qC5rrW73u20tkIqt8yBWPoVzCTC61x2Q2rOKGs,1404
56
56
  fast_agent/human_input/__init__.py,sha256=4Jr_0JLJwdQ3iEUNd6Api9InldtnRMDv_WeZq_WEHpA,935
57
- fast_agent/human_input/elicitation_handler.py,sha256=U6278SJLIu1tosyL6qvo5OVwMSVE03gpGlWoF8YDlvQ,3985
57
+ fast_agent/human_input/elicitation_handler.py,sha256=diR8jWRCAuoqTDQHJ3C097EORcLFxpLjdz6EubVOnI8,4776
58
58
  fast_agent/human_input/elicitation_state.py,sha256=L_vSTpw1-TSDumRYa89Me-twWRbUL2w7GNVhVJmn3KE,1152
59
59
  fast_agent/human_input/form_fields.py,sha256=aE7HdR-wOPO_6HllNaJXtn3BzpPsC4TctUApbveRk8g,7644
60
60
  fast_agent/human_input/simple_form.py,sha256=_flUll9z93VPjKqLS7gvz4L1_YJ3-KNx8_ZpUaaxhoQ,3508
@@ -97,14 +97,14 @@ fast_agent/llm/provider/openai/openai_multipart.py,sha256=uuoRMYWiznYksBODbknBuq
97
97
  fast_agent/llm/provider/openai/openai_utils.py,sha256=RI9UgF7SGkZ02bAep7glvLy3erbismmZp7wXDsRoJPQ,2034
98
98
  fast_agent/mcp/__init__.py,sha256=Q-86MBawKmB602VMut8U3uqUQDOrTAMx2HmHKnWT32Q,1371
99
99
  fast_agent/mcp/common.py,sha256=MpSC0fLO21RcDz4VApah4C8_LisVGz7OXkR17Xw-9mY,431
100
- fast_agent/mcp/elicitation_factory.py,sha256=p9tSTcs1KSCXkFcFv1cG7vJBpS1PedOJ5bcBJp0qKw4,3172
100
+ fast_agent/mcp/elicitation_factory.py,sha256=d4unxJyR0S-qH05yBZ_oQ08XLBToFtUzzFFYvv0wLrY,3172
101
101
  fast_agent/mcp/elicitation_handlers.py,sha256=LWNKn850Ht9V1SGTfAZDptlsgzrycNhOPNsCcqzsuUY,6884
102
102
  fast_agent/mcp/gen_client.py,sha256=Q0hhCVzC659GsvTLJIbhUBgGwsAJRL8b3ejTFokyjn4,3038
103
103
  fast_agent/mcp/hf_auth.py,sha256=ndDvR7E9LCc5dBiMsStFXtvvX9lYrL-edCq_qJw4lDw,4476
104
104
  fast_agent/mcp/interfaces.py,sha256=xCWONGXe4uQSmmBlMZRD3mflPegTJnz2caVNihEl3ok,2411
105
105
  fast_agent/mcp/logger_textio.py,sha256=4YLVXlXghdGm1s_qp1VoAWEX_eWufBfD2iD7l08yoak,3170
106
106
  fast_agent/mcp/mcp_agent_client_session.py,sha256=5_9cW3CSzYspRzb8lOJTFx9E8Qk7ieCBASBbUtNrmiY,15578
107
- fast_agent/mcp/mcp_aggregator.py,sha256=0ekpAHdBb87TCpkwWsTNUcvTsg-MVp-sU8Tpc1bKFi8,67656
107
+ fast_agent/mcp/mcp_aggregator.py,sha256=HzNyKuUelAlp5JBkjMAaOB2JvdMgpm0ItEPGjidI9l0,67198
108
108
  fast_agent/mcp/mcp_connection_manager.py,sha256=nbjcyWGK3LQhuFUABV1Mxf_v3rWFSMXNsDHV1h3qDUI,23803
109
109
  fast_agent/mcp/mcp_content.py,sha256=F9bgJ57EO9sgWg1m-eTNM6xd9js79mHKf4e9O8K8jrI,8829
110
110
  fast_agent/mcp/mime_utils.py,sha256=D6YXNdZJ351BjacSW5o0sVF_hrWuRHD6UyWS4TDlLZI,2915
@@ -114,10 +114,10 @@ fast_agent/mcp/prompt_message_extended.py,sha256=BsiV2SsiZkDlvqzvjeSowq8Ojvowr9X
114
114
  fast_agent/mcp/prompt_render.py,sha256=AqDaQqM6kqciV9X79S5rsRr3VjcQ_2JOiLaHqpRzsS4,2888
115
115
  fast_agent/mcp/prompt_serialization.py,sha256=QMbY0aa_UlJ7bbxl_muOm2TYeYbBVTEeEMHFmEy99ss,20182
116
116
  fast_agent/mcp/resource_utils.py,sha256=cu-l9aOy-NFs8tPihYRNjsB2QSuime8KGOGpUvihp84,6589
117
- fast_agent/mcp/sampling.py,sha256=3EhEguls5GpVMr_SYrVQcYSRXlryGmqidn-zbFaeDMk,6711
117
+ fast_agent/mcp/sampling.py,sha256=6S9bpGCFGC5azIGE-zxODvKgBbBn1x6amL5mc4sMg_4,7491
118
118
  fast_agent/mcp/stdio_tracking_simple.py,sha256=T6kCIb6YjwqKtXHz_6HvlLLYiSCbuggt2xCXSihVnIg,1918
119
119
  fast_agent/mcp/streamable_http_tracking.py,sha256=bcNNReokho6WMjWEH13F33bUSkjJ2F5l3qnegkDqdMA,11465
120
- fast_agent/mcp/transport_tracking.py,sha256=FyY3Ynzq4JGUJ5RDHPNFxBxF1oAZBiUwnL_-C9eqSYA,23936
120
+ fast_agent/mcp/transport_tracking.py,sha256=tsc2Ntf47KbKXs8DzRkqvG0U-FbpwU2VxemNfRbJBpo,24088
121
121
  fast_agent/mcp/ui_agent.py,sha256=OBGEuFpOPPK7EthPRwzxmtzu1SDIeZy-vHwdRsyDNQk,1424
122
122
  fast_agent/mcp/ui_mixin.py,sha256=iOlSNJVPwiMUun0clCiWyot59Qgy8R7ZvUgH2afRnQA,7662
123
123
  fast_agent/mcp/helpers/__init__.py,sha256=o6-HuX6bEVFnfT_wgclFOVb1NxtOsJEOnHX8L2IqDdw,857
@@ -191,19 +191,20 @@ fast_agent/types/__init__.py,sha256=y-53m-C4drf4Rx8Bbnk_GAhko9LdNYCyRUWya8e0mos,
191
191
  fast_agent/types/llm_stop_reason.py,sha256=bWe97OfhALUe8uQeAQOnTdPlYzJiabIfo8u38kPgj3Q,2293
192
192
  fast_agent/ui/__init__.py,sha256=MXxTQjFdF7mI_3JHxBPd-aoZYLlxV_-51-Trqgv5-3w,1104
193
193
  fast_agent/ui/console.py,sha256=Gjf2QLFumwG1Lav__c07X_kZxxEUSkzV-1_-YbAwcwo,813
194
- fast_agent/ui/console_display.py,sha256=8KBhiv1RRvi-WcvddaFgxkgoCDRdq81uJR_QFnLM-M0,41744
194
+ fast_agent/ui/console_display.py,sha256=oZg-SMd_F0_1azsxWF5ug7cee_-ou2xOgGeW_y_6xrI,42034
195
195
  fast_agent/ui/elicitation_form.py,sha256=t3UhBG44YmxTLu1RjCnHwW36eQQaroE45CiBGJB2czg,29410
196
- fast_agent/ui/elicitation_style.py,sha256=rtZiJH4CwTdkDLSzDDvThlZyIyuRX0oVNzscKiHvry8,3835
197
- fast_agent/ui/enhanced_prompt.py,sha256=aFQfKijKzE1y_erDaufdYl74u3wXOXWJLc0LwxfRlyI,42647
196
+ fast_agent/ui/elicitation_style.py,sha256=-WqXgVjVs65oNwhCDw3E0A9cCyw95IOe6LYCJgjT6ok,3939
197
+ fast_agent/ui/enhanced_prompt.py,sha256=nGtYqvabfwYqCNczqWEf36AjY4laetG8CI__ePKtwKs,43496
198
198
  fast_agent/ui/interactive_prompt.py,sha256=Y-tYaU_5jqjDLRRgP3rzoMAXmJxYHVIVPsd8gWkcQso,44823
199
- fast_agent/ui/mcp_display.py,sha256=3Qtx8GZKwWs_PWRVPL1cGc1tLDEsFCPdVu9bnSxkTfQ,23296
199
+ fast_agent/ui/mcp_display.py,sha256=54NVlWL6KsvhLP2tZm3edC3RH7fiLOJeKOYQCJ55uks,27142
200
200
  fast_agent/ui/mcp_ui_utils.py,sha256=hV7z-yHX86BgdH6CMmN5qyOUjyiegQXLJOa5n5A1vQs,8476
201
201
  fast_agent/ui/mermaid_utils.py,sha256=MpcRyVCPMTwU1XeIxnyFg0fQLjcyXZduWRF8NhEqvXE,5332
202
+ fast_agent/ui/notification_tracker.py,sha256=XU2_C_1PwKEReyL2L6sFfFOq-3CczMlBiIIspxUjx0E,4625
202
203
  fast_agent/ui/progress_display.py,sha256=hajDob65PttiJ2mPS6FsCtnmTcnyvDWGn-UqQboXqkQ,361
203
204
  fast_agent/ui/rich_progress.py,sha256=fMiTigtkll4gL4Ik5WYHx-t0a92jfUovj0b580rT6J0,7735
204
205
  fast_agent/ui/usage_display.py,sha256=ltJpn_sDzo8PDNSXWx-QdEUbQWUnhmajCItNt5mA5rM,7285
205
- fast_agent_mcp-0.3.9.dist-info/METADATA,sha256=whumvjkxJyoYdKo5-96rBeYR4wRwaZdxGOm6XlYBEGA,31696
206
- fast_agent_mcp-0.3.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
207
- fast_agent_mcp-0.3.9.dist-info/entry_points.txt,sha256=i6Ujja9J-hRxttOKqTYdbYP_tyaS4gLHg53vupoCSsg,199
208
- fast_agent_mcp-0.3.9.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
209
- fast_agent_mcp-0.3.9.dist-info/RECORD,,
206
+ fast_agent_mcp-0.3.11.dist-info/METADATA,sha256=hXINwYTIB7UUzPSbYOlPN0Mcq-YUKy1nWlfs0Gx5hcQ,31697
207
+ fast_agent_mcp-0.3.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
208
+ fast_agent_mcp-0.3.11.dist-info/entry_points.txt,sha256=i6Ujja9J-hRxttOKqTYdbYP_tyaS4gLHg53vupoCSsg,199
209
+ fast_agent_mcp-0.3.11.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
210
+ fast_agent_mcp-0.3.11.dist-info/RECORD,,