fast-agent-mcp 0.1.12__py3-none-any.whl → 0.1.13__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.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/METADATA +1 -1
- fast_agent_mcp-0.1.13.dist-info/RECORD +164 -0
- mcp_agent/agents/agent.py +37 -79
- mcp_agent/app.py +16 -22
- mcp_agent/cli/commands/bootstrap.py +22 -52
- mcp_agent/cli/commands/config.py +4 -4
- mcp_agent/cli/commands/setup.py +11 -26
- mcp_agent/cli/main.py +6 -9
- mcp_agent/cli/terminal.py +2 -2
- mcp_agent/config.py +1 -5
- mcp_agent/context.py +13 -24
- mcp_agent/context_dependent.py +3 -7
- mcp_agent/core/agent_app.py +45 -121
- mcp_agent/core/agent_utils.py +3 -5
- mcp_agent/core/decorators.py +5 -12
- mcp_agent/core/enhanced_prompt.py +25 -52
- mcp_agent/core/exceptions.py +8 -8
- mcp_agent/core/factory.py +29 -70
- mcp_agent/core/fastagent.py +48 -88
- mcp_agent/core/mcp_content.py +8 -16
- mcp_agent/core/prompt.py +8 -15
- mcp_agent/core/proxies.py +34 -25
- mcp_agent/core/request_params.py +6 -3
- mcp_agent/core/types.py +4 -6
- mcp_agent/core/validation.py +4 -3
- mcp_agent/executor/decorator_registry.py +11 -23
- mcp_agent/executor/executor.py +8 -17
- mcp_agent/executor/task_registry.py +2 -4
- mcp_agent/executor/temporal.py +28 -74
- mcp_agent/executor/workflow.py +3 -5
- mcp_agent/executor/workflow_signal.py +17 -29
- mcp_agent/human_input/handler.py +4 -9
- mcp_agent/human_input/types.py +2 -3
- mcp_agent/logging/events.py +1 -5
- mcp_agent/logging/json_serializer.py +7 -6
- mcp_agent/logging/listeners.py +20 -23
- mcp_agent/logging/logger.py +15 -17
- mcp_agent/logging/rich_progress.py +10 -8
- mcp_agent/logging/tracing.py +4 -6
- mcp_agent/logging/transport.py +22 -22
- mcp_agent/mcp/gen_client.py +4 -12
- mcp_agent/mcp/interfaces.py +71 -86
- mcp_agent/mcp/mcp_agent_client_session.py +11 -19
- mcp_agent/mcp/mcp_agent_server.py +8 -10
- mcp_agent/mcp/mcp_aggregator.py +45 -117
- mcp_agent/mcp/mcp_connection_manager.py +16 -37
- mcp_agent/mcp/prompt_message_multipart.py +12 -18
- mcp_agent/mcp/prompt_serialization.py +13 -38
- mcp_agent/mcp/prompts/prompt_load.py +99 -0
- mcp_agent/mcp/prompts/prompt_server.py +21 -128
- mcp_agent/mcp/prompts/prompt_template.py +20 -42
- mcp_agent/mcp/resource_utils.py +8 -17
- mcp_agent/mcp/sampling.py +5 -14
- mcp_agent/mcp/stdio.py +11 -8
- mcp_agent/mcp_server/agent_server.py +10 -17
- mcp_agent/mcp_server_registry.py +13 -35
- mcp_agent/resources/examples/data-analysis/analysis-campaign.py +1 -1
- mcp_agent/resources/examples/data-analysis/analysis.py +1 -1
- mcp_agent/resources/examples/data-analysis/slides.py +110 -0
- mcp_agent/resources/examples/internal/agent.py +2 -1
- mcp_agent/resources/examples/internal/job.py +2 -1
- mcp_agent/resources/examples/internal/prompt_category.py +1 -1
- mcp_agent/resources/examples/internal/prompt_sizing.py +3 -5
- mcp_agent/resources/examples/internal/sizer.py +2 -1
- mcp_agent/resources/examples/internal/social.py +2 -1
- mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +1 -1
- mcp_agent/resources/examples/prompting/agent.py +2 -1
- mcp_agent/resources/examples/prompting/image_server.py +5 -11
- mcp_agent/resources/examples/researcher/researcher-eval.py +1 -1
- mcp_agent/resources/examples/researcher/researcher-imp.py +3 -4
- mcp_agent/resources/examples/researcher/researcher.py +2 -1
- mcp_agent/resources/examples/workflows/agent_build.py +2 -1
- mcp_agent/resources/examples/workflows/chaining.py +2 -1
- mcp_agent/resources/examples/workflows/evaluator.py +2 -1
- mcp_agent/resources/examples/workflows/human_input.py +2 -1
- mcp_agent/resources/examples/workflows/orchestrator.py +2 -1
- mcp_agent/resources/examples/workflows/parallel.py +2 -1
- mcp_agent/resources/examples/workflows/router.py +2 -1
- mcp_agent/resources/examples/workflows/sse.py +1 -1
- mcp_agent/telemetry/usage_tracking.py +2 -1
- mcp_agent/ui/console_display.py +15 -39
- mcp_agent/workflows/embedding/embedding_base.py +1 -4
- mcp_agent/workflows/embedding/embedding_cohere.py +2 -2
- mcp_agent/workflows/embedding/embedding_openai.py +4 -13
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +23 -57
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +5 -8
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +7 -11
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +4 -8
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +4 -8
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +11 -22
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +3 -3
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +4 -6
- mcp_agent/workflows/llm/anthropic_utils.py +8 -29
- mcp_agent/workflows/llm/augmented_llm.py +69 -247
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +39 -73
- mcp_agent/workflows/llm/augmented_llm_openai.py +42 -97
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +13 -20
- mcp_agent/workflows/llm/augmented_llm_playback.py +8 -6
- mcp_agent/workflows/llm/memory.py +103 -0
- mcp_agent/workflows/llm/model_factory.py +8 -20
- mcp_agent/workflows/llm/openai_utils.py +1 -1
- mcp_agent/workflows/llm/prompt_utils.py +1 -3
- mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +47 -89
- mcp_agent/workflows/llm/providers/multipart_converter_openai.py +20 -55
- mcp_agent/workflows/llm/providers/openai_multipart.py +19 -61
- mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +10 -12
- mcp_agent/workflows/llm/providers/sampling_converter_openai.py +7 -11
- mcp_agent/workflows/llm/sampling_converter.py +4 -11
- mcp_agent/workflows/llm/sampling_format_converter.py +12 -12
- mcp_agent/workflows/orchestrator/orchestrator.py +24 -67
- mcp_agent/workflows/orchestrator/orchestrator_models.py +14 -40
- mcp_agent/workflows/parallel/fan_in.py +17 -47
- mcp_agent/workflows/parallel/fan_out.py +6 -12
- mcp_agent/workflows/parallel/parallel_llm.py +9 -26
- mcp_agent/workflows/router/router_base.py +19 -49
- mcp_agent/workflows/router/router_embedding.py +11 -25
- mcp_agent/workflows/router/router_embedding_cohere.py +2 -2
- mcp_agent/workflows/router/router_embedding_openai.py +2 -2
- mcp_agent/workflows/router/router_llm.py +12 -28
- mcp_agent/workflows/swarm/swarm.py +20 -48
- mcp_agent/workflows/swarm/swarm_anthropic.py +2 -2
- mcp_agent/workflows/swarm/swarm_openai.py +2 -2
- fast_agent_mcp-0.1.12.dist-info/RECORD +0 -161
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
@@ -19,9 +19,9 @@ and PromptMessageMultipart objects. It includes functionality for:
|
|
19
19
|
import json
|
20
20
|
from typing import List
|
21
21
|
|
22
|
-
from mcp.types import
|
23
|
-
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
22
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent, TextResourceContents
|
24
23
|
|
24
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
25
25
|
|
26
26
|
# -------------------------------------------------------------------------
|
27
27
|
# JSON Serialization Functions
|
@@ -43,10 +43,7 @@ def multipart_messages_to_json(messages: List[PromptMessageMultipart]) -> str:
|
|
43
43
|
"""
|
44
44
|
# Convert to dictionaries using model_dump with proper JSON mode
|
45
45
|
# The mode="json" parameter ensures proper handling of AnyUrl objects
|
46
|
-
message_dicts = [
|
47
|
-
message.model_dump(by_alias=True, mode="json", exclude_none=True)
|
48
|
-
for message in messages
|
49
|
-
]
|
46
|
+
message_dicts = [message.model_dump(by_alias=True, mode="json", exclude_none=True) for message in messages]
|
50
47
|
|
51
48
|
# Convert to JSON string
|
52
49
|
return json.dumps(message_dicts, indent=2)
|
@@ -76,9 +73,7 @@ def json_to_multipart_messages(json_str: str) -> List[PromptMessageMultipart]:
|
|
76
73
|
return messages
|
77
74
|
|
78
75
|
|
79
|
-
def save_messages_to_json_file(
|
80
|
-
messages: List[PromptMessageMultipart], file_path: str
|
81
|
-
) -> None:
|
76
|
+
def save_messages_to_json_file(messages: List[PromptMessageMultipart], file_path: str) -> None:
|
82
77
|
"""
|
83
78
|
Save PromptMessageMultipart objects to a JSON file.
|
84
79
|
|
@@ -169,9 +164,7 @@ def multipart_messages_to_delimited_format(
|
|
169
164
|
delimited_content.append(resource_delimiter)
|
170
165
|
|
171
166
|
# Convert to dictionary using proper JSON mode
|
172
|
-
content_dict = content.model_dump(
|
173
|
-
by_alias=True, mode="json", exclude_none=True
|
174
|
-
)
|
167
|
+
content_dict = content.model_dump(by_alias=True, mode="json", exclude_none=True)
|
175
168
|
|
176
169
|
# Add to delimited content as JSON
|
177
170
|
delimited_content.append(json.dumps(content_dict, indent=2))
|
@@ -186,9 +179,7 @@ def multipart_messages_to_delimited_format(
|
|
186
179
|
delimited_content.append(resource_delimiter)
|
187
180
|
|
188
181
|
# Convert to dictionary using proper JSON mode
|
189
|
-
content_dict = content.model_dump(
|
190
|
-
by_alias=True, mode="json", exclude_none=True
|
191
|
-
)
|
182
|
+
content_dict = content.model_dump(by_alias=True, mode="json", exclude_none=True)
|
192
183
|
|
193
184
|
# Add to delimited content as JSON
|
194
185
|
delimited_content.append(json.dumps(content_dict, indent=2))
|
@@ -237,31 +228,23 @@ def delimited_format_to_multipart_messages(
|
|
237
228
|
collecting_text = True
|
238
229
|
|
239
230
|
# Process each line
|
240
|
-
for line in
|
241
|
-
lines[1:] if lines else []
|
242
|
-
): # Skip the first line if already processed above
|
231
|
+
for line in lines[1:] if lines else []: # Skip the first line if already processed above
|
243
232
|
line_stripped = line.strip()
|
244
233
|
|
245
234
|
# Handle role delimiters
|
246
235
|
if line_stripped == user_delimiter or line_stripped == assistant_delimiter:
|
247
236
|
# Save previous message if it exists
|
248
|
-
if current_role is not None and (
|
249
|
-
text_contents or resource_contents or text_lines
|
250
|
-
):
|
237
|
+
if current_role is not None and (text_contents or resource_contents or text_lines):
|
251
238
|
# If we were collecting text, add it to the text contents
|
252
239
|
if collecting_text and text_lines:
|
253
|
-
text_contents.append(
|
254
|
-
TextContent(type="text", text="\n".join(text_lines))
|
255
|
-
)
|
240
|
+
text_contents.append(TextContent(type="text", text="\n".join(text_lines)))
|
256
241
|
text_lines = []
|
257
242
|
|
258
243
|
# Create content list with text parts first, then resource parts
|
259
244
|
combined_content = []
|
260
245
|
|
261
246
|
# Filter out any empty text content items
|
262
|
-
filtered_text_contents = [
|
263
|
-
tc for tc in text_contents if tc.text.strip() != ""
|
264
|
-
]
|
247
|
+
filtered_text_contents = [tc for tc in text_contents if tc.text.strip() != ""]
|
265
248
|
|
266
249
|
combined_content.extend(filtered_text_contents)
|
267
250
|
combined_content.extend(resource_contents)
|
@@ -286,9 +269,7 @@ def delimited_format_to_multipart_messages(
|
|
286
269
|
elif line_stripped == resource_delimiter:
|
287
270
|
# If we were collecting text, add it to text contents
|
288
271
|
if collecting_text and text_lines:
|
289
|
-
text_contents.append(
|
290
|
-
TextContent(type="text", text="\n".join(text_lines))
|
291
|
-
)
|
272
|
+
text_contents.append(TextContent(type="text", text="\n".join(text_lines)))
|
292
273
|
text_lines = []
|
293
274
|
|
294
275
|
# Switch to collecting JSON or legacy format
|
@@ -303,11 +284,7 @@ def delimited_format_to_multipart_messages(
|
|
303
284
|
json_lines.append(line)
|
304
285
|
|
305
286
|
# For legacy format or files where resources are just plain text
|
306
|
-
if (
|
307
|
-
legacy_format
|
308
|
-
and line_stripped
|
309
|
-
and not line_stripped.startswith("{")
|
310
|
-
):
|
287
|
+
if legacy_format and line_stripped and not line_stripped.startswith("{"):
|
311
288
|
# This is probably a legacy resource reference like a filename
|
312
289
|
resource_uri = line_stripped
|
313
290
|
if not resource_uri.startswith("resource://"):
|
@@ -370,9 +347,7 @@ def delimited_format_to_multipart_messages(
|
|
370
347
|
combined_content = []
|
371
348
|
|
372
349
|
# Filter out any empty text content items
|
373
|
-
filtered_text_contents = [
|
374
|
-
tc for tc in text_contents if tc.text.strip() != ""
|
375
|
-
]
|
350
|
+
filtered_text_contents = [tc for tc in text_contents if tc.text.strip() != ""]
|
376
351
|
|
377
352
|
combined_content.extend(filtered_text_contents)
|
378
353
|
combined_content.extend(resource_contents)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import List, Literal
|
3
|
+
|
4
|
+
from mcp.server.fastmcp.prompts.base import (
|
5
|
+
AssistantMessage,
|
6
|
+
Message,
|
7
|
+
UserMessage,
|
8
|
+
)
|
9
|
+
from mcp.types import PromptMessage, TextContent
|
10
|
+
|
11
|
+
from mcp_agent.logging.logger import get_logger
|
12
|
+
from mcp_agent.mcp import mime_utils, resource_utils
|
13
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
14
|
+
from mcp_agent.mcp.prompts.prompt_template import (
|
15
|
+
PromptContent,
|
16
|
+
PromptTemplate,
|
17
|
+
PromptTemplateLoader,
|
18
|
+
)
|
19
|
+
|
20
|
+
# Define message role type
|
21
|
+
MessageRole = Literal["user", "assistant"]
|
22
|
+
logger = get_logger("prompt_load")
|
23
|
+
|
24
|
+
|
25
|
+
def cast_message_role(role: str) -> MessageRole:
|
26
|
+
"""Cast a string role to a MessageRole literal type"""
|
27
|
+
if role == "user" or role == "assistant":
|
28
|
+
return role # type: ignore
|
29
|
+
# Default to user if the role is invalid
|
30
|
+
logger.warning(f"Invalid message role: {role}, defaulting to 'user'")
|
31
|
+
return "user"
|
32
|
+
|
33
|
+
|
34
|
+
def create_messages_with_resources(content_sections: List[PromptContent], prompt_files: List[Path]) -> List[PromptMessage]:
|
35
|
+
"""
|
36
|
+
Create a list of messages from content sections, with resources properly handled.
|
37
|
+
|
38
|
+
This implementation produces one message for each content section's text,
|
39
|
+
followed by separate messages for each resource (with the same role type
|
40
|
+
as the section they belong to).
|
41
|
+
|
42
|
+
Args:
|
43
|
+
content_sections: List of PromptContent objects
|
44
|
+
prompt_files: List of prompt files (to help locate resource files)
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
List of Message objects
|
48
|
+
"""
|
49
|
+
|
50
|
+
messages = []
|
51
|
+
|
52
|
+
for section in content_sections:
|
53
|
+
# Convert to our literal type for role
|
54
|
+
role = cast_message_role(section.role)
|
55
|
+
|
56
|
+
# Add the text message
|
57
|
+
messages.append(create_content_message(section.text, role))
|
58
|
+
|
59
|
+
# Add resource messages with the same role type as the section
|
60
|
+
for resource_path in section.resources:
|
61
|
+
try:
|
62
|
+
# Load resource with information about its type
|
63
|
+
resource_content, mime_type, is_binary = resource_utils.load_resource_content(resource_path, prompt_files)
|
64
|
+
|
65
|
+
# Create and add the resource message
|
66
|
+
resource_message = create_resource_message(resource_path, resource_content, mime_type, is_binary, role)
|
67
|
+
messages.append(resource_message)
|
68
|
+
except Exception as e:
|
69
|
+
logger.error(f"Error loading resource {resource_path}: {e}")
|
70
|
+
|
71
|
+
return messages
|
72
|
+
|
73
|
+
|
74
|
+
def create_content_message(text: str, role: MessageRole) -> PromptMessage:
|
75
|
+
"""Create a text content message with the specified role"""
|
76
|
+
return PromptMessage(role=role, content=TextContent(type="text", text=text))
|
77
|
+
|
78
|
+
|
79
|
+
def create_resource_message(resource_path: str, content: str, mime_type: str, is_binary: bool, role: MessageRole) -> Message:
|
80
|
+
"""Create a resource message with the specified content and role"""
|
81
|
+
message_class = UserMessage if role == "user" else AssistantMessage
|
82
|
+
|
83
|
+
if mime_utils.is_image_mime_type(mime_type):
|
84
|
+
# For images, create an ImageContent
|
85
|
+
image_content = resource_utils.create_image_content(data=content, mime_type=mime_type)
|
86
|
+
return message_class(content=image_content)
|
87
|
+
else:
|
88
|
+
# For other resources, create an EmbeddedResource
|
89
|
+
embedded_resource = resource_utils.create_embedded_resource(resource_path, content, mime_type, is_binary)
|
90
|
+
return message_class(content=embedded_resource)
|
91
|
+
|
92
|
+
|
93
|
+
def load_prompt(file: Path) -> List[PromptMessage]:
|
94
|
+
template: PromptTemplate = PromptTemplateLoader().load_from_file(file)
|
95
|
+
return create_messages_with_resources(template.content_sections, [file])
|
96
|
+
|
97
|
+
|
98
|
+
def load_prompt_multipart(file: Path) -> List[PromptMessageMultipart]:
|
99
|
+
return PromptMessageMultipart.to_multipart(load_prompt(file))
|
@@ -5,33 +5,27 @@ A server that loads prompts from text files with simple delimiters and serves th
|
|
5
5
|
Uses the prompt_template module for clean, testable handling of prompt templates.
|
6
6
|
"""
|
7
7
|
|
8
|
-
import asyncio
|
9
8
|
import argparse
|
9
|
+
import asyncio
|
10
10
|
import base64
|
11
11
|
import logging
|
12
12
|
import sys
|
13
13
|
from pathlib import Path
|
14
|
-
from typing import
|
15
|
-
from mcp.server.fastmcp.resources import FileResource
|
16
|
-
from pydantic import AnyUrl
|
17
|
-
|
18
|
-
from mcp_agent.mcp import mime_utils, resource_utils
|
14
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
19
15
|
|
20
16
|
from mcp.server.fastmcp import FastMCP
|
21
17
|
from mcp.server.fastmcp.prompts.base import (
|
22
|
-
UserMessage,
|
23
|
-
AssistantMessage,
|
24
18
|
Message,
|
25
19
|
)
|
26
|
-
from mcp.
|
27
|
-
|
28
|
-
)
|
20
|
+
from mcp.server.fastmcp.resources import FileResource
|
21
|
+
from pydantic import AnyUrl
|
29
22
|
|
23
|
+
from mcp_agent.mcp import mime_utils, resource_utils
|
24
|
+
from mcp_agent.mcp.prompts.prompt_load import create_messages_with_resources
|
30
25
|
from mcp_agent.mcp.prompts.prompt_template import (
|
31
|
-
PromptTemplateLoader,
|
32
26
|
PromptMetadata,
|
33
|
-
PromptContent,
|
34
27
|
PromptTemplate,
|
28
|
+
PromptTemplateLoader,
|
35
29
|
)
|
36
30
|
|
37
31
|
# Configure logging
|
@@ -61,97 +55,17 @@ config = None
|
|
61
55
|
exposed_resources: Dict[str, Path] = {}
|
62
56
|
prompt_registry: Dict[str, PromptMetadata] = {}
|
63
57
|
|
64
|
-
#
|
65
|
-
|
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"
|
58
|
+
# Default delimiter values
|
59
|
+
DEFAULT_USER_DELIMITER = "---USER"
|
60
|
+
DEFAULT_ASSISTANT_DELIMITER = "---ASSISTANT"
|
61
|
+
DEFAULT_RESOURCE_DELIMITER = "---RESOURCE"
|
146
62
|
|
147
63
|
|
148
64
|
# Define a single type for prompt handlers to avoid mypy issues
|
149
65
|
PromptHandler = Callable[..., Awaitable[List[Message]]]
|
150
66
|
|
151
67
|
|
152
|
-
def create_prompt_handler(
|
153
|
-
template: "PromptTemplate", template_vars: List[str], prompt_files: List[Path]
|
154
|
-
) -> PromptHandler:
|
68
|
+
def create_prompt_handler(template: "PromptTemplate", template_vars: List[str], prompt_files: List[Path]) -> PromptHandler:
|
155
69
|
"""Create a prompt handler function for the given template"""
|
156
70
|
if template_vars:
|
157
71
|
# With template variables
|
@@ -159,23 +73,18 @@ def create_prompt_handler(
|
|
159
73
|
|
160
74
|
async def prompt_handler(**kwargs: Any) -> List[Message]:
|
161
75
|
# 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
|
-
}
|
76
|
+
context = {var: kwargs.get(var) for var in template_vars if var in kwargs and kwargs[var] is not None}
|
167
77
|
|
168
|
-
# Apply substitutions to the template
|
169
78
|
content_sections = template.apply_substitutions(context)
|
170
79
|
|
171
80
|
# Convert to MCP Message objects, handling resources properly
|
172
81
|
return create_messages_with_resources(content_sections, prompt_files)
|
82
|
+
|
173
83
|
else:
|
174
84
|
# No template variables
|
175
85
|
docstring = "Get a prompt with no variable substitution"
|
176
86
|
|
177
87
|
async def prompt_handler(**kwargs: Any) -> List[Message]:
|
178
|
-
# Get the content sections
|
179
88
|
content_sections = template.content_sections
|
180
89
|
|
181
90
|
# Convert to MCP Message objects, handling resources properly
|
@@ -208,12 +117,6 @@ def create_resource_handler(resource_path: Path, mime_type: str) -> ResourceHand
|
|
208
117
|
return get_resource
|
209
118
|
|
210
119
|
|
211
|
-
# Default delimiter values
|
212
|
-
DEFAULT_USER_DELIMITER = "---USER"
|
213
|
-
DEFAULT_ASSISTANT_DELIMITER = "---ASSISTANT"
|
214
|
-
DEFAULT_RESOURCE_DELIMITER = "---RESOURCE"
|
215
|
-
|
216
|
-
|
217
120
|
def get_delimiter_config(file_path: Optional[Path] = None) -> Dict[str, Any]:
|
218
121
|
"""Get delimiter configuration, falling back to defaults if config is None"""
|
219
122
|
# Set defaults
|
@@ -234,7 +137,7 @@ def get_delimiter_config(file_path: Optional[Path] = None) -> Dict[str, Any]:
|
|
234
137
|
return config_values
|
235
138
|
|
236
139
|
|
237
|
-
def register_prompt(file_path: Path):
|
140
|
+
def register_prompt(file_path: Path) -> None:
|
238
141
|
"""Register a prompt file"""
|
239
142
|
try:
|
240
143
|
# Get delimiter configuration
|
@@ -268,9 +171,7 @@ def register_prompt(file_path: Path):
|
|
268
171
|
|
269
172
|
# Create and register prompt handler
|
270
173
|
template_vars = list(metadata.template_variables)
|
271
|
-
handler = create_prompt_handler(
|
272
|
-
template, template_vars, config_values["prompt_files"]
|
273
|
-
)
|
174
|
+
handler = create_prompt_handler(template, template_vars, config_values["prompt_files"])
|
274
175
|
mcp.prompt(name=metadata.name, description=metadata.description)(handler)
|
275
176
|
|
276
177
|
# Register any referenced resources in the prompt
|
@@ -295,9 +196,7 @@ def register_prompt(file_path: Path):
|
|
295
196
|
)
|
296
197
|
)
|
297
198
|
|
298
|
-
logger.info(
|
299
|
-
f"Registered resource: {resource_id} ({resource_file})"
|
300
|
-
)
|
199
|
+
logger.info(f"Registered resource: {resource_id} ({resource_file})")
|
301
200
|
except Exception as e:
|
302
201
|
logger.error(f"Error registering prompt {file_path}: {e}", exc_info=True)
|
303
202
|
|
@@ -305,9 +204,7 @@ def register_prompt(file_path: Path):
|
|
305
204
|
def parse_args():
|
306
205
|
"""Parse command line arguments"""
|
307
206
|
parser = argparse.ArgumentParser(description="FastMCP Prompt Server")
|
308
|
-
parser.add_argument(
|
309
|
-
"prompt_files", nargs="+", type=str, help="Prompt files to serve"
|
310
|
-
)
|
207
|
+
parser.add_argument("prompt_files", nargs="+", type=str, help="Prompt files to serve")
|
311
208
|
parser.add_argument(
|
312
209
|
"--user-delimiter",
|
313
210
|
type=str,
|
@@ -345,14 +242,12 @@ def parse_args():
|
|
345
242
|
default=8000,
|
346
243
|
help="Port to use for SSE transport (default: 8000)",
|
347
244
|
)
|
348
|
-
parser.add_argument(
|
349
|
-
"--test", type=str, help="Test a specific prompt without starting the server"
|
350
|
-
)
|
245
|
+
parser.add_argument("--test", type=str, help="Test a specific prompt without starting the server")
|
351
246
|
|
352
247
|
return parser.parse_args()
|
353
248
|
|
354
249
|
|
355
|
-
async def register_file_resource_handler():
|
250
|
+
async def register_file_resource_handler() -> None:
|
356
251
|
"""Register the general file resource handler"""
|
357
252
|
|
358
253
|
@mcp.resource("file://{path}")
|
@@ -478,9 +373,7 @@ async def async_main():
|
|
478
373
|
logger.info("Starting prompt server")
|
479
374
|
logger.info(f"Registered {len(prompt_registry)} prompts")
|
480
375
|
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
|
-
)
|
376
|
+
logger.info(f"Using delimiters: {config.user_delimiter}, {config.assistant_delimiter}, {config.resource_delimiter}")
|
484
377
|
|
485
378
|
# If a test prompt was specified, print it and exit
|
486
379
|
if args.test:
|
@@ -7,11 +7,15 @@ Provides clean, testable classes for managing template substitution.
|
|
7
7
|
|
8
8
|
import re
|
9
9
|
from pathlib import Path
|
10
|
-
from typing import Dict, List,
|
10
|
+
from typing import Any, Dict, List, Literal, Optional, Set
|
11
11
|
|
12
|
+
from mcp.types import (
|
13
|
+
EmbeddedResource,
|
14
|
+
TextContent,
|
15
|
+
TextResourceContents,
|
16
|
+
)
|
12
17
|
from pydantic import BaseModel, field_validator
|
13
18
|
|
14
|
-
from mcp.types import TextContent, EmbeddedResource, TextResourceContents
|
15
19
|
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
16
20
|
from mcp_agent.mcp.prompt_serialization import (
|
17
21
|
multipart_messages_to_delimited_format,
|
@@ -67,9 +71,7 @@ class PromptContent(BaseModel):
|
|
67
71
|
substituted = substituted.replace(make_placeholder(key), str(value))
|
68
72
|
substituted_resources.append(substituted)
|
69
73
|
|
70
|
-
return PromptContent(
|
71
|
-
text=result, role=self.role, resources=substituted_resources
|
72
|
-
)
|
74
|
+
return PromptContent(text=result, role=self.role, resources=substituted_resources)
|
73
75
|
|
74
76
|
|
75
77
|
class PromptTemplate:
|
@@ -82,7 +84,7 @@ class PromptTemplate:
|
|
82
84
|
template_text: str,
|
83
85
|
delimiter_map: Optional[Dict[str, str]] = None,
|
84
86
|
template_file_path: Optional[Path] = None,
|
85
|
-
):
|
87
|
+
) -> None:
|
86
88
|
"""
|
87
89
|
Initialize a prompt template.
|
88
90
|
|
@@ -127,9 +129,7 @@ class PromptTemplate:
|
|
127
129
|
# Convert to delimited format
|
128
130
|
delimited_content = multipart_messages_to_delimited_format(
|
129
131
|
messages,
|
130
|
-
user_delimiter=next(
|
131
|
-
(k for k, v in delimiter_map.items() if v == "user"), "---USER"
|
132
|
-
),
|
132
|
+
user_delimiter=next((k for k, v in delimiter_map.items() if v == "user"), "---USER"),
|
133
133
|
assistant_delimiter=next(
|
134
134
|
(k for k, v in delimiter_map.items() if v == "assistant"),
|
135
135
|
"---ASSISTANT",
|
@@ -167,9 +167,7 @@ class PromptTemplate:
|
|
167
167
|
result.append(section.apply_substitutions(context))
|
168
168
|
return result
|
169
169
|
|
170
|
-
def apply_substitutions_to_multipart(
|
171
|
-
self, context: Dict[str, Any]
|
172
|
-
) -> List[PromptMessageMultipart]:
|
170
|
+
def apply_substitutions_to_multipart(self, context: Dict[str, Any]) -> List[PromptMessageMultipart]:
|
173
171
|
"""
|
174
172
|
Apply variable substitutions to the template and return PromptMessageMultipart objects.
|
175
173
|
|
@@ -201,9 +199,7 @@ class PromptTemplate:
|
|
201
199
|
)
|
202
200
|
)
|
203
201
|
|
204
|
-
multiparts.append(
|
205
|
-
PromptMessageMultipart(role=section.role, content=content_items)
|
206
|
-
)
|
202
|
+
multiparts.append(PromptMessageMultipart(role=section.role, content=content_items))
|
207
203
|
|
208
204
|
return multiparts
|
209
205
|
|
@@ -241,9 +237,7 @@ class PromptTemplate:
|
|
241
237
|
)
|
242
238
|
)
|
243
239
|
|
244
|
-
multiparts.append(
|
245
|
-
PromptMessageMultipart(role=section.role, content=content_items)
|
246
|
-
)
|
240
|
+
multiparts.append(PromptMessageMultipart(role=section.role, content=content_items))
|
247
241
|
|
248
242
|
return multiparts
|
249
243
|
|
@@ -260,9 +254,7 @@ class PromptTemplate:
|
|
260
254
|
first_non_empty_line = next((line for line in lines if line.strip()), "")
|
261
255
|
delimiter_values = set(self.delimiter_map.keys())
|
262
256
|
|
263
|
-
is_simple_mode =
|
264
|
-
first_non_empty_line and first_non_empty_line not in delimiter_values
|
265
|
-
)
|
257
|
+
is_simple_mode = first_non_empty_line and first_non_empty_line not in delimiter_values
|
266
258
|
|
267
259
|
if is_simple_mode:
|
268
260
|
# Simple mode: treat the entire content as a single user message
|
@@ -330,7 +322,7 @@ class PromptTemplateLoader:
|
|
330
322
|
Loads and processes prompt templates from files.
|
331
323
|
"""
|
332
324
|
|
333
|
-
def __init__(self, delimiter_map: Optional[Dict[str, str]] = None):
|
325
|
+
def __init__(self, delimiter_map: Optional[Dict[str, str]] = None) -> None:
|
334
326
|
"""
|
335
327
|
Initialize the loader with optional custom delimiters.
|
336
328
|
|
@@ -358,9 +350,7 @@ class PromptTemplateLoader:
|
|
358
350
|
|
359
351
|
return PromptTemplate(content, self.delimiter_map, template_file_path=file_path)
|
360
352
|
|
361
|
-
def load_from_multipart(
|
362
|
-
self, messages: List[PromptMessageMultipart]
|
363
|
-
) -> PromptTemplate:
|
353
|
+
def load_from_multipart(self, messages: List[PromptMessageMultipart]) -> PromptTemplate:
|
364
354
|
"""
|
365
355
|
Create a PromptTemplate from a list of PromptMessageMultipart objects.
|
366
356
|
|
@@ -372,9 +362,7 @@ class PromptTemplateLoader:
|
|
372
362
|
"""
|
373
363
|
delimited_content = multipart_messages_to_delimited_format(
|
374
364
|
messages,
|
375
|
-
user_delimiter=next(
|
376
|
-
(k for k, v in self.delimiter_map.items() if v == "user"), "---USER"
|
377
|
-
),
|
365
|
+
user_delimiter=next((k for k, v in self.delimiter_map.items() if v == "user"), "---USER"),
|
378
366
|
assistant_delimiter=next(
|
379
367
|
(k for k, v in self.delimiter_map.items() if v == "assistant"),
|
380
368
|
"---ASSISTANT",
|
@@ -401,18 +389,12 @@ class PromptTemplateLoader:
|
|
401
389
|
first_non_empty_line = next((line for line in lines if line.strip()), "")
|
402
390
|
|
403
391
|
# Check if we're in simple mode
|
404
|
-
is_simple_mode =
|
405
|
-
first_non_empty_line and first_non_empty_line not in self.delimiter_map
|
406
|
-
)
|
392
|
+
is_simple_mode = first_non_empty_line and first_non_empty_line not in self.delimiter_map
|
407
393
|
|
408
394
|
if is_simple_mode:
|
409
395
|
# In simple mode, use first line as description if it seems like one
|
410
396
|
first_line = lines[0].strip() if lines else ""
|
411
|
-
if (
|
412
|
-
len(first_line) < 60
|
413
|
-
and "{{" not in first_line
|
414
|
-
and "}}" not in first_line
|
415
|
-
):
|
397
|
+
if len(first_line) < 60 and "{{" not in first_line and "}}" not in first_line:
|
416
398
|
description = first_line
|
417
399
|
else:
|
418
400
|
description = f"Simple prompt: {file_path.stem}"
|
@@ -434,9 +416,7 @@ class PromptTemplateLoader:
|
|
434
416
|
if first_role and first_content_index and first_content_index < len(lines):
|
435
417
|
# Get up to 3 non-empty lines after the delimiter for a preview
|
436
418
|
preview_lines = []
|
437
|
-
for j in range(
|
438
|
-
first_content_index, min(first_content_index + 10, len(lines))
|
439
|
-
):
|
419
|
+
for j in range(first_content_index, min(first_content_index + 10, len(lines))):
|
440
420
|
stripped = lines[j].strip()
|
441
421
|
if stripped and stripped not in self.delimiter_map:
|
442
422
|
preview_lines.append(stripped)
|
@@ -452,9 +432,7 @@ class PromptTemplateLoader:
|
|
452
432
|
|
453
433
|
# Extract resource paths from all sections that come after RESOURCE delimiters
|
454
434
|
resource_paths = []
|
455
|
-
resource_delimiter = next(
|
456
|
-
(k for k, v in self.delimiter_map.items() if v == "resource"), "---RESOURCE"
|
457
|
-
)
|
435
|
+
resource_delimiter = next((k for k, v in self.delimiter_map.items() if v == "resource"), "---RESOURCE")
|
458
436
|
for i, line in enumerate(lines):
|
459
437
|
if line.strip() == resource_delimiter:
|
460
438
|
if i + 1 < len(lines) and lines[i + 1].strip():
|