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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. 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: ...