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.

Files changed (34) hide show
  1. fast_agent/agents/llm_agent.py +30 -8
  2. fast_agent/agents/llm_decorator.py +2 -7
  3. fast_agent/agents/mcp_agent.py +9 -4
  4. fast_agent/cli/commands/auth.py +14 -1
  5. fast_agent/core/direct_factory.py +20 -8
  6. fast_agent/core/logging/listeners.py +2 -1
  7. fast_agent/interfaces.py +2 -2
  8. fast_agent/llm/model_database.py +7 -1
  9. fast_agent/llm/model_factory.py +2 -3
  10. fast_agent/llm/provider/anthropic/llm_anthropic.py +107 -62
  11. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +4 -3
  12. fast_agent/llm/provider/bedrock/llm_bedrock.py +1 -1
  13. fast_agent/llm/provider/google/google_converter.py +8 -41
  14. fast_agent/llm/provider/google/llm_google_native.py +1 -3
  15. fast_agent/llm/provider/openai/llm_azure.py +1 -1
  16. fast_agent/llm/provider/openai/llm_openai.py +3 -3
  17. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +1 -1
  18. fast_agent/llm/request_params.py +1 -1
  19. fast_agent/mcp/mcp_agent_client_session.py +45 -2
  20. fast_agent/mcp/mcp_aggregator.py +282 -5
  21. fast_agent/mcp/mcp_connection_manager.py +86 -10
  22. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  23. fast_agent/mcp/streamable_http_tracking.py +309 -0
  24. fast_agent/mcp/transport_tracking.py +598 -0
  25. fast_agent/resources/examples/data-analysis/analysis.py +7 -3
  26. fast_agent/ui/console_display.py +22 -1
  27. fast_agent/ui/enhanced_prompt.py +21 -1
  28. fast_agent/ui/interactive_prompt.py +5 -0
  29. fast_agent/ui/mcp_display.py +636 -0
  30. {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/METADATA +6 -6
  31. {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/RECORD +34 -30
  32. {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/WHEEL +0 -0
  33. {fast_agent_mcp-0.3.7.dist-info → fast_agent_mcp-0.3.9.dist-info}/entry_points.txt +0 -0
  34. {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("Streaming APIError during OpenAI completion", exc_info=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("TensorZeroOpenAIAugmentedLLM initialized.")
29
+ self.logger.info("TensorZeroOpenAILLM initialized.")
30
30
 
31
31
  def _initialize_default_params(self, kwargs: dict) -> RequestParams:
32
32
  """
@@ -11,7 +11,7 @@ from pydantic import Field
11
11
 
12
12
  class RequestParams(CreateMessageRequestParams):
13
13
  """
14
- Parameters to configure the AugmentedLLM 'generate' requests.
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, # No decorator-level handler since we're in the else block
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
@@ -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
- return await try_execute(server_connection.session)
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
- return result
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
- return result
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
- return error_factory(error_msg)
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
  """