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.
- fast_agent/__init__.py +9 -1
- fast_agent/agents/agent_types.py +11 -11
- fast_agent/agents/llm_agent.py +76 -40
- fast_agent/agents/llm_decorator.py +355 -6
- fast_agent/agents/mcp_agent.py +154 -59
- fast_agent/agents/tool_agent.py +60 -4
- fast_agent/agents/workflow/router_agent.py +10 -2
- fast_agent/cli/commands/auth.py +52 -29
- fast_agent/cli/commands/check_config.py +26 -5
- fast_agent/cli/commands/go.py +11 -5
- fast_agent/cli/commands/setup.py +4 -7
- fast_agent/config.py +4 -1
- fast_agent/constants.py +2 -0
- fast_agent/core/agent_app.py +2 -0
- fast_agent/core/direct_factory.py +39 -120
- fast_agent/core/fastagent.py +2 -2
- fast_agent/history/history_exporter.py +3 -3
- fast_agent/llm/fastagent_llm.py +3 -3
- fast_agent/llm/provider/openai/llm_openai.py +57 -8
- fast_agent/mcp/__init__.py +1 -2
- fast_agent/mcp/mcp_aggregator.py +34 -1
- fast_agent/mcp/mcp_connection_manager.py +23 -4
- fast_agent/mcp/oauth_client.py +32 -4
- fast_agent/mcp/prompt_message_extended.py +2 -0
- fast_agent/mcp/prompt_serialization.py +124 -39
- fast_agent/mcp/prompts/prompt_load.py +34 -32
- fast_agent/mcp/prompts/prompt_server.py +26 -11
- fast_agent/resources/setup/.gitignore +6 -0
- fast_agent/resources/setup/agent.py +8 -1
- fast_agent/resources/setup/fastagent.config.yaml +2 -2
- fast_agent/resources/setup/pyproject.toml.tmpl +6 -0
- fast_agent/types/__init__.py +3 -1
- fast_agent/ui/console_display.py +48 -31
- fast_agent/ui/enhanced_prompt.py +119 -64
- fast_agent/ui/interactive_prompt.py +66 -40
- fast_agent/ui/rich_progress.py +12 -8
- {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/METADATA +3 -3
- {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/RECORD +41 -41
- {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.5.dist-info → fast_agent_mcp-0.3.7.dist-info}/licenses/LICENSE +0 -0
fast_agent/agents/mcp_agent.py
CHANGED
|
@@ -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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
) -> Tuple[ModelT | None, PromptMessageExtended]:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
fast_agent/agents/tool_agent.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
306
|
-
|
|
313
|
+
bottom_items=agent_keys,
|
|
314
|
+
highlight_index=highlight_index,
|
|
307
315
|
name=self.name,
|
|
308
316
|
)
|
|
309
317
|
|
fast_agent/cli/commands/auth.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
127
|
-
|
|
140
|
+
if backend_usable:
|
|
141
|
+
try:
|
|
142
|
+
import keyring
|
|
128
143
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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", "
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
|
fast_agent/cli/commands/go.py
CHANGED
|
@@ -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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
361
|
+
resolved_instruction = default_instruction # Default
|
|
356
362
|
agent_name = "agent"
|
|
357
363
|
|
|
358
364
|
if instruction:
|