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,235 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import platform
5
+ import re
6
+ import subprocess
7
+ import webbrowser
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Iterable
11
+
12
+ from mcp.types import BlobResourceContents, EmbeddedResource, TextResourceContents
13
+
14
+ """
15
+ Utilities for handling MCP-UI resources carried in PromptMessageExtended.channels.
16
+
17
+ Responsibilities:
18
+ - Identify MCP-UI EmbeddedResources from channels
19
+ - Decode text/blob content depending on mimeType
20
+ - Produce local HTML files that safely embed the UI content (srcdoc or iframe)
21
+ - Return presentable link labels for console display
22
+ """
23
+
24
+ # Control whether to generate data URLs for embedded HTML content
25
+ # When disabled, always use file:// URLs which work better with most terminals
26
+ ENABLE_DATA_URLS = False
27
+
28
+
29
+ @dataclass
30
+ class UILink:
31
+ title: str
32
+ file_path: str # absolute path to local html file
33
+ web_url: str | None = None # Preferable clickable link (http(s) or data URL)
34
+
35
+
36
+ def _safe_filename(name: str) -> str:
37
+ name = re.sub(r"[^A-Za-z0-9_.-]", "_", name)
38
+ return name[:120] if len(name) > 120 else name
39
+
40
+
41
+ def _ensure_output_dir() -> Path:
42
+ # Read output directory from settings, defaulting to .fast-agent/ui
43
+ try:
44
+ from fast_agent.config import get_settings
45
+
46
+ settings = get_settings()
47
+ dir_setting = getattr(settings, "mcp_ui_output_dir", ".fast-agent/ui") or ".fast-agent/ui"
48
+ except Exception:
49
+ dir_setting = ".fast-agent/ui"
50
+
51
+ base = Path(dir_setting)
52
+ if not base.is_absolute():
53
+ base = Path.cwd() / base
54
+ base.mkdir(parents=True, exist_ok=True)
55
+ return base
56
+
57
+
58
+ def _extract_title(uri: str | None) -> str:
59
+ if not uri:
60
+ return "UI"
61
+ try:
62
+ # ui://component/instance -> component:instance
63
+ without_scheme = uri.split("ui://", 1)[1] if uri.startswith("ui://") else uri
64
+ parts = [p for p in re.split(r"[/:]", without_scheme) if p]
65
+ if len(parts) >= 2:
66
+ return f"{parts[0]}:{parts[1]}"
67
+ return parts[0] if parts else "UI"
68
+ except Exception:
69
+ return "UI"
70
+
71
+
72
+ def _decode_text_or_blob(resource) -> str | None:
73
+ """Return string content from TextResourceContents or BlobResourceContents."""
74
+ if isinstance(resource, TextResourceContents):
75
+ return resource.text or ""
76
+ if isinstance(resource, BlobResourceContents):
77
+ try:
78
+ return base64.b64decode(resource.blob or "").decode("utf-8", errors="replace")
79
+ except Exception:
80
+ return None
81
+ return None
82
+
83
+
84
+ def _first_https_url_from_uri_list(text: str) -> str | None:
85
+ for line in text.splitlines():
86
+ line = line.strip()
87
+ if not line or line.startswith("#"):
88
+ continue
89
+ if line.startswith("http://") or line.startswith("https://"):
90
+ return line
91
+ return None
92
+
93
+
94
+ def _make_html_for_raw_html(html_string: str) -> str:
95
+ # Wrap with minimal HTML and sandbox guidance (iframe srcdoc will be used by browsers)
96
+ return html_string
97
+
98
+
99
+ def _make_html_for_uri(url: str) -> str:
100
+ return f"""
101
+ <!doctype html>
102
+ <html>
103
+ <head>
104
+ <meta charset=\"utf-8\" />
105
+ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
106
+ <title>MCP-UI</title>
107
+ <style>html,body,iframe{{margin:0;padding:0;height:100%;width:100%;border:0}}</style>
108
+ </head>
109
+ <body>
110
+ <iframe src=\"{url}\" sandbox=\"allow-scripts allow-forms allow-same-origin\" referrerpolicy=\"no-referrer\"></iframe>
111
+ </body>
112
+ </html>
113
+ """
114
+
115
+
116
+ def _write_html_file(name_hint: str, html: str) -> str:
117
+ out_dir = _ensure_output_dir()
118
+ file_name = _safe_filename(name_hint or "ui") + ".html"
119
+ out_path = out_dir / file_name
120
+ # Ensure unique filename if exists
121
+ i = 1
122
+ while out_path.exists():
123
+ out_path = out_dir / f"{_safe_filename(name_hint)}_{i}.html"
124
+ i += 1
125
+ out_path.write_text(html, encoding="utf-8")
126
+ return str(out_path.resolve())
127
+
128
+
129
+ def ui_links_from_channel(resources: Iterable[EmbeddedResource]) -> list[UILink]:
130
+ """
131
+ Build local HTML files for a list of MCP-UI EmbeddedResources and return clickable links.
132
+
133
+ Supported mime types:
134
+ - text/html: expects text or base64 blob of HTML
135
+ - text/uri-list: expects text or blob of a single URL (first valid URL is used)
136
+ - application/vnd.mcp-ui.remote-dom* : currently unsupported; generate a placeholder page
137
+ """
138
+ links: list[UILink] = []
139
+ for emb in resources:
140
+ res = emb.resource
141
+ uri = str(getattr(res, "uri", "")) if getattr(res, "uri", None) else None
142
+ mime = getattr(res, "mimeType", "") or ""
143
+ title = _extract_title(uri)
144
+ content = _decode_text_or_blob(res)
145
+
146
+ if mime.startswith("text/html"):
147
+ if content is None:
148
+ continue
149
+ html = _make_html_for_raw_html(content)
150
+ file_path = _write_html_file(title, html)
151
+ # Generate data URL only if enabled
152
+ if ENABLE_DATA_URLS:
153
+ try:
154
+ b64 = base64.b64encode(html.encode("utf-8")).decode("ascii")
155
+ data_url = f"data:text/html;base64,{b64}"
156
+ # Some terminals have limits; only attach when reasonably small
157
+ web_url = data_url if len(data_url) < 12000 else None
158
+ except Exception:
159
+ web_url = None
160
+ else:
161
+ web_url = None
162
+ links.append(UILink(title=title, file_path=file_path, web_url=web_url))
163
+
164
+ elif mime.startswith("text/uri-list"):
165
+ if content is None:
166
+ continue
167
+ url = _first_https_url_from_uri_list(content)
168
+ if not url:
169
+ # fallback: try to treat entire content as a URL
170
+ url = content.strip()
171
+ if not (url and (url.startswith("http://") or url.startswith("https://"))):
172
+ continue
173
+ html = _make_html_for_uri(url)
174
+ file_path = _write_html_file(title, html)
175
+ # Prefer the direct URL for clickability; keep file for archival
176
+ links.append(UILink(title=title, file_path=file_path, web_url=url))
177
+
178
+ elif mime.startswith("application/vnd.mcp-ui.remote-dom"):
179
+ # Not supported yet - generate informational page
180
+ placeholder = f"""
181
+ <!doctype html>
182
+ <html><head><meta charset=\"utf-8\" /><title>{title} (Unsupported)</title></head>
183
+ <body>
184
+ <p>Remote DOM resources are not supported yet in this client.</p>
185
+ <p>URI: {uri or ""}</p>
186
+ <p>mimeType: {mime}</p>
187
+ <pre style=\"white-space: pre-wrap;\">{(content or "")[:4000]}</pre>
188
+ <p>Please upgrade fast-agent when support becomes available.</p>
189
+ </body></html>
190
+ """
191
+ file_path = _write_html_file(title + "_unsupported", placeholder)
192
+ links.append(UILink(title=title + " (unsupported)", file_path=file_path))
193
+ else:
194
+ # Unknown, skip quietly
195
+ continue
196
+
197
+ return links
198
+
199
+
200
+ def open_links_in_browser(links: Iterable[UILink], mcp_ui_mode: str = "auto") -> None:
201
+ """Open links in browser/system viewer.
202
+
203
+ Args:
204
+ links: Links to open
205
+ mcp_ui_mode: UI mode setting ("disabled", "enabled", "auto")
206
+ """
207
+ # Only attempt to open files when in auto mode
208
+ if mcp_ui_mode != "auto":
209
+ return
210
+
211
+ for link in links:
212
+ try:
213
+ # Use subprocess for better file:// handling across platforms
214
+ file_path = link.file_path
215
+
216
+ system = platform.system()
217
+ if system == "Darwin": # macOS
218
+ subprocess.run(["open", file_path], check=False, capture_output=True)
219
+ elif system == "Windows":
220
+ subprocess.run(
221
+ ["start", "", file_path], shell=True, check=False, capture_output=True
222
+ )
223
+ elif system == "Linux":
224
+ # Try xdg-open first (most common), fallback to other options
225
+ try:
226
+ subprocess.run(["xdg-open", file_path], check=False, capture_output=True)
227
+ except FileNotFoundError:
228
+ # Fallback to webbrowser for Linux if xdg-open not available
229
+ webbrowser.open(f"file://{file_path}", new=2)
230
+ else:
231
+ # Unknown system, fallback to webbrowser
232
+ webbrowser.open(f"file://{file_path}", new=2)
233
+ except Exception:
234
+ # Silently ignore errors - user can still manually open the file
235
+ pass
@@ -0,0 +1,169 @@
1
+ """Utilities for detecting and processing Mermaid diagrams in text content."""
2
+
3
+ import base64
4
+ import re
5
+ import zlib
6
+ from dataclasses import dataclass
7
+
8
+ # Mermaid chart viewer URL prefix
9
+ MERMAID_VIEWER_URL = "https://www.mermaidchart.com/play#"
10
+ # mermaid.live#pako= also works but the playground has better ux
11
+
12
+
13
+ @dataclass
14
+ class MermaidDiagram:
15
+ """Represents a detected Mermaid diagram."""
16
+
17
+ content: str
18
+ title: str | None = None
19
+ start_pos: int = 0
20
+ end_pos: int = 0
21
+
22
+
23
+ def extract_mermaid_diagrams(text: str) -> list[MermaidDiagram]:
24
+ """
25
+ Extract all Mermaid diagram blocks from text content.
26
+
27
+ Handles both simple mermaid blocks and blocks with titles:
28
+ - ```mermaid
29
+ - ```mermaid title={Some Title}
30
+
31
+ Also extracts titles from within the diagram content.
32
+
33
+ Args:
34
+ text: The text content to search for Mermaid diagrams
35
+
36
+ Returns:
37
+ List of MermaidDiagram objects found in the text
38
+ """
39
+ diagrams = []
40
+
41
+ # Pattern to match mermaid code blocks with optional title
42
+ # Matches: ```mermaid or ```mermaid title={...}
43
+ pattern = r"```mermaid(?:\s+title=\{([^}]+)\})?\s*\n(.*?)```"
44
+
45
+ for match in re.finditer(pattern, text, re.DOTALL):
46
+ title = match.group(1) # May be None if no title
47
+ content = match.group(2).strip()
48
+
49
+ if content: # Only add if there's actual diagram content
50
+ # If no title from code fence, look for title in the content
51
+ if not title:
52
+ # Look for various title patterns in mermaid diagrams
53
+ # pie title, graph title, etc.
54
+ title_patterns = [
55
+ r"^\s*title\s+(.+?)(?:\n|$)", # Generic title
56
+ r"^\s*pie\s+title\s+(.+?)(?:\n|$)", # Pie chart title
57
+ r"^\s*gantt\s+title\s+(.+?)(?:\n|$)", # Gantt chart title
58
+ ]
59
+
60
+ for title_pattern in title_patterns:
61
+ title_match = re.search(title_pattern, content, re.MULTILINE)
62
+ if title_match:
63
+ title = title_match.group(1).strip()
64
+ break
65
+
66
+ diagrams.append(
67
+ MermaidDiagram(
68
+ content=content, title=title, start_pos=match.start(), end_pos=match.end()
69
+ )
70
+ )
71
+
72
+ return diagrams
73
+
74
+
75
+ def create_mermaid_live_link(diagram_content: str) -> str:
76
+ """
77
+ Create a Mermaid Live Editor link from diagram content.
78
+
79
+ The link uses pako compression (zlib) and base64 encoding.
80
+
81
+ Args:
82
+ diagram_content: The Mermaid diagram source code
83
+
84
+ Returns:
85
+ Complete URL to Mermaid Live Editor
86
+ """
87
+ # Create the JSON structure expected by Mermaid Live
88
+ # Escape newlines and quotes in the diagram content
89
+ escaped_content = diagram_content.replace('"', '\\"').replace("\n", "\\n")
90
+ json_str = f'{{"code":"{escaped_content}","mermaid":{{"theme":"default"}},"updateEditor":false,"autoSync":true,"updateDiagram":false}}'
91
+
92
+ # Compress using zlib (pako compatible)
93
+ compressed = zlib.compress(json_str.encode("utf-8"))
94
+
95
+ # Base64 encode
96
+ encoded = base64.urlsafe_b64encode(compressed).decode("utf-8")
97
+
98
+ # Remove padding characters as Mermaid Live doesn't use them
99
+ encoded = encoded.rstrip("=")
100
+
101
+ return f"{MERMAID_VIEWER_URL}pako:{encoded}"
102
+
103
+
104
+ def format_mermaid_links(diagrams: list[MermaidDiagram]) -> list[str]:
105
+ """
106
+ Format Mermaid diagrams as markdown links.
107
+
108
+ Args:
109
+ diagrams: List of MermaidDiagram objects
110
+
111
+ Returns:
112
+ List of formatted markdown strings
113
+ """
114
+ links = []
115
+
116
+ for i, diagram in enumerate(diagrams, 1):
117
+ link = create_mermaid_live_link(diagram.content)
118
+
119
+ if diagram.title:
120
+ # Use the title from the diagram with number
121
+ markdown = f"Diagram {i} - {diagram.title}: [Open Diagram]({link})"
122
+ else:
123
+ # Use generic numbering
124
+ markdown = f"Diagram {i}: [Open Diagram]({link})"
125
+
126
+ links.append(markdown)
127
+
128
+ return links
129
+
130
+
131
+ def detect_diagram_type(content: str) -> str:
132
+ """
133
+ Detect the type of mermaid diagram from content.
134
+
135
+ Args:
136
+ content: The mermaid diagram source code
137
+
138
+ Returns:
139
+ Human-readable diagram type name
140
+ """
141
+ content_lower = content.strip().lower()
142
+
143
+ # Check for common diagram types
144
+ if content_lower.startswith(("graph ", "flowchart ")):
145
+ return "Flowchart"
146
+ elif content_lower.startswith("sequencediagram"):
147
+ return "Sequence"
148
+ elif content_lower.startswith("pie"):
149
+ return "Pie Chart"
150
+ elif content_lower.startswith("gantt"):
151
+ return "Gantt Chart"
152
+ elif content_lower.startswith("classdiagram"):
153
+ return "Class Diagram"
154
+ elif content_lower.startswith("statediagram"):
155
+ return "State Diagram"
156
+ elif content_lower.startswith("erdiagram"):
157
+ return "ER Diagram"
158
+ elif content_lower.startswith("journey"):
159
+ return "User Journey"
160
+ elif content_lower.startswith("gitgraph"):
161
+ return "Git Graph"
162
+ elif content_lower.startswith("c4context"):
163
+ return "C4 Context"
164
+ elif content_lower.startswith("mindmap"):
165
+ return "Mind Map"
166
+ elif content_lower.startswith("timeline"):
167
+ return "Timeline"
168
+ else:
169
+ return "Diagram"
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class MessageType(Enum):
7
+ """Types of messages that can be displayed."""
8
+
9
+ USER = "user"
10
+ ASSISTANT = "assistant"
11
+ SYSTEM = "system"
12
+ TOOL_CALL = "tool_call"
13
+ TOOL_RESULT = "tool_result"
14
+
15
+
16
+ MESSAGE_CONFIGS: dict[MessageType, dict[str, str]] = {
17
+ MessageType.USER: {
18
+ "block_color": "blue",
19
+ "arrow": "▶",
20
+ "arrow_style": "dim blue",
21
+ "highlight_color": "blue",
22
+ },
23
+ MessageType.ASSISTANT: {
24
+ "block_color": "green",
25
+ "arrow": "◀",
26
+ "arrow_style": "dim green",
27
+ "highlight_color": "bright_green",
28
+ },
29
+ MessageType.SYSTEM: {
30
+ "block_color": "yellow",
31
+ "arrow": "●",
32
+ "arrow_style": "dim yellow",
33
+ "highlight_color": "bright_yellow",
34
+ },
35
+ MessageType.TOOL_CALL: {
36
+ "block_color": "magenta",
37
+ "arrow": "◀",
38
+ "arrow_style": "dim magenta",
39
+ "highlight_color": "magenta",
40
+ },
41
+ MessageType.TOOL_RESULT: {
42
+ "block_color": "magenta",
43
+ "arrow": "▶",
44
+ "arrow_style": "dim magenta",
45
+ "highlight_color": "magenta",
46
+ },
47
+ }
48
+
49
+
50
+ __all__ = ["MessageType", "MESSAGE_CONFIGS"]
@@ -0,0 +1,205 @@
1
+ """
2
+ Enhanced notification tracker for prompt_toolkit toolbar display.
3
+ Tracks both active events (sampling/elicitation) and completed notifications.
4
+ """
5
+
6
+ from datetime import datetime
7
+
8
+ # Display metadata for toolbar summaries (singular, plural, compact label)
9
+ _EVENT_ORDER = ("tool_update", "sampling", "elicitation")
10
+ _EVENT_DISPLAY = {
11
+ "tool_update": {"singular": "tool update", "plural": "tool updates", "compact": "tool"},
12
+ "sampling": {"singular": "sample", "plural": "samples", "compact": "samp"},
13
+ "elicitation": {"singular": "elicitation", "plural": "elicitations", "compact": "elic"},
14
+ }
15
+
16
+ # Active events currently in progress
17
+ active_events: dict[str, dict[str, str]] = {}
18
+
19
+ # Completed notifications history
20
+ notifications: list[dict[str, str]] = []
21
+
22
+
23
+ def add_tool_update(server_name: str) -> None:
24
+ """Add a tool update notification.
25
+
26
+ Args:
27
+ server_name: Name of the server that had tools updated
28
+ """
29
+ notifications.append({
30
+ 'type': 'tool_update',
31
+ 'server': server_name
32
+ })
33
+
34
+
35
+ def start_sampling(server_name: str) -> None:
36
+ """Start tracking a sampling operation.
37
+
38
+ Args:
39
+ server_name: Name of the server making the sampling request
40
+ """
41
+ active_events['sampling'] = {
42
+ 'server': server_name,
43
+ 'start_time': datetime.now().isoformat()
44
+ }
45
+
46
+ # Force prompt_toolkit to redraw if active
47
+ try:
48
+ from prompt_toolkit.application.current import get_app
49
+ get_app().invalidate()
50
+ except Exception:
51
+ pass
52
+
53
+
54
+ def end_sampling(server_name: str) -> None:
55
+ """End tracking a sampling operation and add to completed notifications.
56
+
57
+ Args:
58
+ server_name: Name of the server that made the sampling request
59
+ """
60
+ if 'sampling' in active_events:
61
+ del active_events['sampling']
62
+
63
+ notifications.append({
64
+ 'type': 'sampling',
65
+ 'server': server_name
66
+ })
67
+
68
+ # Force prompt_toolkit to redraw if active
69
+ try:
70
+ from prompt_toolkit.application.current import get_app
71
+ get_app().invalidate()
72
+ except Exception:
73
+ pass
74
+
75
+
76
+ def start_elicitation(server_name: str) -> None:
77
+ """Start tracking an elicitation operation.
78
+
79
+ Args:
80
+ server_name: Name of the server making the elicitation request
81
+ """
82
+ active_events['elicitation'] = {
83
+ 'server': server_name,
84
+ 'start_time': datetime.now().isoformat()
85
+ }
86
+
87
+ # Force prompt_toolkit to redraw if active
88
+ try:
89
+ from prompt_toolkit.application.current import get_app
90
+ get_app().invalidate()
91
+ except Exception:
92
+ pass
93
+
94
+
95
+ def end_elicitation(server_name: str) -> None:
96
+ """End tracking an elicitation operation and add to completed notifications.
97
+
98
+ Args:
99
+ server_name: Name of the server that made the elicitation request
100
+ """
101
+ if 'elicitation' in active_events:
102
+ del active_events['elicitation']
103
+
104
+ notifications.append({
105
+ 'type': 'elicitation',
106
+ 'server': server_name
107
+ })
108
+
109
+ # Force prompt_toolkit to redraw if active
110
+ try:
111
+ from prompt_toolkit.application.current import get_app
112
+ get_app().invalidate()
113
+ except Exception:
114
+ pass
115
+
116
+
117
+ def get_active_status() -> dict[str, str] | None:
118
+ """Get currently active operation, if any.
119
+
120
+ Returns:
121
+ Dict with 'type' and 'server' keys, or None if nothing active
122
+ """
123
+ if 'sampling' in active_events:
124
+ return {'type': 'sampling', 'server': active_events['sampling']['server']}
125
+ if 'elicitation' in active_events:
126
+ return {'type': 'elicitation', 'server': active_events['elicitation']['server']}
127
+ return None
128
+
129
+
130
+ def clear() -> None:
131
+ """Clear all notifications and active events."""
132
+ notifications.clear()
133
+ active_events.clear()
134
+
135
+
136
+ def get_count() -> int:
137
+ """Get the current completed notification count."""
138
+ return len(notifications)
139
+
140
+
141
+ def get_latest() -> dict[str, str] | None:
142
+ """Get the most recent completed notification."""
143
+ return notifications[-1] if notifications else None
144
+
145
+
146
+ def get_counts_by_type() -> dict[str, int]:
147
+ """Aggregate completed notifications by event type."""
148
+ counts: dict[str, int] = {}
149
+ for notification in notifications:
150
+ event_type = notification['type']
151
+ counts[event_type] = counts.get(event_type, 0) + 1
152
+
153
+ if not counts:
154
+ return {}
155
+
156
+ ordered: dict[str, int] = {}
157
+ for event_type in _EVENT_ORDER:
158
+ if event_type in counts:
159
+ ordered[event_type] = counts[event_type]
160
+
161
+ for event_type, count in counts.items():
162
+ if event_type not in ordered:
163
+ ordered[event_type] = count
164
+
165
+ return ordered
166
+
167
+
168
+ def format_event_label(event_type: str, count: int, *, compact: bool = False) -> str:
169
+ """Format a human-readable label for an event count."""
170
+ event_display = _EVENT_DISPLAY.get(event_type)
171
+
172
+ if event_display is None:
173
+ base = event_type.replace('_', ' ')
174
+ if compact:
175
+ return f"{base[:1]}:{count}"
176
+ label = base if count == 1 else f"{base}s"
177
+ return f"{count} {label}"
178
+
179
+ if compact:
180
+ return f"{event_display['compact']}:{count}"
181
+
182
+ label = event_display['singular'] if count == 1 else event_display['plural']
183
+ return f"{count} {label}"
184
+
185
+
186
+ def get_summary(*, compact: bool = False) -> str:
187
+ """Get a summary of completed notifications by type.
188
+
189
+ Args:
190
+ compact: When True, use short-form labels for constrained UI areas.
191
+
192
+ Returns:
193
+ String like "3 tool updates, 2 samples" or "tool:3 samp:2" when compact.
194
+ """
195
+ counts = get_counts_by_type()
196
+ if not counts:
197
+ return ""
198
+
199
+ parts = [
200
+ format_event_label(event_type, count, compact=compact)
201
+ for event_type, count in counts.items()
202
+ ]
203
+
204
+ separator = " " if compact else ", "
205
+ return separator.join(parts)