fast-agent-mcp 0.3.13__py3-none-any.whl → 0.3.15__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/agents/llm_agent.py +59 -37
- fast_agent/agents/llm_decorator.py +13 -2
- fast_agent/agents/mcp_agent.py +21 -5
- fast_agent/agents/tool_agent.py +41 -29
- fast_agent/agents/workflow/router_agent.py +2 -1
- fast_agent/cli/commands/check_config.py +48 -1
- fast_agent/config.py +65 -2
- fast_agent/constants.py +3 -0
- fast_agent/context.py +42 -9
- fast_agent/core/fastagent.py +14 -1
- fast_agent/core/logging/listeners.py +1 -1
- fast_agent/core/validation.py +31 -33
- fast_agent/event_progress.py +2 -3
- fast_agent/human_input/form_fields.py +4 -1
- fast_agent/interfaces.py +12 -2
- fast_agent/llm/fastagent_llm.py +31 -0
- fast_agent/llm/model_database.py +2 -2
- fast_agent/llm/model_factory.py +8 -1
- fast_agent/llm/provider_key_manager.py +1 -0
- fast_agent/llm/provider_types.py +1 -0
- fast_agent/llm/request_params.py +3 -1
- fast_agent/mcp/mcp_aggregator.py +313 -40
- fast_agent/mcp/mcp_connection_manager.py +39 -9
- fast_agent/mcp/prompt_message_extended.py +2 -2
- fast_agent/mcp/skybridge.py +45 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/transport_tracking.py +37 -3
- fast_agent/mcp/types.py +24 -0
- fast_agent/resources/examples/workflows/router.py +1 -0
- fast_agent/resources/setup/fastagent.config.yaml +7 -1
- fast_agent/ui/console_display.py +946 -84
- fast_agent/ui/elicitation_form.py +23 -1
- fast_agent/ui/enhanced_prompt.py +153 -58
- fast_agent/ui/interactive_prompt.py +57 -34
- fast_agent/ui/markdown_truncator.py +942 -0
- fast_agent/ui/mcp_display.py +110 -29
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/rich_progress.py +4 -1
- fast_agent/ui/streaming_buffer.py +449 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/METADATA +4 -3
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/RECORD +44 -38
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/licenses/LICENSE +0 -0
fast_agent/mcp/mcp_aggregator.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import (
|
|
|
11
11
|
Mapping,
|
|
12
12
|
Optional,
|
|
13
13
|
TypeVar,
|
|
14
|
+
cast,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
from mcp import GetPromptResult, ReadResourceResult
|
|
@@ -34,6 +35,12 @@ from fast_agent.mcp.common import SEP, create_namespaced_name, is_namespaced_nam
|
|
|
34
35
|
from fast_agent.mcp.gen_client import gen_client
|
|
35
36
|
from fast_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
|
|
36
37
|
from fast_agent.mcp.mcp_connection_manager import MCPConnectionManager
|
|
38
|
+
from fast_agent.mcp.skybridge import (
|
|
39
|
+
SKYBRIDGE_MIME_TYPE,
|
|
40
|
+
SkybridgeResourceConfig,
|
|
41
|
+
SkybridgeServerConfig,
|
|
42
|
+
SkybridgeToolConfig,
|
|
43
|
+
)
|
|
37
44
|
from fast_agent.mcp.transport_tracking import TransportSnapshot
|
|
38
45
|
|
|
39
46
|
if TYPE_CHECKING:
|
|
@@ -96,6 +103,7 @@ class ServerStatus(BaseModel):
|
|
|
96
103
|
spoofing_enabled: bool | None = None
|
|
97
104
|
session_id: str | None = None
|
|
98
105
|
transport_channels: TransportSnapshot | None = None
|
|
106
|
+
skybridge: SkybridgeServerConfig | None = None
|
|
99
107
|
|
|
100
108
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
101
109
|
|
|
@@ -124,12 +132,17 @@ class MCPAggregator(ContextDependent):
|
|
|
124
132
|
# Keep a connection manager to manage persistent connections for this aggregator
|
|
125
133
|
if self.connection_persistence:
|
|
126
134
|
# Try to get existing connection manager from context
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
context = self.context
|
|
136
|
+
if not hasattr(context, "_connection_manager") or context._connection_manager is None:
|
|
137
|
+
server_registry = context.server_registry
|
|
138
|
+
if server_registry is None:
|
|
139
|
+
raise RuntimeError("Context is missing server registry for MCP connections")
|
|
140
|
+
manager = MCPConnectionManager(server_registry, context=context)
|
|
141
|
+
await manager.__aenter__()
|
|
142
|
+
context._connection_manager = manager
|
|
143
|
+
self._persistent_connection_manager = cast(
|
|
144
|
+
"MCPConnectionManager", context._connection_manager
|
|
145
|
+
)
|
|
133
146
|
|
|
134
147
|
# Import the display component here to avoid circular imports
|
|
135
148
|
from fast_agent.ui.console_display import ConsoleDisplay
|
|
@@ -149,7 +162,7 @@ class MCPAggregator(ContextDependent):
|
|
|
149
162
|
server_names: List[str],
|
|
150
163
|
connection_persistence: bool = True,
|
|
151
164
|
context: Optional["Context"] = None,
|
|
152
|
-
name: str = None,
|
|
165
|
+
name: str | None = None,
|
|
153
166
|
config: Optional[Any] = None, # Accept the agent config for elicitation_handler access
|
|
154
167
|
**kwargs,
|
|
155
168
|
) -> None:
|
|
@@ -168,7 +181,6 @@ class MCPAggregator(ContextDependent):
|
|
|
168
181
|
self.connection_persistence = connection_persistence
|
|
169
182
|
self.agent_name = name
|
|
170
183
|
self.config = config # Store the config for access in session factory
|
|
171
|
-
self._persistent_connection_manager: MCPConnectionManager = None
|
|
172
184
|
|
|
173
185
|
# Set up logger with agent name in namespace if available
|
|
174
186
|
global logger
|
|
@@ -192,6 +204,9 @@ class MCPAggregator(ContextDependent):
|
|
|
192
204
|
self._server_stats: Dict[str, ServerStats] = {}
|
|
193
205
|
self._stats_lock = Lock()
|
|
194
206
|
|
|
207
|
+
# Track discovered Skybridge configurations per server
|
|
208
|
+
self._skybridge_configs: Dict[str, SkybridgeServerConfig] = {}
|
|
209
|
+
|
|
195
210
|
def _create_progress_callback(self, server_name: str, tool_name: str) -> "ProgressFnT":
|
|
196
211
|
"""Create a progress callback function for tool execution."""
|
|
197
212
|
|
|
@@ -319,6 +334,8 @@ class MCPAggregator(ContextDependent):
|
|
|
319
334
|
async with self._prompt_cache_lock:
|
|
320
335
|
self._prompt_cache.clear()
|
|
321
336
|
|
|
337
|
+
self._skybridge_configs.clear()
|
|
338
|
+
|
|
322
339
|
for server_name in self.server_names:
|
|
323
340
|
if self.connection_persistence:
|
|
324
341
|
logger.info(
|
|
@@ -399,6 +416,9 @@ class MCPAggregator(ContextDependent):
|
|
|
399
416
|
return_exceptions=True,
|
|
400
417
|
)
|
|
401
418
|
|
|
419
|
+
total_tool_count = 0
|
|
420
|
+
total_prompt_count = 0
|
|
421
|
+
|
|
402
422
|
for result in results:
|
|
403
423
|
if isinstance(result, BaseException):
|
|
404
424
|
logger.error(f"Error loading server data: {result}")
|
|
@@ -419,10 +439,14 @@ class MCPAggregator(ContextDependent):
|
|
|
419
439
|
self._namespaced_tool_map[namespaced_tool_name] = namespaced_tool
|
|
420
440
|
self._server_to_tool_map[server_name].append(namespaced_tool)
|
|
421
441
|
|
|
442
|
+
total_tool_count += len(tools)
|
|
443
|
+
|
|
422
444
|
# Process prompts
|
|
423
445
|
async with self._prompt_cache_lock:
|
|
424
446
|
self._prompt_cache[server_name] = prompts
|
|
425
447
|
|
|
448
|
+
total_prompt_count += len(prompts)
|
|
449
|
+
|
|
426
450
|
logger.debug(
|
|
427
451
|
f"MCP Aggregator initialized for server '{server_name}'",
|
|
428
452
|
data={
|
|
@@ -434,8 +458,195 @@ class MCPAggregator(ContextDependent):
|
|
|
434
458
|
},
|
|
435
459
|
)
|
|
436
460
|
|
|
461
|
+
await self._initialize_skybridge_configs()
|
|
462
|
+
|
|
463
|
+
self._display_startup_state(total_tool_count, total_prompt_count)
|
|
464
|
+
|
|
437
465
|
self.initialized = True
|
|
438
466
|
|
|
467
|
+
async def _initialize_skybridge_configs(self) -> None:
|
|
468
|
+
"""Discover Skybridge resources across servers."""
|
|
469
|
+
if not self.server_names:
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
tasks = [
|
|
473
|
+
self._evaluate_skybridge_for_server(server_name) for server_name in self.server_names
|
|
474
|
+
]
|
|
475
|
+
results = await gather(*tasks, return_exceptions=True)
|
|
476
|
+
|
|
477
|
+
for result in results:
|
|
478
|
+
if isinstance(result, BaseException):
|
|
479
|
+
logger.debug("Skybridge discovery failed: %s", str(result))
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
server_name, config = result
|
|
483
|
+
self._skybridge_configs[server_name] = config
|
|
484
|
+
|
|
485
|
+
async def _evaluate_skybridge_for_server(
|
|
486
|
+
self, server_name: str
|
|
487
|
+
) -> tuple[str, SkybridgeServerConfig]:
|
|
488
|
+
"""Inspect a single server for Skybridge-compatible resources."""
|
|
489
|
+
config = SkybridgeServerConfig(server_name=server_name)
|
|
490
|
+
|
|
491
|
+
tool_entries = self._server_to_tool_map.get(server_name, [])
|
|
492
|
+
tool_configs: List[SkybridgeToolConfig] = []
|
|
493
|
+
|
|
494
|
+
for namespaced_tool in tool_entries:
|
|
495
|
+
tool_meta = getattr(namespaced_tool.tool, "meta", None) or {}
|
|
496
|
+
template_value = tool_meta.get("openai/outputTemplate")
|
|
497
|
+
if not template_value:
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
template_uri = AnyUrl(template_value)
|
|
502
|
+
except Exception as exc:
|
|
503
|
+
warning = (
|
|
504
|
+
f"Tool '{namespaced_tool.namespaced_tool_name}' outputTemplate "
|
|
505
|
+
f"'{template_value}' is invalid: {exc}"
|
|
506
|
+
)
|
|
507
|
+
config.warnings.append(warning)
|
|
508
|
+
logger.error(warning)
|
|
509
|
+
tool_configs.append(
|
|
510
|
+
SkybridgeToolConfig(
|
|
511
|
+
tool_name=namespaced_tool.tool.name,
|
|
512
|
+
namespaced_tool_name=namespaced_tool.namespaced_tool_name,
|
|
513
|
+
warning=warning,
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
tool_configs.append(
|
|
519
|
+
SkybridgeToolConfig(
|
|
520
|
+
tool_name=namespaced_tool.tool.name,
|
|
521
|
+
namespaced_tool_name=namespaced_tool.namespaced_tool_name,
|
|
522
|
+
template_uri=template_uri,
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
raw_resources_capability = await self.server_supports_feature(server_name, "resources")
|
|
527
|
+
supports_resources = bool(raw_resources_capability)
|
|
528
|
+
config.supports_resources = supports_resources
|
|
529
|
+
config.tools = tool_configs
|
|
530
|
+
|
|
531
|
+
if not supports_resources:
|
|
532
|
+
return server_name, config
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
resources = await self._list_resources_from_server(server_name, check_support=False)
|
|
536
|
+
except Exception as exc: # noqa: BLE001 - logging and surfacing gracefully
|
|
537
|
+
config.warnings.append(f"Failed to list resources: {exc}")
|
|
538
|
+
return server_name, config
|
|
539
|
+
|
|
540
|
+
for resource_entry in resources:
|
|
541
|
+
uri = getattr(resource_entry, "uri", None)
|
|
542
|
+
if not uri:
|
|
543
|
+
continue
|
|
544
|
+
|
|
545
|
+
uri_str = str(uri)
|
|
546
|
+
if not uri_str.startswith("ui://"):
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
uri_value = AnyUrl(uri_str)
|
|
551
|
+
except Exception as exc: # noqa: BLE001
|
|
552
|
+
warning = f"Ignoring Skybridge candidate '{uri_str}': invalid URI ({exc})"
|
|
553
|
+
config.warnings.append(warning)
|
|
554
|
+
logger.debug(warning)
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
sky_resource = SkybridgeResourceConfig(uri=uri_value)
|
|
558
|
+
config.ui_resources.append(sky_resource)
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
read_result: ReadResourceResult = await self._get_resource_from_server(
|
|
562
|
+
server_name, uri_str
|
|
563
|
+
)
|
|
564
|
+
except Exception as exc: # noqa: BLE001
|
|
565
|
+
warning = f"Failed to read resource '{uri_str}': {exc}"
|
|
566
|
+
sky_resource.warning = warning
|
|
567
|
+
config.warnings.append(warning)
|
|
568
|
+
continue
|
|
569
|
+
|
|
570
|
+
contents = getattr(read_result, "contents", []) or []
|
|
571
|
+
seen_mime_types: List[str] = []
|
|
572
|
+
|
|
573
|
+
for content in contents:
|
|
574
|
+
mime_type = getattr(content, "mimeType", None)
|
|
575
|
+
if mime_type:
|
|
576
|
+
seen_mime_types.append(mime_type)
|
|
577
|
+
if mime_type == SKYBRIDGE_MIME_TYPE:
|
|
578
|
+
sky_resource.mime_type = mime_type
|
|
579
|
+
sky_resource.is_skybridge = True
|
|
580
|
+
break
|
|
581
|
+
|
|
582
|
+
if sky_resource.mime_type is None and seen_mime_types:
|
|
583
|
+
sky_resource.mime_type = seen_mime_types[0]
|
|
584
|
+
|
|
585
|
+
if not sky_resource.is_skybridge:
|
|
586
|
+
observed_type = sky_resource.mime_type or "unknown MIME type"
|
|
587
|
+
warning = (
|
|
588
|
+
f"served as '{observed_type}' instead of '{SKYBRIDGE_MIME_TYPE}'"
|
|
589
|
+
)
|
|
590
|
+
sky_resource.warning = warning
|
|
591
|
+
config.warnings.append(f"{uri_str}: {warning}")
|
|
592
|
+
|
|
593
|
+
resource_lookup = {str(resource.uri): resource for resource in config.ui_resources}
|
|
594
|
+
for tool_config in tool_configs:
|
|
595
|
+
if tool_config.template_uri is None:
|
|
596
|
+
continue
|
|
597
|
+
|
|
598
|
+
resource_match = resource_lookup.get(str(tool_config.template_uri))
|
|
599
|
+
if not resource_match:
|
|
600
|
+
warning = (
|
|
601
|
+
f"Tool '{tool_config.namespaced_tool_name}' references missing "
|
|
602
|
+
f"Skybridge resource '{tool_config.template_uri}'"
|
|
603
|
+
)
|
|
604
|
+
tool_config.warning = warning
|
|
605
|
+
config.warnings.append(warning)
|
|
606
|
+
logger.error(warning)
|
|
607
|
+
continue
|
|
608
|
+
|
|
609
|
+
tool_config.resource_uri = resource_match.uri
|
|
610
|
+
tool_config.is_valid = resource_match.is_skybridge
|
|
611
|
+
|
|
612
|
+
if not resource_match.is_skybridge:
|
|
613
|
+
warning = (
|
|
614
|
+
f"Tool '{tool_config.namespaced_tool_name}' references resource "
|
|
615
|
+
f"'{resource_match.uri}' served as '{resource_match.mime_type or 'unknown'}' "
|
|
616
|
+
f"instead of '{SKYBRIDGE_MIME_TYPE}'"
|
|
617
|
+
)
|
|
618
|
+
tool_config.warning = warning
|
|
619
|
+
config.warnings.append(warning)
|
|
620
|
+
logger.warning(warning)
|
|
621
|
+
|
|
622
|
+
config.tools = tool_configs
|
|
623
|
+
|
|
624
|
+
valid_tool_count = sum(1 for tool in tool_configs if tool.is_valid)
|
|
625
|
+
if config.enabled and valid_tool_count == 0:
|
|
626
|
+
warning = (
|
|
627
|
+
f"Skybridge resources detected on server '{server_name}' but no tools expose them"
|
|
628
|
+
)
|
|
629
|
+
config.warnings.append(warning)
|
|
630
|
+
logger.warning(warning)
|
|
631
|
+
|
|
632
|
+
return server_name, config
|
|
633
|
+
|
|
634
|
+
def _display_startup_state(self, total_tool_count: int, total_prompt_count: int) -> None:
|
|
635
|
+
"""Display startup summary and Skybridge status information."""
|
|
636
|
+
# In interactive contexts the UI helper will render both the agent summary and the
|
|
637
|
+
# Skybridge status. For non-interactive contexts, the warnings collected during
|
|
638
|
+
# discovery are emitted through the logger, so we don't need to duplicate output here.
|
|
639
|
+
if not self._skybridge_configs:
|
|
640
|
+
return
|
|
641
|
+
|
|
642
|
+
logger.debug(
|
|
643
|
+
"Skybridge discovery completed",
|
|
644
|
+
data={
|
|
645
|
+
"agent_name": self.agent_name,
|
|
646
|
+
"server_count": len(self._skybridge_configs),
|
|
647
|
+
},
|
|
648
|
+
)
|
|
649
|
+
|
|
439
650
|
async def get_capabilities(self, server_name: str):
|
|
440
651
|
"""Get server capabilities if available."""
|
|
441
652
|
if not self.connection_persistence:
|
|
@@ -485,7 +696,15 @@ class MCPAggregator(ContextDependent):
|
|
|
485
696
|
if not capabilities:
|
|
486
697
|
return False
|
|
487
698
|
|
|
488
|
-
|
|
699
|
+
feature_value = getattr(capabilities, feature, False)
|
|
700
|
+
if isinstance(feature_value, bool):
|
|
701
|
+
return feature_value
|
|
702
|
+
if feature_value is None:
|
|
703
|
+
return False
|
|
704
|
+
try:
|
|
705
|
+
return bool(feature_value)
|
|
706
|
+
except Exception: # noqa: BLE001
|
|
707
|
+
return True
|
|
489
708
|
|
|
490
709
|
async def list_servers(self) -> List[str]:
|
|
491
710
|
"""Return the list of server names aggregated by this agent."""
|
|
@@ -501,12 +720,30 @@ class MCPAggregator(ContextDependent):
|
|
|
501
720
|
if not self.initialized:
|
|
502
721
|
await self.load_servers()
|
|
503
722
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
723
|
+
tools: List[Tool] = []
|
|
724
|
+
|
|
725
|
+
for namespaced_tool_name, namespaced_tool in self._namespaced_tool_map.items():
|
|
726
|
+
tool_copy = namespaced_tool.tool.model_copy(
|
|
727
|
+
deep=True, update={"name": namespaced_tool_name}
|
|
728
|
+
)
|
|
729
|
+
skybridge_config = self._skybridge_configs.get(namespaced_tool.server_name)
|
|
730
|
+
if skybridge_config:
|
|
731
|
+
matching_tool = next(
|
|
732
|
+
(
|
|
733
|
+
tool
|
|
734
|
+
for tool in skybridge_config.tools
|
|
735
|
+
if tool.namespaced_tool_name == namespaced_tool_name and tool.is_valid
|
|
736
|
+
),
|
|
737
|
+
None,
|
|
738
|
+
)
|
|
739
|
+
if matching_tool:
|
|
740
|
+
meta = dict(tool_copy.meta or {})
|
|
741
|
+
meta["openai/skybridgeEnabled"] = True
|
|
742
|
+
meta["openai/skybridgeTemplate"] = str(matching_tool.template_uri)
|
|
743
|
+
tool_copy.meta = meta
|
|
744
|
+
tools.append(tool_copy)
|
|
745
|
+
|
|
746
|
+
return ListToolsResult(tools=tools)
|
|
510
747
|
|
|
511
748
|
async def refresh_all_tools(self) -> None:
|
|
512
749
|
"""
|
|
@@ -554,12 +791,14 @@ class MCPAggregator(ContextDependent):
|
|
|
554
791
|
event = ChannelEvent(
|
|
555
792
|
channel="stdio",
|
|
556
793
|
event_type="message",
|
|
557
|
-
detail=f"{operation_type} ({'success' if success else 'error'})"
|
|
794
|
+
detail=f"{operation_type} ({'success' if success else 'error'})",
|
|
558
795
|
)
|
|
559
796
|
transport_metrics.record_event(event)
|
|
560
797
|
except Exception:
|
|
561
798
|
# Don't let transport tracking errors break normal operation
|
|
562
|
-
logger.debug(
|
|
799
|
+
logger.debug(
|
|
800
|
+
"Failed to notify stdio transport activity for %s", server_name, exc_info=True
|
|
801
|
+
)
|
|
563
802
|
|
|
564
803
|
async def get_server_instructions(self) -> Dict[str, tuple[str, List[str]]]:
|
|
565
804
|
"""
|
|
@@ -652,13 +891,13 @@ class MCPAggregator(ContextDependent):
|
|
|
652
891
|
instructions_available = getattr(
|
|
653
892
|
server_conn, "server_instructions_available", None
|
|
654
893
|
)
|
|
655
|
-
instructions_enabled = getattr(
|
|
656
|
-
server_conn, "server_instructions_enabled", None
|
|
657
|
-
)
|
|
894
|
+
instructions_enabled = getattr(server_conn, "server_instructions_enabled", None)
|
|
658
895
|
instructions_included = bool(getattr(server_conn, "server_instructions", None))
|
|
659
896
|
server_cfg = getattr(server_conn, "server_config", None)
|
|
660
897
|
if session:
|
|
661
|
-
elicitation_mode = getattr(
|
|
898
|
+
elicitation_mode = getattr(
|
|
899
|
+
session, "effective_elicitation_mode", elicitation_mode
|
|
900
|
+
)
|
|
662
901
|
session_id = getattr(server_conn, "session_id", None)
|
|
663
902
|
if not session_id and getattr(server_conn, "_get_session_id_cb", None):
|
|
664
903
|
try:
|
|
@@ -681,7 +920,11 @@ class MCPAggregator(ContextDependent):
|
|
|
681
920
|
data={"error": str(exc)},
|
|
682
921
|
)
|
|
683
922
|
|
|
684
|
-
if
|
|
923
|
+
if (
|
|
924
|
+
server_cfg is None
|
|
925
|
+
and self.context
|
|
926
|
+
and getattr(self.context, "server_registry", None)
|
|
927
|
+
):
|
|
685
928
|
try:
|
|
686
929
|
server_cfg = self.context.server_registry.get_server_config(server_name)
|
|
687
930
|
except Exception:
|
|
@@ -699,9 +942,7 @@ class MCPAggregator(ContextDependent):
|
|
|
699
942
|
transport = getattr(server_cfg, "transport", transport)
|
|
700
943
|
elicitation = getattr(server_cfg, "elicitation", None)
|
|
701
944
|
elicitation_mode = (
|
|
702
|
-
getattr(elicitation, "mode", None)
|
|
703
|
-
if elicitation
|
|
704
|
-
else elicitation_mode
|
|
945
|
+
getattr(elicitation, "mode", None) if elicitation else elicitation_mode
|
|
705
946
|
)
|
|
706
947
|
sampling_cfg = getattr(server_cfg, "sampling", None)
|
|
707
948
|
spoofing_enabled = bool(getattr(server_cfg, "implementation", None))
|
|
@@ -756,10 +997,23 @@ class MCPAggregator(ContextDependent):
|
|
|
756
997
|
spoofing_enabled=spoofing_enabled,
|
|
757
998
|
session_id=session_id,
|
|
758
999
|
transport_channels=transport_snapshot,
|
|
1000
|
+
skybridge=self._skybridge_configs.get(server_name),
|
|
759
1001
|
)
|
|
760
1002
|
|
|
761
1003
|
return status_map
|
|
762
1004
|
|
|
1005
|
+
async def get_skybridge_configs(self) -> Dict[str, SkybridgeServerConfig]:
|
|
1006
|
+
"""Expose discovered Skybridge configurations keyed by server."""
|
|
1007
|
+
if not self.initialized:
|
|
1008
|
+
await self.load_servers()
|
|
1009
|
+
return dict(self._skybridge_configs)
|
|
1010
|
+
|
|
1011
|
+
async def get_skybridge_config(self, server_name: str) -> SkybridgeServerConfig | None:
|
|
1012
|
+
"""Return the Skybridge configuration for a specific server, loading if necessary."""
|
|
1013
|
+
if not self.initialized:
|
|
1014
|
+
await self.load_servers()
|
|
1015
|
+
return self._skybridge_configs.get(server_name)
|
|
1016
|
+
|
|
763
1017
|
async def _execute_on_server(
|
|
764
1018
|
self,
|
|
765
1019
|
server_name: str,
|
|
@@ -767,7 +1021,7 @@ class MCPAggregator(ContextDependent):
|
|
|
767
1021
|
operation_name: str,
|
|
768
1022
|
method_name: str,
|
|
769
1023
|
method_args: Dict[str, Any] = None,
|
|
770
|
-
error_factory: Callable[[str], R] = None,
|
|
1024
|
+
error_factory: Callable[[str], R] | None = None,
|
|
771
1025
|
progress_callback: ProgressFnT | None = None,
|
|
772
1026
|
) -> R:
|
|
773
1027
|
"""
|
|
@@ -1500,6 +1754,32 @@ class MCPAggregator(ContextDependent):
|
|
|
1500
1754
|
|
|
1501
1755
|
return result
|
|
1502
1756
|
|
|
1757
|
+
async def _list_resources_from_server(
|
|
1758
|
+
self, server_name: str, *, check_support: bool = True
|
|
1759
|
+
) -> List[Any]:
|
|
1760
|
+
"""
|
|
1761
|
+
Internal helper method to list resources from a specific server.
|
|
1762
|
+
|
|
1763
|
+
Args:
|
|
1764
|
+
server_name: Name of the server whose resources to list
|
|
1765
|
+
check_support: Whether to verify the server supports resources before listing
|
|
1766
|
+
|
|
1767
|
+
Returns:
|
|
1768
|
+
A list of resources as returned by the MCP server
|
|
1769
|
+
"""
|
|
1770
|
+
if check_support and not await self.server_supports_feature(server_name, "resources"):
|
|
1771
|
+
return []
|
|
1772
|
+
|
|
1773
|
+
result = await self._execute_on_server(
|
|
1774
|
+
server_name=server_name,
|
|
1775
|
+
operation_type="resources/list",
|
|
1776
|
+
operation_name="",
|
|
1777
|
+
method_name="list_resources",
|
|
1778
|
+
method_args={},
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
return getattr(result, "resources", []) or []
|
|
1782
|
+
|
|
1503
1783
|
async def list_resources(self, server_name: str | None = None) -> Dict[str, List[str]]:
|
|
1504
1784
|
"""
|
|
1505
1785
|
List available resources from one or all servers.
|
|
@@ -1534,20 +1814,13 @@ class MCPAggregator(ContextDependent):
|
|
|
1534
1814
|
continue
|
|
1535
1815
|
|
|
1536
1816
|
try:
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
# No error_factory to allow exceptions to propagate
|
|
1545
|
-
)
|
|
1546
|
-
|
|
1547
|
-
# Get resources from result
|
|
1548
|
-
resources = getattr(result, "resources", [])
|
|
1549
|
-
results[s_name] = [str(r.uri) for r in resources]
|
|
1550
|
-
|
|
1817
|
+
resources = await self._list_resources_from_server(s_name, check_support=False)
|
|
1818
|
+
formatted_resources: List[str] = []
|
|
1819
|
+
for resource in resources:
|
|
1820
|
+
uri = getattr(resource, "uri", None)
|
|
1821
|
+
if uri is not None:
|
|
1822
|
+
formatted_resources.append(str(uri))
|
|
1823
|
+
results[s_name] = formatted_resources
|
|
1551
1824
|
except Exception as e:
|
|
1552
1825
|
logger.error(f"Error fetching resources from {s_name}: {e}")
|
|
1553
1826
|
|
|
@@ -17,7 +17,6 @@ from anyio import Event, Lock, create_task_group
|
|
|
17
17
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
18
18
|
from httpx import HTTPStatusError
|
|
19
19
|
from mcp import ClientSession
|
|
20
|
-
from mcp.client.sse import sse_client
|
|
21
20
|
from mcp.client.stdio import (
|
|
22
21
|
StdioServerParameters,
|
|
23
22
|
get_default_environment,
|
|
@@ -33,6 +32,7 @@ from fast_agent.event_progress import ProgressAction
|
|
|
33
32
|
from fast_agent.mcp.logger_textio import get_stderr_handler
|
|
34
33
|
from fast_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
|
|
35
34
|
from fast_agent.mcp.oauth_client import build_oauth_provider
|
|
35
|
+
from fast_agent.mcp.sse_tracking import tracking_sse_client
|
|
36
36
|
from fast_agent.mcp.stdio_tracking_simple import tracking_stdio_client
|
|
37
37
|
from fast_agent.mcp.streamable_http_tracking import tracking_streamablehttp_client
|
|
38
38
|
from fast_agent.mcp.transport_tracking import TransportChannelMetrics
|
|
@@ -440,8 +440,26 @@ class MCPConnectionManager(ContextDependent):
|
|
|
440
440
|
|
|
441
441
|
logger.debug(f"{server_name}: Found server configuration=", data=config.model_dump())
|
|
442
442
|
|
|
443
|
+
timeline_steps = 20
|
|
444
|
+
timeline_seconds = 30
|
|
445
|
+
try:
|
|
446
|
+
ctx = self.context
|
|
447
|
+
except RuntimeError:
|
|
448
|
+
ctx = None
|
|
449
|
+
|
|
450
|
+
config_obj = getattr(ctx, "config", None)
|
|
451
|
+
timeline_config = getattr(config_obj, "mcp_timeline", None)
|
|
452
|
+
if timeline_config:
|
|
453
|
+
timeline_steps = getattr(timeline_config, "steps", timeline_steps)
|
|
454
|
+
timeline_seconds = getattr(timeline_config, "step_seconds", timeline_seconds)
|
|
455
|
+
|
|
443
456
|
transport_metrics = (
|
|
444
|
-
TransportChannelMetrics(
|
|
457
|
+
TransportChannelMetrics(
|
|
458
|
+
bucket_seconds=timeline_seconds,
|
|
459
|
+
bucket_count=timeline_steps,
|
|
460
|
+
)
|
|
461
|
+
if config.transport in ("http", "sse", "stdio")
|
|
462
|
+
else None
|
|
445
463
|
)
|
|
446
464
|
|
|
447
465
|
def transport_context_factory():
|
|
@@ -480,13 +498,25 @@ class MCPConnectionManager(ContextDependent):
|
|
|
480
498
|
f"{server_name}: Using user-specified auth header(s); skipping OAuth provider.",
|
|
481
499
|
user_auth_headers=sorted(user_auth_keys),
|
|
482
500
|
)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
501
|
+
channel_hook = None
|
|
502
|
+
if transport_metrics is not None:
|
|
503
|
+
|
|
504
|
+
def channel_hook(event):
|
|
505
|
+
try:
|
|
506
|
+
transport_metrics.record_event(event)
|
|
507
|
+
except Exception: # pragma: no cover - defensive guard
|
|
508
|
+
logger.debug(
|
|
509
|
+
"%s: transport metrics hook failed",
|
|
510
|
+
server_name,
|
|
511
|
+
exc_info=True,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
return tracking_sse_client(
|
|
515
|
+
config.url,
|
|
516
|
+
headers,
|
|
517
|
+
sse_read_timeout=config.read_transport_sse_timeout_seconds,
|
|
518
|
+
auth=oauth_auth,
|
|
519
|
+
channel_hook=channel_hook,
|
|
490
520
|
)
|
|
491
521
|
elif config.transport == "http":
|
|
492
522
|
if not config.url:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, List, Optional
|
|
1
|
+
from typing import Dict, List, Mapping, Optional, Sequence
|
|
2
2
|
|
|
3
3
|
from mcp.types import (
|
|
4
4
|
CallToolRequest,
|
|
@@ -27,7 +27,7 @@ class PromptMessageExtended(BaseModel):
|
|
|
27
27
|
content: List[ContentBlock] = []
|
|
28
28
|
tool_calls: Dict[str, CallToolRequest] | None = None
|
|
29
29
|
tool_results: Dict[str, CallToolResult] | None = None
|
|
30
|
-
channels:
|
|
30
|
+
channels: Mapping[str, Sequence[ContentBlock]] | None = None
|
|
31
31
|
stop_reason: LlmStopReason | None = None
|
|
32
32
|
|
|
33
33
|
@classmethod
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from pydantic import AnyUrl, BaseModel, Field
|
|
4
|
+
|
|
5
|
+
SKYBRIDGE_MIME_TYPE = "text/html+skybridge"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SkybridgeResourceConfig(BaseModel):
|
|
9
|
+
"""Represents a Skybridge (apps SDK) resource exposed by an MCP server."""
|
|
10
|
+
|
|
11
|
+
uri: AnyUrl
|
|
12
|
+
mime_type: str | None = None
|
|
13
|
+
is_skybridge: bool = False
|
|
14
|
+
warning: str | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SkybridgeToolConfig(BaseModel):
|
|
18
|
+
"""Represents Skybridge metadata discovered for a tool."""
|
|
19
|
+
|
|
20
|
+
tool_name: str
|
|
21
|
+
namespaced_tool_name: str
|
|
22
|
+
template_uri: AnyUrl | None = None
|
|
23
|
+
resource_uri: AnyUrl | None = None
|
|
24
|
+
is_valid: bool = False
|
|
25
|
+
warning: str | None = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def display_name(self) -> str:
|
|
29
|
+
return self.namespaced_tool_name or self.tool_name
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SkybridgeServerConfig(BaseModel):
|
|
33
|
+
"""Skybridge configuration discovered for a specific MCP server."""
|
|
34
|
+
|
|
35
|
+
server_name: str
|
|
36
|
+
supports_resources: bool = False
|
|
37
|
+
ui_resources: List[SkybridgeResourceConfig] = Field(default_factory=list)
|
|
38
|
+
warnings: List[str] = Field(default_factory=list)
|
|
39
|
+
tools: List[SkybridgeToolConfig] = Field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def enabled(self) -> bool:
|
|
43
|
+
"""Return True when at least one resource advertises the Skybridge MIME type."""
|
|
44
|
+
return any(resource.is_skybridge for resource in self.ui_resources)
|
|
45
|
+
|