fast-agent-mcp 0.1.7__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 (56) hide show
  1. {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/METADATA +37 -9
  2. {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/RECORD +53 -31
  3. {fast_agent_mcp-0.1.7.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 +125 -44
  6. mcp_agent/core/decorators.py +3 -2
  7. mcp_agent/core/enhanced_prompt.py +106 -20
  8. mcp_agent/core/factory.py +28 -66
  9. mcp_agent/core/fastagent.py +13 -3
  10. mcp_agent/core/mcp_content.py +222 -0
  11. mcp_agent/core/prompt.py +132 -0
  12. mcp_agent/core/proxies.py +41 -36
  13. mcp_agent/human_input/handler.py +4 -1
  14. mcp_agent/logging/transport.py +30 -3
  15. mcp_agent/mcp/mcp_aggregator.py +27 -22
  16. mcp_agent/mcp/mime_utils.py +69 -0
  17. mcp_agent/mcp/prompt_message_multipart.py +64 -0
  18. mcp_agent/mcp/prompt_serialization.py +447 -0
  19. mcp_agent/mcp/prompts/__init__.py +0 -0
  20. mcp_agent/mcp/prompts/__main__.py +10 -0
  21. mcp_agent/mcp/prompts/prompt_server.py +508 -0
  22. mcp_agent/mcp/prompts/prompt_template.py +469 -0
  23. mcp_agent/mcp/resource_utils.py +203 -0
  24. mcp_agent/resources/examples/internal/agent.py +1 -1
  25. mcp_agent/resources/examples/internal/fastagent.config.yaml +2 -2
  26. mcp_agent/resources/examples/internal/sizer.py +0 -5
  27. mcp_agent/resources/examples/prompting/__init__.py +3 -0
  28. mcp_agent/resources/examples/prompting/agent.py +23 -0
  29. mcp_agent/resources/examples/prompting/fastagent.config.yaml +44 -0
  30. mcp_agent/resources/examples/prompting/image_server.py +56 -0
  31. mcp_agent/resources/examples/researcher/researcher-eval.py +1 -1
  32. mcp_agent/resources/examples/workflows/orchestrator.py +5 -4
  33. mcp_agent/resources/examples/workflows/router.py +0 -2
  34. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +57 -87
  35. mcp_agent/workflows/llm/anthropic_utils.py +101 -0
  36. mcp_agent/workflows/llm/augmented_llm.py +155 -141
  37. mcp_agent/workflows/llm/augmented_llm_anthropic.py +135 -281
  38. mcp_agent/workflows/llm/augmented_llm_openai.py +175 -337
  39. mcp_agent/workflows/llm/augmented_llm_passthrough.py +104 -0
  40. mcp_agent/workflows/llm/augmented_llm_playback.py +109 -0
  41. mcp_agent/workflows/llm/model_factory.py +25 -6
  42. mcp_agent/workflows/llm/openai_utils.py +65 -0
  43. mcp_agent/workflows/llm/providers/__init__.py +8 -0
  44. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +348 -0
  45. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +426 -0
  46. mcp_agent/workflows/llm/providers/openai_multipart.py +197 -0
  47. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +258 -0
  48. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +229 -0
  49. mcp_agent/workflows/llm/sampling_format_converter.py +39 -0
  50. mcp_agent/workflows/orchestrator/orchestrator.py +62 -153
  51. mcp_agent/workflows/router/router_llm.py +18 -24
  52. mcp_agent/core/server_validation.py +0 -44
  53. mcp_agent/core/simulator_registry.py +0 -22
  54. mcp_agent/workflows/llm/enhanced_passthrough.py +0 -70
  55. {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/WHEEL +0 -0
  56. {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,426 @@
1
+ from typing import List, Union, Optional, Dict, Any, Tuple
2
+
3
+ from mcp.types import (
4
+ TextContent,
5
+ ImageContent,
6
+ EmbeddedResource,
7
+ CallToolResult,
8
+ )
9
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
10
+ from mcp_agent.mcp.mime_utils import (
11
+ guess_mime_type,
12
+ is_text_mime_type,
13
+ is_image_mime_type,
14
+ )
15
+ from mcp_agent.mcp.resource_utils import extract_title_from_uri
16
+
17
+ from mcp_agent.logging.logger import get_logger
18
+
19
+ _logger = get_logger("multipart_converter_openai")
20
+
21
+ # Define the types for OpenAI API
22
+ OpenAIContentBlock = Dict[str, Any]
23
+ OpenAIMessage = Dict[str, Any]
24
+
25
+
26
+ class OpenAIConverter:
27
+ """Converts MCP message types to OpenAI API format."""
28
+
29
+ @staticmethod
30
+ def convert_to_openai(
31
+ multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False
32
+ ) -> OpenAIMessage:
33
+ """
34
+ Convert a PromptMessageMultipart message to OpenAI API format.
35
+
36
+ Args:
37
+ multipart_msg: The PromptMessageMultipart message to convert
38
+ concatenate_text_blocks: If True, adjacent text blocks will be combined
39
+
40
+ Returns:
41
+ An OpenAI API message object
42
+ """
43
+ role = multipart_msg.role
44
+
45
+ # Handle empty content
46
+ if not multipart_msg.content:
47
+ return {"role": role, "content": ""}
48
+
49
+ # Assistant messages in OpenAI only support string content, not array of content blocks
50
+ if role == "assistant":
51
+ # Extract text from all text content blocks
52
+ content_text = ""
53
+ for item in multipart_msg.content:
54
+ if isinstance(item, TextContent):
55
+ content_text += item.text
56
+ # Other types are ignored for assistant messages in OpenAI
57
+
58
+ return {"role": role, "content": content_text}
59
+
60
+ # For user messages, convert each content block
61
+ content_blocks = []
62
+
63
+ for item in multipart_msg.content:
64
+ try:
65
+ if isinstance(item, TextContent):
66
+ content_blocks.append(OpenAIConverter._convert_text_content(item))
67
+
68
+ elif isinstance(item, ImageContent):
69
+ content_blocks.append(OpenAIConverter._convert_image_content(item))
70
+
71
+ elif isinstance(item, EmbeddedResource):
72
+ block = OpenAIConverter._convert_embedded_resource(item)
73
+ if block:
74
+ content_blocks.append(block)
75
+
76
+ # Handle input_audio if implemented
77
+ elif hasattr(item, "type") and getattr(item, "type") == "input_audio":
78
+ # This assumes an InputAudioContent class structure with input_audio attribute
79
+ if hasattr(item, "input_audio"):
80
+ content_blocks.append(
81
+ {
82
+ "type": "input_audio",
83
+ "input_audio": {
84
+ "data": item.input_audio.get("data", ""),
85
+ "format": item.input_audio.get("format", "wav"),
86
+ },
87
+ }
88
+ )
89
+ else:
90
+ _logger.warning(
91
+ "InputAudio content missing input_audio attribute"
92
+ )
93
+ fallback_text = "[Audio content missing data]"
94
+ content_blocks.append({"type": "text", "text": fallback_text})
95
+
96
+ else:
97
+ _logger.warning(f"Unsupported content type: {type(item)}")
98
+ # Create a text block with information about the skipped content
99
+ fallback_text = f"[Unsupported content type: {type(item).__name__}]"
100
+ content_blocks.append({"type": "text", "text": fallback_text})
101
+
102
+ except Exception as e:
103
+ _logger.warning(f"Error converting content item: {e}")
104
+ # Create a text block with information about the conversion error
105
+ fallback_text = f"[Content conversion error: {str(e)}]"
106
+ content_blocks.append({"type": "text", "text": fallback_text})
107
+
108
+ # Special case: empty content list or only empty text blocks
109
+ if not content_blocks:
110
+ return {"role": role, "content": ""}
111
+
112
+ # If we only have one text content and it's empty, return an empty string for content
113
+ if (
114
+ len(content_blocks) == 1
115
+ and content_blocks[0]["type"] == "text"
116
+ and not content_blocks[0]["text"]
117
+ ):
118
+ return {"role": role, "content": ""}
119
+
120
+ # If concatenate_text_blocks is True, combine adjacent text blocks
121
+ if concatenate_text_blocks:
122
+ combined_blocks = []
123
+ current_text = ""
124
+
125
+ for block in content_blocks:
126
+ if block["type"] == "text":
127
+ # Add to current text accumulator
128
+ if current_text:
129
+ current_text += " " + block["text"]
130
+ else:
131
+ current_text = block["text"]
132
+ else:
133
+ # Non-text block found, flush accumulated text if any
134
+ if current_text:
135
+ combined_blocks.append({"type": "text", "text": current_text})
136
+ current_text = ""
137
+ # Add the non-text block
138
+ combined_blocks.append(block)
139
+
140
+ # Don't forget any remaining text
141
+ if current_text:
142
+ combined_blocks.append({"type": "text", "text": current_text})
143
+
144
+ content_blocks = combined_blocks
145
+
146
+ return {"role": role, "content": content_blocks}
147
+
148
+ @staticmethod
149
+ def _convert_text_content(content: TextContent) -> OpenAIContentBlock:
150
+ """Convert TextContent to OpenAI text content block."""
151
+ return {"type": "text", "text": content.text}
152
+
153
+ @staticmethod
154
+ def _convert_image_content(content: ImageContent) -> OpenAIContentBlock:
155
+ """Convert ImageContent to OpenAI image_url content block."""
156
+ # OpenAI requires image URLs or data URIs for images
157
+ image_url = {"url": f"data:{content.mimeType};base64,{content.data}"}
158
+
159
+ # Check if the image has annotations for detail level
160
+ # This would depend on your ImageContent implementation
161
+ # If annotations are available, use them for the detail parameter
162
+ if hasattr(content, "annotations") and content.annotations:
163
+ if hasattr(content.annotations, "detail"):
164
+ detail = content.annotations.detail
165
+ if detail in ("auto", "low", "high"):
166
+ image_url["detail"] = detail
167
+
168
+ return {"type": "image_url", "image_url": image_url}
169
+
170
+ @staticmethod
171
+ def _convert_embedded_resource(
172
+ resource: EmbeddedResource,
173
+ ) -> Optional[OpenAIContentBlock]:
174
+ """Convert EmbeddedResource to appropriate OpenAI content block."""
175
+ resource_content = resource.resource
176
+ uri = resource_content.uri
177
+
178
+ # Use mime_utils to guess MIME type if not provided
179
+ if resource_content.mimeType is None and uri:
180
+ mime_type = guess_mime_type(str(uri))
181
+ _logger.info(f"MIME type not provided, guessed {mime_type} for {uri}")
182
+ else:
183
+ mime_type = resource_content.mimeType or "application/octet-stream"
184
+
185
+ is_url: bool = str(uri).startswith(("http://", "https://"))
186
+ title = extract_title_from_uri(uri) if uri else "resource"
187
+
188
+ # Handle image resources
189
+ if is_image_mime_type(mime_type) and mime_type != "image/svg+xml":
190
+ image_url = {}
191
+
192
+ if is_url:
193
+ image_url["url"] = str(uri)
194
+ elif hasattr(resource_content, "blob"):
195
+ image_url["url"] = f"data:{mime_type};base64,{resource_content.blob}"
196
+ else:
197
+ _logger.warning(f"Image resource missing both URL and blob data: {uri}")
198
+ return {"type": "text", "text": f"[Image missing data: {title}]"}
199
+
200
+ # Check for detail level in annotations if available
201
+ if hasattr(resource, "annotations") and resource.annotations:
202
+ if hasattr(resource.annotations, "detail"):
203
+ detail = resource.annotations.detail
204
+ if detail in ("auto", "low", "high"):
205
+ image_url["detail"] = detail
206
+
207
+ return {"type": "image_url", "image_url": image_url}
208
+
209
+ # Handle PDF resources - OpenAI has specific file format for PDFs
210
+ elif mime_type == "application/pdf":
211
+ if is_url:
212
+ # OpenAI doesn't directly support PDF URLs, only file_id or base64
213
+ _logger.warning(f"PDF URL not directly supported in OpenAI API: {uri}")
214
+ fallback_text = f"[PDF URL: {uri}]\nOpenAI requires PDF files to be uploaded or provided as base64 data."
215
+ return {"type": "text", "text": fallback_text}
216
+ elif hasattr(resource_content, "blob"):
217
+ return {
218
+ "type": "file",
219
+ "file": {
220
+ "filename": title or "document.pdf",
221
+ "file_data": f"data:application/pdf;base64,{resource_content.blob}",
222
+ },
223
+ }
224
+
225
+ # Handle SVG as text with fastagent:file tags
226
+ elif mime_type == "image/svg+xml":
227
+ if hasattr(resource_content, "text"):
228
+ file_text = (
229
+ f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
230
+ f"{resource_content.text}\n"
231
+ f"</fastagent:file>"
232
+ )
233
+ return {"type": "text", "text": file_text}
234
+
235
+ # Handle text resources with fastagent:file tags
236
+ elif is_text_mime_type(mime_type):
237
+ if hasattr(resource_content, "text"):
238
+ # Wrap in fastagent:file tags for text resources
239
+ file_text = (
240
+ f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
241
+ f"{resource_content.text}\n"
242
+ f"</fastagent:file>"
243
+ )
244
+ return {"type": "text", "text": file_text}
245
+
246
+ # Handle other binary formats that OpenAI supports with file type
247
+ # Currently, OpenAI supports PDFs for comprehensive viewing, but we can try
248
+ # to use the file type for other binary formats as well for future compatibility
249
+ elif hasattr(resource_content, "blob"):
250
+ # For now, we'll use file type for PDFs only, and use fallback for others
251
+ if mime_type == "application/pdf":
252
+ return {
253
+ "type": "file",
254
+ "file": {"file_name": title, "file_data": resource_content.blob},
255
+ }
256
+ else:
257
+ # For other binary formats, create a text message mentioning the resource
258
+ return {
259
+ "type": "text",
260
+ "text": f"[Binary resource: {title} ({mime_type})]",
261
+ }
262
+
263
+ # Default fallback - convert to text if possible
264
+ if hasattr(resource_content, "text"):
265
+ # For anything with text content that isn't handled specially above,
266
+ # use the raw text without special formatting
267
+ return {"type": "text", "text": resource_content.text}
268
+
269
+ _logger.warning(f"Unable to convert resource with MIME type: {mime_type}")
270
+ return {
271
+ "type": "text",
272
+ "text": f"[Unsupported resource: {title} ({mime_type})]",
273
+ }
274
+
275
+ @staticmethod
276
+ def convert_tool_result_to_openai(
277
+ tool_result: CallToolResult,
278
+ tool_call_id: str,
279
+ concatenate_text_blocks: bool = False,
280
+ ) -> Union[OpenAIMessage, Tuple[OpenAIMessage, List[OpenAIMessage]]]:
281
+ """
282
+ Convert a CallToolResult to an OpenAI tool message.
283
+ If the result contains non-text elements, those are converted to separate messages
284
+ since OpenAI tool messages can only contain text.
285
+
286
+ Args:
287
+ tool_result: The tool result from a tool call
288
+ tool_call_id: The ID of the associated tool use
289
+ concatenate_text_blocks: If True, adjacent text blocks will be combined
290
+
291
+ Returns:
292
+ Either a single OpenAI message for the tool response (if text only),
293
+ or a tuple containing the tool message and a list of additional messages for non-text content
294
+ """
295
+ # Handle empty content case
296
+ if not tool_result.content:
297
+ return {
298
+ "role": "tool",
299
+ "tool_call_id": tool_call_id,
300
+ "content": "[No content in tool result]",
301
+ }
302
+
303
+ # First, separate text and non-text content
304
+ text_content = []
305
+ non_text_content = []
306
+
307
+ for item in tool_result.content:
308
+ if isinstance(item, TextContent):
309
+ text_content.append(item)
310
+ else:
311
+ non_text_content.append(item)
312
+
313
+ # If we only have text content, process as before
314
+ if not non_text_content:
315
+ # Create a temporary PromptMessageMultipart to reuse the conversion logic
316
+ temp_multipart = PromptMessageMultipart(role="user", content=text_content)
317
+
318
+ # Convert using the same logic as user messages
319
+ converted = OpenAIConverter.convert_to_openai(
320
+ temp_multipart, concatenate_text_blocks=concatenate_text_blocks
321
+ )
322
+
323
+ # For tool messages, we need to extract and combine all text content
324
+ if isinstance(converted["content"], str):
325
+ content = converted["content"]
326
+ else:
327
+ # For compatibility with OpenAI's tool message format, combine all text blocks
328
+ all_text = all(
329
+ block.get("type") == "text" for block in converted["content"]
330
+ )
331
+
332
+ if all_text and len(converted["content"]) > 0:
333
+ # Combine all text blocks
334
+ content = " ".join(
335
+ block.get("text", "") for block in converted["content"]
336
+ )
337
+ else:
338
+ # Fallback for unexpected cases
339
+ content = "[Complex content converted to text]"
340
+
341
+ # Create a tool message with the converted content
342
+ return {"role": "tool", "tool_call_id": tool_call_id, "content": content}
343
+
344
+ # If we have mixed content or only non-text content
345
+
346
+ # Process text content for the tool message
347
+ tool_message_content = ""
348
+ if text_content:
349
+ temp_multipart = PromptMessageMultipart(role="user", content=text_content)
350
+ converted = OpenAIConverter.convert_to_openai(
351
+ temp_multipart, concatenate_text_blocks=True
352
+ )
353
+
354
+ if isinstance(converted["content"], str):
355
+ tool_message_content = converted["content"]
356
+ else:
357
+ # Combine all text blocks
358
+ all_text = [
359
+ block.get("text", "")
360
+ for block in converted["content"]
361
+ if block.get("type") == "text"
362
+ ]
363
+ tool_message_content = " ".join(all_text)
364
+
365
+ if not tool_message_content:
366
+ tool_message_content = "[Tool returned non-text content]"
367
+
368
+ # Create the tool message with just the text
369
+ tool_message = {
370
+ "role": "tool",
371
+ "tool_call_id": tool_call_id,
372
+ "content": tool_message_content,
373
+ }
374
+
375
+ # Process non-text content as a separate user message
376
+ if non_text_content:
377
+ # Create a multipart message with the non-text content
378
+ non_text_multipart = PromptMessageMultipart(
379
+ role="user", content=non_text_content
380
+ )
381
+
382
+ # Convert to OpenAI format
383
+ user_message = OpenAIConverter.convert_to_openai(non_text_multipart)
384
+ # Add tool_call_id to associate with the tool call
385
+ user_message["tool_call_id"] = tool_call_id
386
+
387
+ return (tool_message, [user_message])
388
+
389
+ return tool_message
390
+
391
+ @staticmethod
392
+ def convert_function_results_to_openai(
393
+ results: List[Tuple[str, CallToolResult]],
394
+ concatenate_text_blocks: bool = False,
395
+ ) -> List[OpenAIMessage]:
396
+ """
397
+ Convert a list of function call results to OpenAI messages.
398
+ Handles cases where tool results contain non-text content by creating
399
+ additional user messages as needed.
400
+
401
+ Args:
402
+ results: List of (tool_call_id, result) tuples
403
+ concatenate_text_blocks: If True, adjacent text blocks will be combined
404
+
405
+ Returns:
406
+ List of OpenAI API messages for tool responses
407
+ """
408
+ messages = []
409
+
410
+ for tool_call_id, result in results:
411
+ converted = OpenAIConverter.convert_tool_result_to_openai(
412
+ tool_result=result,
413
+ tool_call_id=tool_call_id,
414
+ concatenate_text_blocks=concatenate_text_blocks,
415
+ )
416
+
417
+ # Handle the case where we have mixed content and get back a tuple
418
+ if isinstance(converted, tuple):
419
+ tool_message, additional_messages = converted
420
+ messages.append(tool_message)
421
+ messages.extend(additional_messages)
422
+ else:
423
+ # Single message case (text-only)
424
+ messages.append(converted)
425
+
426
+ return messages
@@ -0,0 +1,197 @@
1
+ # openai_multipart.py
2
+ """
3
+ Clean utilities for converting between PromptMessageMultipart and OpenAI message formats.
4
+ Each function handles all content types consistently and is designed for simple testing.
5
+ """
6
+
7
+ from typing import Dict, Any, Union, List
8
+
9
+ from openai.types.chat import (
10
+ ChatCompletionMessage,
11
+ ChatCompletionMessageParam,
12
+ )
13
+
14
+ from mcp.types import (
15
+ TextContent,
16
+ ImageContent,
17
+ EmbeddedResource,
18
+ TextResourceContents,
19
+ BlobResourceContents,
20
+ )
21
+
22
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
23
+
24
+
25
+ def openai_to_multipart(
26
+ message: Union[
27
+ ChatCompletionMessage,
28
+ ChatCompletionMessageParam,
29
+ List[Union[ChatCompletionMessage, ChatCompletionMessageParam]],
30
+ ],
31
+ ) -> Union[PromptMessageMultipart, List[PromptMessageMultipart]]:
32
+ """
33
+ Convert OpenAI messages to PromptMessageMultipart format.
34
+
35
+ Args:
36
+ message: OpenAI Message, MessageParam, or list of them
37
+
38
+ Returns:
39
+ Equivalent message(s) in PromptMessageMultipart format
40
+ """
41
+ if isinstance(message, list):
42
+ return [_openai_message_to_multipart(m) for m in message]
43
+ return _openai_message_to_multipart(message)
44
+
45
+
46
+ def _openai_message_to_multipart(
47
+ message: Union[ChatCompletionMessage, Dict[str, Any]],
48
+ ) -> PromptMessageMultipart:
49
+ """Convert a single OpenAI message to PromptMessageMultipart."""
50
+ # Get role and content from message
51
+ if isinstance(message, dict):
52
+ role = message.get("role", "assistant")
53
+ content = message.get("content", "")
54
+ else:
55
+ role = message.role
56
+ content = message.content
57
+
58
+ mcp_contents = []
59
+
60
+ # Handle string content (simple case)
61
+ if isinstance(content, str):
62
+ mcp_contents.append(TextContent(type="text", text=content))
63
+
64
+ # Handle list of content parts
65
+ elif isinstance(content, list):
66
+ for part in content:
67
+ part_type = (
68
+ part.get("type")
69
+ if isinstance(part, dict)
70
+ else getattr(part, "type", None)
71
+ )
72
+
73
+ # Handle text content
74
+ if part_type == "text":
75
+ text = (
76
+ part.get("text")
77
+ if isinstance(part, dict)
78
+ else getattr(part, "text", "")
79
+ )
80
+
81
+ # Check if this is a resource marker
82
+ if (
83
+ text
84
+ and (
85
+ text.startswith("[Resource:")
86
+ or text.startswith("[Binary Resource:")
87
+ )
88
+ and "\n" in text
89
+ ):
90
+ header, content_text = text.split("\n", 1)
91
+ if "MIME:" in header:
92
+ mime_match = header.split("MIME:", 1)[1].split("]")[0].strip()
93
+
94
+ # If not text/plain, create an embedded resource
95
+ if mime_match != "text/plain":
96
+ if (
97
+ "Resource:" in header
98
+ and "Binary Resource:" not in header
99
+ ):
100
+ uri = (
101
+ header.split("Resource:", 1)[1]
102
+ .split(",")[0]
103
+ .strip()
104
+ )
105
+ mcp_contents.append(
106
+ EmbeddedResource(
107
+ type="resource",
108
+ resource=TextResourceContents(
109
+ uri=uri,
110
+ mimeType=mime_match,
111
+ text=content_text,
112
+ ),
113
+ )
114
+ )
115
+ continue
116
+
117
+ # Regular text content
118
+ mcp_contents.append(TextContent(type="text", text=text))
119
+
120
+ # Handle image content
121
+ elif part_type == "image_url":
122
+ image_url = (
123
+ part.get("image_url", {})
124
+ if isinstance(part, dict)
125
+ else getattr(part, "image_url", None)
126
+ )
127
+ if image_url:
128
+ url = (
129
+ image_url.get("url")
130
+ if isinstance(image_url, dict)
131
+ else getattr(image_url, "url", "")
132
+ )
133
+ if url and url.startswith("data:image/"):
134
+ # Handle base64 data URLs
135
+ mime_type = url.split(";")[0].replace("data:", "")
136
+ data = url.split(",")[1]
137
+ mcp_contents.append(
138
+ ImageContent(type="image", data=data, mimeType=mime_type)
139
+ )
140
+
141
+ # Handle explicit resource types
142
+ elif (
143
+ part_type == "resource"
144
+ and isinstance(part, dict)
145
+ and "resource" in part
146
+ ):
147
+ resource = part["resource"]
148
+ if isinstance(resource, dict):
149
+ # Text resource
150
+ if "text" in resource and "mimeType" in resource:
151
+ mime_type = resource["mimeType"]
152
+ uri = resource.get("uri", "resource://unknown")
153
+
154
+ if mime_type == "text/plain":
155
+ mcp_contents.append(
156
+ TextContent(type="text", text=resource["text"])
157
+ )
158
+ else:
159
+ mcp_contents.append(
160
+ EmbeddedResource(
161
+ type="resource",
162
+ resource=TextResourceContents(
163
+ text=resource["text"],
164
+ mimeType=mime_type,
165
+ uri=uri,
166
+ ),
167
+ )
168
+ )
169
+ # Binary resource
170
+ elif "blob" in resource and "mimeType" in resource:
171
+ mime_type = resource["mimeType"]
172
+ uri = resource.get("uri", "resource://unknown")
173
+
174
+ if (
175
+ mime_type.startswith("image/")
176
+ and mime_type != "image/svg+xml"
177
+ ):
178
+ mcp_contents.append(
179
+ ImageContent(
180
+ type="image",
181
+ data=resource["blob"],
182
+ mimeType=mime_type,
183
+ )
184
+ )
185
+ else:
186
+ mcp_contents.append(
187
+ EmbeddedResource(
188
+ type="resource",
189
+ resource=BlobResourceContents(
190
+ blob=resource["blob"],
191
+ mimeType=mime_type,
192
+ uri=uri,
193
+ ),
194
+ )
195
+ )
196
+
197
+ return PromptMessageMultipart(role=role, content=mcp_contents)