fast-agent-mcp 0.2.42__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.
- {fast_agent_mcp-0.2.42.dist-info → fast_agent_mcp-0.2.44.dist-info}/METADATA +3 -2
- {fast_agent_mcp-0.2.42.dist-info → fast_agent_mcp-0.2.44.dist-info}/RECORD +31 -30
- mcp_agent/agents/base_agent.py +60 -22
- mcp_agent/config.py +2 -0
- mcp_agent/core/agent_app.py +15 -5
- mcp_agent/core/enhanced_prompt.py +87 -13
- mcp_agent/core/fastagent.py +9 -1
- mcp_agent/core/interactive_prompt.py +60 -1
- mcp_agent/core/usage_display.py +10 -3
- mcp_agent/llm/augmented_llm.py +4 -5
- mcp_agent/llm/augmented_llm_passthrough.py +15 -0
- mcp_agent/llm/providers/augmented_llm_anthropic.py +4 -3
- mcp_agent/llm/providers/augmented_llm_bedrock.py +3 -3
- mcp_agent/llm/providers/augmented_llm_google_native.py +4 -7
- mcp_agent/llm/providers/augmented_llm_openai.py +5 -8
- mcp_agent/llm/providers/augmented_llm_tensorzero.py +6 -7
- mcp_agent/llm/providers/google_converter.py +6 -9
- mcp_agent/llm/providers/multipart_converter_anthropic.py +5 -4
- mcp_agent/llm/providers/multipart_converter_openai.py +33 -0
- mcp_agent/llm/providers/multipart_converter_tensorzero.py +3 -2
- mcp_agent/logging/rich_progress.py +6 -2
- mcp_agent/logging/transport.py +30 -36
- mcp_agent/mcp/helpers/content_helpers.py +26 -11
- mcp_agent/mcp/interfaces.py +22 -2
- mcp_agent/mcp/mcp_aggregator.py +22 -3
- mcp_agent/mcp/prompt_message_multipart.py +2 -3
- mcp_agent/ui/console_display.py +353 -142
- mcp_agent/ui/console_display_legacy.py +401 -0
- {fast_agent_mcp-0.2.42.dist-info → fast_agent_mcp-0.2.44.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.42.dist-info → fast_agent_mcp-0.2.44.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.2.42.dist-info → fast_agent_mcp-0.2.44.dist-info}/licenses/LICENSE +0 -0
mcp_agent/logging/transport.py
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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.
|
mcp_agent/mcp/interfaces.py
CHANGED
|
@@ -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(
|
|
161
|
-
|
|
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(
|
mcp_agent/mcp/mcp_aggregator.py
CHANGED
|
@@ -258,7 +258,12 @@ class MCPAggregator(ContextDependent):
|
|
|
258
258
|
},
|
|
259
259
|
)
|
|
260
260
|
|
|
261
|
-
async def fetch_tools(client: ClientSession):
|
|
261
|
+
async def fetch_tools(client: ClientSession, server_name: str) -> List[Tool]:
|
|
262
|
+
# Only fetch tools if the server supports them
|
|
263
|
+
if not await self.server_supports_feature(server_name, "tools"):
|
|
264
|
+
logger.debug(f"Server '{server_name}' does not support tools")
|
|
265
|
+
return []
|
|
266
|
+
|
|
262
267
|
try:
|
|
263
268
|
result: ListToolsResult = await client.list_tools()
|
|
264
269
|
return result.tools or []
|
|
@@ -287,7 +292,7 @@ class MCPAggregator(ContextDependent):
|
|
|
287
292
|
server_connection = await self._persistent_connection_manager.get_server(
|
|
288
293
|
server_name, client_session_factory=MCPAgentClientSession
|
|
289
294
|
)
|
|
290
|
-
tools = await fetch_tools(server_connection.session)
|
|
295
|
+
tools = await fetch_tools(server_connection.session, server_name)
|
|
291
296
|
prompts = await fetch_prompts(server_connection.session, server_name)
|
|
292
297
|
else:
|
|
293
298
|
# Create a factory function for the client session
|
|
@@ -326,7 +331,7 @@ class MCPAggregator(ContextDependent):
|
|
|
326
331
|
server_registry=self.context.server_registry,
|
|
327
332
|
client_session_factory=create_session,
|
|
328
333
|
) as client:
|
|
329
|
-
tools = await fetch_tools(client)
|
|
334
|
+
tools = await fetch_tools(client, server_name)
|
|
330
335
|
prompts = await fetch_prompts(client, server_name)
|
|
331
336
|
|
|
332
337
|
return server_name, tools, prompts
|
|
@@ -962,6 +967,11 @@ class MCPAggregator(ContextDependent):
|
|
|
962
967
|
logger.error(f"Cannot refresh tools for unknown server '{server_name}'")
|
|
963
968
|
return
|
|
964
969
|
|
|
970
|
+
# Check if server supports tools capability
|
|
971
|
+
if not await self.server_supports_feature(server_name, "tools"):
|
|
972
|
+
logger.debug(f"Server '{server_name}' does not support tools")
|
|
973
|
+
return
|
|
974
|
+
|
|
965
975
|
await self.display.show_tool_update(aggregator=self, updated_server=server_name)
|
|
966
976
|
|
|
967
977
|
async with self._refresh_lock:
|
|
@@ -1103,6 +1113,10 @@ class MCPAggregator(ContextDependent):
|
|
|
1103
1113
|
Raises:
|
|
1104
1114
|
Exception: If the resource couldn't be found or other error occurs
|
|
1105
1115
|
"""
|
|
1116
|
+
# Check if server supports resources capability
|
|
1117
|
+
if not await self.server_supports_feature(server_name, "resources"):
|
|
1118
|
+
raise ValueError(f"Server '{server_name}' does not support resources")
|
|
1119
|
+
|
|
1106
1120
|
logger.info(
|
|
1107
1121
|
"Requesting resource",
|
|
1108
1122
|
data={
|
|
@@ -1163,6 +1177,11 @@ class MCPAggregator(ContextDependent):
|
|
|
1163
1177
|
# Initialize empty list for this server
|
|
1164
1178
|
results[s_name] = []
|
|
1165
1179
|
|
|
1180
|
+
# Check if server supports resources capability
|
|
1181
|
+
if not await self.server_supports_feature(s_name, "resources"):
|
|
1182
|
+
logger.debug(f"Server '{s_name}' does not support resources")
|
|
1183
|
+
continue
|
|
1184
|
+
|
|
1166
1185
|
try:
|
|
1167
1186
|
# Use the _execute_on_server method to call list_resources on the server
|
|
1168
1187
|
result = await self._execute_on_server(
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from typing import List, Optional, Union
|
|
2
2
|
|
|
3
3
|
from mcp.types import (
|
|
4
|
-
|
|
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[
|
|
22
|
+
content: List[Union[ContentBlock]]
|
|
24
23
|
|
|
25
24
|
@classmethod
|
|
26
25
|
def to_multipart(cls, messages: List[PromptMessage]) -> List["PromptMessageMultipart"]:
|