fast-agent-mcp 0.1.12__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.12.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 +61 -415
- 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 +11 -21
- mcp_agent/agents/workflow/parallel_agent.py +182 -0
- mcp_agent/agents/workflow/router_agent.py +307 -0
- mcp_agent/app.py +15 -19
- mcp_agent/cli/commands/bootstrap.py +19 -38
- mcp_agent/cli/commands/config.py +4 -4
- mcp_agent/cli/commands/setup.py +7 -14
- mcp_agent/cli/main.py +7 -10
- mcp_agent/cli/terminal.py +3 -3
- mcp_agent/config.py +25 -40
- mcp_agent/context.py +12 -21
- mcp_agent/context_dependent.py +3 -5
- 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 +23 -55
- mcp_agent/core/exceptions.py +8 -8
- mcp_agent/core/fastagent.py +145 -371
- mcp_agent/core/interactive_prompt.py +424 -0
- mcp_agent/core/mcp_content.py +17 -17
- mcp_agent/core/prompt.py +6 -9
- mcp_agent/core/request_params.py +6 -3
- mcp_agent/core/validation.py +92 -18
- mcp_agent/executor/decorator_registry.py +9 -17
- mcp_agent/executor/executor.py +8 -17
- mcp_agent/executor/task_registry.py +2 -4
- mcp_agent/executor/temporal.py +19 -41
- mcp_agent/executor/workflow.py +3 -5
- mcp_agent/executor/workflow_signal.py +15 -21
- mcp_agent/human_input/handler.py +4 -7
- mcp_agent/human_input/types.py +2 -3
- mcp_agent/llm/__init__.py +2 -0
- mcp_agent/llm/augmented_llm.py +450 -0
- mcp_agent/llm/augmented_llm_passthrough.py +162 -0
- mcp_agent/llm/augmented_llm_playback.py +83 -0
- mcp_agent/llm/memory.py +103 -0
- mcp_agent/{workflows/llm → llm}/model_factory.py +22 -16
- mcp_agent/{workflows/llm → llm}/prompt_utils.py +1 -3
- mcp_agent/llm/providers/__init__.py +8 -0
- mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +8 -25
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +56 -194
- mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +99 -190
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +72 -71
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +65 -71
- mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +16 -44
- mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +4 -4
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +9 -11
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +8 -12
- mcp_agent/{workflows/llm → llm}/sampling_converter.py +3 -31
- mcp_agent/llm/sampling_format_converter.py +37 -0
- mcp_agent/logging/events.py +1 -5
- mcp_agent/logging/json_serializer.py +7 -6
- mcp_agent/logging/listeners.py +20 -23
- mcp_agent/logging/logger.py +17 -19
- mcp_agent/logging/rich_progress.py +10 -8
- mcp_agent/logging/tracing.py +4 -6
- mcp_agent/logging/transport.py +22 -22
- mcp_agent/mcp/gen_client.py +1 -3
- mcp_agent/mcp/interfaces.py +117 -110
- mcp_agent/mcp/logger_textio.py +97 -0
- mcp_agent/mcp/mcp_agent_client_session.py +7 -7
- mcp_agent/mcp/mcp_agent_server.py +8 -8
- mcp_agent/mcp/mcp_aggregator.py +102 -143
- mcp_agent/mcp/mcp_connection_manager.py +20 -27
- mcp_agent/mcp/prompt_message_multipart.py +68 -16
- mcp_agent/mcp/prompt_render.py +77 -0
- mcp_agent/mcp/prompt_serialization.py +30 -48
- 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 +109 -0
- mcp_agent/mcp/prompts/prompt_server.py +155 -195
- mcp_agent/mcp/prompts/prompt_template.py +35 -66
- mcp_agent/mcp/resource_utils.py +7 -14
- mcp_agent/mcp/sampling.py +17 -17
- mcp_agent/mcp_server/agent_server.py +13 -17
- mcp_agent/mcp_server_registry.py +13 -22
- mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +3 -2
- mcp_agent/resources/examples/in_dev/slides.py +110 -0
- mcp_agent/resources/examples/internal/agent.py +6 -3
- mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
- mcp_agent/resources/examples/internal/job.py +2 -1
- mcp_agent/resources/examples/internal/prompt_category.py +1 -1
- mcp_agent/resources/examples/internal/prompt_sizing.py +3 -5
- mcp_agent/resources/examples/internal/sizer.py +2 -1
- mcp_agent/resources/examples/internal/social.py +2 -1
- mcp_agent/resources/examples/prompting/agent.py +2 -1
- mcp_agent/resources/examples/prompting/image_server.py +4 -8
- mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
- mcp_agent/ui/console_display.py +16 -20
- fast_agent_mcp-0.1.12.dist-info/RECORD +0 -161
- mcp_agent/core/agent_app.py +0 -646
- mcp_agent/core/agent_utils.py +0 -71
- mcp_agent/core/decorators.py +0 -455
- mcp_agent/core/factory.py +0 -463
- mcp_agent/core/proxies.py +0 -269
- mcp_agent/core/types.py +0 -24
- mcp_agent/eval/__init__.py +0 -0
- mcp_agent/mcp/stdio.py +0 -111
- 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 -190
- mcp_agent/resources/examples/researcher/researcher.py +0 -38
- mcp_agent/resources/examples/workflows/chaining.py +0 -44
- mcp_agent/resources/examples/workflows/evaluator.py +0 -78
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
- mcp_agent/resources/examples/workflows/human_input.py +0 -25
- mcp_agent/resources/examples/workflows/orchestrator.py +0 -73
- mcp_agent/resources/examples/workflows/parallel.py +0 -78
- mcp_agent/resources/examples/workflows/router.py +0 -53
- mcp_agent/resources/examples/workflows/sse.py +0 -23
- mcp_agent/telemetry/__init__.py +0 -0
- mcp_agent/telemetry/usage_tracking.py +0 -18
- mcp_agent/workflows/__init__.py +0 -0
- mcp_agent/workflows/embedding/__init__.py +0 -0
- mcp_agent/workflows/embedding/embedding_base.py +0 -61
- mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
- mcp_agent/workflows/embedding/embedding_openai.py +0 -46
- mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -481
- mcp_agent/workflows/intent_classifier/__init__.py +0 -0
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -120
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -134
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -45
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -45
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -161
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -60
- mcp_agent/workflows/llm/__init__.py +0 -0
- mcp_agent/workflows/llm/augmented_llm.py +0 -753
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +0 -241
- mcp_agent/workflows/llm/augmented_llm_playback.py +0 -109
- mcp_agent/workflows/llm/providers/__init__.py +0 -8
- mcp_agent/workflows/llm/sampling_format_converter.py +0 -22
- mcp_agent/workflows/orchestrator/__init__.py +0 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +0 -578
- mcp_agent/workflows/parallel/__init__.py +0 -0
- mcp_agent/workflows/parallel/fan_in.py +0 -350
- mcp_agent/workflows/parallel/fan_out.py +0 -187
- mcp_agent/workflows/parallel/parallel_llm.py +0 -166
- mcp_agent/workflows/router/__init__.py +0 -0
- mcp_agent/workflows/router/router_base.py +0 -368
- mcp_agent/workflows/router/router_embedding.py +0 -240
- 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 -320
- mcp_agent/workflows/swarm/__init__.py +0 -0
- mcp_agent/workflows/swarm/swarm.py +0 -320
- mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
- mcp_agent/workflows/swarm/swarm_openai.py +0 -41
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.12.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/mcp/mcp_aggregator.py
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
from asyncio import Lock, gather
|
2
2
|
from typing import (
|
3
|
-
List,
|
4
|
-
Dict,
|
5
|
-
Optional,
|
6
3
|
TYPE_CHECKING,
|
7
4
|
Any,
|
8
5
|
Callable,
|
6
|
+
Dict,
|
7
|
+
List,
|
8
|
+
Optional,
|
9
9
|
TypeVar,
|
10
10
|
)
|
11
|
+
|
11
12
|
from mcp import GetPromptResult, ReadResourceResult
|
12
|
-
from pydantic import AnyUrl, BaseModel, ConfigDict
|
13
13
|
from mcp.client.session import ClientSession
|
14
14
|
from mcp.server.lowlevel.server import Server
|
15
15
|
from mcp.server.stdio import stdio_server
|
16
16
|
from mcp.types import (
|
17
17
|
CallToolResult,
|
18
18
|
ListToolsResult,
|
19
|
+
Prompt,
|
19
20
|
TextContent,
|
20
21
|
Tool,
|
21
|
-
Prompt,
|
22
22
|
)
|
23
|
+
from pydantic import AnyUrl, BaseModel, ConfigDict
|
23
24
|
|
25
|
+
from mcp_agent.context_dependent import ContextDependent
|
24
26
|
from mcp_agent.event_progress import ProgressAction
|
25
27
|
from mcp_agent.logging.logger import get_logger
|
26
28
|
from mcp_agent.mcp.gen_client import gen_client
|
27
|
-
|
28
|
-
from mcp_agent.context_dependent import ContextDependent
|
29
29
|
from mcp_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
|
30
30
|
from mcp_agent.mcp.mcp_connection_manager import MCPConnectionManager
|
31
31
|
|
@@ -33,9 +33,7 @@ if TYPE_CHECKING:
|
|
33
33
|
from mcp_agent.context import Context
|
34
34
|
|
35
35
|
|
36
|
-
logger = get_logger(
|
37
|
-
__name__
|
38
|
-
) # This will be replaced per-instance when agent_name is available
|
36
|
+
logger = get_logger(__name__) # This will be replaced per-instance when agent_name is available
|
39
37
|
|
40
38
|
SEP = "-"
|
41
39
|
|
@@ -99,7 +97,7 @@ class MCPAggregator(ContextDependent):
|
|
99
97
|
context: Optional["Context"] = None,
|
100
98
|
name: str = None,
|
101
99
|
**kwargs,
|
102
|
-
):
|
100
|
+
) -> None:
|
103
101
|
"""
|
104
102
|
:param server_names: A list of server names to connect to.
|
105
103
|
:param connection_persistence: Whether to maintain persistent connections to servers (default: True).
|
@@ -130,7 +128,7 @@ class MCPAggregator(ContextDependent):
|
|
130
128
|
self._prompt_cache: Dict[str, List[Prompt]] = {}
|
131
129
|
self._prompt_cache_lock = Lock()
|
132
130
|
|
133
|
-
async def close(self):
|
131
|
+
async def close(self) -> None:
|
134
132
|
"""
|
135
133
|
Close all persistent connections when the aggregator is deleted.
|
136
134
|
"""
|
@@ -139,14 +137,11 @@ class MCPAggregator(ContextDependent):
|
|
139
137
|
# Only attempt cleanup if we own the connection manager
|
140
138
|
if (
|
141
139
|
hasattr(self.context, "_connection_manager")
|
142
|
-
and self.context._connection_manager
|
143
|
-
== self._persistent_connection_manager
|
140
|
+
and self.context._connection_manager == self._persistent_connection_manager
|
144
141
|
):
|
145
142
|
logger.info("Shutting down all persistent connections...")
|
146
143
|
await self._persistent_connection_manager.disconnect_all()
|
147
|
-
await self._persistent_connection_manager.__aexit__(
|
148
|
-
None, None, None
|
149
|
-
)
|
144
|
+
await self._persistent_connection_manager.__aexit__(None, None, None)
|
150
145
|
delattr(self.context, "_connection_manager")
|
151
146
|
self.initialized = False
|
152
147
|
except Exception as e:
|
@@ -185,7 +180,7 @@ class MCPAggregator(ContextDependent):
|
|
185
180
|
logger.error(f"Error creating MCPAggregator: {e}")
|
186
181
|
await instance.__aexit__(None, None, None)
|
187
182
|
|
188
|
-
async def load_servers(self):
|
183
|
+
async def load_servers(self) -> None:
|
189
184
|
"""
|
190
185
|
Discover tools from each server in parallel and build an index of namespaced tool names.
|
191
186
|
Also populate the prompt cache.
|
@@ -232,9 +227,7 @@ class MCPAggregator(ContextDependent):
|
|
232
227
|
logger.error(f"Error loading tools from server '{server_name}'", data=e)
|
233
228
|
return []
|
234
229
|
|
235
|
-
async def fetch_prompts(
|
236
|
-
client: ClientSession, server_name: str
|
237
|
-
) -> List[Prompt]:
|
230
|
+
async def fetch_prompts(client: ClientSession, server_name: str) -> List[Prompt]:
|
238
231
|
# Only fetch prompts if the server supports them
|
239
232
|
capabilities = await self.get_capabilities(server_name)
|
240
233
|
if not capabilities or not capabilities.prompts:
|
@@ -253,10 +246,8 @@ class MCPAggregator(ContextDependent):
|
|
253
246
|
prompts: List[Prompt] = []
|
254
247
|
|
255
248
|
if self.connection_persistence:
|
256
|
-
server_connection = (
|
257
|
-
|
258
|
-
server_name, client_session_factory=MCPAgentClientSession
|
259
|
-
)
|
249
|
+
server_connection = await self._persistent_connection_manager.get_server(
|
250
|
+
server_name, client_session_factory=MCPAgentClientSession
|
260
251
|
)
|
261
252
|
tools = await fetch_tools(server_connection.session)
|
262
253
|
prompts = await fetch_prompts(server_connection.session, server_name)
|
@@ -378,7 +369,9 @@ class MCPAggregator(ContextDependent):
|
|
378
369
|
method = getattr(client, method_name)
|
379
370
|
return await method(**method_args)
|
380
371
|
except Exception as e:
|
381
|
-
error_msg =
|
372
|
+
error_msg = (
|
373
|
+
f"Failed to {method_name} '{operation_name}' on server '{server_name}': {e}"
|
374
|
+
)
|
382
375
|
logger.error(error_msg)
|
383
376
|
return error_factory(error_msg) if error_factory else None
|
384
377
|
|
@@ -410,9 +403,7 @@ class MCPAggregator(ContextDependent):
|
|
410
403
|
)
|
411
404
|
return result
|
412
405
|
|
413
|
-
async def _parse_resource_name(
|
414
|
-
self, name: str, resource_type: str
|
415
|
-
) -> tuple[str, str]:
|
406
|
+
async def _parse_resource_name(self, name: str, resource_type: str) -> tuple[str, str]:
|
416
407
|
"""
|
417
408
|
Parse a possibly namespaced resource name into server name and local resource name.
|
418
409
|
|
@@ -447,9 +438,7 @@ class MCPAggregator(ContextDependent):
|
|
447
438
|
|
448
439
|
return server_name, local_name
|
449
440
|
|
450
|
-
async def call_tool(
|
451
|
-
self, name: str, arguments: dict | None = None
|
452
|
-
) -> CallToolResult:
|
441
|
+
async def call_tool(self, name: str, arguments: dict | None = None) -> CallToolResult:
|
453
442
|
"""
|
454
443
|
Call a namespaced tool, e.g., 'server_name.tool_name'.
|
455
444
|
"""
|
@@ -487,7 +476,7 @@ class MCPAggregator(ContextDependent):
|
|
487
476
|
)
|
488
477
|
|
489
478
|
async def get_prompt(
|
490
|
-
self, prompt_name: str
|
479
|
+
self, prompt_name: str | None, arguments: dict[str, str] | None
|
491
480
|
) -> GetPromptResult:
|
492
481
|
"""
|
493
482
|
Get a prompt from a server.
|
@@ -540,9 +529,7 @@ class MCPAggregator(ContextDependent):
|
|
540
529
|
async with self._prompt_cache_lock:
|
541
530
|
if server_name in self._prompt_cache:
|
542
531
|
# Check if any prompt in the cache has this name
|
543
|
-
prompt_names = [
|
544
|
-
prompt.name for prompt in self._prompt_cache[server_name]
|
545
|
-
]
|
532
|
+
prompt_names = [prompt.name for prompt in self._prompt_cache[server_name]]
|
546
533
|
if local_prompt_name not in prompt_names:
|
547
534
|
logger.debug(
|
548
535
|
f"Prompt '{local_prompt_name}' not found in cache for server '{server_name}'"
|
@@ -568,9 +555,7 @@ class MCPAggregator(ContextDependent):
|
|
568
555
|
|
569
556
|
# Add namespaced name and source server to the result
|
570
557
|
if result and result.messages:
|
571
|
-
result.namespaced_name =
|
572
|
-
namespaced_name or f"{server_name}{SEP}{local_prompt_name}"
|
573
|
-
)
|
558
|
+
result.namespaced_name = namespaced_name or f"{server_name}{SEP}{local_prompt_name}"
|
574
559
|
|
575
560
|
# Store the arguments in the result for display purposes
|
576
561
|
if arguments:
|
@@ -599,9 +584,7 @@ class MCPAggregator(ContextDependent):
|
|
599
584
|
# Check if this server supports prompts
|
600
585
|
capabilities = await self.get_capabilities(s_name)
|
601
586
|
if not capabilities or not capabilities.prompts:
|
602
|
-
logger.debug(
|
603
|
-
f"Server '{s_name}' does not support prompts, skipping"
|
604
|
-
)
|
587
|
+
logger.debug(f"Server '{s_name}' does not support prompts, skipping")
|
605
588
|
continue
|
606
589
|
|
607
590
|
try:
|
@@ -635,9 +618,7 @@ class MCPAggregator(ContextDependent):
|
|
635
618
|
except Exception as e:
|
636
619
|
logger.debug(f"Error retrieving prompt from server '{s_name}': {e}")
|
637
620
|
else:
|
638
|
-
logger.debug(
|
639
|
-
f"Prompt '{local_prompt_name}' not found in any server's cache"
|
640
|
-
)
|
621
|
+
logger.debug(f"Prompt '{local_prompt_name}' not found in any server's cache")
|
641
622
|
|
642
623
|
# If not in cache, perform a full search as fallback (cache might be outdated)
|
643
624
|
# First identify servers that support prompts
|
@@ -691,9 +672,7 @@ class MCPAggregator(ContextDependent):
|
|
691
672
|
)
|
692
673
|
|
693
674
|
prompts = getattr(prompt_list_result, "prompts", [])
|
694
|
-
matching_prompts = [
|
695
|
-
p for p in prompts if p.name == local_prompt_name
|
696
|
-
]
|
675
|
+
matching_prompts = [p for p in prompts if p.name == local_prompt_name]
|
697
676
|
if matching_prompts:
|
698
677
|
async with self._prompt_cache_lock:
|
699
678
|
if s_name not in self._prompt_cache:
|
@@ -703,9 +682,7 @@ class MCPAggregator(ContextDependent):
|
|
703
682
|
p.name for p in self._prompt_cache[s_name]
|
704
683
|
]
|
705
684
|
if local_prompt_name not in prompt_names_in_cache:
|
706
|
-
self._prompt_cache[s_name].append(
|
707
|
-
matching_prompts[0]
|
708
|
-
)
|
685
|
+
self._prompt_cache[s_name].append(matching_prompts[0])
|
709
686
|
except Exception:
|
710
687
|
# Ignore errors when updating cache
|
711
688
|
pass
|
@@ -723,114 +700,102 @@ class MCPAggregator(ContextDependent):
|
|
723
700
|
messages=[],
|
724
701
|
)
|
725
702
|
|
726
|
-
async def list_prompts(self, server_name: str = None):
|
703
|
+
async def list_prompts(self, server_name: str = None) -> Dict[str, List[Prompt]]:
|
727
704
|
"""
|
728
705
|
List available prompts from one or all servers.
|
729
706
|
|
730
707
|
:param server_name: Optional server name to list prompts from. If not provided,
|
731
708
|
lists prompts from all servers.
|
732
|
-
:return: Dictionary mapping server names to lists of
|
709
|
+
:return: Dictionary mapping server names to lists of Prompt objects
|
733
710
|
"""
|
734
711
|
if not self.initialized:
|
735
712
|
await self.load_servers()
|
736
713
|
|
737
|
-
results = {}
|
714
|
+
results: Dict[str, List[Prompt]] = {}
|
715
|
+
|
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
|
738
721
|
|
739
|
-
|
740
|
-
# we can use the cache directly
|
741
|
-
if not server_name:
|
722
|
+
# Check cache first
|
742
723
|
async with self._prompt_cache_lock:
|
743
|
-
if
|
744
|
-
|
745
|
-
|
746
|
-
results[s_name] = prompt_list
|
747
|
-
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}'")
|
748
727
|
return results
|
749
728
|
|
750
|
-
|
751
|
-
|
752
|
-
if
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
results[server_name] = self._prompt_cache[server_name]
|
757
|
-
logger.debug(
|
758
|
-
f"Returning cached prompts for server '{server_name}'"
|
759
|
-
)
|
760
|
-
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
|
761
735
|
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
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] = []
|
768
772
|
|
769
|
-
|
773
|
+
# Fetch prompts from supported servers
|
774
|
+
for s_name in supported_servers:
|
775
|
+
try:
|
770
776
|
result = await self._execute_on_server(
|
771
|
-
server_name=
|
777
|
+
server_name=s_name,
|
772
778
|
operation_type="prompts-list",
|
773
779
|
operation_name="",
|
774
780
|
method_name="list_prompts",
|
775
|
-
error_factory=lambda _:
|
781
|
+
error_factory=lambda _: None,
|
776
782
|
)
|
777
783
|
|
778
|
-
|
779
|
-
async with self._prompt_cache_lock:
|
780
|
-
self._prompt_cache[server_name] = getattr(result, "prompts", [])
|
784
|
+
prompts = getattr(result, "prompts", [])
|
781
785
|
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
else:
|
786
|
-
# We need to filter the servers that support prompts
|
787
|
-
supported_servers = []
|
788
|
-
for s_name in self.server_names:
|
789
|
-
capabilities = await self.get_capabilities(s_name)
|
790
|
-
if capabilities and capabilities.prompts:
|
791
|
-
supported_servers.append(s_name)
|
792
|
-
else:
|
793
|
-
logger.debug(
|
794
|
-
f"Server '{s_name}' does not support prompts, skipping"
|
795
|
-
)
|
796
|
-
# Add empty list to results for this server
|
797
|
-
results[s_name] = []
|
798
|
-
|
799
|
-
# Process servers sequentially to ensure proper resource cleanup
|
800
|
-
# This helps prevent resource leaks especially on Windows
|
801
|
-
if supported_servers:
|
802
|
-
server_results = []
|
803
|
-
for s_name in supported_servers:
|
804
|
-
try:
|
805
|
-
result = await self._execute_on_server(
|
806
|
-
server_name=s_name,
|
807
|
-
operation_type="prompts-list",
|
808
|
-
operation_name="",
|
809
|
-
method_name="list_prompts",
|
810
|
-
error_factory=lambda _: [],
|
811
|
-
)
|
812
|
-
server_results.append(result)
|
813
|
-
except Exception as e:
|
814
|
-
logger.debug(f"Error fetching prompts from {s_name}: {e}")
|
815
|
-
server_results.append(e)
|
816
|
-
|
817
|
-
for i, result in enumerate(server_results):
|
818
|
-
if isinstance(result, BaseException):
|
819
|
-
continue
|
820
|
-
|
821
|
-
s_name = supported_servers[i]
|
822
|
-
results[s_name] = result
|
786
|
+
# Update cache and results
|
787
|
+
async with self._prompt_cache_lock:
|
788
|
+
self._prompt_cache[s_name] = prompts
|
823
789
|
|
824
|
-
|
825
|
-
|
826
|
-
|
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] = []
|
827
794
|
|
828
795
|
logger.debug(f"Available prompts across servers: {results}")
|
829
796
|
return results
|
830
797
|
|
831
|
-
async def get_resource(
|
832
|
-
self, server_name: str, resource_uri: str
|
833
|
-
) -> ReadResourceResult:
|
798
|
+
async def get_resource(self, server_name: str, resource_uri: str) -> ReadResourceResult:
|
834
799
|
"""
|
835
800
|
Get a resource directly from an MCP server by URI.
|
836
801
|
|
@@ -881,7 +846,7 @@ class MCPCompoundServer(Server):
|
|
881
846
|
A compound server (server-of-servers) that aggregates multiple MCP servers and is itself an MCP server
|
882
847
|
"""
|
883
848
|
|
884
|
-
def __init__(self, server_names: List[str], name: str = "MCPCompoundServer"):
|
849
|
+
def __init__(self, server_names: List[str], name: str = "MCPCompoundServer") -> None:
|
885
850
|
super().__init__(name)
|
886
851
|
self.aggregator = MCPAggregator(server_names)
|
887
852
|
|
@@ -896,9 +861,7 @@ class MCPCompoundServer(Server):
|
|
896
861
|
tools_result = await self.aggregator.list_tools()
|
897
862
|
return tools_result.tools
|
898
863
|
|
899
|
-
async def _call_tool(
|
900
|
-
self, name: str, arguments: dict | None = None
|
901
|
-
) -> CallToolResult:
|
864
|
+
async def _call_tool(self, name: str, arguments: dict | None = None) -> CallToolResult:
|
902
865
|
"""Call a specific tool from the aggregated servers."""
|
903
866
|
try:
|
904
867
|
result = await self.aggregator.call_tool(name=name, arguments=arguments)
|
@@ -920,16 +883,12 @@ class MCPCompoundServer(Server):
|
|
920
883
|
arguments: Optional dictionary of string arguments for prompt templating
|
921
884
|
"""
|
922
885
|
try:
|
923
|
-
result = await self.aggregator.get_prompt(
|
924
|
-
prompt_name=name, arguments=arguments
|
925
|
-
)
|
886
|
+
result = await self.aggregator.get_prompt(prompt_name=name, arguments=arguments)
|
926
887
|
return result
|
927
888
|
except Exception as e:
|
928
|
-
return GetPromptResult(
|
929
|
-
description=f"Error getting prompt: {e}", messages=[]
|
930
|
-
)
|
889
|
+
return GetPromptResult(description=f"Error getting prompt: {e}", messages=[])
|
931
890
|
|
932
|
-
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]]:
|
933
892
|
"""List available prompts from the aggregated servers."""
|
934
893
|
try:
|
935
894
|
return await self.aggregator.list_prompts(server_name=server_name)
|
@@ -2,38 +2,38 @@
|
|
2
2
|
Manages the lifecycle of multiple MCP server connections.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from datetime import timedelta
|
6
5
|
import asyncio
|
6
|
+
from datetime import timedelta
|
7
7
|
from typing import (
|
8
|
+
TYPE_CHECKING,
|
8
9
|
AsyncGenerator,
|
9
10
|
Callable,
|
10
11
|
Dict,
|
11
12
|
Optional,
|
12
|
-
TYPE_CHECKING,
|
13
13
|
)
|
14
14
|
|
15
|
-
from anyio import Event,
|
15
|
+
from anyio import Event, Lock, create_task_group
|
16
16
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
17
|
-
|
18
17
|
from mcp import ClientSession
|
18
|
+
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
|
-
from mcp.client.sse import sse_client
|
24
24
|
from mcp.types import JSONRPCMessage, ServerCapabilities
|
25
25
|
|
26
26
|
from mcp_agent.config import MCPServerSettings
|
27
|
+
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
|
-
from mcp_agent.context_dependent import ContextDependent
|
33
33
|
|
34
34
|
if TYPE_CHECKING:
|
35
|
-
from mcp_agent.mcp_server_registry import InitHookCallable, ServerRegistry
|
36
35
|
from mcp_agent.context import Context
|
36
|
+
from mcp_agent.mcp_server_registry import InitHookCallable, ServerRegistry
|
37
37
|
|
38
38
|
logger = get_logger(__name__)
|
39
39
|
|
@@ -64,7 +64,7 @@ class ServerConnection:
|
|
64
64
|
ClientSession,
|
65
65
|
],
|
66
66
|
init_hook: Optional["InitHookCallable"] = None,
|
67
|
-
):
|
67
|
+
) -> None:
|
68
68
|
self.server_name = server_name
|
69
69
|
self.server_config = server_config
|
70
70
|
self.session: ClientSession | None = None
|
@@ -194,7 +194,7 @@ class MCPConnectionManager(ContextDependent):
|
|
194
194
|
|
195
195
|
def __init__(
|
196
196
|
self, server_registry: "ServerRegistry", context: Optional["Context"] = None
|
197
|
-
):
|
197
|
+
) -> None:
|
198
198
|
super().__init__(context=context)
|
199
199
|
self.server_registry = server_registry
|
200
200
|
self.running_servers: Dict[str, ServerConnection] = {}
|
@@ -255,9 +255,7 @@ class MCPConnectionManager(ContextDependent):
|
|
255
255
|
if not config:
|
256
256
|
raise ValueError(f"Server '{server_name}' not found in registry.")
|
257
257
|
|
258
|
-
logger.debug(
|
259
|
-
f"{server_name}: Found server configuration=", data=config.model_dump()
|
260
|
-
)
|
258
|
+
logger.debug(f"{server_name}: Found server configuration=", data=config.model_dump())
|
261
259
|
|
262
260
|
def transport_context_factory():
|
263
261
|
if config.transport == "stdio":
|
@@ -266,8 +264,11 @@ class MCPConnectionManager(ContextDependent):
|
|
266
264
|
args=config.args,
|
267
265
|
env={**get_default_environment(), **(config.env or {})},
|
268
266
|
)
|
269
|
-
# Create
|
270
|
-
|
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)
|
271
272
|
elif config.transport == "sse":
|
272
273
|
return sse_client(config.url)
|
273
274
|
else:
|
@@ -309,9 +310,7 @@ class MCPConnectionManager(ContextDependent):
|
|
309
310
|
|
310
311
|
# If server exists but isn't healthy, remove it so we can create a new one
|
311
312
|
if server_conn:
|
312
|
-
logger.info(
|
313
|
-
f"{server_name}: Server exists but is unhealthy, recreating..."
|
314
|
-
)
|
313
|
+
logger.info(f"{server_name}: Server exists but is unhealthy, recreating...")
|
315
314
|
self.running_servers.pop(server_name)
|
316
315
|
server_conn.request_shutdown()
|
317
316
|
|
@@ -334,9 +333,7 @@ class MCPConnectionManager(ContextDependent):
|
|
334
333
|
|
335
334
|
return server_conn
|
336
335
|
|
337
|
-
async def get_server_capabilities(
|
338
|
-
self, server_name: str
|
339
|
-
) -> ServerCapabilities | None:
|
336
|
+
async def get_server_capabilities(self, server_name: str) -> ServerCapabilities | None:
|
340
337
|
"""Get the capabilities of a specific server."""
|
341
338
|
server_conn = await self.get_server(
|
342
339
|
server_name, client_session_factory=MCPAgentClientSession
|
@@ -353,13 +350,9 @@ class MCPConnectionManager(ContextDependent):
|
|
353
350
|
server_conn = self.running_servers.pop(server_name, None)
|
354
351
|
if server_conn:
|
355
352
|
server_conn.request_shutdown()
|
356
|
-
logger.info(
|
357
|
-
f"{server_name}: Shutdown signal sent (lifecycle task will exit)."
|
358
|
-
)
|
353
|
+
logger.info(f"{server_name}: Shutdown signal sent (lifecycle task will exit).")
|
359
354
|
else:
|
360
|
-
logger.info(
|
361
|
-
f"{server_name}: No persistent connection found. Skipping server shutdown"
|
362
|
-
)
|
355
|
+
logger.info(f"{server_name}: No persistent connection found. Skipping server shutdown")
|
363
356
|
|
364
357
|
async def disconnect_all(self) -> None:
|
365
358
|
"""Disconnect all servers that are running under this connection manager."""
|