fast-agent-mcp 0.1.8__py3-none-any.whl → 0.1.10__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_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.10.dist-info}/METADATA +27 -4
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.10.dist-info}/RECORD +51 -30
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.10.dist-info}/entry_points.txt +1 -0
- mcp_agent/agents/agent.py +114 -8
- mcp_agent/context.py +0 -2
- mcp_agent/core/agent_app.py +89 -13
- mcp_agent/core/factory.py +14 -13
- mcp_agent/core/fastagent.py +15 -5
- mcp_agent/core/mcp_content.py +222 -0
- mcp_agent/core/prompt.py +132 -0
- mcp_agent/core/proxies.py +79 -36
- mcp_agent/logging/listeners.py +3 -6
- mcp_agent/logging/transport.py +30 -3
- mcp_agent/mcp/mcp_agent_client_session.py +21 -145
- mcp_agent/mcp/mcp_aggregator.py +61 -12
- mcp_agent/mcp/mcp_connection_manager.py +0 -1
- mcp_agent/mcp/mime_utils.py +69 -0
- mcp_agent/mcp/prompt_message_multipart.py +64 -0
- mcp_agent/mcp/prompt_serialization.py +447 -0
- mcp_agent/mcp/prompts/__init__.py +0 -0
- mcp_agent/mcp/prompts/__main__.py +10 -0
- mcp_agent/mcp/prompts/prompt_server.py +509 -0
- mcp_agent/mcp/prompts/prompt_template.py +469 -0
- mcp_agent/mcp/resource_utils.py +223 -0
- mcp_agent/mcp/stdio.py +23 -15
- mcp_agent/mcp_server_registry.py +5 -2
- mcp_agent/resources/examples/internal/agent.py +1 -1
- mcp_agent/resources/examples/internal/fastagent.config.yaml +2 -2
- mcp_agent/resources/examples/internal/sizer.py +0 -5
- mcp_agent/resources/examples/prompting/__init__.py +3 -0
- mcp_agent/resources/examples/prompting/agent.py +23 -0
- mcp_agent/resources/examples/prompting/fastagent.config.yaml +44 -0
- mcp_agent/resources/examples/prompting/image_server.py +56 -0
- mcp_agent/resources/examples/workflows/orchestrator.py +3 -3
- mcp_agent/workflows/llm/anthropic_utils.py +101 -0
- mcp_agent/workflows/llm/augmented_llm.py +139 -66
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +127 -251
- mcp_agent/workflows/llm/augmented_llm_openai.py +149 -305
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +99 -1
- mcp_agent/workflows/llm/augmented_llm_playback.py +109 -0
- mcp_agent/workflows/llm/model_factory.py +20 -3
- mcp_agent/workflows/llm/openai_utils.py +65 -0
- mcp_agent/workflows/llm/providers/__init__.py +8 -0
- mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +348 -0
- mcp_agent/workflows/llm/providers/multipart_converter_openai.py +426 -0
- mcp_agent/workflows/llm/providers/openai_multipart.py +197 -0
- mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +258 -0
- mcp_agent/workflows/llm/providers/sampling_converter_openai.py +229 -0
- mcp_agent/workflows/llm/sampling_format_converter.py +39 -0
- mcp_agent/core/server_validation.py +0 -44
- mcp_agent/core/simulator_registry.py +0 -22
- mcp_agent/workflows/llm/enhanced_passthrough.py +0 -70
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.10.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,509 @@
|
|
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 asyncio
|
9
|
+
import argparse
|
10
|
+
import base64
|
11
|
+
import logging
|
12
|
+
import sys
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import List, Dict, Optional, Callable, Awaitable, Literal, Any
|
15
|
+
from mcp.server.fastmcp.resources import FileResource
|
16
|
+
from pydantic import AnyUrl
|
17
|
+
|
18
|
+
from mcp_agent.mcp import mime_utils, resource_utils
|
19
|
+
|
20
|
+
from mcp.server.fastmcp import FastMCP
|
21
|
+
from mcp.server.fastmcp.prompts.base import (
|
22
|
+
UserMessage,
|
23
|
+
AssistantMessage,
|
24
|
+
Message,
|
25
|
+
)
|
26
|
+
from mcp.types import (
|
27
|
+
TextContent,
|
28
|
+
)
|
29
|
+
|
30
|
+
from mcp_agent.mcp.prompts.prompt_template import (
|
31
|
+
PromptTemplateLoader,
|
32
|
+
PromptMetadata,
|
33
|
+
PromptContent,
|
34
|
+
PromptTemplate,
|
35
|
+
)
|
36
|
+
|
37
|
+
# Configure logging
|
38
|
+
logging.basicConfig(level=logging.INFO)
|
39
|
+
logger = logging.getLogger("prompt_server")
|
40
|
+
|
41
|
+
# Create FastMCP server
|
42
|
+
mcp = FastMCP("Prompt Server")
|
43
|
+
|
44
|
+
|
45
|
+
class PromptConfig(PromptMetadata):
|
46
|
+
"""Configuration for the prompt server"""
|
47
|
+
|
48
|
+
prompt_files: List[Path] = []
|
49
|
+
user_delimiter: str = "---USER"
|
50
|
+
assistant_delimiter: str = "---ASSISTANT"
|
51
|
+
resource_delimiter: str = "---RESOURCE"
|
52
|
+
http_timeout: float = 10.0
|
53
|
+
transport: str = "stdio"
|
54
|
+
port: int = 8000
|
55
|
+
|
56
|
+
|
57
|
+
# Will be initialized with command line args
|
58
|
+
config = None
|
59
|
+
|
60
|
+
# We'll maintain registries of all exposed resources and prompts
|
61
|
+
exposed_resources: Dict[str, Path] = {}
|
62
|
+
prompt_registry: Dict[str, PromptMetadata] = {}
|
63
|
+
|
64
|
+
# Define message role type
|
65
|
+
MessageRole = Literal["user", "assistant"]
|
66
|
+
|
67
|
+
|
68
|
+
def create_content_message(text: str, role: MessageRole) -> Message:
|
69
|
+
"""Create a text content message with the specified role"""
|
70
|
+
message_class = UserMessage if role == "user" else AssistantMessage
|
71
|
+
return message_class(content=TextContent(type="text", text=text))
|
72
|
+
|
73
|
+
|
74
|
+
def create_resource_message(
|
75
|
+
resource_path: str, content: str, mime_type: str, is_binary: bool, role: MessageRole
|
76
|
+
) -> Message:
|
77
|
+
"""Create a resource message with the specified content and role"""
|
78
|
+
message_class = UserMessage if role == "user" else AssistantMessage
|
79
|
+
|
80
|
+
if mime_utils.is_image_mime_type(mime_type):
|
81
|
+
# For images, create an ImageContent
|
82
|
+
image_content = resource_utils.create_image_content(
|
83
|
+
data=content, mime_type=mime_type
|
84
|
+
)
|
85
|
+
return message_class(content=image_content)
|
86
|
+
else:
|
87
|
+
# For other resources, create an EmbeddedResource
|
88
|
+
embedded_resource = resource_utils.create_embedded_resource(
|
89
|
+
resource_path, content, mime_type, is_binary
|
90
|
+
)
|
91
|
+
return message_class(content=embedded_resource)
|
92
|
+
|
93
|
+
|
94
|
+
def create_messages_with_resources(
|
95
|
+
content_sections: List[PromptContent], prompt_files: List[Path]
|
96
|
+
) -> List[Message]:
|
97
|
+
"""
|
98
|
+
Create a list of messages from content sections, with resources properly handled.
|
99
|
+
|
100
|
+
This implementation produces one message for each content section's text,
|
101
|
+
followed by separate messages for each resource (with the same role type
|
102
|
+
as the section they belong to).
|
103
|
+
|
104
|
+
Args:
|
105
|
+
content_sections: List of PromptContent objects
|
106
|
+
prompt_files: List of prompt files (to help locate resource files)
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
List of Message objects
|
110
|
+
"""
|
111
|
+
messages = []
|
112
|
+
|
113
|
+
for section in content_sections:
|
114
|
+
# Convert to our literal type for role
|
115
|
+
role = cast_message_role(section.role)
|
116
|
+
|
117
|
+
# Add the text message
|
118
|
+
messages.append(create_content_message(section.text, role))
|
119
|
+
|
120
|
+
# Add resource messages with the same role type as the section
|
121
|
+
for resource_path in section.resources:
|
122
|
+
try:
|
123
|
+
# Load resource with information about its type
|
124
|
+
resource_content, mime_type, is_binary = (
|
125
|
+
resource_utils.load_resource_content(resource_path, prompt_files)
|
126
|
+
)
|
127
|
+
|
128
|
+
# Create and add the resource message
|
129
|
+
resource_message = create_resource_message(
|
130
|
+
resource_path, resource_content, mime_type, is_binary, role
|
131
|
+
)
|
132
|
+
messages.append(resource_message)
|
133
|
+
except Exception as e:
|
134
|
+
logger.error(f"Error loading resource {resource_path}: {e}")
|
135
|
+
|
136
|
+
return messages
|
137
|
+
|
138
|
+
|
139
|
+
def cast_message_role(role: str) -> MessageRole:
|
140
|
+
"""Cast a string role to a MessageRole literal type"""
|
141
|
+
if role == "user" or role == "assistant":
|
142
|
+
return role # type: ignore
|
143
|
+
# Default to user if the role is invalid
|
144
|
+
logger.warning(f"Invalid message role: {role}, defaulting to 'user'")
|
145
|
+
return "user"
|
146
|
+
|
147
|
+
|
148
|
+
# Define a single type for prompt handlers to avoid mypy issues
|
149
|
+
PromptHandler = Callable[..., Awaitable[List[Message]]]
|
150
|
+
|
151
|
+
|
152
|
+
def create_prompt_handler(
|
153
|
+
template: "PromptTemplate", template_vars: List[str], prompt_files: List[Path]
|
154
|
+
) -> PromptHandler:
|
155
|
+
"""Create a prompt handler function for the given template"""
|
156
|
+
if template_vars:
|
157
|
+
# With template variables
|
158
|
+
docstring = f"Prompt with template variables: {', '.join(template_vars)}"
|
159
|
+
|
160
|
+
async def prompt_handler(**kwargs: Any) -> List[Message]:
|
161
|
+
# Build context from parameters
|
162
|
+
context = {
|
163
|
+
var: kwargs.get(var)
|
164
|
+
for var in template_vars
|
165
|
+
if var in kwargs and kwargs[var] is not None
|
166
|
+
}
|
167
|
+
|
168
|
+
# Apply substitutions to the template
|
169
|
+
content_sections = template.apply_substitutions(context)
|
170
|
+
|
171
|
+
# Convert to MCP Message objects, handling resources properly
|
172
|
+
return create_messages_with_resources(content_sections, prompt_files)
|
173
|
+
else:
|
174
|
+
# No template variables
|
175
|
+
docstring = "Get a prompt with no variable substitution"
|
176
|
+
|
177
|
+
async def prompt_handler(**kwargs: Any) -> List[Message]:
|
178
|
+
# Get the content sections
|
179
|
+
content_sections = template.content_sections
|
180
|
+
|
181
|
+
# Convert to MCP Message objects, handling resources properly
|
182
|
+
return create_messages_with_resources(content_sections, prompt_files)
|
183
|
+
|
184
|
+
# Set the docstring
|
185
|
+
prompt_handler.__doc__ = docstring
|
186
|
+
return prompt_handler
|
187
|
+
|
188
|
+
|
189
|
+
# Type for resource handler
|
190
|
+
ResourceHandler = Callable[[], Awaitable[str | bytes]]
|
191
|
+
|
192
|
+
|
193
|
+
def create_resource_handler(resource_path: Path, mime_type: str) -> ResourceHandler:
|
194
|
+
"""Create a resource handler function for the given resource"""
|
195
|
+
|
196
|
+
async def get_resource() -> str | bytes:
|
197
|
+
is_binary = mime_utils.is_binary_content(mime_type)
|
198
|
+
|
199
|
+
if is_binary:
|
200
|
+
# For binary files, read in binary mode and base64 encode
|
201
|
+
with open(resource_path, "rb") as f:
|
202
|
+
return f.read()
|
203
|
+
else:
|
204
|
+
# For text files, read as utf-8 text
|
205
|
+
with open(resource_path, "r", encoding="utf-8") as f:
|
206
|
+
return f.read()
|
207
|
+
|
208
|
+
return get_resource
|
209
|
+
|
210
|
+
|
211
|
+
# Default delimiter values
|
212
|
+
DEFAULT_USER_DELIMITER = "---USER"
|
213
|
+
DEFAULT_ASSISTANT_DELIMITER = "---ASSISTANT"
|
214
|
+
DEFAULT_RESOURCE_DELIMITER = "---RESOURCE"
|
215
|
+
|
216
|
+
|
217
|
+
def get_delimiter_config(file_path: Optional[Path] = None) -> Dict[str, Any]:
|
218
|
+
"""Get delimiter configuration, falling back to defaults if config is None"""
|
219
|
+
# Set defaults
|
220
|
+
config_values = {
|
221
|
+
"user_delimiter": DEFAULT_USER_DELIMITER,
|
222
|
+
"assistant_delimiter": DEFAULT_ASSISTANT_DELIMITER,
|
223
|
+
"resource_delimiter": DEFAULT_RESOURCE_DELIMITER,
|
224
|
+
"prompt_files": [file_path] if file_path else [],
|
225
|
+
}
|
226
|
+
|
227
|
+
# Override with config values if available
|
228
|
+
if config is not None:
|
229
|
+
config_values["user_delimiter"] = config.user_delimiter
|
230
|
+
config_values["assistant_delimiter"] = config.assistant_delimiter
|
231
|
+
config_values["resource_delimiter"] = config.resource_delimiter
|
232
|
+
config_values["prompt_files"] = config.prompt_files
|
233
|
+
|
234
|
+
return config_values
|
235
|
+
|
236
|
+
|
237
|
+
def register_prompt(file_path: Path):
|
238
|
+
"""Register a prompt file"""
|
239
|
+
try:
|
240
|
+
# Get delimiter configuration
|
241
|
+
config_values = get_delimiter_config(file_path)
|
242
|
+
|
243
|
+
# Use our prompt template loader to analyze the file
|
244
|
+
loader = PromptTemplateLoader(
|
245
|
+
{
|
246
|
+
config_values["user_delimiter"]: "user",
|
247
|
+
config_values["assistant_delimiter"]: "assistant",
|
248
|
+
config_values["resource_delimiter"]: "resource",
|
249
|
+
}
|
250
|
+
)
|
251
|
+
|
252
|
+
# Get metadata and load the template
|
253
|
+
metadata = loader.get_metadata(file_path)
|
254
|
+
template = loader.load_from_file(file_path)
|
255
|
+
|
256
|
+
# Ensure unique name
|
257
|
+
prompt_name = metadata.name
|
258
|
+
if prompt_name in prompt_registry:
|
259
|
+
base_name = prompt_name
|
260
|
+
suffix = 1
|
261
|
+
while prompt_name in prompt_registry:
|
262
|
+
prompt_name = f"{base_name}_{suffix}"
|
263
|
+
suffix += 1
|
264
|
+
metadata.name = prompt_name
|
265
|
+
|
266
|
+
prompt_registry[metadata.name] = metadata
|
267
|
+
logger.info(f"Registered prompt: {metadata.name} ({file_path})")
|
268
|
+
|
269
|
+
# Create and register prompt handler
|
270
|
+
template_vars = list(metadata.template_variables)
|
271
|
+
handler = create_prompt_handler(
|
272
|
+
template, template_vars, config_values["prompt_files"]
|
273
|
+
)
|
274
|
+
mcp.prompt(name=metadata.name, description=metadata.description)(handler)
|
275
|
+
|
276
|
+
# Register any referenced resources in the prompt
|
277
|
+
for resource_path in metadata.resource_paths:
|
278
|
+
if not resource_path.startswith(("http://", "https://")):
|
279
|
+
# It's a local resource
|
280
|
+
resource_file = file_path.parent / resource_path
|
281
|
+
if resource_file.exists():
|
282
|
+
resource_id = f"resource://fast-agent/{resource_file.name}"
|
283
|
+
|
284
|
+
# Register the resource if not already registered
|
285
|
+
if resource_id not in exposed_resources:
|
286
|
+
exposed_resources[resource_id] = resource_file
|
287
|
+
mime_type = mime_utils.guess_mime_type(str(resource_file))
|
288
|
+
|
289
|
+
mcp.add_resource(
|
290
|
+
FileResource(
|
291
|
+
uri=AnyUrl(resource_id),
|
292
|
+
path=resource_file,
|
293
|
+
mime_type=mime_type,
|
294
|
+
is_binary=mime_utils.is_binary_content(mime_type),
|
295
|
+
)
|
296
|
+
)
|
297
|
+
|
298
|
+
logger.info(
|
299
|
+
f"Registered resource: {resource_id} ({resource_file})"
|
300
|
+
)
|
301
|
+
except Exception as e:
|
302
|
+
logger.error(f"Error registering prompt {file_path}: {e}", exc_info=True)
|
303
|
+
|
304
|
+
|
305
|
+
def parse_args():
|
306
|
+
"""Parse command line arguments"""
|
307
|
+
parser = argparse.ArgumentParser(description="FastMCP Prompt Server")
|
308
|
+
parser.add_argument(
|
309
|
+
"prompt_files", nargs="+", type=str, help="Prompt files to serve"
|
310
|
+
)
|
311
|
+
parser.add_argument(
|
312
|
+
"--user-delimiter",
|
313
|
+
type=str,
|
314
|
+
default="---USER",
|
315
|
+
help="Delimiter for user messages (default: ---USER)",
|
316
|
+
)
|
317
|
+
parser.add_argument(
|
318
|
+
"--assistant-delimiter",
|
319
|
+
type=str,
|
320
|
+
default="---ASSISTANT",
|
321
|
+
help="Delimiter for assistant messages (default: ---ASSISTANT)",
|
322
|
+
)
|
323
|
+
parser.add_argument(
|
324
|
+
"--resource-delimiter",
|
325
|
+
type=str,
|
326
|
+
default="---RESOURCE",
|
327
|
+
help="Delimiter for resource references (default: ---RESOURCE)",
|
328
|
+
)
|
329
|
+
parser.add_argument(
|
330
|
+
"--http-timeout",
|
331
|
+
type=float,
|
332
|
+
default=10.0,
|
333
|
+
help="Timeout for HTTP requests in seconds (default: 10.0)",
|
334
|
+
)
|
335
|
+
parser.add_argument(
|
336
|
+
"--transport",
|
337
|
+
type=str,
|
338
|
+
choices=["stdio", "sse"],
|
339
|
+
default="stdio",
|
340
|
+
help="Transport to use (default: stdio)",
|
341
|
+
)
|
342
|
+
parser.add_argument(
|
343
|
+
"--port",
|
344
|
+
type=int,
|
345
|
+
default=8000,
|
346
|
+
help="Port to use for SSE transport (default: 8000)",
|
347
|
+
)
|
348
|
+
parser.add_argument(
|
349
|
+
"--test", type=str, help="Test a specific prompt without starting the server"
|
350
|
+
)
|
351
|
+
|
352
|
+
return parser.parse_args()
|
353
|
+
|
354
|
+
|
355
|
+
async def register_file_resource_handler():
|
356
|
+
"""Register the general file resource handler"""
|
357
|
+
|
358
|
+
@mcp.resource("file://{path}")
|
359
|
+
async def get_file_resource(path: str):
|
360
|
+
"""Read a file from the given path."""
|
361
|
+
try:
|
362
|
+
# Find the file, checking relative paths first
|
363
|
+
file_path = resource_utils.find_resource_file(path, config.prompt_files)
|
364
|
+
if file_path is None:
|
365
|
+
# If not found as relative path, try absolute path
|
366
|
+
file_path = Path(path)
|
367
|
+
if not file_path.exists():
|
368
|
+
raise FileNotFoundError(f"Resource file not found: {path}")
|
369
|
+
|
370
|
+
mime_type = mime_utils.guess_mime_type(str(file_path))
|
371
|
+
is_binary = mime_utils.is_binary_content(mime_type)
|
372
|
+
|
373
|
+
if is_binary:
|
374
|
+
# For binary files, read as binary and base64 encode
|
375
|
+
with open(file_path, "rb") as f:
|
376
|
+
return base64.b64encode(f.read()).decode("utf-8")
|
377
|
+
else:
|
378
|
+
# For text files, read as text with UTF-8 encoding
|
379
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
380
|
+
return f.read()
|
381
|
+
except Exception as e:
|
382
|
+
# Log the error and re-raise
|
383
|
+
logger.error(f"Error accessing resource at '{path}': {e}")
|
384
|
+
raise
|
385
|
+
|
386
|
+
|
387
|
+
async def test_prompt(prompt_name: str) -> int:
|
388
|
+
"""Test a prompt and print its details"""
|
389
|
+
if prompt_name not in prompt_registry:
|
390
|
+
logger.error(f"Test prompt not found: {prompt_name}")
|
391
|
+
return 1
|
392
|
+
|
393
|
+
# Get delimiter configuration with reasonable defaults
|
394
|
+
config_values = get_delimiter_config()
|
395
|
+
|
396
|
+
metadata = prompt_registry[prompt_name]
|
397
|
+
print(f"\nTesting prompt: {prompt_name}")
|
398
|
+
print(f"Description: {metadata.description}")
|
399
|
+
print(f"Template variables: {', '.join(metadata.template_variables)}")
|
400
|
+
|
401
|
+
# Load and print the template
|
402
|
+
loader = PromptTemplateLoader(
|
403
|
+
{
|
404
|
+
config_values["user_delimiter"]: "user",
|
405
|
+
config_values["assistant_delimiter"]: "assistant",
|
406
|
+
config_values["resource_delimiter"]: "resource",
|
407
|
+
}
|
408
|
+
)
|
409
|
+
template = loader.load_from_file(metadata.file_path)
|
410
|
+
|
411
|
+
# Print each content section
|
412
|
+
print("\nContent sections:")
|
413
|
+
for i, section in enumerate(template.content_sections):
|
414
|
+
print(f"\n[{i + 1}] Role: {section.role}")
|
415
|
+
print(f"Content: {section.text}")
|
416
|
+
if section.resources:
|
417
|
+
print(f"Resources: {', '.join(section.resources)}")
|
418
|
+
|
419
|
+
# If there are template variables, test with dummy values
|
420
|
+
if metadata.template_variables:
|
421
|
+
print("\nTemplate substitution test:")
|
422
|
+
test_context = {var: f"[TEST-{var}]" for var in metadata.template_variables}
|
423
|
+
applied = template.apply_substitutions(test_context)
|
424
|
+
|
425
|
+
for i, section in enumerate(applied):
|
426
|
+
print(f"\n[{i + 1}] Role: {section.role}")
|
427
|
+
print(f"Content with substitutions: {section.text}")
|
428
|
+
if section.resources:
|
429
|
+
print(f"Resources with substitutions: {', '.join(section.resources)}")
|
430
|
+
|
431
|
+
return 0
|
432
|
+
|
433
|
+
|
434
|
+
async def async_main():
|
435
|
+
"""Run the FastMCP server (async version)"""
|
436
|
+
global config
|
437
|
+
|
438
|
+
# Parse command line arguments
|
439
|
+
args = parse_args()
|
440
|
+
|
441
|
+
# Resolve file paths
|
442
|
+
prompt_files = []
|
443
|
+
for file_path in args.prompt_files:
|
444
|
+
path = Path(file_path)
|
445
|
+
if not path.exists():
|
446
|
+
logger.warning(f"File not found: {path}")
|
447
|
+
continue
|
448
|
+
prompt_files.append(path.resolve())
|
449
|
+
|
450
|
+
if not prompt_files:
|
451
|
+
logger.error("No valid prompt files specified")
|
452
|
+
return 1
|
453
|
+
|
454
|
+
# Initialize configuration
|
455
|
+
config = PromptConfig(
|
456
|
+
name="prompt_server",
|
457
|
+
description="FastMCP Prompt Server",
|
458
|
+
template_variables=set(),
|
459
|
+
resource_paths=[],
|
460
|
+
file_path=Path(__file__),
|
461
|
+
prompt_files=prompt_files,
|
462
|
+
user_delimiter=args.user_delimiter,
|
463
|
+
assistant_delimiter=args.assistant_delimiter,
|
464
|
+
resource_delimiter=args.resource_delimiter,
|
465
|
+
http_timeout=args.http_timeout,
|
466
|
+
transport=args.transport,
|
467
|
+
port=args.port,
|
468
|
+
)
|
469
|
+
|
470
|
+
# Register resource handlers
|
471
|
+
await register_file_resource_handler()
|
472
|
+
|
473
|
+
# Register all prompts
|
474
|
+
for file_path in config.prompt_files:
|
475
|
+
register_prompt(file_path)
|
476
|
+
|
477
|
+
# Print startup info
|
478
|
+
logger.info("Starting prompt server")
|
479
|
+
logger.info(f"Registered {len(prompt_registry)} prompts")
|
480
|
+
logger.info(f"Registered {len(exposed_resources)} resources")
|
481
|
+
logger.info(
|
482
|
+
f"Using delimiters: {config.user_delimiter}, {config.assistant_delimiter}, {config.resource_delimiter}"
|
483
|
+
)
|
484
|
+
|
485
|
+
# If a test prompt was specified, print it and exit
|
486
|
+
if args.test:
|
487
|
+
return await test_prompt(args.test)
|
488
|
+
|
489
|
+
# Start the server with the specified transport
|
490
|
+
if config.transport == "stdio":
|
491
|
+
await mcp.run_stdio_async()
|
492
|
+
else: # sse
|
493
|
+
await mcp.run_sse_async(port=config.port)
|
494
|
+
|
495
|
+
|
496
|
+
def main() -> int:
|
497
|
+
"""Run the FastMCP server"""
|
498
|
+
try:
|
499
|
+
return asyncio.run(async_main())
|
500
|
+
except KeyboardInterrupt:
|
501
|
+
logger.info("\nServer stopped by user")
|
502
|
+
except Exception as e:
|
503
|
+
logger.error(f"\nError: {e}", exc_info=True)
|
504
|
+
return 1
|
505
|
+
return 0
|
506
|
+
|
507
|
+
|
508
|
+
if __name__ == "__main__":
|
509
|
+
sys.exit(main())
|