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

Files changed (30) hide show
  1. {fast_agent_mcp-0.2.43.dist-info → fast_agent_mcp-0.2.44.dist-info}/METADATA +3 -2
  2. {fast_agent_mcp-0.2.43.dist-info → fast_agent_mcp-0.2.44.dist-info}/RECORD +30 -29
  3. mcp_agent/agents/base_agent.py +60 -22
  4. mcp_agent/config.py +2 -0
  5. mcp_agent/core/agent_app.py +15 -5
  6. mcp_agent/core/enhanced_prompt.py +81 -11
  7. mcp_agent/core/fastagent.py +9 -1
  8. mcp_agent/core/interactive_prompt.py +60 -1
  9. mcp_agent/core/usage_display.py +10 -3
  10. mcp_agent/llm/augmented_llm.py +4 -5
  11. mcp_agent/llm/augmented_llm_passthrough.py +15 -0
  12. mcp_agent/llm/providers/augmented_llm_anthropic.py +4 -3
  13. mcp_agent/llm/providers/augmented_llm_bedrock.py +3 -3
  14. mcp_agent/llm/providers/augmented_llm_google_native.py +4 -7
  15. mcp_agent/llm/providers/augmented_llm_openai.py +5 -8
  16. mcp_agent/llm/providers/augmented_llm_tensorzero.py +6 -7
  17. mcp_agent/llm/providers/google_converter.py +6 -9
  18. mcp_agent/llm/providers/multipart_converter_anthropic.py +5 -4
  19. mcp_agent/llm/providers/multipart_converter_openai.py +33 -0
  20. mcp_agent/llm/providers/multipart_converter_tensorzero.py +3 -2
  21. mcp_agent/logging/rich_progress.py +6 -2
  22. mcp_agent/logging/transport.py +30 -36
  23. mcp_agent/mcp/helpers/content_helpers.py +26 -11
  24. mcp_agent/mcp/interfaces.py +22 -2
  25. mcp_agent/mcp/prompt_message_multipart.py +2 -3
  26. mcp_agent/ui/console_display.py +353 -142
  27. mcp_agent/ui/console_display_legacy.py +401 -0
  28. {fast_agent_mcp-0.2.43.dist-info → fast_agent_mcp-0.2.44.dist-info}/WHEEL +0 -0
  29. {fast_agent_mcp-0.2.43.dist-info → fast_agent_mcp-0.2.44.dist-info}/entry_points.txt +0 -0
  30. {fast_agent_mcp-0.2.43.dist-info → fast_agent_mcp-0.2.44.dist-info}/licenses/LICENSE +0 -0
@@ -269,17 +269,9 @@ class AsyncEventBus:
269
269
  def __init__(self, transport: EventTransport | None = None) -> None:
270
270
  self.transport: EventTransport = transport or NoOpTransport()
271
271
  self.listeners: Dict[str, EventListener] = {}
272
- self._queue = asyncio.Queue()
272
+ self._queue: asyncio.Queue | None = None
273
273
  self._task: asyncio.Task | None = None
274
274
  self._running = False
275
- self._stop_event = asyncio.Event()
276
-
277
- # Store the loop we're created on
278
- try:
279
- self._loop = asyncio.get_running_loop()
280
- except RuntimeError:
281
- self._loop = asyncio.new_event_loop()
282
- asyncio.set_event_loop(self._loop)
283
275
 
284
276
  @classmethod
285
277
  def get(cls, transport: EventTransport | None = None) -> "AsyncEventBus":
@@ -301,7 +293,6 @@ class AsyncEventBus:
301
293
  if cls._instance:
302
294
  # Signal shutdown
303
295
  cls._instance._running = False
304
- cls._instance._stop_event.set()
305
296
 
306
297
  # Clear the singleton instance
307
298
  cls._instance = None
@@ -311,13 +302,20 @@ class AsyncEventBus:
311
302
  if self._running:
312
303
  return
313
304
 
305
+ try:
306
+ asyncio.get_running_loop()
307
+ except RuntimeError:
308
+ loop = asyncio.new_event_loop()
309
+ asyncio.set_event_loop(loop)
310
+
311
+ self._queue = asyncio.Queue()
312
+
314
313
  # Start each lifecycle-aware listener
315
314
  for listener in self.listeners.values():
316
315
  if isinstance(listener, LifecycleAwareListener):
317
316
  await listener.start()
318
317
 
319
- # Clear stop event and start processing
320
- self._stop_event.clear()
318
+ # Start processing
321
319
  self._running = True
322
320
  self._task = asyncio.create_task(self._process_events())
323
321
 
@@ -328,7 +326,6 @@ class AsyncEventBus:
328
326
 
329
327
  # Signal processing to stop
330
328
  self._running = False
331
- self._stop_event.set()
332
329
 
333
330
  # Try to process remaining items with a timeout
334
331
  if not self._queue.empty():
@@ -345,6 +342,7 @@ class AsyncEventBus:
345
342
  break
346
343
  except Exception as e:
347
344
  print(f"Error during queue cleanup: {e}")
345
+ self._queue = None
348
346
 
349
347
  # Cancel and wait for task with timeout
350
348
  if self._task and not self._task.done():
@@ -356,8 +354,7 @@ class AsyncEventBus:
356
354
  pass # Task was cancelled or timed out
357
355
  except Exception as e:
358
356
  print(f"Error cancelling process task: {e}")
359
- finally:
360
- self._task = None
357
+ self._task = None
361
358
 
362
359
  # Stop each lifecycle-aware listener
363
360
  for listener in self.listeners.values():
@@ -371,6 +368,9 @@ class AsyncEventBus:
371
368
 
372
369
  async def emit(self, event: Event) -> None:
373
370
  """Emit an event to all listeners and transport."""
371
+ if not self._running:
372
+ return
373
+
374
374
  # Inject current tracing info if available
375
375
  span = trace.get_current_span()
376
376
  if span.is_recording():
@@ -402,15 +402,8 @@ class AsyncEventBus:
402
402
  try:
403
403
  # Use wait_for with a timeout to allow checking running state
404
404
  try:
405
- # Check if we should be stopping first
406
- if not self._running or self._stop_event.is_set():
407
- break
408
-
409
405
  event = await asyncio.wait_for(self._queue.get(), timeout=0.1)
410
406
  except asyncio.TimeoutError:
411
- # Check again before continuing
412
- if not self._running or self._stop_event.is_set():
413
- break
414
407
  continue
415
408
 
416
409
  # Process the event through all listeners
@@ -443,20 +436,21 @@ class AsyncEventBus:
443
436
  self._queue.task_done()
444
437
 
445
438
  # Process remaining events in queue
446
- while not self._queue.empty():
447
- try:
448
- event = self._queue.get_nowait()
449
- tasks = []
450
- for listener in self.listeners.values():
451
- try:
452
- tasks.append(listener.handle_event(event))
453
- except Exception:
454
- pass
455
- if tasks:
456
- await asyncio.gather(*tasks, return_exceptions=True)
457
- self._queue.task_done()
458
- except asyncio.QueueEmpty:
459
- break
439
+ if self._queue:
440
+ while not self._queue.empty():
441
+ try:
442
+ event = self._queue.get_nowait()
443
+ tasks = []
444
+ for listener in self.listeners.values():
445
+ try:
446
+ tasks.append(listener.handle_event(event))
447
+ except Exception:
448
+ pass
449
+ if tasks:
450
+ await asyncio.gather(*tasks, return_exceptions=True)
451
+ self._queue.task_done()
452
+ except asyncio.QueueEmpty:
453
+ break
460
454
 
461
455
 
462
456
  def create_transport(
@@ -9,20 +9,22 @@ from typing import Optional, Union
9
9
 
10
10
  from mcp.types import (
11
11
  BlobResourceContents,
12
+ ContentBlock,
12
13
  EmbeddedResource,
13
14
  ImageContent,
14
15
  ReadResourceResult,
16
+ ResourceLink,
15
17
  TextContent,
16
18
  TextResourceContents,
17
19
  )
18
20
 
19
21
 
20
- def get_text(content: Union[TextContent, ImageContent, EmbeddedResource]) -> Optional[str]:
22
+ def get_text(content: ContentBlock) -> Optional[str]:
21
23
  """
22
24
  Extract text content from a content object if available.
23
25
 
24
26
  Args:
25
- content: A content object (TextContent, ImageContent, or EmbeddedResource)
27
+ content: A content object ContentBlock
26
28
 
27
29
  Returns:
28
30
  The text content as a string or None if not a text content
@@ -40,12 +42,12 @@ def get_text(content: Union[TextContent, ImageContent, EmbeddedResource]) -> Opt
40
42
  return None
41
43
 
42
44
 
43
- def get_image_data(content: Union[TextContent, ImageContent, EmbeddedResource]) -> Optional[str]:
45
+ def get_image_data(content: ContentBlock) -> Optional[str]:
44
46
  """
45
47
  Extract image data from a content object if available.
46
48
 
47
49
  Args:
48
- content: A content object (TextContent, ImageContent, or EmbeddedResource)
50
+ content: A content object ContentBlock
49
51
 
50
52
  Returns:
51
53
  The image data as a base64 string or None if not an image content
@@ -62,12 +64,12 @@ def get_image_data(content: Union[TextContent, ImageContent, EmbeddedResource])
62
64
  return None
63
65
 
64
66
 
65
- def get_resource_uri(content: Union[TextContent, ImageContent, EmbeddedResource]) -> Optional[str]:
67
+ def get_resource_uri(content: ContentBlock) -> Optional[str]:
66
68
  """
67
69
  Extract resource URI from an EmbeddedResource if available.
68
70
 
69
71
  Args:
70
- content: A content object (TextContent, ImageContent, or EmbeddedResource)
72
+ content: A content object ContentBlock
71
73
 
72
74
  Returns:
73
75
  The resource URI as a string or None if not an embedded resource
@@ -78,12 +80,12 @@ def get_resource_uri(content: Union[TextContent, ImageContent, EmbeddedResource]
78
80
  return None
79
81
 
80
82
 
81
- def is_text_content(content: Union[TextContent, ImageContent, EmbeddedResource]) -> bool:
83
+ def is_text_content(content: ContentBlock) -> bool:
82
84
  """
83
85
  Check if the content is text content.
84
86
 
85
87
  Args:
86
- content: A content object (TextContent, ImageContent, or EmbeddedResource)
88
+ content: A content object ContentBlock
87
89
 
88
90
  Returns:
89
91
  True if the content is TextContent, False otherwise
@@ -96,7 +98,7 @@ def is_image_content(content: Union[TextContent, ImageContent, EmbeddedResource]
96
98
  Check if the content is image content.
97
99
 
98
100
  Args:
99
- content: A content object (TextContent, ImageContent, or EmbeddedResource)
101
+ content: A content object ContentBlock
100
102
 
101
103
  Returns:
102
104
  True if the content is ImageContent, False otherwise
@@ -104,12 +106,12 @@ def is_image_content(content: Union[TextContent, ImageContent, EmbeddedResource]
104
106
  return isinstance(content, ImageContent)
105
107
 
106
108
 
107
- def is_resource_content(content: Union[TextContent, ImageContent, EmbeddedResource]) -> bool:
109
+ def is_resource_content(content: ContentBlock) -> bool:
108
110
  """
109
111
  Check if the content is an embedded resource.
110
112
 
111
113
  Args:
112
- content: A content object (TextContent, ImageContent, or EmbeddedResource)
114
+ content: A content object ContentBlock
113
115
 
114
116
  Returns:
115
117
  True if the content is EmbeddedResource, False otherwise
@@ -117,6 +119,19 @@ def is_resource_content(content: Union[TextContent, ImageContent, EmbeddedResour
117
119
  return isinstance(content, EmbeddedResource)
118
120
 
119
121
 
122
+ def is_resource_link(content: ContentBlock) -> bool:
123
+ """
124
+ Check if the content is an embedded resource.
125
+
126
+ Args:
127
+ content: A ContentBlock object
128
+
129
+ Returns:
130
+ True if the content is ResourceLink, False otherwise
131
+ """
132
+ return isinstance(content, ResourceLink)
133
+
134
+
120
135
  def get_resource_text(result: ReadResourceResult, index: int = 0) -> Optional[str]:
121
136
  """
122
137
  Extract text content from a ReadResourceResult at the specified index.
@@ -126,6 +126,21 @@ class AugmentedLLMProtocol(Protocol):
126
126
  """
127
127
  ...
128
128
 
129
+ async def apply_prompt_template(
130
+ self, prompt_result: "GetPromptResult", prompt_name: str
131
+ ) -> str:
132
+ """
133
+ Apply a prompt template as persistent context that will be included in all future conversations.
134
+
135
+ Args:
136
+ prompt_result: The GetPromptResult containing prompt messages
137
+ prompt_name: The name of the prompt being applied
138
+
139
+ Returns:
140
+ String representation of the assistant's response if generated
141
+ """
142
+ ...
143
+
129
144
  @property
130
145
  def message_history(self) -> List[PromptMessageMultipart]:
131
146
  """
@@ -157,8 +172,13 @@ class AgentProtocol(AugmentedLLMProtocol, Protocol):
157
172
  """Send a message to the agent and get a response"""
158
173
  ...
159
174
 
160
- async def apply_prompt(self, prompt_name: str, arguments: Dict[str, str] | None = None) -> str:
161
- """Apply an MCP prompt template by name"""
175
+ async def apply_prompt(
176
+ self,
177
+ prompt: Union[str, "GetPromptResult"],
178
+ arguments: Dict[str, str] | None = None,
179
+ as_template: bool = False,
180
+ ) -> str:
181
+ """Apply an MCP prompt template by name or GetPromptResult object"""
162
182
  ...
163
183
 
164
184
  async def get_prompt(
@@ -1,9 +1,8 @@
1
1
  from typing import List, Optional, Union
2
2
 
3
3
  from mcp.types import (
4
- EmbeddedResource,
4
+ ContentBlock,
5
5
  GetPromptResult,
6
- ImageContent,
7
6
  PromptMessage,
8
7
  Role,
9
8
  TextContent,
@@ -20,7 +19,7 @@ class PromptMessageMultipart(BaseModel):
20
19
  """
21
20
 
22
21
  role: Role
23
- content: List[Union[TextContent, ImageContent, EmbeddedResource]]
22
+ content: List[Union[ContentBlock]]
24
23
 
25
24
  @classmethod
26
25
  def to_multipart(cls, messages: List[PromptMessage]) -> List["PromptMessageMultipart"]: