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,348 @@
1
+ from typing import List, Union, Sequence
2
+
3
+ from mcp.types import (
4
+ TextContent,
5
+ ImageContent,
6
+ EmbeddedResource,
7
+ CallToolResult,
8
+ TextResourceContents,
9
+ BlobResourceContents,
10
+ )
11
+ from pydantic import AnyUrl
12
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
13
+ from mcp_agent.mcp.mime_utils import (
14
+ guess_mime_type,
15
+ is_text_mime_type,
16
+ is_image_mime_type,
17
+ )
18
+
19
+ from anthropic.types import (
20
+ MessageParam,
21
+ TextBlockParam,
22
+ ImageBlockParam,
23
+ DocumentBlockParam,
24
+ Base64ImageSourceParam,
25
+ URLImageSourceParam,
26
+ Base64PDFSourceParam,
27
+ URLPDFSourceParam,
28
+ PlainTextSourceParam,
29
+ ToolResultBlockParam,
30
+ ContentBlockParam,
31
+ )
32
+ from mcp_agent.logging.logger import get_logger
33
+ from mcp_agent.mcp.resource_utils import extract_title_from_uri
34
+
35
+ _logger = get_logger("multipart_converter_anthropic")
36
+ # List of image MIME types supported by Anthropic API
37
+ SUPPORTED_IMAGE_MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
38
+
39
+
40
+ class AnthropicConverter:
41
+ """Converts MCP message types to Anthropic API format."""
42
+
43
+ @staticmethod
44
+ def _convert_content_items(
45
+ content_items: Sequence[Union[TextContent, ImageContent, EmbeddedResource]],
46
+ documentMode: bool = True,
47
+ ) -> List[ContentBlockParam]:
48
+ """
49
+ Helper method to convert a list of content items to Anthropic format.
50
+
51
+ Args:
52
+ content_items: Sequence of MCP content items
53
+ documentMode: Whether to convert text resources to document blocks (True) or text blocks (False)
54
+
55
+ Returns:
56
+ List of Anthropic content blocks
57
+ """
58
+
59
+ anthropic_blocks: List[ContentBlockParam] = []
60
+
61
+ for content_item in content_items:
62
+ if isinstance(content_item, TextContent):
63
+ anthropic_block = AnthropicConverter._convert_text_content(content_item)
64
+ anthropic_blocks.append(anthropic_block)
65
+ elif isinstance(content_item, ImageContent):
66
+ # Check if image MIME type is supported
67
+ if content_item.mimeType not in SUPPORTED_IMAGE_MIME_TYPES:
68
+ anthropic_block = AnthropicConverter._format_fail_message(
69
+ content_item, content_item.mimeType
70
+ )
71
+ else:
72
+ anthropic_block = AnthropicConverter._convert_image_content(
73
+ content_item
74
+ )
75
+ anthropic_blocks.append(anthropic_block)
76
+ elif isinstance(content_item, EmbeddedResource):
77
+ anthropic_block = AnthropicConverter._convert_embedded_resource(
78
+ content_item, documentMode
79
+ )
80
+ anthropic_blocks.append(anthropic_block)
81
+
82
+ return anthropic_blocks
83
+
84
+ @staticmethod
85
+ def _format_fail_message(
86
+ resource: Union[TextContent, ImageContent, EmbeddedResource], mimetype: str
87
+ ) -> TextBlockParam:
88
+ """Create a fallback text block for unsupported resource types"""
89
+ fallback_text: str = f"Unknown resource with format {mimetype}"
90
+ if resource.type == "image":
91
+ fallback_text = f"Image with unsupported format '{mimetype}' ({len(resource.data)} characters)"
92
+ if isinstance(resource, EmbeddedResource):
93
+ if isinstance(resource.resource, BlobResourceContents):
94
+ fallback_text = f"Embedded Resource {resource.resource.uri._url} with unsupported format {resource.resource.mimeType} ({len(resource.resource.blob)} characters)"
95
+
96
+ return TextBlockParam(type="text", text=fallback_text)
97
+
98
+ @staticmethod
99
+ def convert_to_anthropic(multipart_msg: PromptMessageMultipart) -> MessageParam:
100
+ """
101
+ Convert a PromptMessageMultipart message to Anthropic API format.
102
+
103
+ Args:
104
+ multipart_msg: The PromptMessageMultipart message to convert
105
+
106
+ Returns:
107
+ An Anthropic API MessageParam object
108
+ """
109
+ # Extract role
110
+ role: str = multipart_msg.role
111
+
112
+ # Convert content blocks
113
+ anthropic_blocks: List[MessageParam] = (
114
+ AnthropicConverter._convert_content_items(multipart_msg.content)
115
+ )
116
+
117
+ # Filter blocks based on role (assistant can only have text blocks)
118
+ if role == "assistant":
119
+ text_blocks = []
120
+ for block in anthropic_blocks:
121
+ if block.get("type") == "text":
122
+ text_blocks.append(block)
123
+ else:
124
+ _logger.warning(
125
+ f"Removing non-text block from assistant message: {block.get('type')}"
126
+ )
127
+ anthropic_blocks = text_blocks
128
+
129
+ # Create the Anthropic message
130
+ return MessageParam(role=role, content=anthropic_blocks)
131
+
132
+ @staticmethod
133
+ def _convert_text_content(content: TextContent) -> TextBlockParam:
134
+ """Convert TextContent to Anthropic TextBlockParam."""
135
+ return TextBlockParam(type="text", text=content.text)
136
+
137
+ @staticmethod
138
+ def _convert_image_content(content: ImageContent) -> ImageBlockParam:
139
+ """Convert ImageContent to Anthropic ImageBlockParam."""
140
+ # MIME type validation already done in the main convert method
141
+ return ImageBlockParam(
142
+ type="image",
143
+ source=Base64ImageSourceParam(
144
+ type="base64", media_type=content.mimeType, data=content.data
145
+ ),
146
+ )
147
+
148
+ @staticmethod
149
+ def _determine_mime_type(
150
+ resource: TextResourceContents | BlobResourceContents,
151
+ ) -> str:
152
+ if resource.mimeType:
153
+ return resource.mimeType
154
+
155
+ if resource.uri:
156
+ return guess_mime_type(resource.uri.serialize_url)
157
+
158
+ if resource.blob:
159
+ return "application/octet-stream"
160
+ else:
161
+ return "text/plain"
162
+
163
+ @staticmethod
164
+ def _convert_embedded_resource(
165
+ resource: EmbeddedResource,
166
+ documentMode: bool = True,
167
+ ) -> ContentBlockParam:
168
+ """Convert EmbeddedResource to appropriate Anthropic block type.
169
+
170
+ Args:
171
+ resource: The embedded resource to convert
172
+ documentMode: Whether to convert text resources to Document blocks (True) or Text blocks (False)
173
+
174
+ Returns:
175
+ An appropriate ContentBlockParam for the resource
176
+ """
177
+ resource_content: TextResourceContents | BlobResourceContents = (
178
+ resource.resource
179
+ )
180
+ uri: AnyUrl = resource_content.uri
181
+ is_url: bool = uri.scheme in ("http", "https")
182
+ mime_type = AnthropicConverter._determine_mime_type(resource_content)
183
+ # Extract title from URI
184
+ title = extract_title_from_uri(uri) if uri else None
185
+
186
+ # Special case for SVG - it's actually text/XML, so extract as text
187
+ if mime_type == "image/svg+xml":
188
+ if hasattr(resource_content, "text"):
189
+ # For SVG from text resource
190
+ svg_content = resource_content.text
191
+ return TextBlockParam(type="text", text=f"```xml\n{svg_content}\n```")
192
+
193
+ # Handle image resources
194
+ if is_image_mime_type(mime_type):
195
+ # Check if image MIME type is supported
196
+ if mime_type not in SUPPORTED_IMAGE_MIME_TYPES:
197
+ return AnthropicConverter._format_fail_message(resource, mime_type)
198
+
199
+ # Handle supported image types
200
+ if is_url:
201
+ return ImageBlockParam(
202
+ type="image", source=URLImageSourceParam(type="url", url=str(uri))
203
+ )
204
+ elif hasattr(resource_content, "blob"):
205
+ return ImageBlockParam(
206
+ type="image",
207
+ source=Base64ImageSourceParam(
208
+ type="base64", media_type=mime_type, data=resource_content.blob
209
+ ),
210
+ )
211
+
212
+ # Handle PDF resources
213
+ elif mime_type == "application/pdf":
214
+ if is_url:
215
+ return DocumentBlockParam(
216
+ type="document",
217
+ title=title,
218
+ source=URLPDFSourceParam(type="url", url=str(uri)),
219
+ )
220
+ elif hasattr(resource_content, "blob"):
221
+ return DocumentBlockParam(
222
+ type="document",
223
+ title=title,
224
+ source=Base64PDFSourceParam(
225
+ type="base64",
226
+ media_type="application/pdf",
227
+ data=resource_content.blob,
228
+ ),
229
+ )
230
+
231
+ # Handle text resources (default for all other text mime types)
232
+ elif is_text_mime_type(mime_type):
233
+ if documentMode:
234
+ if hasattr(resource_content, "text"):
235
+ return DocumentBlockParam(
236
+ type="document",
237
+ title=title,
238
+ source=PlainTextSourceParam(
239
+ type="text",
240
+ media_type="text/plain",
241
+ data=resource_content.text,
242
+ ),
243
+ )
244
+ # Return as text block when documentMode is False
245
+ if hasattr(resource_content, "text"):
246
+ return TextBlockParam(type="text", text=resource_content.text)
247
+
248
+ # Default fallback - convert to text if possible
249
+ if hasattr(resource_content, "text"):
250
+ return TextBlockParam(type="text", text=resource_content.text)
251
+
252
+ return AnthropicConverter._format_fail_message(resource, mime_type)
253
+
254
+ @staticmethod
255
+ def convert_tool_result_to_anthropic(
256
+ tool_result: CallToolResult, tool_use_id: str
257
+ ) -> ToolResultBlockParam:
258
+ """
259
+ Convert an MCP CallToolResult to an Anthropic ToolResultBlockParam.
260
+
261
+ Args:
262
+ tool_result: The tool result from a tool call
263
+ tool_use_id: The ID of the associated tool use
264
+
265
+ Returns:
266
+ An Anthropic ToolResultBlockParam ready to be included in a user message
267
+ """
268
+ # For tool results, we always use documentMode=False to get text blocks instead of document blocks
269
+ anthropic_content = []
270
+
271
+ for item in tool_result.content:
272
+ if isinstance(item, EmbeddedResource):
273
+ # For embedded resources, always use text mode in tool results
274
+ resource_block = AnthropicConverter._convert_embedded_resource(
275
+ item, documentMode=False
276
+ )
277
+ anthropic_content.append(resource_block)
278
+ else:
279
+ # For other types (Text, Image), use standard conversion
280
+ blocks = AnthropicConverter._convert_content_items(
281
+ [item], documentMode=False
282
+ )
283
+ anthropic_content.extend(blocks)
284
+
285
+ # If we ended up with no valid content blocks, create a placeholder
286
+ if not anthropic_content:
287
+ anthropic_content = [
288
+ TextBlockParam(type="text", text="[No content in tool result]")
289
+ ]
290
+
291
+ # Create the tool result block
292
+ return ToolResultBlockParam(
293
+ type="tool_result",
294
+ tool_use_id=tool_use_id,
295
+ content=anthropic_content,
296
+ is_error=tool_result.isError,
297
+ )
298
+
299
+ @staticmethod
300
+ def create_tool_results_message(
301
+ tool_results: List[tuple[str, CallToolResult]],
302
+ ) -> MessageParam:
303
+ """
304
+ Create a user message containing tool results.
305
+
306
+ Args:
307
+ tool_results: List of (tool_use_id, tool_result) tuples
308
+
309
+ Returns:
310
+ A MessageParam with role='user' containing all tool results
311
+ """
312
+ content_blocks = []
313
+
314
+ for tool_use_id, result in tool_results:
315
+ # Split into text/image content vs other content
316
+ tool_content = []
317
+ separate_blocks = []
318
+
319
+ for item in result.content:
320
+ # Text and images go in tool results, other resources (PDFs) go as separate blocks
321
+ if isinstance(item, (TextContent, ImageContent)):
322
+ tool_content.append(item)
323
+ elif isinstance(item, EmbeddedResource):
324
+ # If it's a text resource, keep it in tool_content
325
+ if isinstance(item.resource, TextResourceContents):
326
+ tool_content.append(item)
327
+ else:
328
+ # For binary resources like PDFs, convert and add as separate block
329
+ block = AnthropicConverter._convert_embedded_resource(
330
+ item, documentMode=True
331
+ )
332
+ separate_blocks.append(block)
333
+ else:
334
+ tool_content.append(item)
335
+
336
+ # Always create a tool result block, even if empty
337
+ # If tool_content is empty, we'll get a placeholder text block added in convert_tool_result_to_anthropic
338
+ tool_result = CallToolResult(content=tool_content, isError=result.isError)
339
+ content_blocks.append(
340
+ AnthropicConverter.convert_tool_result_to_anthropic(
341
+ tool_result, tool_use_id
342
+ )
343
+ )
344
+
345
+ # Add separate blocks directly to the message
346
+ content_blocks.extend(separate_blocks)
347
+
348
+ return MessageParam(role="user", content=content_blocks)