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
@@ -16,8 +16,6 @@ from typing import (
16
16
  Mapping,
17
17
  Optional,
18
18
  Sequence,
19
- Tuple,
20
- Type,
21
19
  TypeVar,
22
20
  Union,
23
21
  )
@@ -156,6 +154,9 @@ class McpAgent(ABC, ToolAgent):
156
154
  """
157
155
  await self.__aenter__()
158
156
 
157
+ # Apply template substitution to the instruction with server instructions
158
+ await self._apply_instruction_templates()
159
+
159
160
  async def shutdown(self) -> None:
160
161
  """
161
162
  Shutdown the agent and close all MCP server connections.
@@ -174,18 +175,71 @@ class McpAgent(ABC, ToolAgent):
174
175
  self._initialized = value
175
176
  self._aggregator.initialized = value
176
177
 
177
- async def __call__(
178
- self,
179
- message: Union[
180
- str,
181
- PromptMessage,
182
- PromptMessageExtended,
183
- Sequence[Union[str, PromptMessage, PromptMessageExtended]],
184
- ],
178
+ async def _apply_instruction_templates(self) -> None:
179
+ """
180
+ Apply template substitution to the instruction, including server instructions.
181
+ This is called during initialization after servers are connected.
182
+ """
183
+ if not self.instruction:
184
+ return
185
+
186
+ # Gather server instructions if the template includes {{serverInstructions}}
187
+ if "{{serverInstructions}}" in self.instruction:
188
+ try:
189
+ instructions_data = await self._aggregator.get_server_instructions()
190
+ server_instructions = self._format_server_instructions(instructions_data)
191
+ except Exception as e:
192
+ self.logger.warning(f"Failed to get server instructions: {e}")
193
+ server_instructions = ""
194
+
195
+ # Replace the template variable
196
+ self.instruction = self.instruction.replace(
197
+ "{{serverInstructions}}", server_instructions
198
+ )
199
+
200
+ # Update default request params to match
201
+ if self._default_request_params:
202
+ self._default_request_params.systemPrompt = self.instruction
203
+
204
+ self.logger.debug(f"Applied instruction templates for agent {self._name}")
205
+
206
+ def _format_server_instructions(
207
+ self, instructions_data: Dict[str, tuple[str | None, List[str]]]
185
208
  ) -> str:
186
- return await self.send(message)
209
+ """
210
+ Format server instructions with XML tags and tool lists.
211
+
212
+ Args:
213
+ instructions_data: Dict mapping server name to (instructions, tool_names)
214
+
215
+ Returns:
216
+ Formatted string with server instructions
217
+ """
218
+ if not instructions_data:
219
+ return ""
220
+
221
+ formatted_parts = []
222
+ for server_name, (instructions, tool_names) in instructions_data.items():
223
+ # Skip servers with no instructions
224
+ if instructions is None:
225
+ continue
226
+
227
+ # Format tool names with server prefix
228
+ prefixed_tools = [f"{server_name}-{tool}" for tool in tool_names]
229
+ tools_list = ", ".join(prefixed_tools) if prefixed_tools else "No tools available"
230
+
231
+ formatted_parts.append(
232
+ f'<mcp-server name="{server_name}">\n'
233
+ f"<tools>{tools_list}</tools>\n"
234
+ f"<instructions>\n{instructions}\n</instructions>\n"
235
+ f"</mcp-server>"
236
+ )
187
237
 
188
- async def send(
238
+ if formatted_parts:
239
+ return "\n\n".join(formatted_parts)
240
+ return ""
241
+
242
+ async def __call__(
189
243
  self,
190
244
  message: Union[
191
245
  str,
@@ -193,23 +247,34 @@ class McpAgent(ABC, ToolAgent):
193
247
  PromptMessageExtended,
194
248
  Sequence[Union[str, PromptMessage, PromptMessageExtended]],
195
249
  ],
196
- request_params: RequestParams | None = None,
197
250
  ) -> str:
198
- """
199
- Send a message to the agent and get a response.
200
-
201
- Args:
202
- message: Message content in various formats:
203
- - String: Converted to a user PromptMessageExtended
204
- - PromptMessage: Converted to PromptMessageExtended
205
- - PromptMessageExtended: Used directly
206
- - request_params: Optional request parameters
251
+ return await self.send(message)
207
252
 
208
- Returns:
209
- The agent's response as a string
210
- """
211
- response = await self.generate(message, request_params)
212
- return response.last_text() or ""
253
+ # async def send(
254
+ # self,
255
+ # message: Union[
256
+ # str,
257
+ # PromptMessage,
258
+ # PromptMessageExtended,
259
+ # Sequence[Union[str, PromptMessage, PromptMessageExtended]],
260
+ # ],
261
+ # request_params: RequestParams | None = None,
262
+ # ) -> str:
263
+ # """
264
+ # Send a message to the agent and get a response.
265
+
266
+ # Args:
267
+ # message: Message content in various formats:
268
+ # - String: Converted to a user PromptMessageExtended
269
+ # - PromptMessage: Converted to PromptMessageExtended
270
+ # - PromptMessageExtended: Used directly
271
+ # - request_params: Optional request parameters
272
+
273
+ # Returns:
274
+ # The agent's response as a string
275
+ # """
276
+ # response = await self.generate(message, request_params)
277
+ # return response.last_text() or ""
213
278
 
214
279
  def _matches_pattern(self, name: str, pattern: str, server_name: str) -> bool:
215
280
  """
@@ -533,6 +598,7 @@ class McpAgent(ABC, ToolAgent):
533
598
  return PromptMessageExtended(role="user", tool_results={})
534
599
 
535
600
  tool_results: dict[str, CallToolResult] = {}
601
+ self._tool_loop_error = None
536
602
 
537
603
  # Cache available tool names (original, not namespaced) for display
538
604
  available_tools = [
@@ -549,12 +615,41 @@ class McpAgent(ABC, ToolAgent):
549
615
  namespaced_tool = self._aggregator._namespaced_tool_map.get(tool_name)
550
616
  display_tool_name = namespaced_tool.tool.name if namespaced_tool else tool_name
551
617
 
618
+ tool_available = False
619
+ if tool_name == HUMAN_INPUT_TOOL_NAME:
620
+ tool_available = True
621
+ elif namespaced_tool:
622
+ tool_available = True
623
+ else:
624
+ tool_available = any(
625
+ candidate.tool.name == tool_name
626
+ for candidate in self._aggregator._namespaced_tool_map.values()
627
+ )
628
+
629
+ if not tool_available:
630
+ error_message = f"Tool '{display_tool_name}' is not available"
631
+ self.logger.error(error_message)
632
+ self._mark_tool_loop_error(
633
+ correlation_id=correlation_id,
634
+ error_message=error_message,
635
+ tool_results=tool_results,
636
+ )
637
+ break
638
+
639
+ # Find the index of the current tool in available_tools for highlighting
640
+ highlight_index = None
641
+ try:
642
+ highlight_index = available_tools.index(display_tool_name)
643
+ except ValueError:
644
+ # Tool not found in list, no highlighting
645
+ pass
646
+
552
647
  self.display.show_tool_call(
553
648
  name=self._name,
554
649
  tool_args=tool_args,
555
650
  bottom_items=available_tools,
556
651
  tool_name=display_tool_name,
557
- highlight_items=tool_name,
652
+ highlight_index=highlight_index,
558
653
  max_item_length=12,
559
654
  )
560
655
 
@@ -578,7 +673,7 @@ class McpAgent(ABC, ToolAgent):
578
673
  # Show error result too
579
674
  self.display.show_tool_result(name=self._name, result=error_result)
580
675
 
581
- return PromptMessageExtended(role="user", tool_results=tool_results)
676
+ return self._finalize_tool_results(tool_results)
582
677
 
583
678
  async def apply_prompt_template(self, prompt_result: GetPromptResult, prompt_name: str) -> str:
584
679
  """
@@ -596,36 +691,36 @@ class McpAgent(ABC, ToolAgent):
596
691
  with self._tracer.start_as_current_span(f"Agent: '{self._name}' apply_prompt_template"):
597
692
  return await self._llm.apply_prompt_template(prompt_result, prompt_name)
598
693
 
599
- async def structured(
600
- self,
601
- messages: Union[
602
- str,
603
- PromptMessage,
604
- PromptMessageExtended,
605
- List[Union[str, PromptMessage, PromptMessageExtended]],
606
- ],
607
- model: Type[ModelT],
608
- request_params: RequestParams | None = None,
609
- ) -> Tuple[ModelT | None, PromptMessageExtended]:
610
- """
611
- Apply the prompt and return the result as a Pydantic model.
612
- Normalizes input messages and delegates to the attached LLM.
613
-
614
- Args:
615
- messages: Message(s) in various formats:
616
- - String: Converted to a user PromptMessageExtended
617
- - PromptMessage: Converted to PromptMessageExtended
618
- - PromptMessageExtended: Used directly
619
- - List of any combination of the above
620
- model: The Pydantic model class to parse the result into
621
- request_params: Optional parameters to configure the LLM request
622
-
623
- Returns:
624
- An instance of the specified model, or None if coercion fails
625
- """
626
-
627
- with self._tracer.start_as_current_span(f"Agent: '{self._name}' structured"):
628
- return await super().structured(messages, model, request_params)
694
+ # async def structured(
695
+ # self,
696
+ # messages: Union[
697
+ # str,
698
+ # PromptMessage,
699
+ # PromptMessageExtended,
700
+ # Sequence[Union[str, PromptMessage, PromptMessageExtended]],
701
+ # ],
702
+ # model: Type[ModelT],
703
+ # request_params: RequestParams | None = None,
704
+ # ) -> Tuple[ModelT | None, PromptMessageExtended]:
705
+ # """
706
+ # Apply the prompt and return the result as a Pydantic model.
707
+ # Normalizes input messages and delegates to the attached LLM.
708
+
709
+ # Args:
710
+ # messages: Message(s) in various formats:
711
+ # - String: Converted to a user PromptMessageExtended
712
+ # - PromptMessage: Converted to PromptMessageExtended
713
+ # - PromptMessageExtended: Used directly
714
+ # - List of any combination of the above
715
+ # model: The Pydantic model class to parse the result into
716
+ # request_params: Optional parameters to configure the LLM request
717
+
718
+ # Returns:
719
+ # An instance of the specified model, or None if coercion fails
720
+ # """
721
+
722
+ # with self._tracer.start_as_current_span(f"Agent: '{self._name}' structured"):
723
+ # return await super().structured(messages, model, request_params)
629
724
 
630
725
  async def apply_prompt_messages(
631
726
  self, prompts: List[PromptMessageExtended], request_params: RequestParams | None = None
@@ -5,7 +5,7 @@ from mcp.types import CallToolResult, ListToolsResult, Tool
5
5
 
6
6
  from fast_agent.agents.agent_types import AgentConfig
7
7
  from fast_agent.agents.llm_agent import LlmAgent
8
- from fast_agent.constants import HUMAN_INPUT_TOOL_NAME
8
+ from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL, HUMAN_INPUT_TOOL_NAME
9
9
  from fast_agent.context import Context
10
10
  from fast_agent.core.logging.logger import get_logger
11
11
  from fast_agent.mcp.helpers.content_helpers import text_content
@@ -42,6 +42,7 @@ class ToolAgent(LlmAgent):
42
42
 
43
43
  self._execution_tools: dict[str, FastMCPTool] = {}
44
44
  self._tool_schemas: list[Tool] = []
45
+ self._tool_loop_error: str | None = None
45
46
 
46
47
  # Build a working list of tools and auto-inject human-input tool if missing
47
48
  working_tools: list[FastMCPTool | Callable] = list(tools) if tools else []
@@ -97,10 +98,19 @@ class ToolAgent(LlmAgent):
97
98
  )
98
99
 
99
100
  if LlmStopReason.TOOL_USE == result.stop_reason:
101
+ self._tool_loop_error = None
100
102
  if self.config.use_history:
101
- messages = [await self.run_tools(result)]
103
+ tool_message = await self.run_tools(result)
104
+ if self._tool_loop_error:
105
+ result.stop_reason = LlmStopReason.ERROR
106
+ break
107
+ messages = [tool_message]
102
108
  else:
103
- messages.extend([result, await self.run_tools(result)])
109
+ tool_message = await self.run_tools(result)
110
+ if self._tool_loop_error:
111
+ result.stop_reason = LlmStopReason.ERROR
112
+ break
113
+ messages.extend([result, tool_message])
104
114
  else:
105
115
  break
106
116
 
@@ -123,16 +133,37 @@ class ToolAgent(LlmAgent):
123
133
  return PromptMessageExtended(role="user", tool_results={})
124
134
 
125
135
  tool_results: dict[str, CallToolResult] = {}
136
+ self._tool_loop_error = None
126
137
  # TODO -- use gather() for parallel results, update display
127
138
  available_tools = [t.name for t in (await self.list_tools()).tools]
128
139
  for correlation_id, tool_request in request.tool_calls.items():
129
140
  tool_name = tool_request.params.name
130
141
  tool_args = tool_request.params.arguments or {}
142
+
143
+ if tool_name not in self._execution_tools:
144
+ error_message = f"Tool '{tool_name}' is not available"
145
+ logger.error(error_message)
146
+ self._mark_tool_loop_error(
147
+ correlation_id=correlation_id,
148
+ error_message=error_message,
149
+ tool_results=tool_results,
150
+ )
151
+ break
152
+
153
+ # Find the index of the current tool in available_tools for highlighting
154
+ highlight_index = None
155
+ try:
156
+ highlight_index = available_tools.index(tool_name)
157
+ except ValueError:
158
+ # Tool not found in list, no highlighting
159
+ pass
160
+
131
161
  self.display.show_tool_call(
132
162
  name=self.name,
133
163
  tool_args=tool_args,
134
164
  bottom_items=available_tools,
135
165
  tool_name=tool_name,
166
+ highlight_index=highlight_index,
136
167
  max_item_length=12,
137
168
  )
138
169
 
@@ -141,7 +172,32 @@ class ToolAgent(LlmAgent):
141
172
  tool_results[correlation_id] = result
142
173
  self.display.show_tool_result(name=self.name, result=result)
143
174
 
144
- return PromptMessageExtended(role="user", tool_results=tool_results)
175
+ return self._finalize_tool_results(tool_results)
176
+
177
+ def _mark_tool_loop_error(
178
+ self,
179
+ *,
180
+ correlation_id: str,
181
+ error_message: str,
182
+ tool_results: dict[str, CallToolResult],
183
+ ) -> None:
184
+ error_result = CallToolResult(
185
+ content=[text_content(error_message)],
186
+ isError=True,
187
+ )
188
+ tool_results[correlation_id] = error_result
189
+ self.display.show_tool_result(name=self.name, result=error_result)
190
+ self._tool_loop_error = error_message
191
+
192
+ def _finalize_tool_results(
193
+ self, tool_results: dict[str, CallToolResult]
194
+ ) -> PromptMessageExtended:
195
+ channels = None
196
+ if self._tool_loop_error:
197
+ channels = {
198
+ FAST_AGENT_ERROR_CHANNEL: [text_content(self._tool_loop_error)],
199
+ }
200
+ return PromptMessageExtended(role="user", tool_results=tool_results, channels=channels)
145
201
 
146
202
  async def list_tools(self) -> ListToolsResult:
147
203
  """Return available tools for this agent. Overridable by subclasses."""
@@ -300,10 +300,18 @@ class RouterAgent(LlmAgent):
300
300
  if response.reasoning:
301
301
  routing_message += f" ({response.reasoning})"
302
302
 
303
+ # Convert highlight_items to highlight_index
304
+ agent_keys = list(self.agent_map.keys())
305
+ highlight_index = None
306
+ try:
307
+ highlight_index = agent_keys.index(response.agent)
308
+ except ValueError:
309
+ pass
310
+
303
311
  await self.display.show_assistant_message(
304
312
  routing_message,
305
- bottom_items=list(self.agent_map.keys()),
306
- highlight_items=[response.agent],
313
+ bottom_items=agent_keys,
314
+ highlight_index=highlight_index,
307
315
  name=self.name,
308
316
  )
309
317
 
@@ -22,14 +22,28 @@ from fast_agent.ui.console import console
22
22
  app = typer.Typer(help="Manage OAuth authentication state for MCP servers")
23
23
 
24
24
 
25
- def _get_keyring_backend_name() -> str:
25
+ def _get_keyring_status() -> tuple[str, bool]:
26
+ """Return (backend_name, usable) where usable=False for the fail backend or missing keyring."""
26
27
  try:
27
28
  import keyring
28
29
 
29
30
  kr = keyring.get_keyring()
30
- return getattr(kr, "name", kr.__class__.__name__)
31
+ name = getattr(kr, "name", kr.__class__.__name__)
32
+ try:
33
+ from keyring.backends.fail import Keyring as FailKeyring # type: ignore
34
+
35
+ return name, not isinstance(kr, FailKeyring)
36
+ except Exception:
37
+ # If fail backend marker cannot be imported, assume usable
38
+ return name, True
31
39
  except Exception:
32
- return "unavailable"
40
+ return "unavailable", False
41
+
42
+
43
+ def _get_keyring_backend_name() -> str:
44
+ # Backwards-compat helper; prefer _get_keyring_status in new code
45
+ name, _ = _get_keyring_status()
46
+ return name
33
47
 
34
48
 
35
49
  def _keyring_get_password(service: str, username: str) -> str | None:
@@ -106,7 +120,7 @@ def status(
106
120
  ) -> None:
107
121
  """Show keyring backend and token status for configured MCP servers."""
108
122
  settings = get_settings(config_path)
109
- backend = _get_keyring_backend_name()
123
+ backend, backend_usable = _get_keyring_status()
110
124
 
111
125
  # Single-target view if target provided
112
126
  if target:
@@ -123,12 +137,15 @@ def status(
123
137
 
124
138
  # Direct presence check
125
139
  present = False
126
- try:
127
- import keyring
140
+ if backend_usable:
141
+ try:
142
+ import keyring
128
143
 
129
- present = keyring.get_password("fast-agent-mcp", f"oauth:tokens:{identity}") is not None
130
- except Exception:
131
- present = False
144
+ present = (
145
+ keyring.get_password("fast-agent-mcp", f"oauth:tokens:{identity}") is not None
146
+ )
147
+ except Exception:
148
+ present = False
132
149
 
133
150
  table = Table(show_header=True, box=None)
134
151
  table.add_column("Identity", header_style="bold")
@@ -139,7 +156,10 @@ def status(
139
156
  token_disp = "[bold green]✓[/bold green]" if present else "[dim]✗[/dim]"
140
157
  table.add_row(identity, token_disp, servers_for_id)
141
158
 
142
- console.print(f"Keyring backend: [green]{backend}[/green]")
159
+ if backend_usable and backend != "unavailable":
160
+ console.print(f"Keyring backend: [green]{backend}[/green]")
161
+ else:
162
+ console.print("Keyring backend: [red]not available[/red]")
143
163
  console.print(table)
144
164
  console.print(
145
165
  "\n[dim]Run 'fast-agent auth clear --identity "
@@ -148,7 +168,10 @@ def status(
148
168
  return
149
169
 
150
170
  # Full status view
151
- console.print(f"Keyring backend: [green]{backend}[/green]")
171
+ if backend_usable and backend != "unavailable":
172
+ console.print(f"Keyring backend: [green]{backend}[/green]")
173
+ else:
174
+ console.print("Keyring backend: [red]not available[/red]")
152
175
 
153
176
  tokens = list_keyring_tokens()
154
177
  token_table = Table(show_header=True, box=None)
@@ -181,25 +204,25 @@ def status(
181
204
  )
182
205
  # Direct presence check for each identity so status works even without index
183
206
  has_token = False
207
+ token_disp = "[dim]✗[/dim]"
184
208
  if persist == "keyring" and row["oauth"]:
185
- try:
186
- import keyring
187
-
188
- has_token = (
189
- keyring.get_password("fast-agent-mcp", f"oauth:tokens:{row['identity']}")
190
- is not None
191
- )
192
- except Exception:
193
- has_token = False
194
- token_disp = (
195
- "[bold green]✓[/bold green]"
196
- if has_token
197
- else (
198
- "[yellow]memory[/yellow]"
199
- if persist == "memory" and row["oauth"]
200
- else "[dim]✗[/dim]"
201
- )
202
- )
209
+ if backend_usable:
210
+ try:
211
+ import keyring
212
+
213
+ has_token = (
214
+ keyring.get_password(
215
+ "fast-agent-mcp", f"oauth:tokens:{row['identity']}"
216
+ )
217
+ is not None
218
+ )
219
+ except Exception:
220
+ has_token = False
221
+ token_disp = "[bold green]✓[/bold green]" if has_token else "[dim]✗[/dim]"
222
+ else:
223
+ token_disp = "[red]not available[/red]"
224
+ elif persist == "memory" and row["oauth"]:
225
+ token_disp = "[yellow]memory[/yellow]"
203
226
  map_table.add_row(
204
227
  row["name"],
205
228
  row["transport"].upper(),
@@ -305,14 +305,25 @@ def show_check_summary() -> None:
305
305
  env_table.add_column("Value")
306
306
 
307
307
  # Determine keyring backend early so it can appear in the top section
308
+ # Also detect whether the backend is actually usable (not the fail backend)
309
+ keyring_usable = False
308
310
  try:
309
311
  import keyring # type: ignore
310
312
 
311
313
  keyring_backend = keyring.get_keyring()
312
314
  keyring_name = getattr(keyring_backend, "name", keyring_backend.__class__.__name__)
315
+ try:
316
+ # Detect the "fail" backend explicitly; it's present but unusable
317
+ from keyring.backends.fail import Keyring as FailKeyring # type: ignore
318
+
319
+ keyring_usable = not isinstance(keyring_backend, FailKeyring)
320
+ except Exception:
321
+ # If we can't import the fail backend marker, assume usable
322
+ keyring_usable = True
313
323
  except Exception:
314
324
  keyring = None # type: ignore
315
325
  keyring_name = "unavailable"
326
+ keyring_usable = False
316
327
 
317
328
  # Python info (highlight version and path in green)
318
329
  env_table.add_row(
@@ -345,11 +356,14 @@ def show_check_summary() -> None:
345
356
  )
346
357
  else: # parsed successfully
347
358
  env_table.add_row("Config File", f"[green]Found[/green] ({config_path})")
348
- default_model_value = config_summary.get("default_model", "haiku (system default)")
359
+ default_model_value = config_summary.get("default_model", "gpt-5-mini.low (system default)")
349
360
  env_table.add_row("Default Model", f"[green]{default_model_value}[/green]")
350
361
 
351
362
  # Keyring backend (always shown in application-level settings)
352
- env_table.add_row("Keyring Backend", f"[green]{keyring_name}[/green]")
363
+ if keyring_usable and keyring_name != "unavailable":
364
+ env_table.add_row("Keyring Backend", f"[green]{keyring_name}[/green]")
365
+ else:
366
+ env_table.add_row("Keyring Backend", "[red]not available[/red]")
353
367
 
354
368
  console.print(env_table)
355
369
 
@@ -514,7 +528,9 @@ def show_check_summary() -> None:
514
528
  try:
515
529
  cfg = MCPServerSettings(
516
530
  name=name,
517
- transport="sse" if transport == "SSE" else ("stdio" if transport == "STDIO" else "http"),
531
+ transport="sse"
532
+ if transport == "SSE"
533
+ else ("stdio" if transport == "STDIO" else "http"),
518
534
  url=(server.get("url") or None),
519
535
  auth=server.get("auth") if isinstance(server.get("auth"), dict) else None,
520
536
  )
@@ -532,11 +548,16 @@ def show_check_summary() -> None:
532
548
  persist = "keyring"
533
549
  if cfg.auth is not None and hasattr(cfg.auth, "persist"):
534
550
  persist = getattr(cfg.auth, "persist") or "keyring"
535
- if keyring and persist == "keyring" and oauth_enabled:
551
+ if keyring and keyring_usable and persist == "keyring" and oauth_enabled:
536
552
  identity = compute_server_identity(cfg)
537
553
  tkey = f"oauth:tokens:{identity}"
538
- has = keyring.get_password("fast-agent-mcp", tkey) is not None
554
+ try:
555
+ has = keyring.get_password("fast-agent-mcp", tkey) is not None
556
+ except Exception:
557
+ has = False
539
558
  token_status = "[bold green]✓[/bold green]" if has else "[dim]✗[/dim]"
559
+ elif persist == "keyring" and not keyring_usable and oauth_enabled:
560
+ token_status = "[red]not available[/red]"
540
561
  elif persist == "memory" and oauth_enabled:
541
562
  token_status = "[yellow]memory[/yellow]"
542
563
 
@@ -18,10 +18,16 @@ app = typer.Typer(
18
18
  context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
19
19
  )
20
20
 
21
+ default_instruction = """You are a helpful AI Agent.
22
+
23
+ {{serverInstructions}}
24
+
25
+ The current date is {{currentDate}}."""
26
+
21
27
 
22
28
  async def _run_agent(
23
29
  name: str = "fast-agent cli",
24
- instruction: str = "You are a helpful AI Agent.",
30
+ instruction: str = default_instruction,
25
31
  config_path: Optional[str] = None,
26
32
  server_list: Optional[List[str]] = None,
27
33
  model: Optional[str] = None,
@@ -34,7 +40,7 @@ async def _run_agent(
34
40
  """Async implementation to run an interactive agent."""
35
41
  from pathlib import Path
36
42
 
37
- from fast_agent.mcp.prompts.prompt_load import load_prompt_multipart
43
+ from fast_agent.mcp.prompts.prompt_load import load_prompt
38
44
 
39
45
  # Create the FastAgent instance
40
46
 
@@ -104,7 +110,7 @@ async def _run_agent(
104
110
  display = ConsoleDisplay(config=None)
105
111
  display.show_parallel_results(agent.parallel)
106
112
  elif prompt_file:
107
- prompt = load_prompt_multipart(Path(prompt_file))
113
+ prompt = load_prompt(Path(prompt_file))
108
114
  await agent.parallel.generate(prompt)
109
115
  display = ConsoleDisplay(config=None)
110
116
  display.show_parallel_results(agent.parallel)
@@ -129,7 +135,7 @@ async def _run_agent(
129
135
  # Print the response and exit
130
136
  print(response)
131
137
  elif prompt_file:
132
- prompt = load_prompt_multipart(Path(prompt_file))
138
+ prompt = load_prompt(Path(prompt_file))
133
139
  response = await agent.agent.generate(prompt)
134
140
  print(f"\nLoaded {len(prompt)} messages from prompt file '{prompt_file}'")
135
141
  await agent.interactive()
@@ -352,7 +358,7 @@ def go(
352
358
  stdio_commands.append(stdio)
353
359
 
354
360
  # Resolve instruction from file/URL or use default
355
- resolved_instruction = "You are a helpful AI Agent." # Default
361
+ resolved_instruction = default_instruction # Default
356
362
  agent_name = "agent"
357
363
 
358
364
  if instruction: