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,552 @@
1
+ """
2
+ FastMCP Prompt Server V2
3
+
4
+ A server that loads prompts from text files with simple delimiters and serves them via MCP.
5
+ Uses the prompt_template module for clean, testable handling of prompt templates.
6
+ """
7
+
8
+ import argparse
9
+ import asyncio
10
+ import base64
11
+ import logging
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any, Awaitable, Callable, Union
15
+
16
+ from mcp.server.fastmcp import FastMCP
17
+ from mcp.server.fastmcp.prompts.base import (
18
+ AssistantMessage,
19
+ Message,
20
+ UserMessage,
21
+ )
22
+ from mcp.server.fastmcp.resources import FileResource
23
+ from mcp.types import PromptMessage
24
+ from pydantic import AnyUrl
25
+
26
+ from fast_agent.mcp import mime_utils, resource_utils
27
+ from fast_agent.mcp.prompts.prompt_constants import (
28
+ ASSISTANT_DELIMITER as DEFAULT_ASSISTANT_DELIMITER,
29
+ )
30
+ from fast_agent.mcp.prompts.prompt_constants import (
31
+ RESOURCE_DELIMITER as DEFAULT_RESOURCE_DELIMITER,
32
+ )
33
+ from fast_agent.mcp.prompts.prompt_constants import (
34
+ USER_DELIMITER as DEFAULT_USER_DELIMITER,
35
+ )
36
+ from fast_agent.mcp.prompts.prompt_load import create_messages_with_resources
37
+ from fast_agent.mcp.prompts.prompt_template import (
38
+ PromptMetadata,
39
+ PromptTemplateLoader,
40
+ )
41
+ from fast_agent.types import PromptMessageExtended
42
+
43
+ # Configure logging
44
+ logging.basicConfig(level=logging.ERROR)
45
+ logger = logging.getLogger("prompt_server")
46
+
47
+ # Create FastMCP server
48
+ mcp = FastMCP("Prompt Server")
49
+
50
+
51
+ def convert_to_fastmcp_messages(prompt_messages: list[Union[PromptMessage, PromptMessageExtended]]) -> list[Message]:
52
+ """
53
+ Convert PromptMessage or PromptMessageExtended objects to FastMCP Message objects.
54
+ This adapter prevents double-wrapping of messages and handles both types.
55
+
56
+ Args:
57
+ prompt_messages: List of PromptMessage or PromptMessageExtended objects
58
+
59
+ Returns:
60
+ List of FastMCP Message objects
61
+ """
62
+ result = []
63
+
64
+ for msg in prompt_messages:
65
+ # Handle both PromptMessage and PromptMessageExtended
66
+ if hasattr(msg, 'from_multipart'):
67
+ # PromptMessageExtended - convert to regular PromptMessage format
68
+ flat_messages = msg.from_multipart()
69
+ for flat_msg in flat_messages:
70
+ if flat_msg.role == "user":
71
+ result.append(UserMessage(content=flat_msg.content))
72
+ elif flat_msg.role == "assistant":
73
+ result.append(AssistantMessage(content=flat_msg.content))
74
+ else:
75
+ logger.warning(f"Unknown message role: {flat_msg.role}, defaulting to user")
76
+ result.append(UserMessage(content=flat_msg.content))
77
+ else:
78
+ # Regular PromptMessage - use directly
79
+ if msg.role == "user":
80
+ result.append(UserMessage(content=msg.content))
81
+ elif msg.role == "assistant":
82
+ result.append(AssistantMessage(content=msg.content))
83
+ else:
84
+ logger.warning(f"Unknown message role: {msg.role}, defaulting to user")
85
+ result.append(UserMessage(content=msg.content))
86
+
87
+ return result
88
+
89
+
90
+ class PromptConfig(PromptMetadata):
91
+ """Configuration for the prompt server"""
92
+
93
+ prompt_files: list[Path] = []
94
+ user_delimiter: str = DEFAULT_USER_DELIMITER
95
+ assistant_delimiter: str = DEFAULT_ASSISTANT_DELIMITER
96
+ resource_delimiter: str = DEFAULT_RESOURCE_DELIMITER
97
+ http_timeout: float = 10.0
98
+ transport: str = "stdio"
99
+ port: int = 8000
100
+ host: str = "0.0.0.0"
101
+
102
+
103
+ # We'll maintain registries of all exposed resources and prompts
104
+ exposed_resources: dict[str, Path] = {}
105
+ prompt_registry: dict[str, PromptMetadata] = {}
106
+
107
+
108
+ # Define a single type for prompt handlers to avoid mypy issues
109
+ PromptHandler = Callable[..., Awaitable[list[Message]]]
110
+
111
+
112
+ # Type for resource handler
113
+ ResourceHandler = Callable[[], Awaitable[str | bytes]]
114
+
115
+
116
+ def create_resource_handler(resource_path: Path, mime_type: str) -> ResourceHandler:
117
+ """Create a resource handler function for the given resource"""
118
+
119
+ async def get_resource() -> str | bytes:
120
+ is_binary = mime_utils.is_binary_content(mime_type)
121
+
122
+ if is_binary:
123
+ # For binary files, read in binary mode and base64 encode
124
+ with open(resource_path, "rb") as f:
125
+ return f.read()
126
+ else:
127
+ # For text files, read as utf-8 text
128
+ with open(resource_path, "r", encoding="utf-8") as f:
129
+ return f.read()
130
+
131
+ return get_resource
132
+
133
+
134
+ def get_delimiter_config(
135
+ config: PromptConfig | None = None, file_path: Path | None = None
136
+ ) -> dict[str, Any]:
137
+ """Get delimiter configuration, falling back to defaults if config is None"""
138
+ # Set defaults
139
+ config_values = {
140
+ "user_delimiter": DEFAULT_USER_DELIMITER,
141
+ "assistant_delimiter": DEFAULT_ASSISTANT_DELIMITER,
142
+ "resource_delimiter": DEFAULT_RESOURCE_DELIMITER,
143
+ "prompt_files": [file_path] if file_path else [],
144
+ }
145
+
146
+ # Override with config values if available
147
+ if config is not None:
148
+ config_values["user_delimiter"] = config.user_delimiter
149
+ config_values["assistant_delimiter"] = config.assistant_delimiter
150
+ config_values["resource_delimiter"] = config.resource_delimiter
151
+ config_values["prompt_files"] = config.prompt_files
152
+
153
+ return config_values
154
+
155
+
156
+ def register_prompt(file_path: Path, config: PromptConfig | None = None) -> None:
157
+ """Register a prompt file"""
158
+ try:
159
+ # Check if it's a JSON file for ultra-minimal path
160
+ file_str = str(file_path).lower()
161
+ if file_str.endswith(".json"):
162
+ # Simple JSON handling - just load and register directly
163
+ from mcp.server.fastmcp.prompts.base import Prompt, PromptArgument
164
+
165
+ from fast_agent.mcp.prompts.prompt_load import load_prompt
166
+
167
+ # Create metadata with minimal information
168
+ metadata = PromptMetadata(
169
+ name=file_path.stem,
170
+ description=f"JSON prompt: {file_path.stem}",
171
+ template_variables=set(),
172
+ resource_paths=[], # Skip resource handling
173
+ file_path=file_path,
174
+ )
175
+
176
+ # Ensure unique name
177
+ prompt_name = metadata.name
178
+ if prompt_name in prompt_registry:
179
+ base_name = prompt_name
180
+ suffix = 1
181
+ while prompt_name in prompt_registry:
182
+ prompt_name = f"{base_name}_{suffix}"
183
+ suffix += 1
184
+ metadata.name = prompt_name
185
+
186
+ prompt_registry[metadata.name] = metadata
187
+
188
+ # Create a simple handler that directly loads the JSON file each time
189
+ async def json_prompt_handler():
190
+ # Load the messages from the JSON file
191
+ messages = load_prompt(file_path)
192
+ # Convert to FastMCP format
193
+ return convert_to_fastmcp_messages(messages)
194
+
195
+ # Register directly with MCP
196
+ prompt = Prompt(
197
+ name=metadata.name,
198
+ description=metadata.description,
199
+ arguments=[], # No arguments for JSON prompts
200
+ fn=json_prompt_handler,
201
+ )
202
+ mcp._prompt_manager.add_prompt(prompt)
203
+
204
+ logger.info(f"Registered JSON prompt: {metadata.name} ({file_path})")
205
+ return # Early return - we're done with JSON files
206
+
207
+ # For non-JSON files, continue with the standard approach
208
+ # Get delimiter configuration
209
+ config_values = get_delimiter_config(config, file_path)
210
+
211
+ # Use standard template loader for text files
212
+ loader = PromptTemplateLoader(
213
+ {
214
+ config_values["user_delimiter"]: "user",
215
+ config_values["assistant_delimiter"]: "assistant",
216
+ config_values["resource_delimiter"]: "resource",
217
+ }
218
+ )
219
+
220
+ # Get metadata and load the template
221
+ metadata = loader.get_metadata(file_path)
222
+ template = loader.load_from_file(file_path)
223
+
224
+ # Ensure unique name
225
+ prompt_name = metadata.name
226
+ if prompt_name in prompt_registry:
227
+ base_name = prompt_name
228
+ suffix = 1
229
+ while prompt_name in prompt_registry:
230
+ prompt_name = f"{base_name}_{suffix}"
231
+ suffix += 1
232
+ metadata.name = prompt_name
233
+
234
+ prompt_registry[metadata.name] = metadata
235
+ logger.info(f"Registered prompt: {metadata.name} ({file_path})")
236
+
237
+ # Create and register prompt handler
238
+ template_vars = list(metadata.template_variables)
239
+
240
+ from mcp.server.fastmcp.prompts.base import Prompt, PromptArgument
241
+
242
+ # For prompts with variables, create arguments list for FastMCP
243
+ if template_vars:
244
+ # Create a function with properly typed parameters
245
+ async def template_handler_with_vars(**kwargs):
246
+ # Extract template variables from kwargs
247
+ context = {var: kwargs.get(var) for var in template_vars if var in kwargs}
248
+
249
+ # Check for missing variables
250
+ missing_vars = [var for var in template_vars if var not in context]
251
+ if missing_vars:
252
+ raise ValueError(
253
+ f"Missing required template variables: {', '.join(missing_vars)}"
254
+ )
255
+
256
+ # Apply template and create messages
257
+ content_sections = template.apply_substitutions(context)
258
+ prompt_messages = create_messages_with_resources(
259
+ content_sections, config_values["prompt_files"]
260
+ )
261
+ return convert_to_fastmcp_messages(prompt_messages)
262
+
263
+ # Create a Prompt directly
264
+ arguments = [
265
+ PromptArgument(name=var, description=f"Template variable: {var}", required=True)
266
+ for var in template_vars
267
+ ]
268
+
269
+ # Create and add the prompt directly to the prompt manager
270
+ prompt = Prompt(
271
+ name=metadata.name,
272
+ description=metadata.description,
273
+ arguments=arguments,
274
+ fn=template_handler_with_vars,
275
+ )
276
+ mcp._prompt_manager.add_prompt(prompt)
277
+ else:
278
+ # Create a simple prompt without variables
279
+ async def template_handler_without_vars() -> list[Message]:
280
+ content_sections = template.content_sections
281
+ prompt_messages = create_messages_with_resources(
282
+ content_sections, config_values["prompt_files"]
283
+ )
284
+ return convert_to_fastmcp_messages(prompt_messages)
285
+
286
+ # Create a Prompt object directly instead of using the decorator
287
+ prompt = Prompt(
288
+ name=metadata.name,
289
+ description=metadata.description,
290
+ arguments=[],
291
+ fn=template_handler_without_vars,
292
+ )
293
+ mcp._prompt_manager.add_prompt(prompt)
294
+
295
+ # Register any referenced resources in the prompt
296
+ for resource_path in metadata.resource_paths:
297
+ if not resource_path.startswith(("http://", "https://")):
298
+ # It's a local resource
299
+ resource_file = file_path.parent / resource_path
300
+ if resource_file.exists():
301
+ resource_id = f"resource://fast-agent/{resource_file.name}"
302
+
303
+ # Register the resource if not already registered
304
+ if resource_id not in exposed_resources:
305
+ exposed_resources[resource_id] = resource_file
306
+ mime_type = mime_utils.guess_mime_type(str(resource_file))
307
+
308
+ mcp.add_resource(
309
+ FileResource(
310
+ uri=AnyUrl(resource_id),
311
+ path=resource_file,
312
+ mime_type=mime_type,
313
+ is_binary=mime_utils.is_binary_content(mime_type),
314
+ )
315
+ )
316
+
317
+ logger.info(f"Registered resource: {resource_id} ({resource_file})")
318
+ except Exception as e:
319
+ logger.error(f"Error registering prompt {file_path}: {e}", exc_info=True)
320
+
321
+
322
+ def parse_args():
323
+ """Parse command line arguments"""
324
+ parser = argparse.ArgumentParser(description="FastMCP Prompt Server")
325
+ parser.add_argument("prompt_files", nargs="+", type=str, help="Prompt files to serve")
326
+ parser.add_argument(
327
+ "--user-delimiter",
328
+ type=str,
329
+ default="---USER",
330
+ help="Delimiter for user messages (default: ---USER)",
331
+ )
332
+ parser.add_argument(
333
+ "--assistant-delimiter",
334
+ type=str,
335
+ default="---ASSISTANT",
336
+ help="Delimiter for assistant messages (default: ---ASSISTANT)",
337
+ )
338
+ parser.add_argument(
339
+ "--resource-delimiter",
340
+ type=str,
341
+ default="---RESOURCE",
342
+ help="Delimiter for resource references (default: ---RESOURCE)",
343
+ )
344
+ parser.add_argument(
345
+ "--http-timeout",
346
+ type=float,
347
+ default=10.0,
348
+ help="Timeout for HTTP requests in seconds (default: 10.0)",
349
+ )
350
+ parser.add_argument(
351
+ "--transport",
352
+ type=str,
353
+ choices=["stdio", "sse", "http"],
354
+ default="stdio",
355
+ help="Transport to use (default: stdio)",
356
+ )
357
+ parser.add_argument(
358
+ "--port",
359
+ type=int,
360
+ default=8000,
361
+ help="Port to use for SSE transport (default: 8000)",
362
+ )
363
+ parser.add_argument(
364
+ "--host",
365
+ type=str,
366
+ default="0.0.0.0",
367
+ help="Host to bind to for SSE transport (default: 0.0.0.0)",
368
+ )
369
+ parser.add_argument(
370
+ "--test", type=str, help="Test a specific prompt without starting the server"
371
+ )
372
+
373
+ return parser.parse_args()
374
+
375
+
376
+ def initialize_config(args) -> PromptConfig:
377
+ """Initialize configuration from command line arguments"""
378
+ # Resolve file paths
379
+ prompt_files = []
380
+ for file_path in args.prompt_files:
381
+ path = Path(file_path)
382
+ if not path.exists():
383
+ logger.warning(f"File not found: {path}")
384
+ continue
385
+ prompt_files.append(path.resolve())
386
+
387
+ if not prompt_files:
388
+ logger.error("No valid prompt files specified")
389
+ raise ValueError("No valid prompt files specified")
390
+
391
+ # Initialize configuration
392
+ return PromptConfig(
393
+ name="prompt_server",
394
+ description="FastMCP Prompt Server",
395
+ template_variables=set(),
396
+ resource_paths=[],
397
+ file_path=Path(__file__),
398
+ prompt_files=prompt_files,
399
+ user_delimiter=args.user_delimiter,
400
+ assistant_delimiter=args.assistant_delimiter,
401
+ resource_delimiter=args.resource_delimiter,
402
+ http_timeout=args.http_timeout,
403
+ transport=args.transport,
404
+ port=args.port,
405
+ host=args.host,
406
+ )
407
+
408
+
409
+ async def register_file_resource_handler(config: PromptConfig) -> None:
410
+ """Register the general file resource handler"""
411
+
412
+ @mcp.resource("file://{path}")
413
+ async def get_file_resource(path: str):
414
+ """Read a file from the given path."""
415
+ try:
416
+ # Find the file, checking relative paths first
417
+ file_path = resource_utils.find_resource_file(path, config.prompt_files)
418
+ if file_path is None:
419
+ # If not found as relative path, try absolute path
420
+ file_path = Path(path)
421
+ if not file_path.exists():
422
+ raise FileNotFoundError(f"Resource file not found: {path}")
423
+
424
+ mime_type = mime_utils.guess_mime_type(str(file_path))
425
+ is_binary = mime_utils.is_binary_content(mime_type)
426
+
427
+ if is_binary:
428
+ # For binary files, read as binary and base64 encode
429
+ with open(file_path, "rb") as f:
430
+ return base64.b64encode(f.read()).decode("utf-8")
431
+ else:
432
+ # For text files, read as text with UTF-8 encoding
433
+ with open(file_path, "r", encoding="utf-8") as f:
434
+ return f.read()
435
+ except Exception as e:
436
+ # Log the error and re-raise
437
+ logger.error(f"Error accessing resource at '{path}': {e}")
438
+ raise
439
+
440
+
441
+ async def test_prompt(prompt_name: str, config: PromptConfig) -> int:
442
+ """Test a prompt and print its details"""
443
+ if prompt_name not in prompt_registry:
444
+ logger.error(f"Test prompt not found: {prompt_name}")
445
+ return 1
446
+
447
+ # Get delimiter configuration with reasonable defaults
448
+ config_values = get_delimiter_config(config)
449
+
450
+ metadata = prompt_registry[prompt_name]
451
+ print(f"\nTesting prompt: {prompt_name}")
452
+ print(f"Description: {metadata.description}")
453
+ print(f"Template variables: {', '.join(metadata.template_variables)}")
454
+
455
+ # Load and print the template
456
+ loader = PromptTemplateLoader(
457
+ {
458
+ config_values["user_delimiter"]: "user",
459
+ config_values["assistant_delimiter"]: "assistant",
460
+ config_values["resource_delimiter"]: "resource",
461
+ }
462
+ )
463
+ template = loader.load_from_file(metadata.file_path)
464
+
465
+ # Print each content section
466
+ print("\nContent sections:")
467
+ for i, section in enumerate(template.content_sections):
468
+ print(f"\n[{i + 1}] Role: {section.role}")
469
+ print(f"Content: {section.text}")
470
+ if section.resources:
471
+ print(f"Resources: {', '.join(section.resources)}")
472
+
473
+ # If there are template variables, test with dummy values
474
+ if metadata.template_variables:
475
+ print("\nTemplate substitution test:")
476
+ test_context = {var: f"[TEST-{var}]" for var in metadata.template_variables}
477
+ applied = template.apply_substitutions(test_context)
478
+
479
+ for i, section in enumerate(applied):
480
+ print(f"\n[{i + 1}] Role: {section.role}")
481
+ print(f"Content with substitutions: {section.text}")
482
+ if section.resources:
483
+ print(f"Resources with substitutions: {', '.join(section.resources)}")
484
+
485
+ return 0
486
+
487
+
488
+ async def async_main() -> int:
489
+ """Run the FastMCP server (async version)"""
490
+ # Parse command line arguments
491
+ args = parse_args()
492
+
493
+ try:
494
+ # Initialize configuration
495
+ config = initialize_config(args)
496
+ except ValueError as e:
497
+ logger.error(str(e))
498
+ return 1
499
+
500
+ # Register resource handlers
501
+ await register_file_resource_handler(config)
502
+
503
+ # Register all prompts
504
+ for file_path in config.prompt_files:
505
+ register_prompt(file_path, config)
506
+
507
+ # Print startup info
508
+ logger.info("Starting prompt server")
509
+ logger.info(f"Registered {len(prompt_registry)} prompts")
510
+ logger.info(f"Registered {len(exposed_resources)} resources")
511
+ logger.info(
512
+ f"Using delimiters: {config.user_delimiter}, {config.assistant_delimiter}, {config.resource_delimiter}"
513
+ )
514
+
515
+ # If a test prompt was specified, print it and exit
516
+ if args.test:
517
+ return await test_prompt(args.test, config)
518
+
519
+ # Start the server with the specified transport
520
+ if config.transport == "sse": # sse
521
+ # Set the host and port in settings before running the server
522
+ mcp.settings.host = config.host
523
+ mcp.settings.port = config.port
524
+ logger.info(f"Starting SSE server on {config.host}:{config.port}")
525
+ await mcp.run_sse_async()
526
+ elif config.transport == "http":
527
+ mcp.settings.host = config.host
528
+ mcp.settings.port = config.port
529
+ logger.info(f"Starting SSE server on {config.host}:{config.port}")
530
+ await mcp.run_streamable_http_async()
531
+ elif config.transport == "stdio":
532
+ await mcp.run_stdio_async()
533
+ else:
534
+ logger.error(f"Unknown transport: {config.transport}")
535
+ return 1
536
+ return 0
537
+
538
+
539
+ def main() -> int:
540
+ """Run the FastMCP server"""
541
+ try:
542
+ return asyncio.run(async_main())
543
+ except KeyboardInterrupt:
544
+ logger.info("\nServer stopped by user")
545
+ except Exception as e:
546
+ logger.error(f"\nError: {e}", exc_info=True)
547
+ return 1
548
+ return 0
549
+
550
+
551
+ if __name__ == "__main__":
552
+ sys.exit(main())