fast-agent-mcp 0.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,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())
|