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/cli/commands/setup.py
CHANGED
|
@@ -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
|
-
|
|
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 '
|
|
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 = "
|
|
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"
|
fast_agent/core/agent_app.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
fast_agent/core/fastagent.py
CHANGED
|
@@ -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
|
|
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] =
|
|
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
|
|
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')}
|
|
38
|
+
target = filename or f"{getattr(agent, 'name', 'assistant')}.json"
|
|
39
39
|
|
|
40
40
|
messages = agent.message_history
|
|
41
|
-
|
|
41
|
+
save_messages(messages, target)
|
|
42
42
|
|
|
43
43
|
# Return and optionally print a small confirmation
|
|
44
44
|
return target
|
fast_agent/llm/fastagent_llm.py
CHANGED
|
@@ -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'}
|
|
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
|
|
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
|
-
|
|
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.
|
|
34
|
-
from fast_agent.types
|
|
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
|
-
|
|
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
|
|
fast_agent/mcp/__init__.py
CHANGED
|
@@ -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}'")
|
fast_agent/mcp/mcp_aggregator.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
fast_agent/mcp/oauth_client.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|