fast-agent-mcp 0.3.5__py3-none-any.whl → 0.3.7__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 (41) hide show
  1. fast_agent/__init__.py +9 -1
  2. fast_agent/agents/agent_types.py +11 -11
  3. fast_agent/agents/llm_agent.py +76 -40
  4. fast_agent/agents/llm_decorator.py +355 -6
  5. fast_agent/agents/mcp_agent.py +154 -59
  6. fast_agent/agents/tool_agent.py +60 -4
  7. fast_agent/agents/workflow/router_agent.py +10 -2
  8. fast_agent/cli/commands/auth.py +52 -29
  9. fast_agent/cli/commands/check_config.py +26 -5
  10. fast_agent/cli/commands/go.py +11 -5
  11. fast_agent/cli/commands/setup.py +4 -7
  12. fast_agent/config.py +4 -1
  13. fast_agent/constants.py +2 -0
  14. fast_agent/core/agent_app.py +2 -0
  15. fast_agent/core/direct_factory.py +39 -120
  16. fast_agent/core/fastagent.py +2 -2
  17. fast_agent/history/history_exporter.py +3 -3
  18. fast_agent/llm/fastagent_llm.py +3 -3
  19. fast_agent/llm/provider/openai/llm_openai.py +57 -8
  20. fast_agent/mcp/__init__.py +1 -2
  21. fast_agent/mcp/mcp_aggregator.py +34 -1
  22. fast_agent/mcp/mcp_connection_manager.py +23 -4
  23. fast_agent/mcp/oauth_client.py +32 -4
  24. fast_agent/mcp/prompt_message_extended.py +2 -0
  25. fast_agent/mcp/prompt_serialization.py +124 -39
  26. fast_agent/mcp/prompts/prompt_load.py +34 -32
  27. fast_agent/mcp/prompts/prompt_server.py +26 -11
  28. fast_agent/resources/setup/.gitignore +6 -0
  29. fast_agent/resources/setup/agent.py +8 -1
  30. fast_agent/resources/setup/fastagent.config.yaml +2 -2
  31. fast_agent/resources/setup/pyproject.toml.tmpl +6 -0
  32. fast_agent/types/__init__.py +3 -1
  33. fast_agent/ui/console_display.py +48 -31
  34. fast_agent/ui/enhanced_prompt.py +119 -64
  35. fast_agent/ui/interactive_prompt.py +66 -40
  36. fast_agent/ui/rich_progress.py +12 -8
  37. {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/METADATA +3 -3
  38. {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/RECORD +41 -41
  39. {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/WHEEL +0 -0
  40. {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/entry_points.txt +0 -0
  41. {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/licenses/LICENSE +0 -0
@@ -26,9 +26,7 @@ def load_template_text(filename: str) -> str:
26
26
  res_name = "pyproject.toml.tmpl"
27
27
  else:
28
28
  res_name = filename
29
- resource_path = (
30
- files("fast_agent").joinpath("resources").joinpath("setup").joinpath(res_name)
31
- )
29
+ resource_path = files("fast_agent").joinpath("resources").joinpath("setup").joinpath(res_name)
32
30
  if resource_path.is_file():
33
31
  return resource_path.read_text()
34
32
 
@@ -137,9 +135,8 @@ def init(
137
135
  # Always use latest fast-agent-mcp (no version pin)
138
136
  fast_agent_dep = '"fast-agent-mcp"'
139
137
 
140
- return (
141
- template_text.replace("{{python_requires}}", py_req)
142
- .replace("{{fast_agent_dep}}", fast_agent_dep)
138
+ return template_text.replace("{{python_requires}}", py_req).replace(
139
+ "{{fast_agent_dep}}", fast_agent_dep
143
140
  )
144
141
 
145
142
  pyproject_template = load_template_text("pyproject.toml")
@@ -169,7 +166,7 @@ def init(
169
166
  "2. Keep fastagent.secrets.yaml secure and never commit it to version control"
170
167
  )
171
168
  console.print(
172
- "3. Update fastagent.config.yaml to set a default model (currently system default is 'haiku')"
169
+ "3. Update fastagent.config.yaml to set a default model (currently system default is 'gpt-5-mini.low')"
173
170
  )
174
171
  console.print("\nTo get started, run:")
175
172
  console.print(" uv run agent.py")
fast_agent/config.py CHANGED
@@ -37,7 +37,7 @@ class MCPServerAuthSettings(BaseModel):
37
37
 
38
38
 
39
39
  class MCPSamplingSettings(BaseModel):
40
- model: str = "haiku"
40
+ model: str = "gpt-5-mini.low"
41
41
 
42
42
  model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
43
43
 
@@ -122,6 +122,9 @@ class MCPServerSettings(BaseModel):
122
122
  cwd: str | None = None
123
123
  """Working directory for the executed server command."""
124
124
 
125
+ include_instructions: bool = True
126
+ """Whether to include this server's instructions in the system prompt (default: True)."""
127
+
125
128
  implementation: Implementation | None = None
126
129
 
127
130
  @model_validator(mode="before")
fast_agent/constants.py CHANGED
@@ -6,3 +6,5 @@ Global constants for fast_agent with minimal dependencies to avoid circular impo
6
6
  HUMAN_INPUT_TOOL_NAME = "__human_input"
7
7
  MCP_UI = "mcp-ui"
8
8
  REASONING = "reasoning"
9
+ FAST_AGENT_ERROR_CHANNEL = "fast-agent-error"
10
+ FAST_AGENT_REMOVED_METADATA_CHANNEL = "fast-agent-removed-meta"
@@ -34,6 +34,8 @@ class AgentApp:
34
34
  Args:
35
35
  agents: Dictionary of agent instances keyed by name
36
36
  """
37
+ if len(agents) == 0:
38
+ raise ValueError("No agents provided!")
37
39
  self._agents = agents
38
40
 
39
41
  def __getitem__(self, key: str) -> AgentProtocol:
@@ -3,7 +3,8 @@ Direct factory functions for creating agent and workflow instances without proxi
3
3
  Implements type-safe factories with improved error handling.
4
4
  """
5
5
 
6
- from typing import Any, Dict, Optional, Protocol, TypeVar
6
+ from functools import partial
7
+ from typing import Any, Dict, List, Optional, Protocol, TypeVar
7
8
 
8
9
  from fast_agent.agents import McpAgent
9
10
  from fast_agent.agents.agent_types import AgentConfig, AgentType
@@ -379,6 +380,35 @@ async def create_agents_by_type(
379
380
  return result_agents
380
381
 
381
382
 
383
+ async def active_agents_in_dependency_group(
384
+ app_instance: Core,
385
+ agents_dict: AgentConfigDict,
386
+ model_factory_func: ModelFactoryFunctionProtocol,
387
+ group: List[str],
388
+ active_agents: AgentDict,
389
+ ):
390
+ """
391
+ For each of the possible agent types, create agents and update the active agents dictionary.
392
+
393
+ Notice: This function modifies the active_agents dictionary in-place which is a feature (no copies).
394
+ """
395
+ type_of_agents = list(map(lambda c: (c, c.value), AgentType))
396
+ for agent_type, agent_type_value in type_of_agents:
397
+ agents_dict_local = {
398
+ name: agents_dict[name]
399
+ for name in group
400
+ if agents_dict[name]["type"] == agent_type_value
401
+ }
402
+ agents = await create_agents_by_type(
403
+ app_instance,
404
+ agents_dict_local,
405
+ agent_type,
406
+ model_factory_func,
407
+ active_agents,
408
+ )
409
+ active_agents.update(agents)
410
+
411
+
382
412
  async def create_agents_in_dependency_order(
383
413
  app_instance: Core,
384
414
  agents_dict: AgentConfigDict,
@@ -403,127 +433,16 @@ async def create_agents_in_dependency_order(
403
433
  # Create a dictionary to store all active agents/workflows
404
434
  active_agents: AgentDict = {}
405
435
 
436
+ active_agents_in_dependency_group_partial = partial(
437
+ active_agents_in_dependency_group,
438
+ app_instance,
439
+ agents_dict,
440
+ model_factory_func,
441
+ )
442
+
406
443
  # Create agent proxies for each group in dependency order
407
444
  for group in dependencies:
408
- # Create basic agents first
409
- # Note: We compare string values from config with the Enum's string value
410
- if AgentType.BASIC.value in [agents_dict[name]["type"] for name in group]:
411
- basic_agents = await create_agents_by_type(
412
- app_instance,
413
- {
414
- name: agents_dict[name]
415
- for name in group
416
- if agents_dict[name]["type"] == AgentType.BASIC.value
417
- },
418
- AgentType.BASIC,
419
- model_factory_func,
420
- active_agents,
421
- )
422
- active_agents.update(basic_agents)
423
-
424
- # Create custom agents first
425
- if AgentType.CUSTOM.value in [agents_dict[name]["type"] for name in group]:
426
- basic_agents = await create_agents_by_type(
427
- app_instance,
428
- {
429
- name: agents_dict[name]
430
- for name in group
431
- if agents_dict[name]["type"] == AgentType.CUSTOM.value
432
- },
433
- AgentType.CUSTOM,
434
- model_factory_func,
435
- active_agents,
436
- )
437
- active_agents.update(basic_agents)
438
-
439
- # Create parallel agents
440
- if AgentType.PARALLEL.value in [agents_dict[name]["type"] for name in group]:
441
- parallel_agents = await create_agents_by_type(
442
- app_instance,
443
- {
444
- name: agents_dict[name]
445
- for name in group
446
- if agents_dict[name]["type"] == AgentType.PARALLEL.value
447
- },
448
- AgentType.PARALLEL,
449
- model_factory_func,
450
- active_agents,
451
- )
452
- active_agents.update(parallel_agents)
453
-
454
- # Create router agents
455
- if AgentType.ROUTER.value in [agents_dict[name]["type"] for name in group]:
456
- router_agents = await create_agents_by_type(
457
- app_instance,
458
- {
459
- name: agents_dict[name]
460
- for name in group
461
- if agents_dict[name]["type"] == AgentType.ROUTER.value
462
- },
463
- AgentType.ROUTER,
464
- model_factory_func,
465
- active_agents,
466
- )
467
- active_agents.update(router_agents)
468
-
469
- # Create chain agents
470
- if AgentType.CHAIN.value in [agents_dict[name]["type"] for name in group]:
471
- chain_agents = await create_agents_by_type(
472
- app_instance,
473
- {
474
- name: agents_dict[name]
475
- for name in group
476
- if agents_dict[name]["type"] == AgentType.CHAIN.value
477
- },
478
- AgentType.CHAIN,
479
- model_factory_func,
480
- active_agents,
481
- )
482
- active_agents.update(chain_agents)
483
-
484
- # Create evaluator-optimizer agents
485
- if AgentType.EVALUATOR_OPTIMIZER.value in [agents_dict[name]["type"] for name in group]:
486
- evaluator_agents = await create_agents_by_type(
487
- app_instance,
488
- {
489
- name: agents_dict[name]
490
- for name in group
491
- if agents_dict[name]["type"] == AgentType.EVALUATOR_OPTIMIZER.value
492
- },
493
- AgentType.EVALUATOR_OPTIMIZER,
494
- model_factory_func,
495
- active_agents,
496
- )
497
- active_agents.update(evaluator_agents)
498
-
499
- if AgentType.ORCHESTRATOR.value in [agents_dict[name]["type"] for name in group]:
500
- orchestrator_agents = await create_agents_by_type(
501
- app_instance,
502
- {
503
- name: agents_dict[name]
504
- for name in group
505
- if agents_dict[name]["type"] == AgentType.ORCHESTRATOR.value
506
- },
507
- AgentType.ORCHESTRATOR,
508
- model_factory_func,
509
- active_agents,
510
- )
511
- active_agents.update(orchestrator_agents)
512
-
513
- # Create orchestrator2 agents last since they might depend on other agents
514
- if AgentType.ITERATIVE_PLANNER.value in [agents_dict[name]["type"] for name in group]:
515
- orchestrator2_agents = await create_agents_by_type(
516
- app_instance,
517
- {
518
- name: agents_dict[name]
519
- for name in group
520
- if agents_dict[name]["type"] == AgentType.ITERATIVE_PLANNER.value
521
- },
522
- AgentType.ITERATIVE_PLANNER,
523
- model_factory_func,
524
- active_agents,
525
- )
526
- active_agents.update(orchestrator2_agents)
445
+ await active_agents_in_dependency_group_partial(group, active_agents)
527
446
 
528
447
  return active_agents
529
448
 
@@ -75,7 +75,7 @@ from fast_agent.core.validation import (
75
75
  validate_server_references,
76
76
  validate_workflow_references,
77
77
  )
78
- from fast_agent.mcp.prompts.prompt_load import load_prompt_multipart
78
+ from fast_agent.mcp.prompts.prompt_load import load_prompt
79
79
  from fast_agent.ui.usage_display import display_usage_report
80
80
 
81
81
  if TYPE_CHECKING:
@@ -543,7 +543,7 @@ class FastAgent:
543
543
 
544
544
  if hasattr(self.args, "prompt_file") and self.args.prompt_file:
545
545
  agent_name = self.args.agent
546
- prompt: List[PromptMessageExtended] = load_prompt_multipart(
546
+ prompt: List[PromptMessageExtended] = load_prompt(
547
547
  Path(self.args.prompt_file)
548
548
  )
549
549
  if agent_name not in active_agents:
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from typing import TYPE_CHECKING, Optional
12
12
 
13
- from fast_agent.mcp.prompt_serialization import save_messages_to_file
13
+ from fast_agent.mcp.prompt_serialization import save_messages
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from fast_agent.interfaces import AgentProtocol
@@ -35,10 +35,10 @@ class HistoryExporter:
35
35
  The path that was written to.
36
36
  """
37
37
  # Determine a default filename when not provided
38
- target = filename or f"{getattr(agent, 'name', 'assistant')}_prompts.txt"
38
+ target = filename or f"{getattr(agent, 'name', 'assistant')}.json"
39
39
 
40
40
  messages = agent.message_history
41
- save_messages_to_file(messages, target)
41
+ save_messages(messages, target)
42
42
 
43
43
  # Return and optionally print a small confirmation
44
44
  return target
@@ -198,7 +198,7 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
198
198
  if messages[-1].first_text().startswith("***SAVE_HISTORY"):
199
199
  parts: list[str] = messages[-1].first_text().split(" ", 1)
200
200
  filename: str = (
201
- parts[1].strip() if len(parts) > 1 else f"{self.name or 'assistant'}_prompts.txt"
201
+ parts[1].strip() if len(parts) > 1 else f"{self.name or 'assistant'}.json"
202
202
  )
203
203
  await self._save_history(filename)
204
204
  return Prompt.assistant(f"History saved to {filename}")
@@ -589,10 +589,10 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
589
589
  Uses JSON format for .json files (MCP SDK compatible format) and
590
590
  delimited text format for other extensions.
591
591
  """
592
- from fast_agent.mcp.prompt_serialization import save_messages_to_file
592
+ from fast_agent.mcp.prompt_serialization import save_messages
593
593
 
594
594
  # Save messages using the unified save function that auto-detects format
595
- save_messages_to_file(self._message_history, filename)
595
+ save_messages(self._message_history, filename)
596
596
 
597
597
  @property
598
598
  def message_history(self) -> List[PromptMessageExtended]:
@@ -7,7 +7,7 @@ from mcp.types import (
7
7
  ContentBlock,
8
8
  TextContent,
9
9
  )
10
- from openai import AsyncOpenAI, AuthenticationError
10
+ from openai import APIError, AsyncOpenAI, AuthenticationError
11
11
  from openai.lib.streaming.chat import ChatCompletionStreamState
12
12
 
13
13
  # from openai.types.beta.chat import
@@ -19,19 +19,17 @@ from openai.types.chat import (
19
19
  )
20
20
  from pydantic_core import from_json
21
21
 
22
+ from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL
22
23
  from fast_agent.core.exceptions import ProviderKeyError
23
24
  from fast_agent.core.logging.logger import get_logger
24
25
  from fast_agent.core.prompt import Prompt
25
26
  from fast_agent.event_progress import ProgressAction
26
- from fast_agent.llm.fastagent_llm import (
27
- FastAgentLLM,
28
- RequestParams,
29
- )
27
+ from fast_agent.llm.fastagent_llm import FastAgentLLM, RequestParams
30
28
  from fast_agent.llm.provider.openai.multipart_converter_openai import OpenAIConverter, OpenAIMessage
31
29
  from fast_agent.llm.provider_types import Provider
32
30
  from fast_agent.llm.usage_tracking import TurnUsage
33
- from fast_agent.types import PromptMessageExtended
34
- from fast_agent.types.llm_stop_reason import LlmStopReason
31
+ from fast_agent.mcp.helpers.content_helpers import text_content
32
+ from fast_agent.types import LlmStopReason, PromptMessageExtended
35
33
 
36
34
  _logger = get_logger(__name__)
37
35
 
@@ -348,7 +346,11 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
348
346
  # Use basic streaming API
349
347
  stream = await self._openai_client().chat.completions.create(**arguments)
350
348
  # Process the stream
351
- response = await self._process_stream(stream, model_name)
349
+ try:
350
+ response = await self._process_stream(stream, model_name)
351
+ except APIError as error:
352
+ self.logger.error("Streaming APIError during OpenAI completion", exc_info=error)
353
+ return self._stream_failure_response(error, model_name)
352
354
  # Track usage if response is valid and has usage data
353
355
  if (
354
356
  hasattr(response, "usage")
@@ -438,6 +440,53 @@ class OpenAILLM(FastAgentLLM[ChatCompletionMessageParam, ChatCompletionMessage])
438
440
  *response_content_blocks, stop_reason=stop_reason, tool_calls=requested_tool_calls
439
441
  )
440
442
 
443
+ def _stream_failure_response(self, error: APIError, model_name: str) -> PromptMessageExtended:
444
+ """Convert streaming API errors into a graceful assistant reply."""
445
+
446
+ provider_label = (
447
+ self.provider.value if isinstance(self.provider, Provider) else str(self.provider)
448
+ )
449
+ detail = getattr(error, "message", None) or str(error)
450
+ detail = detail.strip() if isinstance(detail, str) else ""
451
+
452
+ parts: list[str] = [f"{provider_label} request failed"]
453
+ if model_name:
454
+ parts.append(f"for model '{model_name}'")
455
+ code = getattr(error, "code", None)
456
+ if code:
457
+ parts.append(f"(code: {code})")
458
+ status = getattr(error, "status_code", None)
459
+ if status:
460
+ parts.append(f"(status={status})")
461
+
462
+ message = " ".join(parts)
463
+ if detail:
464
+ message = f"{message}: {detail}"
465
+
466
+ user_summary = " ".join(message.split()) if message else ""
467
+ if user_summary and len(user_summary) > 280:
468
+ user_summary = user_summary[:277].rstrip() + "..."
469
+
470
+ if user_summary:
471
+ assistant_text = f"I hit an internal error while calling the model: {user_summary}"
472
+ if not assistant_text.endswith((".", "!", "?")):
473
+ assistant_text += "."
474
+ assistant_text += " See fast-agent-error for additional details."
475
+ else:
476
+ assistant_text = (
477
+ "I hit an internal error while calling the model; see fast-agent-error for details."
478
+ )
479
+
480
+ assistant_block = text_content(assistant_text)
481
+ error_block = text_content(message)
482
+
483
+ return PromptMessageExtended(
484
+ role="assistant",
485
+ content=[assistant_block],
486
+ channels={FAST_AGENT_ERROR_CHANNEL: [error_block]},
487
+ stop_reason=LlmStopReason.ERROR,
488
+ )
489
+
441
490
  async def _is_tool_stop_reason(self, finish_reason: str) -> bool:
442
491
  return True
443
492
 
@@ -24,11 +24,9 @@ from .helpers import (
24
24
  split_thinking_content,
25
25
  text_content,
26
26
  )
27
- from .prompt_message_extended import PromptMessageExtended
28
27
 
29
28
  __all__ = [
30
29
  "Prompt",
31
- "PromptMessageExtended",
32
30
  # Helpers
33
31
  "get_text",
34
32
  "get_image_data",
@@ -51,4 +49,5 @@ def __getattr__(name: str):
51
49
  from .prompt import Prompt # local import
52
50
 
53
51
  return Prompt
52
+
54
53
  raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@@ -461,6 +461,37 @@ class MCPAggregator(ContextDependent):
461
461
  for server_name in self.server_names:
462
462
  await self._refresh_server_tools(server_name)
463
463
 
464
+ async def get_server_instructions(self) -> Dict[str, tuple[str, List[str]]]:
465
+ """
466
+ Get instructions from all connected servers along with their tool names.
467
+
468
+ Returns:
469
+ Dict mapping server name to tuple of (instructions, list of tool names)
470
+ """
471
+ instructions = {}
472
+
473
+ if self.connection_persistence and hasattr(self, "_persistent_connection_manager"):
474
+ # Get instructions from persistent connections
475
+ for server_name in self.server_names:
476
+ try:
477
+ server_conn = await self._persistent_connection_manager.get_server(
478
+ server_name,
479
+ client_session_factory=self._create_session_factory(server_name),
480
+ )
481
+ # Always include server, even if no instructions
482
+ # Get tool names for this server
483
+ tool_names = [
484
+ namespaced_tool.tool.name
485
+ for namespaced_tool_name, namespaced_tool in self._namespaced_tool_map.items()
486
+ if namespaced_tool.server_name == server_name
487
+ ]
488
+ # Include server even if instructions is None
489
+ instructions[server_name] = (server_conn.server_instructions, tool_names)
490
+ except Exception as e:
491
+ logger.debug(f"Failed to get instructions from server {server_name}: {e}")
492
+
493
+ return instructions
494
+
464
495
  async def _execute_on_server(
465
496
  self,
466
497
  server_name: str,
@@ -1044,7 +1075,9 @@ class MCPAggregator(ContextDependent):
1044
1075
  logger.debug(f"Server '{server_name}' does not support tools")
1045
1076
  return
1046
1077
 
1047
- await self.display.show_tool_update(aggregator=self, updated_server=server_name)
1078
+ await self.display.show_tool_update(
1079
+ updated_server=server_name, agent_name="Tool List Change Notification"
1080
+ )
1048
1081
 
1049
1082
  async with self._refresh_lock:
1050
1083
  try:
@@ -105,6 +105,9 @@ class ServerConnection:
105
105
  self._error_occurred = False
106
106
  self._error_message = None
107
107
 
108
+ # Server instructions from initialization
109
+ self.server_instructions: str | None = None
110
+
108
111
  def is_healthy(self) -> bool:
109
112
  """Check if the server connection is healthy and ready to use."""
110
113
  return self.session is not None and not self._error_occurred
@@ -131,10 +134,20 @@ class ServerConnection:
131
134
  Initializes the server connection and session.
132
135
  Must be called within an async context.
133
136
  """
134
-
137
+ assert self.session, "Session must be created before initialization"
135
138
  result = await self.session.initialize()
136
139
 
137
140
  self.server_capabilities = result.capabilities
141
+
142
+ # Store instructions if provided by the server and enabled in config
143
+ if self.server_config.include_instructions:
144
+ self.server_instructions = getattr(result, 'instructions', None)
145
+ if self.server_instructions:
146
+ logger.debug(f"{self.server_name}: Received server instructions", data={"instructions": self.server_instructions})
147
+ else:
148
+ self.server_instructions = None
149
+ logger.debug(f"{self.server_name}: Server instructions disabled by configuration")
150
+
138
151
  # If there's an init hook, run it
139
152
 
140
153
  # Now the session is ready for use
@@ -343,7 +356,9 @@ class MCPConnectionManager(ContextDependent):
343
356
  def transport_context_factory():
344
357
  if config.transport == "stdio":
345
358
  if not config.command:
346
- raise ValueError(f"Server '{server_name}' uses stdio transport but no command is specified")
359
+ raise ValueError(
360
+ f"Server '{server_name}' uses stdio transport but no command is specified"
361
+ )
347
362
  server_params = StdioServerParameters(
348
363
  command=config.command,
349
364
  args=config.args if config.args is not None else [],
@@ -357,7 +372,9 @@ class MCPConnectionManager(ContextDependent):
357
372
  return _add_none_to_context(stdio_client(server_params, errlog=error_handler))
358
373
  elif config.transport == "sse":
359
374
  if not config.url:
360
- raise ValueError(f"Server '{server_name}' uses sse transport but no url is specified")
375
+ raise ValueError(
376
+ f"Server '{server_name}' uses sse transport but no url is specified"
377
+ )
361
378
  # Suppress MCP library error spam
362
379
  self._suppress_mcp_sse_errors()
363
380
  oauth_auth = build_oauth_provider(config)
@@ -376,7 +393,9 @@ class MCPConnectionManager(ContextDependent):
376
393
  )
377
394
  elif config.transport == "http":
378
395
  if not config.url:
379
- raise ValueError(f"Server '{server_name}' uses http transport but no url is specified")
396
+ raise ValueError(
397
+ f"Server '{server_name}' uses http transport but no url is specified"
398
+ )
380
399
  oauth_auth = build_oauth_provider(config)
381
400
  headers = dict(config.headers or {})
382
401
  if oauth_auth is not None:
@@ -109,7 +109,7 @@ class _CallbackHandler(BaseHTTPRequestHandler):
109
109
  self.send_response(404)
110
110
  self.end_headers()
111
111
 
112
- def log_message(self, fmt: str, *args: Any) -> None: # silence default logging
112
+ def log_message(self, format: str, *args: Any) -> None: # silence default logging
113
113
  return
114
114
 
115
115
 
@@ -218,10 +218,33 @@ def keyring_has_token(server_config: MCPServerSettings) -> bool:
218
218
  return False
219
219
 
220
220
 
221
- async def _print_authorization_link(auth_url: str) -> None:
222
- """Emit a clickable authorization link using rich console markup."""
221
+ async def _print_authorization_link(auth_url: str, warn_if_no_keyring: bool = False) -> None:
222
+ """Emit a clickable authorization link using rich console markup.
223
+
224
+ If warn_if_no_keyring is True and the OS keyring backend is unavailable,
225
+ print a warning to indicate tokens won't be persisted.
226
+ """
223
227
  console.console.print("[bold]Open this link to authorize:[/bold]", markup=True)
224
228
  console.console.print(f"[link={auth_url}]{auth_url}[/link]")
229
+ if warn_if_no_keyring:
230
+ try:
231
+ import keyring # type: ignore
232
+
233
+ backend = keyring.get_keyring()
234
+ try:
235
+ from keyring.backends.fail import Keyring as FailKeyring # type: ignore
236
+
237
+ if isinstance(backend, FailKeyring):
238
+ console.console.print(
239
+ "[yellow]Warning:[/yellow] Keyring backend not available — tokens will not be persisted."
240
+ )
241
+ except Exception:
242
+ # If we cannot detect the fail backend, do nothing
243
+ pass
244
+ except Exception:
245
+ console.console.print(
246
+ "[yellow]Warning:[/yellow] Keyring backend not available — tokens will not be persisted."
247
+ )
225
248
  logger.info("OAuth authorization URL emitted to console")
226
249
 
227
250
 
@@ -283,6 +306,7 @@ class KeyringTokenStorage(TokenStorage):
283
306
 
284
307
  # --- Keyring index helpers (to enable cross-platform token enumeration) ---
285
308
 
309
+
286
310
  def _index_username() -> str:
287
311
  return "oauth:index"
288
312
 
@@ -431,7 +455,11 @@ def build_oauth_provider(server_config: MCPServerSettings) -> OAuthClientProvide
431
455
 
432
456
  # Local callback server handler
433
457
  async def _redirect_handler(authorization_url: str) -> None:
434
- await _print_authorization_link(authorization_url)
458
+ # Warn if persisting to keyring but no backend is available
459
+ await _print_authorization_link(
460
+ authorization_url,
461
+ warn_if_no_keyring=(persist_mode == "keyring"),
462
+ )
435
463
 
436
464
  async def _callback_handler() -> tuple[str, str | None]:
437
465
  # Try local HTTP capture first
@@ -12,6 +12,8 @@ from mcp.types import (
12
12
  from pydantic import BaseModel
13
13
 
14
14
  from fast_agent.mcp.helpers.content_helpers import get_text
15
+
16
+ # Import directly to avoid circular dependency with types/__init__.py
15
17
  from fast_agent.types.llm_stop_reason import LlmStopReason
16
18
 
17
19