fast-agent-mcp 0.1.8__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/METADATA +26 -4
  2. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/RECORD +43 -22
  3. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/entry_points.txt +1 -0
  4. mcp_agent/agents/agent.py +5 -11
  5. mcp_agent/core/agent_app.py +89 -13
  6. mcp_agent/core/fastagent.py +13 -3
  7. mcp_agent/core/mcp_content.py +222 -0
  8. mcp_agent/core/prompt.py +132 -0
  9. mcp_agent/core/proxies.py +41 -36
  10. mcp_agent/logging/transport.py +30 -3
  11. mcp_agent/mcp/mcp_aggregator.py +11 -10
  12. mcp_agent/mcp/mime_utils.py +69 -0
  13. mcp_agent/mcp/prompt_message_multipart.py +64 -0
  14. mcp_agent/mcp/prompt_serialization.py +447 -0
  15. mcp_agent/mcp/prompts/__init__.py +0 -0
  16. mcp_agent/mcp/prompts/__main__.py +10 -0
  17. mcp_agent/mcp/prompts/prompt_server.py +508 -0
  18. mcp_agent/mcp/prompts/prompt_template.py +469 -0
  19. mcp_agent/mcp/resource_utils.py +203 -0
  20. mcp_agent/resources/examples/internal/agent.py +1 -1
  21. mcp_agent/resources/examples/internal/fastagent.config.yaml +2 -2
  22. mcp_agent/resources/examples/internal/sizer.py +0 -5
  23. mcp_agent/resources/examples/prompting/__init__.py +3 -0
  24. mcp_agent/resources/examples/prompting/agent.py +23 -0
  25. mcp_agent/resources/examples/prompting/fastagent.config.yaml +44 -0
  26. mcp_agent/resources/examples/prompting/image_server.py +56 -0
  27. mcp_agent/workflows/llm/anthropic_utils.py +101 -0
  28. mcp_agent/workflows/llm/augmented_llm.py +139 -66
  29. mcp_agent/workflows/llm/augmented_llm_anthropic.py +127 -251
  30. mcp_agent/workflows/llm/augmented_llm_openai.py +149 -305
  31. mcp_agent/workflows/llm/augmented_llm_passthrough.py +43 -0
  32. mcp_agent/workflows/llm/augmented_llm_playback.py +109 -0
  33. mcp_agent/workflows/llm/model_factory.py +20 -3
  34. mcp_agent/workflows/llm/openai_utils.py +65 -0
  35. mcp_agent/workflows/llm/providers/__init__.py +8 -0
  36. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +348 -0
  37. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +426 -0
  38. mcp_agent/workflows/llm/providers/openai_multipart.py +197 -0
  39. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +258 -0
  40. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +229 -0
  41. mcp_agent/workflows/llm/sampling_format_converter.py +39 -0
  42. mcp_agent/core/server_validation.py +0 -44
  43. mcp_agent/core/simulator_registry.py +0 -22
  44. mcp_agent/workflows/llm/enhanced_passthrough.py +0 -70
  45. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/WHEEL +0 -0
  46. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,447 @@
1
+ """
2
+ Utilities for converting between different prompt message formats.
3
+
4
+ This module provides utilities for converting between different serialization formats
5
+ and PromptMessageMultipart objects. It includes functionality for:
6
+
7
+ 1. JSON Serialization:
8
+ - Converting PromptMessageMultipart objects to pure JSON format
9
+ - Parsing JSON into PromptMessageMultipart objects
10
+ - This is ideal for programmatic use and ensures all data fidelity
11
+
12
+ 2. Delimited Text Format:
13
+ - Converting PromptMessageMultipart objects to delimited text (---USER, ---ASSISTANT)
14
+ - Converting resources to JSON after resource delimiter (---RESOURCE)
15
+ - Parsing delimited text back into PromptMessageMultipart objects
16
+ - This maintains human readability for text content while preserving structure for resources
17
+ """
18
+
19
+ import json
20
+ from typing import List
21
+
22
+ from mcp.types import TextContent, EmbeddedResource, ImageContent, TextResourceContents
23
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
24
+
25
+
26
+ # -------------------------------------------------------------------------
27
+ # JSON Serialization Functions
28
+ # -------------------------------------------------------------------------
29
+
30
+
31
+ def multipart_messages_to_json(messages: List[PromptMessageMultipart]) -> str:
32
+ """
33
+ Convert PromptMessageMultipart objects to a pure JSON string.
34
+
35
+ This approach preserves all data and structure exactly as is, but is less
36
+ human-readable for text content.
37
+
38
+ Args:
39
+ messages: List of PromptMessageMultipart objects
40
+
41
+ Returns:
42
+ JSON string representation of the messages
43
+ """
44
+ # Convert to dictionaries using model_dump with proper JSON mode
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
+ ]
50
+
51
+ # Convert to JSON string
52
+ return json.dumps(message_dicts, indent=2)
53
+
54
+
55
+ def json_to_multipart_messages(json_str: str) -> List[PromptMessageMultipart]:
56
+ """
57
+ Parse a JSON string into PromptMessageMultipart objects.
58
+
59
+ Args:
60
+ json_str: JSON string representation of messages
61
+
62
+ Returns:
63
+ List of PromptMessageMultipart objects
64
+ """
65
+ # Parse JSON to list of dictionaries
66
+ message_dicts = json.loads(json_str)
67
+
68
+ # Convert dictionaries to PromptMessageMultipart objects
69
+ messages = []
70
+ for message_dict in message_dicts:
71
+ # Parse message using Pydantic's model_validate method (Pydantic v2)
72
+ # For Pydantic v1, this would use parse_obj instead
73
+ message = PromptMessageMultipart.model_validate(message_dict)
74
+ messages.append(message)
75
+
76
+ return messages
77
+
78
+
79
+ def save_messages_to_json_file(
80
+ messages: List[PromptMessageMultipart], file_path: str
81
+ ) -> None:
82
+ """
83
+ Save PromptMessageMultipart objects to a JSON file.
84
+
85
+ Args:
86
+ messages: List of PromptMessageMultipart objects
87
+ file_path: Path to save the JSON file
88
+ """
89
+ json_str = multipart_messages_to_json(messages)
90
+
91
+ with open(file_path, "w", encoding="utf-8") as f:
92
+ f.write(json_str)
93
+
94
+
95
+ def load_messages_from_json_file(file_path: str) -> List[PromptMessageMultipart]:
96
+ """
97
+ Load PromptMessageMultipart objects from a JSON file.
98
+
99
+ Args:
100
+ file_path: Path to the JSON file
101
+
102
+ Returns:
103
+ List of PromptMessageMultipart objects
104
+ """
105
+ with open(file_path, "r", encoding="utf-8") as f:
106
+ json_str = f.read()
107
+
108
+ return json_to_multipart_messages(json_str)
109
+
110
+
111
+ # -------------------------------------------------------------------------
112
+ # Delimited Text Format Functions
113
+ # -------------------------------------------------------------------------
114
+
115
+
116
+ def multipart_messages_to_delimited_format(
117
+ messages: List[PromptMessageMultipart],
118
+ user_delimiter: str = "---USER",
119
+ assistant_delimiter: str = "---ASSISTANT",
120
+ resource_delimiter: str = "---RESOURCE",
121
+ combine_text: bool = True, # Set to False to maintain backward compatibility
122
+ ) -> List[str]:
123
+ """
124
+ Convert PromptMessageMultipart objects to a hybrid delimited format:
125
+ - Plain text for user/assistant text content with delimiters
126
+ - JSON for resources after resource delimiter
127
+
128
+ This approach maintains human readability for text content while
129
+ preserving structure for resources.
130
+
131
+ Args:
132
+ messages: List of PromptMessageMultipart objects
133
+ user_delimiter: Delimiter for user messages
134
+ assistant_delimiter: Delimiter for assistant messages
135
+ resource_delimiter: Delimiter for resources
136
+ combine_text: Whether to combine multiple text contents into one (default: True)
137
+
138
+ Returns:
139
+ List of strings representing the delimited content
140
+ """
141
+ delimited_content = []
142
+
143
+ for message in messages:
144
+ # Add role delimiter
145
+ if message.role == "user":
146
+ delimited_content.append(user_delimiter)
147
+ else:
148
+ delimited_content.append(assistant_delimiter)
149
+
150
+ # Process content parts based on combine_text preference
151
+ if combine_text:
152
+ # Collect text content parts
153
+ text_contents = []
154
+
155
+ # First, add all text content
156
+ for content in message.content:
157
+ if content.type == "text":
158
+ # Collect text content to combine
159
+ text_contents.append(content.text)
160
+
161
+ # Add combined text content if any exists
162
+ if text_contents:
163
+ delimited_content.append("\n\n".join(text_contents))
164
+
165
+ # Then add resources and images
166
+ for content in message.content:
167
+ if content.type != "text":
168
+ # Resource or image - add delimiter and JSON
169
+ delimited_content.append(resource_delimiter)
170
+
171
+ # Convert to dictionary using proper JSON mode
172
+ content_dict = content.model_dump(
173
+ by_alias=True, mode="json", exclude_none=True
174
+ )
175
+
176
+ # Add to delimited content as JSON
177
+ delimited_content.append(json.dumps(content_dict, indent=2))
178
+ else:
179
+ # Don't combine text contents - preserve each content part in sequence
180
+ for content in message.content:
181
+ if content.type == "text":
182
+ # Add each text content separately
183
+ delimited_content.append(content.text)
184
+ else:
185
+ # Resource or image - add delimiter and JSON
186
+ delimited_content.append(resource_delimiter)
187
+
188
+ # Convert to dictionary using proper JSON mode
189
+ content_dict = content.model_dump(
190
+ by_alias=True, mode="json", exclude_none=True
191
+ )
192
+
193
+ # Add to delimited content as JSON
194
+ delimited_content.append(json.dumps(content_dict, indent=2))
195
+
196
+ return delimited_content
197
+
198
+
199
+ def delimited_format_to_multipart_messages(
200
+ content: str,
201
+ user_delimiter: str = "---USER",
202
+ assistant_delimiter: str = "---ASSISTANT",
203
+ resource_delimiter: str = "---RESOURCE",
204
+ ) -> List[PromptMessageMultipart]:
205
+ """
206
+ Parse hybrid delimited format into PromptMessageMultipart objects:
207
+ - Plain text for user/assistant text content with delimiters
208
+ - JSON for resources after resource delimiter
209
+
210
+ Args:
211
+ content: String containing the delimited content
212
+ user_delimiter: Delimiter for user messages
213
+ assistant_delimiter: Delimiter for assistant messages
214
+ resource_delimiter: Delimiter for resources
215
+
216
+ Returns:
217
+ List of PromptMessageMultipart objects
218
+ """
219
+ lines = content.split("\n")
220
+ messages = []
221
+
222
+ current_role = None
223
+ text_contents = [] # List of TextContent
224
+ resource_contents = [] # List of EmbeddedResource or ImageContent
225
+ collecting_json = False
226
+ json_lines = []
227
+ collecting_text = False
228
+ text_lines = []
229
+
230
+ # Check if this is a legacy format (pre-JSON serialization)
231
+ legacy_format = resource_delimiter in content and '"type":' not in content
232
+
233
+ # Add a condition to ensure we process the first user message properly
234
+ # This is the key fix: We need to process the first line correctly
235
+ if lines and lines[0].strip() == user_delimiter:
236
+ current_role = "user"
237
+ collecting_text = True
238
+
239
+ # Process each line
240
+ for line in (
241
+ lines[1:] if lines else []
242
+ ): # Skip the first line if already processed above
243
+ line_stripped = line.strip()
244
+
245
+ # Handle role delimiters
246
+ if line_stripped == user_delimiter or line_stripped == assistant_delimiter:
247
+ # Save previous message if it exists
248
+ if current_role is not None and (
249
+ text_contents or resource_contents or text_lines
250
+ ):
251
+ # If we were collecting text, add it to the text contents
252
+ if collecting_text and text_lines:
253
+ text_contents.append(
254
+ TextContent(type="text", text="\n".join(text_lines))
255
+ )
256
+ text_lines = []
257
+
258
+ # Create content list with text parts first, then resource parts
259
+ combined_content = []
260
+
261
+ # Filter out any empty text content items
262
+ filtered_text_contents = [
263
+ tc for tc in text_contents if tc.text.strip() != ""
264
+ ]
265
+
266
+ combined_content.extend(filtered_text_contents)
267
+ combined_content.extend(resource_contents)
268
+
269
+ messages.append(
270
+ PromptMessageMultipart(
271
+ role=current_role,
272
+ content=combined_content,
273
+ )
274
+ )
275
+
276
+ # Start a new message
277
+ current_role = "user" if line_stripped == user_delimiter else "assistant"
278
+ text_contents = []
279
+ resource_contents = []
280
+ collecting_json = False
281
+ json_lines = []
282
+ collecting_text = False
283
+ text_lines = []
284
+
285
+ # Handle resource delimiter
286
+ elif line_stripped == resource_delimiter:
287
+ # If we were collecting text, add it to text contents
288
+ if collecting_text and text_lines:
289
+ text_contents.append(
290
+ TextContent(type="text", text="\n".join(text_lines))
291
+ )
292
+ text_lines = []
293
+
294
+ # Switch to collecting JSON or legacy format
295
+ collecting_text = False
296
+ collecting_json = True
297
+ json_lines = []
298
+
299
+ # Process content based on context
300
+ elif current_role is not None:
301
+ if collecting_json:
302
+ # Collect JSON data
303
+ json_lines.append(line)
304
+
305
+ # 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
+ ):
311
+ # This is probably a legacy resource reference like a filename
312
+ resource_uri = line_stripped
313
+ if not resource_uri.startswith("resource://"):
314
+ resource_uri = f"resource://fast-agent/{resource_uri}"
315
+
316
+ # Create a simple resource with just the URI
317
+ resource = EmbeddedResource(
318
+ type="resource",
319
+ resource=TextResourceContents(
320
+ uri=resource_uri,
321
+ mimeType="text/plain",
322
+ ),
323
+ )
324
+ resource_contents.append(resource)
325
+ collecting_json = False
326
+ json_lines = []
327
+ continue
328
+
329
+ # Try to parse the JSON to see if we have a complete object
330
+ try:
331
+ json_text = "\n".join(json_lines)
332
+ json_data = json.loads(json_text)
333
+
334
+ # Successfully parsed JSON
335
+ content_type = json_data.get("type")
336
+
337
+ if content_type == "resource":
338
+ # Create resource object using model_validate
339
+ resource = EmbeddedResource.model_validate(json_data)
340
+ resource_contents.append(resource) # Add to resource contents
341
+ elif content_type == "image":
342
+ # Create image object using model_validate
343
+ image = ImageContent.model_validate(json_data)
344
+ resource_contents.append(image) # Add to resource contents
345
+
346
+ # Reset JSON collection
347
+ collecting_json = False
348
+ json_lines = []
349
+
350
+ except json.JSONDecodeError:
351
+ # Not a complete JSON object yet, keep collecting
352
+ pass
353
+ else:
354
+ # Regular text content
355
+ if not collecting_text:
356
+ collecting_text = True
357
+ text_lines = []
358
+
359
+ text_lines.append(line)
360
+
361
+ # Handle any remaining content
362
+ if current_role is not None:
363
+ # Add any remaining text
364
+ if collecting_text and text_lines:
365
+ text_contents.append(TextContent(type="text", text="\n".join(text_lines)))
366
+
367
+ # Add the final message if it has content
368
+ if text_contents or resource_contents:
369
+ # Create content list with text parts first, then resource parts
370
+ combined_content = []
371
+
372
+ # Filter out any empty text content items
373
+ filtered_text_contents = [
374
+ tc for tc in text_contents if tc.text.strip() != ""
375
+ ]
376
+
377
+ combined_content.extend(filtered_text_contents)
378
+ combined_content.extend(resource_contents)
379
+
380
+ messages.append(
381
+ PromptMessageMultipart(
382
+ role=current_role,
383
+ content=combined_content,
384
+ )
385
+ )
386
+
387
+ return messages
388
+
389
+
390
+ def save_messages_to_delimited_file(
391
+ messages: List[PromptMessageMultipart],
392
+ file_path: str,
393
+ user_delimiter: str = "---USER",
394
+ assistant_delimiter: str = "---ASSISTANT",
395
+ resource_delimiter: str = "---RESOURCE",
396
+ combine_text: bool = True,
397
+ ) -> None:
398
+ """
399
+ Save PromptMessageMultipart objects to a file in hybrid delimited format.
400
+
401
+ Args:
402
+ messages: List of PromptMessageMultipart objects
403
+ file_path: Path to save the file
404
+ user_delimiter: Delimiter for user messages
405
+ assistant_delimiter: Delimiter for assistant messages
406
+ resource_delimiter: Delimiter for resources
407
+ combine_text: Whether to combine multiple text contents into one (default: True)
408
+ """
409
+ delimited_content = multipart_messages_to_delimited_format(
410
+ messages,
411
+ user_delimiter,
412
+ assistant_delimiter,
413
+ resource_delimiter,
414
+ combine_text=combine_text,
415
+ )
416
+
417
+ with open(file_path, "w", encoding="utf-8") as f:
418
+ f.write("\n".join(delimited_content))
419
+
420
+
421
+ def load_messages_from_delimited_file(
422
+ file_path: str,
423
+ user_delimiter: str = "---USER",
424
+ assistant_delimiter: str = "---ASSISTANT",
425
+ resource_delimiter: str = "---RESOURCE",
426
+ ) -> List[PromptMessageMultipart]:
427
+ """
428
+ Load PromptMessageMultipart objects from a file in hybrid delimited format.
429
+
430
+ Args:
431
+ file_path: Path to the file
432
+ user_delimiter: Delimiter for user messages
433
+ assistant_delimiter: Delimiter for assistant messages
434
+ resource_delimiter: Delimiter for resources
435
+
436
+ Returns:
437
+ List of PromptMessageMultipart objects
438
+ """
439
+ with open(file_path, "r", encoding="utf-8") as f:
440
+ content = f.read()
441
+
442
+ return delimited_format_to_multipart_messages(
443
+ content,
444
+ user_delimiter,
445
+ assistant_delimiter,
446
+ resource_delimiter,
447
+ )
File without changes
@@ -0,0 +1,10 @@
1
+ from mcp_agent.mcp.prompts.prompt_server import main
2
+
3
+ # This must be here for the console entry points defined in pyproject.toml
4
+ # DO NOT REMOVE!
5
+
6
+ # For the entry point in pyproject.toml
7
+ app = main
8
+
9
+ if __name__ == "__main__":
10
+ main()