fast-agent-mcp 0.3.7__py3-none-any.whl → 0.3.9__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 +30 -8
- fast_agent/agents/llm_decorator.py +2 -7
- fast_agent/agents/mcp_agent.py +9 -4
- fast_agent/cli/commands/auth.py +14 -1
- fast_agent/core/direct_factory.py +20 -8
- fast_agent/core/logging/listeners.py +2 -1
- fast_agent/interfaces.py +2 -2
- fast_agent/llm/model_database.py +7 -1
- fast_agent/llm/model_factory.py +2 -3
- fast_agent/llm/provider/anthropic/llm_anthropic.py +107 -62
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +4 -3
- fast_agent/llm/provider/bedrock/llm_bedrock.py +1 -1
- fast_agent/llm/provider/google/google_converter.py +8 -41
- fast_agent/llm/provider/google/llm_google_native.py +1 -3
- fast_agent/llm/provider/openai/llm_azure.py +1 -1
- fast_agent/llm/provider/openai/llm_openai.py +3 -3
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +1 -1
- fast_agent/llm/request_params.py +1 -1
- fast_agent/mcp/mcp_agent_client_session.py +45 -2
- fast_agent/mcp/mcp_aggregator.py +282 -5
- fast_agent/mcp/mcp_connection_manager.py +86 -10
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/transport_tracking.py +598 -0
- fast_agent/resources/examples/data-analysis/analysis.py +7 -3
- fast_agent/ui/console_display.py +22 -1
- fast_agent/ui/enhanced_prompt.py +21 -1
- fast_agent/ui/interactive_prompt.py +5 -0
- fast_agent/ui/mcp_display.py +636 -0
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/METADATA +6 -6
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/RECORD +34 -30
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -344,12 +344,12 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
|
|
|
344
344
|
model_name = self.default_request_params.model or DEFAULT_OPENAI_MODEL
|
|
345
345
|
|
|
346
346
|
# Use basic streaming API
|
|
347
|
-
stream = await self._openai_client().chat.completions.create(**arguments)
|
|
348
|
-
# Process the stream
|
|
349
347
|
try:
|
|
348
|
+
stream = await self._openai_client().chat.completions.create(**arguments)
|
|
349
|
+
# Process the stream
|
|
350
350
|
response = await self._process_stream(stream, model_name)
|
|
351
351
|
except APIError as error:
|
|
352
|
-
self.logger.error("
|
|
352
|
+
self.logger.error("APIError during OpenAI completion", exc_info=error)
|
|
353
353
|
return self._stream_failure_response(error, model_name)
|
|
354
354
|
# Track usage if response is valid and has usage data
|
|
355
355
|
if (
|
|
@@ -26,7 +26,7 @@ class TensorZeroOpenAILLM(OpenAILLM):
|
|
|
26
26
|
self._t0_function_name = kwargs.get("model", "")
|
|
27
27
|
|
|
28
28
|
super().__init__(*args, provider=Provider.TENSORZERO, **kwargs)
|
|
29
|
-
self.logger.info("
|
|
29
|
+
self.logger.info("TensorZeroOpenAILLM initialized.")
|
|
30
30
|
|
|
31
31
|
def _initialize_default_params(self, kwargs: dict) -> RequestParams:
|
|
32
32
|
"""
|
fast_agent/llm/request_params.py
CHANGED
|
@@ -11,7 +11,7 @@ from pydantic import Field
|
|
|
11
11
|
|
|
12
12
|
class RequestParams(CreateMessageRequestParams):
|
|
13
13
|
"""
|
|
14
|
-
Parameters to configure the
|
|
14
|
+
Parameters to configure the FastAgentLLM 'generate' requests.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
messages: List[SamplingMessage] = Field(exclude=True, default=[])
|
|
@@ -37,6 +37,7 @@ from fast_agent.mcp.sampling import sample
|
|
|
37
37
|
|
|
38
38
|
if TYPE_CHECKING:
|
|
39
39
|
from fast_agent.config import MCPServerSettings
|
|
40
|
+
from fast_agent.mcp.transport_tracking import TransportChannelMetrics
|
|
40
41
|
|
|
41
42
|
logger = get_logger(__name__)
|
|
42
43
|
|
|
@@ -90,6 +91,13 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
90
91
|
custom_elicitation_handler = kwargs.pop("elicitation_handler", None)
|
|
91
92
|
# Extract optional context for ContextDependent mixin without passing it to ClientSession
|
|
92
93
|
self._context = kwargs.pop("context", None)
|
|
94
|
+
# Extract transport metrics tracker if provided
|
|
95
|
+
self._transport_metrics: TransportChannelMetrics | None = kwargs.pop(
|
|
96
|
+
"transport_metrics", None
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Track the effective elicitation mode for diagnostics
|
|
100
|
+
self.effective_elicitation_mode: str | None = "none"
|
|
93
101
|
|
|
94
102
|
version = version("fast-agent-mcp") or "dev"
|
|
95
103
|
fast_agent: Implementation = Implementation(name="fast-agent-mcp", version=version)
|
|
@@ -131,7 +139,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
131
139
|
agent_config = AgentConfig(
|
|
132
140
|
name=self.agent_name or "unknown",
|
|
133
141
|
model=self.agent_model or "unknown",
|
|
134
|
-
elicitation_handler=None,
|
|
142
|
+
elicitation_handler=None,
|
|
135
143
|
)
|
|
136
144
|
elicitation_handler = resolve_elicitation_handler(
|
|
137
145
|
agent_config, context.config, self.server_config
|
|
@@ -141,12 +149,33 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
141
149
|
pass
|
|
142
150
|
|
|
143
151
|
# Fallback to forms handler only if factory resolution wasn't attempted
|
|
144
|
-
# If factory was attempted and returned None, respect that (means no elicitation capability)
|
|
145
152
|
if elicitation_handler is None and not self.server_config:
|
|
146
153
|
from fast_agent.mcp.elicitation_handlers import forms_elicitation_handler
|
|
147
154
|
|
|
148
155
|
elicitation_handler = forms_elicitation_handler
|
|
149
156
|
|
|
157
|
+
# Determine effective elicitation mode for diagnostics
|
|
158
|
+
if self.server_config and getattr(self.server_config, "elicitation", None):
|
|
159
|
+
self.effective_elicitation_mode = self.server_config.elicitation.mode or "forms"
|
|
160
|
+
elif elicitation_handler is not None:
|
|
161
|
+
# Use global config if available to distinguish auto-cancel
|
|
162
|
+
try:
|
|
163
|
+
from fast_agent.context import get_current_context
|
|
164
|
+
|
|
165
|
+
context = get_current_context()
|
|
166
|
+
mode = None
|
|
167
|
+
if context and getattr(context, "config", None):
|
|
168
|
+
elicitation_cfg = getattr(context.config, "elicitation", None)
|
|
169
|
+
if isinstance(elicitation_cfg, dict):
|
|
170
|
+
mode = elicitation_cfg.get("mode")
|
|
171
|
+
else:
|
|
172
|
+
mode = getattr(elicitation_cfg, "mode", None)
|
|
173
|
+
self.effective_elicitation_mode = (mode or "forms").lower()
|
|
174
|
+
except Exception:
|
|
175
|
+
self.effective_elicitation_mode = "forms"
|
|
176
|
+
else:
|
|
177
|
+
self.effective_elicitation_mode = "none"
|
|
178
|
+
|
|
150
179
|
super().__init__(
|
|
151
180
|
*args,
|
|
152
181
|
**kwargs,
|
|
@@ -177,6 +206,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
177
206
|
progress_callback: ProgressFnT | None = None,
|
|
178
207
|
) -> ReceiveResultT:
|
|
179
208
|
logger.debug("send_request: request=", data=request.model_dump())
|
|
209
|
+
request_id = getattr(self, "_request_id", None)
|
|
180
210
|
try:
|
|
181
211
|
result = await super().send_request(
|
|
182
212
|
request=request,
|
|
@@ -189,6 +219,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
189
219
|
"send_request: response=",
|
|
190
220
|
data=result.model_dump() if result is not None else "no response returned",
|
|
191
221
|
)
|
|
222
|
+
self._attach_transport_channel(request_id, result)
|
|
192
223
|
return result
|
|
193
224
|
except Exception as e:
|
|
194
225
|
# Handle connection errors cleanly
|
|
@@ -207,6 +238,18 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
207
238
|
logger.error(f"send_request failed: {str(e)}")
|
|
208
239
|
raise
|
|
209
240
|
|
|
241
|
+
def _attach_transport_channel(self, request_id, result) -> None:
|
|
242
|
+
if self._transport_metrics is None or request_id is None or result is None:
|
|
243
|
+
return
|
|
244
|
+
channel = self._transport_metrics.consume_response_channel(request_id)
|
|
245
|
+
if not channel:
|
|
246
|
+
return
|
|
247
|
+
try:
|
|
248
|
+
setattr(result, "transport_channel", channel)
|
|
249
|
+
except Exception:
|
|
250
|
+
# If result cannot be mutated, ignore silently
|
|
251
|
+
pass
|
|
252
|
+
|
|
210
253
|
async def _received_notification(self, notification: ServerNotification) -> None:
|
|
211
254
|
"""
|
|
212
255
|
Can be overridden by subclasses to handle a notification without needing
|
fast_agent/mcp/mcp_aggregator.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
from asyncio import Lock, gather
|
|
2
|
+
from collections import Counter
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
2
5
|
from typing import (
|
|
3
6
|
TYPE_CHECKING,
|
|
4
7
|
Any,
|
|
@@ -17,11 +20,12 @@ from mcp.types import (
|
|
|
17
20
|
CallToolResult,
|
|
18
21
|
ListToolsResult,
|
|
19
22
|
Prompt,
|
|
23
|
+
ServerCapabilities,
|
|
20
24
|
TextContent,
|
|
21
25
|
Tool,
|
|
22
26
|
)
|
|
23
27
|
from opentelemetry import trace
|
|
24
|
-
from pydantic import AnyUrl, BaseModel, ConfigDict
|
|
28
|
+
from pydantic import AnyUrl, BaseModel, ConfigDict, Field
|
|
25
29
|
|
|
26
30
|
from fast_agent.context_dependent import ContextDependent
|
|
27
31
|
from fast_agent.core.logging.logger import get_logger
|
|
@@ -30,6 +34,7 @@ from fast_agent.mcp.common import SEP, create_namespaced_name, is_namespaced_nam
|
|
|
30
34
|
from fast_agent.mcp.gen_client import gen_client
|
|
31
35
|
from fast_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
|
|
32
36
|
from fast_agent.mcp.mcp_connection_manager import MCPConnectionManager
|
|
37
|
+
from fast_agent.mcp.transport_tracking import TransportSnapshot
|
|
33
38
|
|
|
34
39
|
if TYPE_CHECKING:
|
|
35
40
|
from fast_agent.context import Context
|
|
@@ -52,6 +57,49 @@ class NamespacedTool(BaseModel):
|
|
|
52
57
|
namespaced_tool_name: str
|
|
53
58
|
|
|
54
59
|
|
|
60
|
+
@dataclass
|
|
61
|
+
class ServerStats:
|
|
62
|
+
call_counts: Counter = field(default_factory=Counter)
|
|
63
|
+
last_call_at: datetime | None = None
|
|
64
|
+
last_error_at: datetime | None = None
|
|
65
|
+
|
|
66
|
+
def record(self, operation_type: str, success: bool) -> None:
|
|
67
|
+
self.call_counts[operation_type] += 1
|
|
68
|
+
now = datetime.now(timezone.utc)
|
|
69
|
+
self.last_call_at = now
|
|
70
|
+
if not success:
|
|
71
|
+
self.last_error_at = now
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ServerStatus(BaseModel):
|
|
75
|
+
server_name: str
|
|
76
|
+
implementation_name: str | None = None
|
|
77
|
+
implementation_version: str | None = None
|
|
78
|
+
server_capabilities: ServerCapabilities | None = None
|
|
79
|
+
client_capabilities: Mapping[str, Any] | None = None
|
|
80
|
+
client_info_name: str | None = None
|
|
81
|
+
client_info_version: str | None = None
|
|
82
|
+
transport: str | None = None
|
|
83
|
+
is_connected: bool | None = None
|
|
84
|
+
last_call_at: datetime | None = None
|
|
85
|
+
last_error_at: datetime | None = None
|
|
86
|
+
staleness_seconds: float | None = None
|
|
87
|
+
call_counts: Dict[str, int] = Field(default_factory=dict)
|
|
88
|
+
error_message: str | None = None
|
|
89
|
+
instructions_available: bool | None = None
|
|
90
|
+
instructions_enabled: bool | None = None
|
|
91
|
+
instructions_included: bool | None = None
|
|
92
|
+
roots_configured: bool | None = None
|
|
93
|
+
roots_count: int | None = None
|
|
94
|
+
elicitation_mode: str | None = None
|
|
95
|
+
sampling_mode: str | None = None
|
|
96
|
+
spoofing_enabled: bool | None = None
|
|
97
|
+
session_id: str | None = None
|
|
98
|
+
transport_channels: TransportSnapshot | None = None
|
|
99
|
+
|
|
100
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
101
|
+
|
|
102
|
+
|
|
55
103
|
class MCPAggregator(ContextDependent):
|
|
56
104
|
"""
|
|
57
105
|
Aggregates multiple MCP servers. When a developer calls, e.g. call_tool(...),
|
|
@@ -140,6 +188,10 @@ class MCPAggregator(ContextDependent):
|
|
|
140
188
|
# Lock for refreshing tools from a server
|
|
141
189
|
self._refresh_lock = Lock()
|
|
142
190
|
|
|
191
|
+
# Track runtime stats per server
|
|
192
|
+
self._server_stats: Dict[str, ServerStats] = {}
|
|
193
|
+
self._stats_lock = Lock()
|
|
194
|
+
|
|
143
195
|
def _create_progress_callback(self, server_name: str, tool_name: str) -> "ProgressFnT":
|
|
144
196
|
"""Create a progress callback function for tool execution."""
|
|
145
197
|
|
|
@@ -461,6 +513,50 @@ class MCPAggregator(ContextDependent):
|
|
|
461
513
|
for server_name in self.server_names:
|
|
462
514
|
await self._refresh_server_tools(server_name)
|
|
463
515
|
|
|
516
|
+
async def _record_server_call(
|
|
517
|
+
self, server_name: str, operation_type: str, success: bool
|
|
518
|
+
) -> None:
|
|
519
|
+
async with self._stats_lock:
|
|
520
|
+
stats = self._server_stats.setdefault(server_name, ServerStats())
|
|
521
|
+
stats.record(operation_type, success)
|
|
522
|
+
|
|
523
|
+
# For stdio servers, also emit synthetic transport events to create activity timeline
|
|
524
|
+
await self._notify_stdio_transport_activity(server_name, operation_type, success)
|
|
525
|
+
|
|
526
|
+
async def _notify_stdio_transport_activity(
|
|
527
|
+
self, server_name: str, operation_type: str, success: bool
|
|
528
|
+
) -> None:
|
|
529
|
+
"""Notify transport metrics of activity for stdio servers to create activity timeline."""
|
|
530
|
+
if not self._persistent_connection_manager:
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
try:
|
|
534
|
+
# Get the server connection and check if it's stdio transport
|
|
535
|
+
server_conn = self._persistent_connection_manager.running_servers.get(server_name)
|
|
536
|
+
if not server_conn:
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
server_config = getattr(server_conn, "server_config", None)
|
|
540
|
+
if not server_config or server_config.transport != "stdio":
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
# Get transport metrics and emit synthetic message event
|
|
544
|
+
transport_metrics = getattr(server_conn, "transport_metrics", None)
|
|
545
|
+
if transport_metrics:
|
|
546
|
+
# Import here to avoid circular imports
|
|
547
|
+
from fast_agent.mcp.transport_tracking import ChannelEvent
|
|
548
|
+
|
|
549
|
+
# Create a synthetic message event to represent the MCP operation
|
|
550
|
+
event = ChannelEvent(
|
|
551
|
+
channel="stdio",
|
|
552
|
+
event_type="message",
|
|
553
|
+
detail=f"{operation_type} ({'success' if success else 'error'})"
|
|
554
|
+
)
|
|
555
|
+
transport_metrics.record_event(event)
|
|
556
|
+
except Exception:
|
|
557
|
+
# Don't let transport tracking errors break normal operation
|
|
558
|
+
logger.debug("Failed to notify stdio transport activity for %s", server_name, exc_info=True)
|
|
559
|
+
|
|
464
560
|
async def get_server_instructions(self) -> Dict[str, tuple[str, List[str]]]:
|
|
465
561
|
"""
|
|
466
562
|
Get instructions from all connected servers along with their tool names.
|
|
@@ -492,6 +588,174 @@ class MCPAggregator(ContextDependent):
|
|
|
492
588
|
|
|
493
589
|
return instructions
|
|
494
590
|
|
|
591
|
+
async def collect_server_status(self) -> Dict[str, ServerStatus]:
|
|
592
|
+
"""Return aggregated status information for each configured server."""
|
|
593
|
+
if not self.initialized:
|
|
594
|
+
await self.load_servers()
|
|
595
|
+
|
|
596
|
+
now = datetime.now(timezone.utc)
|
|
597
|
+
status_map: Dict[str, ServerStatus] = {}
|
|
598
|
+
|
|
599
|
+
for server_name in self.server_names:
|
|
600
|
+
stats = self._server_stats.get(server_name)
|
|
601
|
+
last_call = stats.last_call_at if stats else None
|
|
602
|
+
last_error = stats.last_error_at if stats else None
|
|
603
|
+
staleness = (now - last_call).total_seconds() if last_call else None
|
|
604
|
+
call_counts = dict(stats.call_counts) if stats else {}
|
|
605
|
+
|
|
606
|
+
implementation_name = None
|
|
607
|
+
implementation_version = None
|
|
608
|
+
capabilities: ServerCapabilities | None = None
|
|
609
|
+
client_capabilities: Mapping[str, Any] | None = None
|
|
610
|
+
client_info_name = None
|
|
611
|
+
client_info_version = None
|
|
612
|
+
is_connected = None
|
|
613
|
+
error_message = None
|
|
614
|
+
instructions_available = None
|
|
615
|
+
instructions_enabled = None
|
|
616
|
+
instructions_included = None
|
|
617
|
+
roots_configured = None
|
|
618
|
+
roots_count = None
|
|
619
|
+
elicitation_mode = None
|
|
620
|
+
sampling_mode = None
|
|
621
|
+
spoofing_enabled = None
|
|
622
|
+
server_cfg = None
|
|
623
|
+
session_id = None
|
|
624
|
+
server_conn = None
|
|
625
|
+
transport: str | None = None
|
|
626
|
+
transport_snapshot: TransportSnapshot | None = None
|
|
627
|
+
|
|
628
|
+
manager = getattr(self, "_persistent_connection_manager", None)
|
|
629
|
+
if self.connection_persistence and manager is not None:
|
|
630
|
+
try:
|
|
631
|
+
server_conn = await manager.get_server(
|
|
632
|
+
server_name,
|
|
633
|
+
client_session_factory=self._create_session_factory(server_name),
|
|
634
|
+
)
|
|
635
|
+
implementation = getattr(server_conn, "server_implementation", None)
|
|
636
|
+
if implementation:
|
|
637
|
+
implementation_name = getattr(implementation, "name", None)
|
|
638
|
+
implementation_version = getattr(implementation, "version", None)
|
|
639
|
+
capabilities = getattr(server_conn, "server_capabilities", None)
|
|
640
|
+
client_capabilities = getattr(server_conn, "client_capabilities", None)
|
|
641
|
+
session = server_conn.session
|
|
642
|
+
client_info = getattr(session, "client_info", None) if session else None
|
|
643
|
+
if client_info:
|
|
644
|
+
client_info_name = getattr(client_info, "name", None)
|
|
645
|
+
client_info_version = getattr(client_info, "version", None)
|
|
646
|
+
is_connected = server_conn.is_healthy()
|
|
647
|
+
error_message = getattr(server_conn, "_error_message", None)
|
|
648
|
+
instructions_available = getattr(
|
|
649
|
+
server_conn, "server_instructions_available", None
|
|
650
|
+
)
|
|
651
|
+
instructions_enabled = getattr(
|
|
652
|
+
server_conn, "server_instructions_enabled", None
|
|
653
|
+
)
|
|
654
|
+
instructions_included = bool(getattr(server_conn, "server_instructions", None))
|
|
655
|
+
server_cfg = getattr(server_conn, "server_config", None)
|
|
656
|
+
if session:
|
|
657
|
+
elicitation_mode = getattr(session, "effective_elicitation_mode", elicitation_mode)
|
|
658
|
+
session_id = getattr(server_conn, "session_id", None)
|
|
659
|
+
if not session_id and getattr(server_conn, "_get_session_id_cb", None):
|
|
660
|
+
try:
|
|
661
|
+
session_id = server_conn._get_session_id_cb() # type: ignore[attr-defined]
|
|
662
|
+
except Exception:
|
|
663
|
+
session_id = None
|
|
664
|
+
metrics = getattr(server_conn, "transport_metrics", None)
|
|
665
|
+
if metrics is not None:
|
|
666
|
+
try:
|
|
667
|
+
transport_snapshot = metrics.snapshot()
|
|
668
|
+
except Exception:
|
|
669
|
+
logger.debug(
|
|
670
|
+
"Failed to snapshot transport metrics for server '%s'",
|
|
671
|
+
server_name,
|
|
672
|
+
exc_info=True,
|
|
673
|
+
)
|
|
674
|
+
except Exception as exc:
|
|
675
|
+
logger.debug(
|
|
676
|
+
f"Failed to collect status for server '{server_name}'",
|
|
677
|
+
data={"error": str(exc)},
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
if server_cfg is None and self.context and getattr(self.context, "server_registry", None):
|
|
681
|
+
try:
|
|
682
|
+
server_cfg = self.context.server_registry.get_server_config(server_name)
|
|
683
|
+
except Exception:
|
|
684
|
+
server_cfg = None
|
|
685
|
+
|
|
686
|
+
if server_cfg is not None:
|
|
687
|
+
instructions_enabled = (
|
|
688
|
+
instructions_enabled
|
|
689
|
+
if instructions_enabled is not None
|
|
690
|
+
else server_cfg.include_instructions
|
|
691
|
+
)
|
|
692
|
+
roots = getattr(server_cfg, "roots", None)
|
|
693
|
+
roots_configured = bool(roots)
|
|
694
|
+
roots_count = len(roots) if roots else 0
|
|
695
|
+
transport = getattr(server_cfg, "transport", transport)
|
|
696
|
+
elicitation = getattr(server_cfg, "elicitation", None)
|
|
697
|
+
elicitation_mode = (
|
|
698
|
+
getattr(elicitation, "mode", None)
|
|
699
|
+
if elicitation
|
|
700
|
+
else elicitation_mode
|
|
701
|
+
)
|
|
702
|
+
sampling_cfg = getattr(server_cfg, "sampling", None)
|
|
703
|
+
spoofing_enabled = bool(getattr(server_cfg, "implementation", None))
|
|
704
|
+
if implementation_name is None and getattr(server_cfg, "implementation", None):
|
|
705
|
+
implementation_name = server_cfg.implementation.name
|
|
706
|
+
implementation_version = getattr(server_cfg.implementation, "version", None)
|
|
707
|
+
if session_id is None:
|
|
708
|
+
if server_cfg.transport == "stdio":
|
|
709
|
+
session_id = "local"
|
|
710
|
+
elif server_conn and getattr(server_conn, "_get_session_id_cb", None):
|
|
711
|
+
try:
|
|
712
|
+
session_id = server_conn._get_session_id_cb() # type: ignore[attr-defined]
|
|
713
|
+
except Exception:
|
|
714
|
+
session_id = None
|
|
715
|
+
|
|
716
|
+
if sampling_cfg is not None:
|
|
717
|
+
sampling_mode = "configured"
|
|
718
|
+
else:
|
|
719
|
+
auto_sampling = True
|
|
720
|
+
if self.context and getattr(self.context, "config", None):
|
|
721
|
+
auto_sampling = getattr(self.context.config, "auto_sampling", True)
|
|
722
|
+
sampling_mode = "auto" if auto_sampling else "off"
|
|
723
|
+
else:
|
|
724
|
+
# Fall back to defaults when config missing
|
|
725
|
+
auto_sampling = True
|
|
726
|
+
if self.context and getattr(self.context, "config", None):
|
|
727
|
+
auto_sampling = getattr(self.context.config, "auto_sampling", True)
|
|
728
|
+
sampling_mode = sampling_mode or ("auto" if auto_sampling else "off")
|
|
729
|
+
|
|
730
|
+
status_map[server_name] = ServerStatus(
|
|
731
|
+
server_name=server_name,
|
|
732
|
+
implementation_name=implementation_name,
|
|
733
|
+
implementation_version=implementation_version,
|
|
734
|
+
server_capabilities=capabilities,
|
|
735
|
+
client_capabilities=client_capabilities,
|
|
736
|
+
client_info_name=client_info_name,
|
|
737
|
+
client_info_version=client_info_version,
|
|
738
|
+
transport=transport,
|
|
739
|
+
is_connected=is_connected,
|
|
740
|
+
last_call_at=last_call,
|
|
741
|
+
last_error_at=last_error,
|
|
742
|
+
staleness_seconds=staleness,
|
|
743
|
+
call_counts=call_counts,
|
|
744
|
+
error_message=error_message,
|
|
745
|
+
instructions_available=instructions_available,
|
|
746
|
+
instructions_enabled=instructions_enabled,
|
|
747
|
+
instructions_included=instructions_included,
|
|
748
|
+
roots_configured=roots_configured,
|
|
749
|
+
roots_count=roots_count,
|
|
750
|
+
elicitation_mode=elicitation_mode,
|
|
751
|
+
sampling_mode=sampling_mode,
|
|
752
|
+
spoofing_enabled=spoofing_enabled,
|
|
753
|
+
session_id=session_id,
|
|
754
|
+
transport_channels=transport_snapshot,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
return status_map
|
|
758
|
+
|
|
495
759
|
async def _execute_on_server(
|
|
496
760
|
self,
|
|
497
761
|
server_name: str,
|
|
@@ -554,13 +818,17 @@ class MCPAggregator(ContextDependent):
|
|
|
554
818
|
# Re-raise the original exception to propagate it
|
|
555
819
|
raise e
|
|
556
820
|
|
|
821
|
+
success_flag: bool | None = None
|
|
822
|
+
result: R | None = None
|
|
823
|
+
|
|
557
824
|
# Try initial execution
|
|
558
825
|
try:
|
|
559
826
|
if self.connection_persistence:
|
|
560
827
|
server_connection = await self._persistent_connection_manager.get_server(
|
|
561
828
|
server_name, client_session_factory=self._create_session_factory(server_name)
|
|
562
829
|
)
|
|
563
|
-
|
|
830
|
+
result = await try_execute(server_connection.session)
|
|
831
|
+
success_flag = True
|
|
564
832
|
else:
|
|
565
833
|
logger.debug(
|
|
566
834
|
f"Creating temporary connection to server: {server_name}",
|
|
@@ -582,7 +850,7 @@ class MCPAggregator(ContextDependent):
|
|
|
582
850
|
"agent_name": self.agent_name,
|
|
583
851
|
},
|
|
584
852
|
)
|
|
585
|
-
|
|
853
|
+
success_flag = True
|
|
586
854
|
except ConnectionError:
|
|
587
855
|
# Server offline - attempt reconnection
|
|
588
856
|
from fast_agent.ui import console
|
|
@@ -613,7 +881,7 @@ class MCPAggregator(ContextDependent):
|
|
|
613
881
|
|
|
614
882
|
# Success!
|
|
615
883
|
console.console.print(f"[dim green]MCP server {server_name} online[/dim green]")
|
|
616
|
-
|
|
884
|
+
success_flag = True
|
|
617
885
|
|
|
618
886
|
except Exception:
|
|
619
887
|
# Reconnection failed
|
|
@@ -621,10 +889,19 @@ class MCPAggregator(ContextDependent):
|
|
|
621
889
|
f"[dim red]MCP server {server_name} offline - failed to reconnect[/dim red]"
|
|
622
890
|
)
|
|
623
891
|
error_msg = f"MCP server {server_name} offline - failed to reconnect"
|
|
892
|
+
success_flag = False
|
|
624
893
|
if error_factory:
|
|
625
|
-
|
|
894
|
+
result = error_factory(error_msg)
|
|
626
895
|
else:
|
|
627
896
|
raise Exception(error_msg)
|
|
897
|
+
except Exception:
|
|
898
|
+
success_flag = False
|
|
899
|
+
raise
|
|
900
|
+
finally:
|
|
901
|
+
if success_flag is not None:
|
|
902
|
+
await self._record_server_call(server_name, operation_type, success_flag)
|
|
903
|
+
|
|
904
|
+
return result
|
|
628
905
|
|
|
629
906
|
async def _parse_resource_name(self, name: str, resource_type: str) -> tuple[str, str]:
|
|
630
907
|
"""
|