fast-agent-mcp 0.4.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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper functions for working with content objects (Fast Agent namespace).
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, Sequence, Union
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
|
|
10
|
+
|
|
11
|
+
from mcp.types import (
|
|
12
|
+
BlobResourceContents,
|
|
13
|
+
ContentBlock,
|
|
14
|
+
EmbeddedResource,
|
|
15
|
+
ImageContent,
|
|
16
|
+
PromptMessage,
|
|
17
|
+
ReadResourceResult,
|
|
18
|
+
ResourceLink,
|
|
19
|
+
TextContent,
|
|
20
|
+
TextResourceContents,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_text(content: ContentBlock) -> str | None:
|
|
25
|
+
"""Extract text content from a content object if available."""
|
|
26
|
+
if isinstance(content, TextContent):
|
|
27
|
+
return content.text
|
|
28
|
+
|
|
29
|
+
if isinstance(content, TextResourceContents):
|
|
30
|
+
return content.text
|
|
31
|
+
|
|
32
|
+
if isinstance(content, EmbeddedResource):
|
|
33
|
+
if isinstance(content.resource, TextResourceContents):
|
|
34
|
+
return content.resource.text
|
|
35
|
+
|
|
36
|
+
if isinstance(content, ResourceLink):
|
|
37
|
+
name = content.name or "unknown"
|
|
38
|
+
uri_str = str(content.uri)
|
|
39
|
+
mime_type = content.mimeType or "unknown"
|
|
40
|
+
description = content.description or ""
|
|
41
|
+
|
|
42
|
+
lines = [
|
|
43
|
+
f"[ResourceLink: {name} ({mime_type})]",
|
|
44
|
+
f"URI: {uri_str}",
|
|
45
|
+
]
|
|
46
|
+
if description:
|
|
47
|
+
lines.append(description)
|
|
48
|
+
|
|
49
|
+
return "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_image_data(content: ContentBlock) -> str | None:
|
|
55
|
+
"""Extract image data from a content object if available."""
|
|
56
|
+
if isinstance(content, ImageContent):
|
|
57
|
+
return content.data
|
|
58
|
+
|
|
59
|
+
if isinstance(content, EmbeddedResource):
|
|
60
|
+
if isinstance(content.resource, BlobResourceContents):
|
|
61
|
+
return content.resource.blob
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_resource_uri(content: ContentBlock) -> str | None:
|
|
67
|
+
"""Extract resource URI from an EmbeddedResource if available."""
|
|
68
|
+
if isinstance(content, EmbeddedResource):
|
|
69
|
+
return str(content.resource.uri)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_text_content(content: ContentBlock) -> bool:
|
|
74
|
+
"""Check if the content is text content."""
|
|
75
|
+
return isinstance(content, TextContent) or isinstance(content, TextResourceContents)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_image_content(content: Union[TextContent, ImageContent, EmbeddedResource]) -> bool:
|
|
79
|
+
"""Check if the content is image content."""
|
|
80
|
+
return isinstance(content, ImageContent)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_resource_content(content: ContentBlock) -> bool:
|
|
84
|
+
"""Check if the content is an embedded resource."""
|
|
85
|
+
return isinstance(content, EmbeddedResource)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_resource_link(content: ContentBlock) -> bool:
|
|
89
|
+
"""Check if the content is a resource link."""
|
|
90
|
+
return isinstance(content, ResourceLink)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_resource_text(result: ReadResourceResult, index: int = 0) -> str | None:
|
|
94
|
+
"""Extract text content from a ReadResourceResult at the specified index."""
|
|
95
|
+
if index >= len(result.contents):
|
|
96
|
+
raise IndexError(
|
|
97
|
+
f"Index {index} out of bounds for contents list of length {len(result.contents)}"
|
|
98
|
+
)
|
|
99
|
+
content = result.contents[index]
|
|
100
|
+
if isinstance(content, TextResourceContents):
|
|
101
|
+
return content.text
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def split_thinking_content(message: str) -> tuple[str | None, str]:
|
|
106
|
+
"""Split a message into thinking and content parts."""
|
|
107
|
+
import re
|
|
108
|
+
|
|
109
|
+
pattern = r"^<think>(.*?)</think>\s*(.*)$"
|
|
110
|
+
match = re.match(pattern, message, re.DOTALL)
|
|
111
|
+
|
|
112
|
+
if match:
|
|
113
|
+
thinking_content = match.group(1).strip()
|
|
114
|
+
main_content = match.group(2).strip()
|
|
115
|
+
if main_content.startswith("<think>"):
|
|
116
|
+
nested_thinking, remaining = split_thinking_content(main_content)
|
|
117
|
+
if nested_thinking is not None:
|
|
118
|
+
thinking_content = "\n".join(
|
|
119
|
+
part for part in [thinking_content, nested_thinking] if part
|
|
120
|
+
)
|
|
121
|
+
main_content = remaining
|
|
122
|
+
return (thinking_content, main_content)
|
|
123
|
+
else:
|
|
124
|
+
return (None, message)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def text_content(text: str) -> TextContent:
|
|
128
|
+
"""Convenience to create a TextContent block from a string."""
|
|
129
|
+
return TextContent(type="text", text=text)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _infer_mime_type(url: str, default: str = "application/octet-stream") -> str:
|
|
133
|
+
"""Infer MIME type from URL using the mimetypes database."""
|
|
134
|
+
from urllib.parse import urlparse
|
|
135
|
+
|
|
136
|
+
from fast_agent.mcp.mime_utils import guess_mime_type
|
|
137
|
+
|
|
138
|
+
# Special case: YouTube URLs (Google has native support)
|
|
139
|
+
parsed = urlparse(url.lower())
|
|
140
|
+
youtube_hosts = ("youtube.com", "www.youtube.com", "youtu.be", "m.youtube.com")
|
|
141
|
+
if parsed.netloc in youtube_hosts:
|
|
142
|
+
return "video/mp4"
|
|
143
|
+
|
|
144
|
+
mime = guess_mime_type(url)
|
|
145
|
+
# guess_mime_type returns "application/octet-stream" for unknown types
|
|
146
|
+
if mime == "application/octet-stream":
|
|
147
|
+
return default
|
|
148
|
+
return mime
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _extract_name_from_url(url: str) -> str:
|
|
152
|
+
"""Extract a reasonable name from a URL."""
|
|
153
|
+
from urllib.parse import unquote, urlparse
|
|
154
|
+
|
|
155
|
+
path = urlparse(url).path
|
|
156
|
+
if path:
|
|
157
|
+
# Get the last path segment
|
|
158
|
+
name = unquote(path.rstrip("/").split("/")[-1])
|
|
159
|
+
if name:
|
|
160
|
+
return name
|
|
161
|
+
# Fallback to domain
|
|
162
|
+
return urlparse(url).netloc or "resource"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resource_link(
|
|
166
|
+
url: str,
|
|
167
|
+
*,
|
|
168
|
+
name: str | None = None,
|
|
169
|
+
mime_type: str | None = None,
|
|
170
|
+
description: str | None = None,
|
|
171
|
+
) -> ResourceLink:
|
|
172
|
+
"""
|
|
173
|
+
Create a ResourceLink from a URL with automatic MIME type inference.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
url: The URL to the resource
|
|
177
|
+
name: Optional name (defaults to filename from URL)
|
|
178
|
+
mime_type: Optional MIME type (inferred from extension if not provided)
|
|
179
|
+
description: Optional description
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A ResourceLink object
|
|
183
|
+
"""
|
|
184
|
+
from pydantic import AnyUrl
|
|
185
|
+
|
|
186
|
+
return ResourceLink(
|
|
187
|
+
type="resource_link",
|
|
188
|
+
uri=AnyUrl(url),
|
|
189
|
+
name=name or _extract_name_from_url(url),
|
|
190
|
+
mimeType=mime_type or _infer_mime_type(url),
|
|
191
|
+
description=description,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def image_link(
|
|
196
|
+
url: str,
|
|
197
|
+
*,
|
|
198
|
+
name: str | None = None,
|
|
199
|
+
mime_type: str | None = None,
|
|
200
|
+
description: str | None = None,
|
|
201
|
+
) -> ResourceLink:
|
|
202
|
+
"""
|
|
203
|
+
Create a ResourceLink for an image URL.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
url: The URL to the image
|
|
207
|
+
name: Optional name (defaults to filename from URL)
|
|
208
|
+
mime_type: Optional MIME type (inferred from extension, defaults to image/jpeg)
|
|
209
|
+
description: Optional description
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
A ResourceLink object with image MIME type
|
|
213
|
+
"""
|
|
214
|
+
inferred = _infer_mime_type(url, default="image/jpeg")
|
|
215
|
+
# Ensure it's an image type
|
|
216
|
+
if not inferred.startswith("image/"):
|
|
217
|
+
inferred = "image/jpeg"
|
|
218
|
+
|
|
219
|
+
return resource_link(
|
|
220
|
+
url,
|
|
221
|
+
name=name,
|
|
222
|
+
mime_type=mime_type or inferred,
|
|
223
|
+
description=description,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def video_link(
|
|
228
|
+
url: str,
|
|
229
|
+
*,
|
|
230
|
+
name: str | None = None,
|
|
231
|
+
mime_type: str | None = None,
|
|
232
|
+
description: str | None = None,
|
|
233
|
+
) -> ResourceLink:
|
|
234
|
+
"""
|
|
235
|
+
Create a ResourceLink for a video URL.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
url: The URL to the video
|
|
239
|
+
name: Optional name (defaults to filename from URL)
|
|
240
|
+
mime_type: Optional MIME type (inferred from extension, defaults to video/mp4)
|
|
241
|
+
description: Optional description
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
A ResourceLink object with video MIME type
|
|
245
|
+
"""
|
|
246
|
+
inferred = _infer_mime_type(url, default="video/mp4")
|
|
247
|
+
# Ensure it's a video type
|
|
248
|
+
if not inferred.startswith("video/"):
|
|
249
|
+
inferred = "video/mp4"
|
|
250
|
+
|
|
251
|
+
return resource_link(
|
|
252
|
+
url,
|
|
253
|
+
name=name,
|
|
254
|
+
mime_type=mime_type or inferred,
|
|
255
|
+
description=description,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def audio_link(
|
|
260
|
+
url: str,
|
|
261
|
+
*,
|
|
262
|
+
name: str | None = None,
|
|
263
|
+
mime_type: str | None = None,
|
|
264
|
+
description: str | None = None,
|
|
265
|
+
) -> ResourceLink:
|
|
266
|
+
"""
|
|
267
|
+
Create a ResourceLink for an audio URL.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
url: The URL to the audio file
|
|
271
|
+
name: Optional name (defaults to filename from URL)
|
|
272
|
+
mime_type: Optional MIME type (inferred from extension, defaults to audio/mpeg)
|
|
273
|
+
description: Optional description
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
A ResourceLink object with audio MIME type
|
|
277
|
+
"""
|
|
278
|
+
inferred = _infer_mime_type(url, default="audio/mpeg")
|
|
279
|
+
# Ensure it's an audio type
|
|
280
|
+
if not inferred.startswith("audio/"):
|
|
281
|
+
inferred = "audio/mpeg"
|
|
282
|
+
|
|
283
|
+
return resource_link(
|
|
284
|
+
url,
|
|
285
|
+
name=name,
|
|
286
|
+
mime_type=mime_type or inferred,
|
|
287
|
+
description=description,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def ensure_multipart_messages(
|
|
292
|
+
messages: list[Union["PromptMessageExtended", PromptMessage]],
|
|
293
|
+
) -> list["PromptMessageExtended"]:
|
|
294
|
+
"""Ensure all messages in a list are PromptMessageExtended objects."""
|
|
295
|
+
# Import here to avoid circular dependency
|
|
296
|
+
from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
|
|
297
|
+
|
|
298
|
+
if not messages:
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
result = []
|
|
302
|
+
for message in messages:
|
|
303
|
+
if isinstance(message, PromptMessage):
|
|
304
|
+
result.append(PromptMessageExtended(role=message.role, content=[message.content]))
|
|
305
|
+
else:
|
|
306
|
+
result.append(message)
|
|
307
|
+
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def normalize_to_extended_list(
|
|
312
|
+
messages: Union[
|
|
313
|
+
str,
|
|
314
|
+
PromptMessage,
|
|
315
|
+
"PromptMessageExtended",
|
|
316
|
+
Sequence[Union[str, PromptMessage, "PromptMessageExtended"]],
|
|
317
|
+
],
|
|
318
|
+
) -> list["PromptMessageExtended"]:
|
|
319
|
+
"""Normalize various input types to a list of PromptMessageExtended objects."""
|
|
320
|
+
# Import here to avoid circular dependency
|
|
321
|
+
from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
|
|
322
|
+
|
|
323
|
+
if messages is None:
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
# Single string → convert to user PromptMessageExtended
|
|
327
|
+
if isinstance(messages, str):
|
|
328
|
+
return [
|
|
329
|
+
PromptMessageExtended(role="user", content=[TextContent(type="text", text=messages)])
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
# Single PromptMessage → convert to PromptMessageExtended
|
|
333
|
+
if isinstance(messages, PromptMessage):
|
|
334
|
+
return [PromptMessageExtended(role=messages.role, content=[messages.content])]
|
|
335
|
+
|
|
336
|
+
# Single PromptMessageExtended → wrap in a list
|
|
337
|
+
if isinstance(messages, PromptMessageExtended):
|
|
338
|
+
return [messages]
|
|
339
|
+
|
|
340
|
+
# List of mixed types → convert each element
|
|
341
|
+
result: list[PromptMessageExtended] = []
|
|
342
|
+
for item in messages:
|
|
343
|
+
if isinstance(item, str):
|
|
344
|
+
result.append(
|
|
345
|
+
PromptMessageExtended(role="user", content=[TextContent(type="text", text=item)])
|
|
346
|
+
)
|
|
347
|
+
elif isinstance(item, PromptMessage):
|
|
348
|
+
result.append(PromptMessageExtended(role=item.role, content=[item.content]))
|
|
349
|
+
else:
|
|
350
|
+
result.append(item)
|
|
351
|
+
|
|
352
|
+
return result
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Helper functions for type-safe server config access."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Union
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from fast_agent.config import MCPServerSettings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_server_config(ctx: Any) -> Union["MCPServerSettings", None]:
|
|
10
|
+
"""Extract server config from context if available.
|
|
11
|
+
|
|
12
|
+
Type guard helper that safely accesses server_config with proper type checking.
|
|
13
|
+
"""
|
|
14
|
+
# Import here to avoid circular import
|
|
15
|
+
from fast_agent.mcp.mcp_agent_client_session import MCPAgentClientSession
|
|
16
|
+
|
|
17
|
+
# Check if ctx has a session attribute (RequestContext case)
|
|
18
|
+
if hasattr(ctx, "session"):
|
|
19
|
+
if isinstance(ctx.session, MCPAgentClientSession):
|
|
20
|
+
return ctx.session.server_config
|
|
21
|
+
# Also check if ctx itself is MCPAgentClientSession (direct call case)
|
|
22
|
+
elif isinstance(ctx, MCPAgentClientSession):
|
|
23
|
+
return ctx.server_config
|
|
24
|
+
|
|
25
|
+
return None
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""HuggingFace authentication utilities for MCP connections."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_huggingface_url(url: str) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Check if a URL is a HuggingFace URL that should receive HF_TOKEN authentication.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
url: The URL to check
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
True if the URL is a HuggingFace URL, False otherwise
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
parsed = urlparse(url)
|
|
19
|
+
hostname = parsed.hostname
|
|
20
|
+
if hostname is None:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
# Check for HuggingFace domains
|
|
24
|
+
if hostname in {"hf.co", "huggingface.co"}:
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
# Check for HuggingFace Spaces (*.hf.space)
|
|
28
|
+
# Use endswith to match subdomains like space-name.hf.space
|
|
29
|
+
# but ensure exact match to prevent spoofing like evil.hf.space.com
|
|
30
|
+
if hostname.endswith(".hf.space") and hostname.count(".") >= 2:
|
|
31
|
+
# Additional validation: ensure it's a valid HF Space domain
|
|
32
|
+
# Format should be: {space-name}.hf.space
|
|
33
|
+
parts = hostname.split(".")
|
|
34
|
+
if len(parts) == 3 and parts[-2:] == ["hf", "space"]:
|
|
35
|
+
space_name = parts[0]
|
|
36
|
+
# Validate space name: not empty, not just hyphens/dots, no spaces
|
|
37
|
+
return (
|
|
38
|
+
len(space_name) > 0
|
|
39
|
+
and space_name != "-"
|
|
40
|
+
and not space_name.startswith(".")
|
|
41
|
+
and not space_name.endswith(".")
|
|
42
|
+
and " " not in space_name
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return False
|
|
46
|
+
except Exception:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_hf_token_from_env() -> str | None:
|
|
51
|
+
"""
|
|
52
|
+
Get the HuggingFace token from the HF_TOKEN environment variable.
|
|
53
|
+
|
|
54
|
+
Falls back to `huggingface_hub.get_token()` when available, so users who have
|
|
55
|
+
authenticated via `hf auth login` don't need to manually export HF_TOKEN.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The HF_TOKEN value if set, None otherwise
|
|
59
|
+
"""
|
|
60
|
+
token = os.environ.get("HF_TOKEN")
|
|
61
|
+
if token:
|
|
62
|
+
return token
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
from huggingface_hub import get_token # type: ignore
|
|
66
|
+
|
|
67
|
+
return get_token()
|
|
68
|
+
except Exception:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def should_add_hf_auth(url: str, existing_headers: dict[str, str] | None) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Determine if HuggingFace authentication should be added to the headers.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
url: The URL to check
|
|
78
|
+
existing_headers: Existing headers dictionary (may be None)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if HF auth should be added, False otherwise
|
|
82
|
+
"""
|
|
83
|
+
# Only add HF auth if:
|
|
84
|
+
# 1. URL is a HuggingFace URL
|
|
85
|
+
# 2. No existing Authorization/X-HF-Authorization header is set
|
|
86
|
+
# 3. HF_TOKEN environment variable is available
|
|
87
|
+
|
|
88
|
+
if not is_huggingface_url(url):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
if existing_headers:
|
|
92
|
+
# Check if this is a .hf.space domain
|
|
93
|
+
try:
|
|
94
|
+
parsed = urlparse(url)
|
|
95
|
+
hostname = parsed.hostname
|
|
96
|
+
if hostname and hostname.endswith(".hf.space"):
|
|
97
|
+
# For .hf.space, check for X-HF-Authorization header
|
|
98
|
+
if "X-HF-Authorization" in existing_headers:
|
|
99
|
+
return False
|
|
100
|
+
else:
|
|
101
|
+
# For other HF domains, check for Authorization header
|
|
102
|
+
if "Authorization" in existing_headers:
|
|
103
|
+
return False
|
|
104
|
+
except Exception:
|
|
105
|
+
# Fallback to checking Authorization header
|
|
106
|
+
if "Authorization" in existing_headers:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
return get_hf_token_from_env() is not None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def add_hf_auth_header(url: str, headers: dict[str, str] | None) -> dict[str, str] | None:
|
|
113
|
+
"""
|
|
114
|
+
Add HuggingFace authentication header if appropriate.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
url: The URL to check
|
|
118
|
+
headers: Existing headers dictionary (may be None)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Updated headers dictionary with HF auth if appropriate, or original headers
|
|
122
|
+
"""
|
|
123
|
+
if not should_add_hf_auth(url, headers):
|
|
124
|
+
return headers
|
|
125
|
+
|
|
126
|
+
hf_token = get_hf_token_from_env()
|
|
127
|
+
if hf_token is None:
|
|
128
|
+
return headers
|
|
129
|
+
|
|
130
|
+
# Create new headers dict or copy existing one
|
|
131
|
+
result_headers = dict(headers) if headers else {}
|
|
132
|
+
|
|
133
|
+
# Check if this is a .hf.space domain
|
|
134
|
+
try:
|
|
135
|
+
parsed = urlparse(url)
|
|
136
|
+
hostname = parsed.hostname
|
|
137
|
+
if hostname and hostname.endswith(".hf.space"):
|
|
138
|
+
# Use X-HF-Authorization for .hf.space domains
|
|
139
|
+
result_headers["X-HF-Authorization"] = f"Bearer {hf_token}"
|
|
140
|
+
else:
|
|
141
|
+
# Use standard Authorization header for other HF domains
|
|
142
|
+
result_headers["Authorization"] = f"Bearer {hf_token}"
|
|
143
|
+
except Exception:
|
|
144
|
+
# Fallback to standard Authorization header
|
|
145
|
+
result_headers["Authorization"] = f"Bearer {hf_token}"
|
|
146
|
+
|
|
147
|
+
return result_headers
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interface definitions to prevent circular imports.
|
|
3
|
+
This module defines protocols (interfaces) that can be used to break circular dependencies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import (
|
|
8
|
+
AsyncContextManager,
|
|
9
|
+
Callable,
|
|
10
|
+
Protocol,
|
|
11
|
+
runtime_checkable,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
15
|
+
from mcp import ClientSession
|
|
16
|
+
|
|
17
|
+
from fast_agent.interfaces import (
|
|
18
|
+
AgentProtocol,
|
|
19
|
+
FastAgentLLMProtocol,
|
|
20
|
+
LlmAgentProtocol,
|
|
21
|
+
LLMFactoryProtocol,
|
|
22
|
+
ModelFactoryFunctionProtocol,
|
|
23
|
+
ModelT,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"MCPConnectionManagerProtocol",
|
|
28
|
+
"ServerRegistryProtocol",
|
|
29
|
+
"ServerConnection",
|
|
30
|
+
"FastAgentLLMProtocol",
|
|
31
|
+
"AgentProtocol",
|
|
32
|
+
"LlmAgentProtocol",
|
|
33
|
+
"LLMFactoryProtocol",
|
|
34
|
+
"ModelFactoryFunctionProtocol",
|
|
35
|
+
"ModelT",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@runtime_checkable
|
|
40
|
+
class MCPConnectionManagerProtocol(Protocol):
|
|
41
|
+
"""Protocol for MCPConnectionManager functionality needed by ServerRegistry."""
|
|
42
|
+
|
|
43
|
+
async def get_server(
|
|
44
|
+
self,
|
|
45
|
+
server_name: str,
|
|
46
|
+
client_session_factory:
|
|
47
|
+
Callable[
|
|
48
|
+
[
|
|
49
|
+
MemoryObjectReceiveStream,
|
|
50
|
+
MemoryObjectSendStream,
|
|
51
|
+
timedelta | None,
|
|
52
|
+
],
|
|
53
|
+
ClientSession,
|
|
54
|
+
]
|
|
55
|
+
| None = None,
|
|
56
|
+
) -> "ServerConnection": ...
|
|
57
|
+
|
|
58
|
+
async def disconnect_server(self, server_name: str) -> None: ...
|
|
59
|
+
|
|
60
|
+
async def disconnect_all_servers(self) -> None: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@runtime_checkable
|
|
64
|
+
class ServerRegistryProtocol(Protocol):
|
|
65
|
+
"""Protocol defining the minimal interface of ServerRegistry needed by gen_client."""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def connection_manager(self) -> MCPConnectionManagerProtocol: ...
|
|
69
|
+
|
|
70
|
+
def initialize_server(
|
|
71
|
+
self,
|
|
72
|
+
server_name: str,
|
|
73
|
+
client_session_factory:
|
|
74
|
+
Callable[
|
|
75
|
+
[
|
|
76
|
+
MemoryObjectReceiveStream,
|
|
77
|
+
MemoryObjectSendStream,
|
|
78
|
+
timedelta | None,
|
|
79
|
+
],
|
|
80
|
+
ClientSession,
|
|
81
|
+
]
|
|
82
|
+
| None = None,
|
|
83
|
+
) -> AsyncContextManager[ClientSession]:
|
|
84
|
+
"""Initialize a server and yield a client session."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ServerConnection(Protocol):
|
|
89
|
+
"""Protocol for server connection objects returned by MCPConnectionManager."""
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def session(self) -> ClientSession: ...
|