fast-agent-mcp 0.1.13__py3-none-any.whl → 0.2.0__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.
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/METADATA +3 -4
- fast_agent_mcp-0.2.0.dist-info/RECORD +123 -0
- mcp_agent/__init__.py +75 -0
- mcp_agent/agents/agent.py +59 -371
- mcp_agent/agents/base_agent.py +522 -0
- mcp_agent/agents/workflow/__init__.py +1 -0
- mcp_agent/agents/workflow/chain_agent.py +173 -0
- mcp_agent/agents/workflow/evaluator_optimizer.py +362 -0
- mcp_agent/agents/workflow/orchestrator_agent.py +591 -0
- mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_models.py +27 -11
- mcp_agent/agents/workflow/parallel_agent.py +182 -0
- mcp_agent/agents/workflow/router_agent.py +307 -0
- mcp_agent/app.py +3 -1
- mcp_agent/cli/commands/bootstrap.py +18 -7
- mcp_agent/cli/commands/setup.py +12 -4
- mcp_agent/cli/main.py +1 -1
- mcp_agent/cli/terminal.py +1 -1
- mcp_agent/config.py +24 -35
- mcp_agent/context.py +3 -1
- mcp_agent/context_dependent.py +3 -1
- mcp_agent/core/agent_types.py +10 -7
- mcp_agent/core/direct_agent_app.py +179 -0
- mcp_agent/core/direct_decorators.py +443 -0
- mcp_agent/core/direct_factory.py +476 -0
- mcp_agent/core/enhanced_prompt.py +15 -20
- mcp_agent/core/fastagent.py +151 -337
- mcp_agent/core/interactive_prompt.py +424 -0
- mcp_agent/core/mcp_content.py +19 -11
- mcp_agent/core/prompt.py +6 -2
- mcp_agent/core/validation.py +89 -16
- mcp_agent/executor/decorator_registry.py +6 -2
- mcp_agent/executor/temporal.py +35 -11
- mcp_agent/executor/workflow_signal.py +8 -2
- mcp_agent/human_input/handler.py +3 -1
- mcp_agent/llm/__init__.py +2 -0
- mcp_agent/{workflows/llm → llm}/augmented_llm.py +131 -256
- mcp_agent/{workflows/llm → llm}/augmented_llm_passthrough.py +35 -107
- mcp_agent/llm/augmented_llm_playback.py +83 -0
- mcp_agent/{workflows/llm → llm}/model_factory.py +26 -8
- mcp_agent/llm/providers/__init__.py +8 -0
- mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +5 -1
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +37 -141
- mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +112 -148
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +78 -35
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +73 -44
- mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +18 -4
- mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +3 -3
- mcp_agent/{workflows/llm → llm}/sampling_converter.py +0 -21
- mcp_agent/{workflows/llm → llm}/sampling_format_converter.py +16 -1
- mcp_agent/logging/logger.py +2 -2
- mcp_agent/mcp/gen_client.py +9 -3
- mcp_agent/mcp/interfaces.py +67 -45
- mcp_agent/mcp/logger_textio.py +97 -0
- mcp_agent/mcp/mcp_agent_client_session.py +12 -4
- mcp_agent/mcp/mcp_agent_server.py +3 -1
- mcp_agent/mcp/mcp_aggregator.py +124 -93
- mcp_agent/mcp/mcp_connection_manager.py +21 -7
- mcp_agent/mcp/prompt_message_multipart.py +59 -1
- mcp_agent/mcp/prompt_render.py +77 -0
- mcp_agent/mcp/prompt_serialization.py +20 -13
- mcp_agent/mcp/prompts/prompt_constants.py +18 -0
- mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
- mcp_agent/mcp/prompts/prompt_load.py +15 -5
- mcp_agent/mcp/prompts/prompt_server.py +154 -87
- mcp_agent/mcp/prompts/prompt_template.py +26 -35
- mcp_agent/mcp/resource_utils.py +3 -1
- mcp_agent/mcp/sampling.py +24 -15
- mcp_agent/mcp_server/agent_server.py +8 -5
- mcp_agent/mcp_server_registry.py +22 -9
- mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +1 -1
- mcp_agent/resources/examples/{data-analysis → in_dev}/slides.py +1 -1
- mcp_agent/resources/examples/internal/agent.py +4 -2
- mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
- mcp_agent/resources/examples/prompting/image_server.py +3 -1
- mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
- mcp_agent/ui/console_display.py +27 -7
- fast_agent_mcp-0.1.13.dist-info/RECORD +0 -164
- mcp_agent/core/agent_app.py +0 -570
- mcp_agent/core/agent_utils.py +0 -69
- mcp_agent/core/decorators.py +0 -448
- mcp_agent/core/factory.py +0 -422
- mcp_agent/core/proxies.py +0 -278
- mcp_agent/core/types.py +0 -22
- mcp_agent/eval/__init__.py +0 -0
- mcp_agent/mcp/stdio.py +0 -114
- mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
- mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
- mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
- mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
- mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
- mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/researcher-imp.py +0 -189
- mcp_agent/resources/examples/researcher/researcher.py +0 -39
- mcp_agent/resources/examples/workflows/chaining.py +0 -45
- mcp_agent/resources/examples/workflows/evaluator.py +0 -79
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
- mcp_agent/resources/examples/workflows/human_input.py +0 -26
- mcp_agent/resources/examples/workflows/orchestrator.py +0 -74
- mcp_agent/resources/examples/workflows/parallel.py +0 -79
- mcp_agent/resources/examples/workflows/router.py +0 -54
- mcp_agent/resources/examples/workflows/sse.py +0 -23
- mcp_agent/telemetry/__init__.py +0 -0
- mcp_agent/telemetry/usage_tracking.py +0 -19
- mcp_agent/workflows/__init__.py +0 -0
- mcp_agent/workflows/embedding/__init__.py +0 -0
- mcp_agent/workflows/embedding/embedding_base.py +0 -58
- mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
- mcp_agent/workflows/embedding/embedding_openai.py +0 -37
- mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -447
- mcp_agent/workflows/intent_classifier/__init__.py +0 -0
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -117
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -130
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -150
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -58
- mcp_agent/workflows/llm/__init__.py +0 -0
- mcp_agent/workflows/llm/augmented_llm_playback.py +0 -111
- mcp_agent/workflows/llm/providers/__init__.py +0 -8
- mcp_agent/workflows/orchestrator/__init__.py +0 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +0 -535
- mcp_agent/workflows/parallel/__init__.py +0 -0
- mcp_agent/workflows/parallel/fan_in.py +0 -320
- mcp_agent/workflows/parallel/fan_out.py +0 -181
- mcp_agent/workflows/parallel/parallel_llm.py +0 -149
- mcp_agent/workflows/router/__init__.py +0 -0
- mcp_agent/workflows/router/router_base.py +0 -338
- mcp_agent/workflows/router/router_embedding.py +0 -226
- mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
- mcp_agent/workflows/router/router_embedding_openai.py +0 -59
- mcp_agent/workflows/router/router_llm.py +0 -304
- mcp_agent/workflows/swarm/__init__.py +0 -0
- mcp_agent/workflows/swarm/swarm.py +0 -292
- mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
- mcp_agent/workflows/swarm/swarm_openai.py +0 -41
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- /mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_prompts.py +0 -0
- /mcp_agent/{workflows/llm → llm}/memory.py +0 -0
- /mcp_agent/{workflows/llm → llm}/prompt_utils.py +0 -0
mcp_agent/mcp/mcp_aggregator.py
CHANGED
@@ -77,7 +77,9 @@ class MCPAggregator(ContextDependent):
|
|
77
77
|
if self.connection_persistence:
|
78
78
|
# Try to get existing connection manager from context
|
79
79
|
if not hasattr(self.context, "_connection_manager"):
|
80
|
-
self.context._connection_manager = MCPConnectionManager(
|
80
|
+
self.context._connection_manager = MCPConnectionManager(
|
81
|
+
self.context.server_registry
|
82
|
+
)
|
81
83
|
await self.context._connection_manager.__aenter__()
|
82
84
|
self._persistent_connection_manager = self.context._connection_manager
|
83
85
|
|
@@ -133,7 +135,10 @@ class MCPAggregator(ContextDependent):
|
|
133
135
|
if self.connection_persistence and self._persistent_connection_manager:
|
134
136
|
try:
|
135
137
|
# Only attempt cleanup if we own the connection manager
|
136
|
-
if
|
138
|
+
if (
|
139
|
+
hasattr(self.context, "_connection_manager")
|
140
|
+
and self.context._connection_manager == self._persistent_connection_manager
|
141
|
+
):
|
137
142
|
logger.info("Shutting down all persistent connections...")
|
138
143
|
await self._persistent_connection_manager.disconnect_all()
|
139
144
|
await self._persistent_connection_manager.__aexit__(None, None, None)
|
@@ -202,7 +207,9 @@ class MCPAggregator(ContextDependent):
|
|
202
207
|
},
|
203
208
|
)
|
204
209
|
|
205
|
-
await self._persistent_connection_manager.get_server(
|
210
|
+
await self._persistent_connection_manager.get_server(
|
211
|
+
server_name, client_session_factory=MCPAgentClientSession
|
212
|
+
)
|
206
213
|
|
207
214
|
logger.info(
|
208
215
|
f"MCP Servers initialized for agent '{self.agent_name}'",
|
@@ -239,11 +246,15 @@ class MCPAggregator(ContextDependent):
|
|
239
246
|
prompts: List[Prompt] = []
|
240
247
|
|
241
248
|
if self.connection_persistence:
|
242
|
-
server_connection = await self._persistent_connection_manager.get_server(
|
249
|
+
server_connection = await self._persistent_connection_manager.get_server(
|
250
|
+
server_name, client_session_factory=MCPAgentClientSession
|
251
|
+
)
|
243
252
|
tools = await fetch_tools(server_connection.session)
|
244
253
|
prompts = await fetch_prompts(server_connection.session, server_name)
|
245
254
|
else:
|
246
|
-
async with gen_client(
|
255
|
+
async with gen_client(
|
256
|
+
server_name, server_registry=self.context.server_registry
|
257
|
+
) as client:
|
247
258
|
tools = await fetch_tools(client)
|
248
259
|
prompts = await fetch_prompts(client, server_name)
|
249
260
|
|
@@ -299,7 +310,9 @@ class MCPAggregator(ContextDependent):
|
|
299
310
|
return None
|
300
311
|
|
301
312
|
try:
|
302
|
-
server_conn = await self._persistent_connection_manager.get_server(
|
313
|
+
server_conn = await self._persistent_connection_manager.get_server(
|
314
|
+
server_name, client_session_factory=MCPAgentClientSession
|
315
|
+
)
|
303
316
|
# server_capabilities is a property, not a coroutine
|
304
317
|
return server_conn.server_capabilities
|
305
318
|
except Exception as e:
|
@@ -356,12 +369,16 @@ class MCPAggregator(ContextDependent):
|
|
356
369
|
method = getattr(client, method_name)
|
357
370
|
return await method(**method_args)
|
358
371
|
except Exception as e:
|
359
|
-
error_msg =
|
372
|
+
error_msg = (
|
373
|
+
f"Failed to {method_name} '{operation_name}' on server '{server_name}': {e}"
|
374
|
+
)
|
360
375
|
logger.error(error_msg)
|
361
376
|
return error_factory(error_msg) if error_factory else None
|
362
377
|
|
363
378
|
if self.connection_persistence:
|
364
|
-
server_connection = await self._persistent_connection_manager.get_server(
|
379
|
+
server_connection = await self._persistent_connection_manager.get_server(
|
380
|
+
server_name, client_session_factory=MCPAgentClientSession
|
381
|
+
)
|
365
382
|
return await try_execute(server_connection.session)
|
366
383
|
else:
|
367
384
|
logger.debug(
|
@@ -372,7 +389,9 @@ class MCPAggregator(ContextDependent):
|
|
372
389
|
"agent_name": self.agent_name,
|
373
390
|
},
|
374
391
|
)
|
375
|
-
async with gen_client(
|
392
|
+
async with gen_client(
|
393
|
+
server_name, server_registry=self.context.server_registry
|
394
|
+
) as client:
|
376
395
|
result = await try_execute(client)
|
377
396
|
logger.debug(
|
378
397
|
f"Closing temporary connection to server: {server_name}",
|
@@ -451,10 +470,14 @@ class MCPAggregator(ContextDependent):
|
|
451
470
|
operation_name=local_tool_name,
|
452
471
|
method_name="call_tool",
|
453
472
|
method_args={"name": local_tool_name, "arguments": arguments},
|
454
|
-
error_factory=lambda msg: CallToolResult(
|
473
|
+
error_factory=lambda msg: CallToolResult(
|
474
|
+
isError=True, content=[TextContent(type="text", text=msg)]
|
475
|
+
),
|
455
476
|
)
|
456
477
|
|
457
|
-
async def get_prompt(
|
478
|
+
async def get_prompt(
|
479
|
+
self, prompt_name: str | None, arguments: dict[str, str] | None
|
480
|
+
) -> GetPromptResult:
|
458
481
|
"""
|
459
482
|
Get a prompt from a server.
|
460
483
|
|
@@ -508,7 +531,9 @@ class MCPAggregator(ContextDependent):
|
|
508
531
|
# Check if any prompt in the cache has this name
|
509
532
|
prompt_names = [prompt.name for prompt in self._prompt_cache[server_name]]
|
510
533
|
if local_prompt_name not in prompt_names:
|
511
|
-
logger.debug(
|
534
|
+
logger.debug(
|
535
|
+
f"Prompt '{local_prompt_name}' not found in cache for server '{server_name}'"
|
536
|
+
)
|
512
537
|
return GetPromptResult(
|
513
538
|
description=f"Prompt '{local_prompt_name}' not found on server '{server_name}'",
|
514
539
|
messages=[],
|
@@ -550,7 +575,9 @@ class MCPAggregator(ContextDependent):
|
|
550
575
|
potential_servers.append(s_name)
|
551
576
|
|
552
577
|
if potential_servers:
|
553
|
-
logger.debug(
|
578
|
+
logger.debug(
|
579
|
+
f"Found prompt '{local_prompt_name}' in cache for servers: {potential_servers}"
|
580
|
+
)
|
554
581
|
|
555
582
|
# Try each server from the cache
|
556
583
|
for s_name in potential_servers:
|
@@ -576,7 +603,9 @@ class MCPAggregator(ContextDependent):
|
|
576
603
|
|
577
604
|
# If we got a successful result with messages, return it
|
578
605
|
if result and result.messages:
|
579
|
-
logger.debug(
|
606
|
+
logger.debug(
|
607
|
+
f"Successfully retrieved prompt '{local_prompt_name}' from server '{s_name}'"
|
608
|
+
)
|
580
609
|
# Add namespaced name using the actual server where found
|
581
610
|
result.namespaced_name = f"{s_name}{SEP}{local_prompt_name}"
|
582
611
|
|
@@ -599,7 +628,9 @@ class MCPAggregator(ContextDependent):
|
|
599
628
|
if capabilities and capabilities.prompts:
|
600
629
|
supported_servers.append(s_name)
|
601
630
|
else:
|
602
|
-
logger.debug(
|
631
|
+
logger.debug(
|
632
|
+
f"Server '{s_name}' does not support prompts, skipping from fallback search"
|
633
|
+
)
|
603
634
|
|
604
635
|
# Try all supported servers in order
|
605
636
|
for s_name in supported_servers:
|
@@ -620,7 +651,9 @@ class MCPAggregator(ContextDependent):
|
|
620
651
|
|
621
652
|
# If we got a successful result with messages, return it
|
622
653
|
if result and result.messages:
|
623
|
-
logger.debug(
|
654
|
+
logger.debug(
|
655
|
+
f"Found prompt '{local_prompt_name}' on server '{s_name}' (not in cache)"
|
656
|
+
)
|
624
657
|
# Add namespaced name using the actual server where found
|
625
658
|
result.namespaced_name = f"{s_name}{SEP}{local_prompt_name}"
|
626
659
|
|
@@ -645,7 +678,9 @@ class MCPAggregator(ContextDependent):
|
|
645
678
|
if s_name not in self._prompt_cache:
|
646
679
|
self._prompt_cache[s_name] = []
|
647
680
|
# Add if not already in the cache
|
648
|
-
prompt_names_in_cache = [
|
681
|
+
prompt_names_in_cache = [
|
682
|
+
p.name for p in self._prompt_cache[s_name]
|
683
|
+
]
|
649
684
|
if local_prompt_name not in prompt_names_in_cache:
|
650
685
|
self._prompt_cache[s_name].append(matching_prompts[0])
|
651
686
|
except Exception:
|
@@ -665,103 +700,97 @@ class MCPAggregator(ContextDependent):
|
|
665
700
|
messages=[],
|
666
701
|
)
|
667
702
|
|
668
|
-
async def list_prompts(self, server_name: str = None):
|
703
|
+
async def list_prompts(self, server_name: str = None) -> Dict[str, List[Prompt]]:
|
669
704
|
"""
|
670
705
|
List available prompts from one or all servers.
|
671
706
|
|
672
707
|
:param server_name: Optional server name to list prompts from. If not provided,
|
673
708
|
lists prompts from all servers.
|
674
|
-
:return: Dictionary mapping server names to lists of
|
709
|
+
:return: Dictionary mapping server names to lists of Prompt objects
|
675
710
|
"""
|
676
711
|
if not self.initialized:
|
677
712
|
await self.load_servers()
|
678
713
|
|
679
|
-
results = {}
|
714
|
+
results: Dict[str, List[Prompt]] = {}
|
680
715
|
|
681
|
-
# If
|
682
|
-
|
683
|
-
|
716
|
+
# If specific server requested
|
717
|
+
if server_name:
|
718
|
+
if server_name not in self.server_names:
|
719
|
+
logger.error(f"Server '{server_name}' not found")
|
720
|
+
return results
|
721
|
+
|
722
|
+
# Check cache first
|
684
723
|
async with self._prompt_cache_lock:
|
685
|
-
if
|
686
|
-
|
687
|
-
|
688
|
-
results[s_name] = prompt_list
|
689
|
-
logger.debug("Returning cached prompts for all servers")
|
724
|
+
if server_name in self._prompt_cache:
|
725
|
+
results[server_name] = self._prompt_cache[server_name]
|
726
|
+
logger.debug(f"Returning cached prompts for server '{server_name}'")
|
690
727
|
return results
|
691
728
|
|
692
|
-
|
693
|
-
|
694
|
-
if
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
results[server_name] = self._prompt_cache[server_name]
|
699
|
-
logger.debug(f"Returning cached prompts for server '{server_name}'")
|
700
|
-
return results
|
729
|
+
# Check if server supports prompts
|
730
|
+
capabilities = await self.get_capabilities(server_name)
|
731
|
+
if not capabilities or not capabilities.prompts:
|
732
|
+
logger.debug(f"Server '{server_name}' does not support prompts")
|
733
|
+
results[server_name] = []
|
734
|
+
return results
|
701
735
|
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
736
|
+
# Fetch from server
|
737
|
+
result = await self._execute_on_server(
|
738
|
+
server_name=server_name,
|
739
|
+
operation_type="prompts-list",
|
740
|
+
operation_name="",
|
741
|
+
method_name="list_prompts",
|
742
|
+
error_factory=lambda _: None,
|
743
|
+
)
|
744
|
+
|
745
|
+
# Get prompts from result
|
746
|
+
prompts = getattr(result, "prompts", [])
|
747
|
+
|
748
|
+
# Update cache
|
749
|
+
async with self._prompt_cache_lock:
|
750
|
+
self._prompt_cache[server_name] = prompts
|
751
|
+
|
752
|
+
results[server_name] = prompts
|
753
|
+
return results
|
754
|
+
|
755
|
+
# No specific server - check if we can use the cache for all servers
|
756
|
+
async with self._prompt_cache_lock:
|
757
|
+
if all(s_name in self._prompt_cache for s_name in self.server_names):
|
758
|
+
for s_name, prompt_list in self._prompt_cache.items():
|
759
|
+
results[s_name] = prompt_list
|
760
|
+
logger.debug("Returning cached prompts for all servers")
|
761
|
+
return results
|
762
|
+
|
763
|
+
# Identify servers that support prompts
|
764
|
+
supported_servers = []
|
765
|
+
for s_name in self.server_names:
|
766
|
+
capabilities = await self.get_capabilities(s_name)
|
767
|
+
if capabilities and capabilities.prompts:
|
768
|
+
supported_servers.append(s_name)
|
769
|
+
else:
|
770
|
+
logger.debug(f"Server '{s_name}' does not support prompts, skipping")
|
771
|
+
results[s_name] = []
|
708
772
|
|
709
|
-
|
773
|
+
# Fetch prompts from supported servers
|
774
|
+
for s_name in supported_servers:
|
775
|
+
try:
|
710
776
|
result = await self._execute_on_server(
|
711
|
-
server_name=
|
777
|
+
server_name=s_name,
|
712
778
|
operation_type="prompts-list",
|
713
779
|
operation_name="",
|
714
780
|
method_name="list_prompts",
|
715
|
-
error_factory=lambda _:
|
781
|
+
error_factory=lambda _: None,
|
716
782
|
)
|
717
783
|
|
718
|
-
|
719
|
-
async with self._prompt_cache_lock:
|
720
|
-
self._prompt_cache[server_name] = getattr(result, "prompts", [])
|
784
|
+
prompts = getattr(result, "prompts", [])
|
721
785
|
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
else:
|
726
|
-
# We need to filter the servers that support prompts
|
727
|
-
supported_servers = []
|
728
|
-
for s_name in self.server_names:
|
729
|
-
capabilities = await self.get_capabilities(s_name)
|
730
|
-
if capabilities and capabilities.prompts:
|
731
|
-
supported_servers.append(s_name)
|
732
|
-
else:
|
733
|
-
logger.debug(f"Server '{s_name}' does not support prompts, skipping")
|
734
|
-
# Add empty list to results for this server
|
735
|
-
results[s_name] = []
|
736
|
-
|
737
|
-
# Process servers sequentially to ensure proper resource cleanup
|
738
|
-
# This helps prevent resource leaks especially on Windows
|
739
|
-
if supported_servers:
|
740
|
-
server_results = []
|
741
|
-
for s_name in supported_servers:
|
742
|
-
try:
|
743
|
-
result = await self._execute_on_server(
|
744
|
-
server_name=s_name,
|
745
|
-
operation_type="prompts-list",
|
746
|
-
operation_name="",
|
747
|
-
method_name="list_prompts",
|
748
|
-
error_factory=lambda _: [],
|
749
|
-
)
|
750
|
-
server_results.append(result)
|
751
|
-
except Exception as e:
|
752
|
-
logger.debug(f"Error fetching prompts from {s_name}: {e}")
|
753
|
-
server_results.append(e)
|
754
|
-
|
755
|
-
for i, result in enumerate(server_results):
|
756
|
-
if isinstance(result, BaseException):
|
757
|
-
continue
|
758
|
-
|
759
|
-
s_name = supported_servers[i]
|
760
|
-
results[s_name] = result
|
786
|
+
# Update cache and results
|
787
|
+
async with self._prompt_cache_lock:
|
788
|
+
self._prompt_cache[s_name] = prompts
|
761
789
|
|
762
|
-
|
763
|
-
|
764
|
-
|
790
|
+
results[s_name] = prompts
|
791
|
+
except Exception as e:
|
792
|
+
logger.debug(f"Error fetching prompts from {s_name}: {e}")
|
793
|
+
results[s_name] = []
|
765
794
|
|
766
795
|
logger.debug(f"Available prompts across servers: {results}")
|
767
796
|
return results
|
@@ -843,7 +872,9 @@ class MCPCompoundServer(Server):
|
|
843
872
|
content=[TextContent(type="text", text=f"Error calling tool: {e}")],
|
844
873
|
)
|
845
874
|
|
846
|
-
async def _get_prompt(
|
875
|
+
async def _get_prompt(
|
876
|
+
self, name: str = None, arguments: dict[str, str] = None
|
877
|
+
) -> GetPromptResult:
|
847
878
|
"""
|
848
879
|
Get a prompt from the aggregated servers.
|
849
880
|
|
@@ -857,7 +888,7 @@ class MCPCompoundServer(Server):
|
|
857
888
|
except Exception as e:
|
858
889
|
return GetPromptResult(description=f"Error getting prompt: {e}", messages=[])
|
859
890
|
|
860
|
-
async def _list_prompts(self, server_name: str = None) -> Dict[str, List[
|
891
|
+
async def _list_prompts(self, server_name: str = None) -> Dict[str, List[Prompt]]:
|
861
892
|
"""List available prompts from the aggregated servers."""
|
862
893
|
try:
|
863
894
|
return await self.aggregator.list_prompts(server_name=server_name)
|
@@ -19,6 +19,7 @@ from mcp.client.sse import sse_client
|
|
19
19
|
from mcp.client.stdio import (
|
20
20
|
StdioServerParameters,
|
21
21
|
get_default_environment,
|
22
|
+
stdio_client,
|
22
23
|
)
|
23
24
|
from mcp.types import JSONRPCMessage, ServerCapabilities
|
24
25
|
|
@@ -27,8 +28,8 @@ from mcp_agent.context_dependent import ContextDependent
|
|
27
28
|
from mcp_agent.core.exceptions import ServerInitializationError
|
28
29
|
from mcp_agent.event_progress import ProgressAction
|
29
30
|
from mcp_agent.logging.logger import get_logger
|
31
|
+
from mcp_agent.mcp.logger_textio import get_stderr_handler
|
30
32
|
from mcp_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
|
31
|
-
from mcp_agent.mcp.stdio import stdio_client_with_rich_stderr
|
32
33
|
|
33
34
|
if TYPE_CHECKING:
|
34
35
|
from mcp_agent.context import Context
|
@@ -133,7 +134,11 @@ class ServerConnection:
|
|
133
134
|
Create a new session instance for this server connection.
|
134
135
|
"""
|
135
136
|
|
136
|
-
read_timeout =
|
137
|
+
read_timeout = (
|
138
|
+
timedelta(seconds=self.server_config.read_timeout_seconds)
|
139
|
+
if self.server_config.read_timeout_seconds
|
140
|
+
else None
|
141
|
+
)
|
137
142
|
|
138
143
|
session = self._client_session_factory(read_stream, send_stream, read_timeout)
|
139
144
|
|
@@ -187,7 +192,9 @@ class MCPConnectionManager(ContextDependent):
|
|
187
192
|
Integrates with the application context system for proper resource management.
|
188
193
|
"""
|
189
194
|
|
190
|
-
def __init__(
|
195
|
+
def __init__(
|
196
|
+
self, server_registry: "ServerRegistry", context: Optional["Context"] = None
|
197
|
+
) -> None:
|
191
198
|
super().__init__(context=context)
|
192
199
|
self.server_registry = server_registry
|
193
200
|
self.running_servers: Dict[str, ServerConnection] = {}
|
@@ -257,8 +264,11 @@ class MCPConnectionManager(ContextDependent):
|
|
257
264
|
args=config.args,
|
258
265
|
env={**get_default_environment(), **(config.env or {})},
|
259
266
|
)
|
260
|
-
# Create
|
261
|
-
|
267
|
+
# Create custom error handler to ensure all output is captured
|
268
|
+
error_handler = get_stderr_handler(server_name)
|
269
|
+
# Explicitly ensure we're using our custom logger for stderr
|
270
|
+
logger.debug(f"{server_name}: Creating stdio client with custom error handler")
|
271
|
+
return stdio_client(server_params, errlog=error_handler)
|
262
272
|
elif config.transport == "sse":
|
263
273
|
return sse_client(config.url)
|
264
274
|
else:
|
@@ -317,13 +327,17 @@ class MCPConnectionManager(ContextDependent):
|
|
317
327
|
# Check if the server is healthy after initialization
|
318
328
|
if not server_conn.is_healthy():
|
319
329
|
error_msg = server_conn._error_message or "Unknown error"
|
320
|
-
raise ServerInitializationError(
|
330
|
+
raise ServerInitializationError(
|
331
|
+
f"MCP Server: '{server_name}': Failed to initialize with error: '{error_msg}'. Check fastagent.config.yaml"
|
332
|
+
)
|
321
333
|
|
322
334
|
return server_conn
|
323
335
|
|
324
336
|
async def get_server_capabilities(self, server_name: str) -> ServerCapabilities | None:
|
325
337
|
"""Get the capabilities of a specific server."""
|
326
|
-
server_conn = await self.get_server(
|
338
|
+
server_conn = await self.get_server(
|
339
|
+
server_name, client_session_factory=MCPAgentClientSession
|
340
|
+
)
|
327
341
|
return server_conn.server_capabilities if server_conn else None
|
328
342
|
|
329
343
|
async def disconnect_server(self, server_name: str) -> None:
|
@@ -7,10 +7,31 @@ from mcp.types import (
|
|
7
7
|
PromptMessage,
|
8
8
|
Role,
|
9
9
|
TextContent,
|
10
|
+
TextResourceContents,
|
10
11
|
)
|
11
12
|
from pydantic import BaseModel
|
12
13
|
|
13
14
|
|
15
|
+
def get_text(content: Union[TextContent, ImageContent, EmbeddedResource]) -> str | None:
|
16
|
+
"""
|
17
|
+
Extract text content from a content object if available.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
content: A content object (TextContent, ImageContent, or EmbeddedResource)
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
The text content as a string or None if not a text content
|
24
|
+
"""
|
25
|
+
if isinstance(content, TextContent):
|
26
|
+
return content.text
|
27
|
+
|
28
|
+
if isinstance(content, EmbeddedResource):
|
29
|
+
if isinstance(content.resource, TextResourceContents):
|
30
|
+
return content.resource.text
|
31
|
+
|
32
|
+
return None
|
33
|
+
|
34
|
+
|
14
35
|
class PromptMessageMultipart(BaseModel):
|
15
36
|
"""
|
16
37
|
Extension of PromptMessage that handles multiple content parts.
|
@@ -50,7 +71,44 @@ class PromptMessageMultipart(BaseModel):
|
|
50
71
|
|
51
72
|
def from_multipart(self) -> List[PromptMessage]:
|
52
73
|
"""Convert this PromptMessageMultipart to a sequence of standard PromptMessages."""
|
53
|
-
return [
|
74
|
+
return [
|
75
|
+
PromptMessage(role=self.role, content=content_part) for content_part in self.content
|
76
|
+
]
|
77
|
+
|
78
|
+
def first_text(self) -> str:
|
79
|
+
"""
|
80
|
+
Get the first available text content from a message.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
message: A PromptMessage or PromptMessageMultipart
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
First text content or None if no text content exists
|
87
|
+
"""
|
88
|
+
for content in self.content:
|
89
|
+
text = get_text(content)
|
90
|
+
if text is not None:
|
91
|
+
return text
|
92
|
+
|
93
|
+
return "<no text>"
|
94
|
+
|
95
|
+
def all_text(self) -> str:
|
96
|
+
"""
|
97
|
+
Get all the text available.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
message: A PromptMessage or PromptMessageMultipart
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
First text content or None if no text content exists
|
104
|
+
"""
|
105
|
+
result = []
|
106
|
+
for content in self.content:
|
107
|
+
text = get_text(content)
|
108
|
+
if text is not None:
|
109
|
+
result.append(text)
|
110
|
+
|
111
|
+
return "\n".join(result)
|
54
112
|
|
55
113
|
@classmethod
|
56
114
|
def parse_get_prompt_result(cls, result: GetPromptResult) -> List["PromptMessageMultipart"]:
|
@@ -0,0 +1,77 @@
|
|
1
|
+
"""
|
2
|
+
Utilities for rendering PromptMessageMultipart objects for display.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import List
|
6
|
+
|
7
|
+
from mcp.types import BlobResourceContents, TextResourceContents
|
8
|
+
|
9
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
10
|
+
from mcp_agent.mcp.prompts.prompt_helpers import (
|
11
|
+
get_resource_uri,
|
12
|
+
get_text,
|
13
|
+
is_image_content,
|
14
|
+
is_resource_content,
|
15
|
+
is_text_content,
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
def render_multipart_message(message: PromptMessageMultipart) -> str:
|
20
|
+
"""
|
21
|
+
Render a multipart message for display purposes.
|
22
|
+
|
23
|
+
This function formats the message content for user-friendly display,
|
24
|
+
handling different content types appropriately.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
message: A PromptMessageMultipart object to render
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
A string representation of the message's content
|
31
|
+
"""
|
32
|
+
rendered_parts: List[str] = []
|
33
|
+
|
34
|
+
for content in message.content:
|
35
|
+
if is_text_content(content):
|
36
|
+
# Handle text content
|
37
|
+
text_content = content # type: TextContent
|
38
|
+
rendered_parts.append(text_content.text)
|
39
|
+
|
40
|
+
elif is_image_content(content):
|
41
|
+
# Format details about the image
|
42
|
+
image_content = content # type: ImageContent
|
43
|
+
data_size = len(image_content.data) if image_content.data else 0
|
44
|
+
image_info = f"[IMAGE: {image_content.mimeType}, {data_size} bytes]"
|
45
|
+
rendered_parts.append(image_info)
|
46
|
+
|
47
|
+
elif is_resource_content(content):
|
48
|
+
# Handle embedded resources
|
49
|
+
resource = content # type: EmbeddedResource
|
50
|
+
uri = get_resource_uri(resource)
|
51
|
+
|
52
|
+
if isinstance(resource.resource, TextResourceContents):
|
53
|
+
# Handle text resources
|
54
|
+
text = resource.resource.text
|
55
|
+
text_length = len(text)
|
56
|
+
mime_type = resource.resource.mimeType
|
57
|
+
|
58
|
+
# Preview with truncation for long content
|
59
|
+
preview = text[:300] + ("..." if text_length > 300 else "")
|
60
|
+
resource_info = f"[EMBEDDED TEXT RESOURCE: {mime_type}, {uri}, {text_length} chars]\n{preview}"
|
61
|
+
rendered_parts.append(resource_info)
|
62
|
+
|
63
|
+
elif isinstance(resource.resource, BlobResourceContents):
|
64
|
+
# Handle blob resources (binary data)
|
65
|
+
blob_length = len(resource.resource.blob) if resource.resource.blob else 0
|
66
|
+
mime_type = resource.resource.mimeType
|
67
|
+
|
68
|
+
resource_info = f"[EMBEDDED BLOB RESOURCE: {mime_type}, {uri}, {blob_length} bytes]"
|
69
|
+
rendered_parts.append(resource_info)
|
70
|
+
|
71
|
+
else:
|
72
|
+
# Fallback for other content types
|
73
|
+
text = get_text(content)
|
74
|
+
if text is not None:
|
75
|
+
rendered_parts.append(text)
|
76
|
+
|
77
|
+
return "\n".join(rendered_parts)
|
@@ -22,6 +22,11 @@ from typing import List
|
|
22
22
|
from mcp.types import EmbeddedResource, ImageContent, TextContent, TextResourceContents
|
23
23
|
|
24
24
|
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
25
|
+
from mcp_agent.mcp.prompts.prompt_constants import (
|
26
|
+
ASSISTANT_DELIMITER,
|
27
|
+
RESOURCE_DELIMITER,
|
28
|
+
USER_DELIMITER,
|
29
|
+
)
|
25
30
|
|
26
31
|
# -------------------------------------------------------------------------
|
27
32
|
# JSON Serialization Functions
|
@@ -43,7 +48,9 @@ def multipart_messages_to_json(messages: List[PromptMessageMultipart]) -> str:
|
|
43
48
|
"""
|
44
49
|
# Convert to dictionaries using model_dump with proper JSON mode
|
45
50
|
# The mode="json" parameter ensures proper handling of AnyUrl objects
|
46
|
-
message_dicts = [
|
51
|
+
message_dicts = [
|
52
|
+
message.model_dump(by_alias=True, mode="json", exclude_none=True) for message in messages
|
53
|
+
]
|
47
54
|
|
48
55
|
# Convert to JSON string
|
49
56
|
return json.dumps(message_dicts, indent=2)
|
@@ -110,9 +117,9 @@ def load_messages_from_json_file(file_path: str) -> List[PromptMessageMultipart]
|
|
110
117
|
|
111
118
|
def multipart_messages_to_delimited_format(
|
112
119
|
messages: List[PromptMessageMultipart],
|
113
|
-
user_delimiter: str =
|
114
|
-
assistant_delimiter: str =
|
115
|
-
resource_delimiter: str =
|
120
|
+
user_delimiter: str = USER_DELIMITER,
|
121
|
+
assistant_delimiter: str = ASSISTANT_DELIMITER,
|
122
|
+
resource_delimiter: str = RESOURCE_DELIMITER,
|
116
123
|
combine_text: bool = True, # Set to False to maintain backward compatibility
|
117
124
|
) -> List[str]:
|
118
125
|
"""
|
@@ -189,9 +196,9 @@ def multipart_messages_to_delimited_format(
|
|
189
196
|
|
190
197
|
def delimited_format_to_multipart_messages(
|
191
198
|
content: str,
|
192
|
-
user_delimiter: str =
|
193
|
-
assistant_delimiter: str =
|
194
|
-
resource_delimiter: str =
|
199
|
+
user_delimiter: str = USER_DELIMITER,
|
200
|
+
assistant_delimiter: str = ASSISTANT_DELIMITER,
|
201
|
+
resource_delimiter: str = RESOURCE_DELIMITER,
|
195
202
|
) -> List[PromptMessageMultipart]:
|
196
203
|
"""
|
197
204
|
Parse hybrid delimited format into PromptMessageMultipart objects:
|
@@ -365,9 +372,9 @@ def delimited_format_to_multipart_messages(
|
|
365
372
|
def save_messages_to_delimited_file(
|
366
373
|
messages: List[PromptMessageMultipart],
|
367
374
|
file_path: str,
|
368
|
-
user_delimiter: str =
|
369
|
-
assistant_delimiter: str =
|
370
|
-
resource_delimiter: str =
|
375
|
+
user_delimiter: str = USER_DELIMITER,
|
376
|
+
assistant_delimiter: str = ASSISTANT_DELIMITER,
|
377
|
+
resource_delimiter: str = RESOURCE_DELIMITER,
|
371
378
|
combine_text: bool = True,
|
372
379
|
) -> None:
|
373
380
|
"""
|
@@ -395,9 +402,9 @@ def save_messages_to_delimited_file(
|
|
395
402
|
|
396
403
|
def load_messages_from_delimited_file(
|
397
404
|
file_path: str,
|
398
|
-
user_delimiter: str =
|
399
|
-
assistant_delimiter: str =
|
400
|
-
resource_delimiter: str =
|
405
|
+
user_delimiter: str = USER_DELIMITER,
|
406
|
+
assistant_delimiter: str = ASSISTANT_DELIMITER,
|
407
|
+
resource_delimiter: str = RESOURCE_DELIMITER,
|
401
408
|
) -> List[PromptMessageMultipart]:
|
402
409
|
"""
|
403
410
|
Load PromptMessageMultipart objects from a file in hybrid delimited format.
|