fast-agent-mcp 0.1.10__py3-none-any.whl → 0.1.12__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.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (46) hide show
  1. {fast_agent_mcp-0.1.10.dist-info → fast_agent_mcp-0.1.12.dist-info}/METADATA +36 -38
  2. {fast_agent_mcp-0.1.10.dist-info → fast_agent_mcp-0.1.12.dist-info}/RECORD +45 -42
  3. mcp_agent/agents/agent.py +1 -24
  4. mcp_agent/app.py +0 -5
  5. mcp_agent/config.py +9 -0
  6. mcp_agent/context.py +0 -2
  7. mcp_agent/core/agent_app.py +29 -0
  8. mcp_agent/core/agent_types.py +29 -2
  9. mcp_agent/core/decorators.py +1 -2
  10. mcp_agent/core/error_handling.py +1 -1
  11. mcp_agent/core/factory.py +2 -3
  12. mcp_agent/core/mcp_content.py +2 -3
  13. mcp_agent/core/proxies.py +3 -0
  14. mcp_agent/core/request_params.py +43 -0
  15. mcp_agent/core/types.py +4 -2
  16. mcp_agent/core/validation.py +14 -15
  17. mcp_agent/logging/transport.py +2 -2
  18. mcp_agent/mcp/gen_client.py +4 -4
  19. mcp_agent/mcp/interfaces.py +186 -0
  20. mcp_agent/mcp/mcp_agent_client_session.py +10 -2
  21. mcp_agent/mcp/mcp_aggregator.py +12 -3
  22. mcp_agent/mcp/sampling.py +140 -0
  23. mcp_agent/mcp/stdio.py +1 -2
  24. mcp_agent/mcp_server/__init__.py +1 -1
  25. mcp_agent/resources/examples/internal/agent.py +1 -1
  26. mcp_agent/resources/examples/internal/fastagent.config.yaml +3 -0
  27. mcp_agent/resources/examples/prompting/__init__.py +1 -1
  28. mcp_agent/ui/console_display.py +2 -2
  29. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +2 -2
  30. mcp_agent/workflows/llm/augmented_llm.py +42 -102
  31. mcp_agent/workflows/llm/augmented_llm_anthropic.py +4 -3
  32. mcp_agent/workflows/llm/augmented_llm_openai.py +4 -3
  33. mcp_agent/workflows/llm/augmented_llm_passthrough.py +119 -37
  34. mcp_agent/workflows/llm/model_factory.py +1 -1
  35. mcp_agent/workflows/llm/prompt_utils.py +42 -28
  36. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +244 -140
  37. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +230 -185
  38. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +5 -204
  39. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +9 -207
  40. mcp_agent/workflows/llm/sampling_converter.py +124 -0
  41. mcp_agent/workflows/llm/sampling_format_converter.py +0 -17
  42. mcp_agent/workflows/router/router_base.py +10 -10
  43. mcp_agent/workflows/llm/llm_selector.py +0 -345
  44. {fast_agent_mcp-0.1.10.dist-info → fast_agent_mcp-0.1.12.dist-info}/WHEEL +0 -0
  45. {fast_agent_mcp-0.1.10.dist-info → fast_agent_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
  46. {fast_agent_mcp-0.1.10.dist-info → fast_agent_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- from typing import List, Union, Sequence
1
+ from typing import List, Union, Sequence, Optional
2
2
 
3
3
  from mcp.types import (
4
4
  TextContent,
@@ -7,6 +7,7 @@ from mcp.types import (
7
7
  CallToolResult,
8
8
  TextResourceContents,
9
9
  BlobResourceContents,
10
+ PromptMessage,
10
11
  )
11
12
  from pydantic import AnyUrl
12
13
  from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
@@ -33,6 +34,7 @@ from mcp_agent.logging.logger import get_logger
33
34
  from mcp_agent.mcp.resource_utils import extract_title_from_uri
34
35
 
35
36
  _logger = get_logger("multipart_converter_anthropic")
37
+
36
38
  # List of image MIME types supported by Anthropic API
37
39
  SUPPORTED_IMAGE_MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
38
40
 
@@ -41,59 +43,16 @@ class AnthropicConverter:
41
43
  """Converts MCP message types to Anthropic API format."""
42
44
 
43
45
  @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.
46
+ def _is_supported_image_type(mime_type: str) -> bool:
47
+ """Check if the given MIME type is supported by Anthropic's image API.
50
48
 
51
49
  Args:
52
- content_items: Sequence of MCP content items
53
- documentMode: Whether to convert text resources to document blocks (True) or text blocks (False)
50
+ mime_type: The MIME type to check
54
51
 
55
52
  Returns:
56
- List of Anthropic content blocks
53
+ True if the MIME type is supported, False otherwise
57
54
  """
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)
55
+ return mime_type in SUPPORTED_IMAGE_MIME_TYPES
97
56
 
98
57
  @staticmethod
99
58
  def convert_to_anthropic(multipart_msg: PromptMessageMultipart) -> MessageParam:
@@ -106,12 +65,15 @@ class AnthropicConverter:
106
65
  Returns:
107
66
  An Anthropic API MessageParam object
108
67
  """
109
- # Extract role
110
- role: str = multipart_msg.role
68
+ role = multipart_msg.role
69
+
70
+ # Handle empty content case - create an empty list instead of a text block
71
+ if not multipart_msg.content:
72
+ return MessageParam(role=role, content=[])
111
73
 
112
74
  # Convert content blocks
113
- anthropic_blocks: List[MessageParam] = (
114
- AnthropicConverter._convert_content_items(multipart_msg.content)
75
+ anthropic_blocks = AnthropicConverter._convert_content_items(
76
+ multipart_msg.content, document_mode=True
115
77
  )
116
78
 
117
79
  # Filter blocks based on role (assistant can only have text blocks)
@@ -130,73 +92,111 @@ class AnthropicConverter:
130
92
  return MessageParam(role=role, content=anthropic_blocks)
131
93
 
132
94
  @staticmethod
133
- def _convert_text_content(content: TextContent) -> TextBlockParam:
134
- """Convert TextContent to Anthropic TextBlockParam."""
135
- return TextBlockParam(type="text", text=content.text)
95
+ def convert_prompt_message_to_anthropic(message: PromptMessage) -> MessageParam:
96
+ """
97
+ Convert a standard PromptMessage to Anthropic API format.
136
98
 
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
- )
99
+ Args:
100
+ message: The PromptMessage to convert
101
+
102
+ Returns:
103
+ An Anthropic API MessageParam object
104
+ """
105
+ # Convert the PromptMessage to a PromptMessageMultipart containing a single content item
106
+ multipart = PromptMessageMultipart(role=message.role, content=[message.content])
107
+
108
+ # Use the existing conversion method
109
+ return AnthropicConverter.convert_to_anthropic(multipart)
147
110
 
148
111
  @staticmethod
149
- def _determine_mime_type(
150
- resource: TextResourceContents | BlobResourceContents,
151
- ) -> str:
152
- if resource.mimeType:
153
- return resource.mimeType
112
+ def _convert_content_items(
113
+ content_items: Sequence[Union[TextContent, ImageContent, EmbeddedResource]],
114
+ document_mode: bool = True,
115
+ ) -> List[ContentBlockParam]:
116
+ """
117
+ Convert a list of content items to Anthropic content blocks.
154
118
 
155
- if resource.uri:
156
- return guess_mime_type(resource.uri.serialize_url)
119
+ Args:
120
+ content_items: Sequence of MCP content items
121
+ document_mode: Whether to convert text resources to document blocks (True) or text blocks (False)
157
122
 
158
- if resource.blob:
159
- return "application/octet-stream"
160
- else:
161
- return "text/plain"
123
+ Returns:
124
+ List of Anthropic content blocks
125
+ """
126
+ anthropic_blocks: List[ContentBlockParam] = []
127
+
128
+ for content_item in content_items:
129
+ if isinstance(content_item, TextContent):
130
+ anthropic_blocks.append(
131
+ TextBlockParam(type="text", text=content_item.text)
132
+ )
133
+
134
+ elif isinstance(content_item, ImageContent):
135
+ # Check if image MIME type is supported
136
+ if not AnthropicConverter._is_supported_image_type(
137
+ content_item.mimeType
138
+ ):
139
+ anthropic_blocks.append(
140
+ TextBlockParam(
141
+ type="text",
142
+ text=f"Image with unsupported format '{content_item.mimeType}' ({len(content_item.data)} bytes)",
143
+ )
144
+ )
145
+ else:
146
+ anthropic_blocks.append(
147
+ ImageBlockParam(
148
+ type="image",
149
+ source=Base64ImageSourceParam(
150
+ type="base64",
151
+ media_type=content_item.mimeType,
152
+ data=content_item.data,
153
+ ),
154
+ )
155
+ )
156
+
157
+ elif isinstance(content_item, EmbeddedResource):
158
+ block = AnthropicConverter._convert_embedded_resource(
159
+ content_item, document_mode
160
+ )
161
+ anthropic_blocks.append(block)
162
+
163
+ return anthropic_blocks
162
164
 
163
165
  @staticmethod
164
166
  def _convert_embedded_resource(
165
167
  resource: EmbeddedResource,
166
- documentMode: bool = True,
168
+ document_mode: bool = True,
167
169
  ) -> ContentBlockParam:
168
- """Convert EmbeddedResource to appropriate Anthropic block type.
170
+ """
171
+ Convert EmbeddedResource to appropriate Anthropic block type.
169
172
 
170
173
  Args:
171
174
  resource: The embedded resource to convert
172
- documentMode: Whether to convert text resources to Document blocks (True) or Text blocks (False)
175
+ document_mode: Whether to convert text resources to Document blocks (True) or Text blocks (False)
173
176
 
174
177
  Returns:
175
178
  An appropriate ContentBlockParam for the resource
176
179
  """
177
- resource_content: TextResourceContents | BlobResourceContents = (
178
- resource.resource
179
- )
180
- uri: AnyUrl = resource_content.uri
181
- is_url: bool = uri.scheme in ("http", "https")
180
+ resource_content = resource.resource
181
+ uri: Optional[AnyUrl] = getattr(resource_content, "uri", None)
182
+ is_url: bool = uri and uri.scheme in ("http", "https")
183
+
184
+ # Determine MIME type
182
185
  mime_type = AnthropicConverter._determine_mime_type(resource_content)
186
+
183
187
  # Extract title from URI
184
- title = extract_title_from_uri(uri) if uri else None
188
+ title = extract_title_from_uri(uri) if uri else "resource"
185
189
 
186
- # Special case for SVG - it's actually text/XML, so extract as text
190
+ # Convert based on MIME type
187
191
  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
192
+ return AnthropicConverter._convert_svg_resource(resource_content)
193
+
194
+ elif is_image_mime_type(mime_type):
195
+ if not AnthropicConverter._is_supported_image_type(mime_type):
196
+ return AnthropicConverter._create_fallback_text(
197
+ f"Image with unsupported format '{mime_type}'", resource
198
+ )
199
+
200
200
  if is_url:
201
201
  return ImageBlockParam(
202
202
  type="image", source=URLImageSourceParam(type="url", url=str(uri))
@@ -208,8 +208,10 @@ class AnthropicConverter:
208
208
  type="base64", media_type=mime_type, data=resource_content.blob
209
209
  ),
210
210
  )
211
+ return AnthropicConverter._create_fallback_text(
212
+ "Image missing data", resource
213
+ )
211
214
 
212
- # Handle PDF resources
213
215
  elif mime_type == "application/pdf":
214
216
  if is_url:
215
217
  return DocumentBlockParam(
@@ -227,29 +229,109 @@ class AnthropicConverter:
227
229
  data=resource_content.blob,
228
230
  ),
229
231
  )
232
+ return TextBlockParam(
233
+ type="text", text=f"[PDF resource missing data: {title}]"
234
+ )
230
235
 
231
- # Handle text resources (default for all other text mime types)
232
236
  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)
237
+ if not hasattr(resource_content, "text"):
238
+ return TextBlockParam(
239
+ type="text",
240
+ text=f"[Text content could not be extracted from {title}]",
241
+ )
242
+
243
+ # Create document block when in document mode
244
+ if document_mode:
245
+ return DocumentBlockParam(
246
+ type="document",
247
+ title=title,
248
+ source=PlainTextSourceParam(
249
+ type="text",
250
+ media_type="text/plain",
251
+ data=resource_content.text,
252
+ ),
253
+ )
254
+
255
+ # Return as simple text block when not in document mode
256
+ return TextBlockParam(type="text", text=resource_content.text)
247
257
 
248
258
  # Default fallback - convert to text if possible
249
259
  if hasattr(resource_content, "text"):
250
260
  return TextBlockParam(type="text", text=resource_content.text)
251
261
 
252
- return AnthropicConverter._format_fail_message(resource, mime_type)
262
+ # This is for binary resources - match the format expected by the test
263
+ if isinstance(resource.resource, BlobResourceContents) and hasattr(
264
+ resource.resource, "blob"
265
+ ):
266
+ blob_length = len(resource.resource.blob)
267
+ return TextBlockParam(
268
+ type="text",
269
+ text=f"Embedded Resource {uri._url} with unsupported format {mime_type} ({blob_length} characters)",
270
+ )
271
+
272
+ return AnthropicConverter._create_fallback_text(
273
+ f"Unsupported resource ({mime_type})", resource
274
+ )
275
+
276
+ @staticmethod
277
+ def _determine_mime_type(
278
+ resource: Union[TextResourceContents, BlobResourceContents],
279
+ ) -> str:
280
+ """
281
+ Determine the MIME type of a resource.
282
+
283
+ Args:
284
+ resource: The resource to check
285
+
286
+ Returns:
287
+ The MIME type as a string
288
+ """
289
+ if getattr(resource, "mimeType", None):
290
+ return resource.mimeType
291
+
292
+ if getattr(resource, "uri", None):
293
+ return guess_mime_type(resource.uri.serialize_url)
294
+
295
+ if hasattr(resource, "blob"):
296
+ return "application/octet-stream"
297
+
298
+ return "text/plain"
299
+
300
+ @staticmethod
301
+ def _convert_svg_resource(resource_content) -> TextBlockParam:
302
+ """
303
+ Convert SVG resource to text block with XML code formatting.
304
+
305
+ Args:
306
+ resource_content: The resource content containing SVG data
307
+
308
+ Returns:
309
+ A TextBlockParam with formatted SVG content
310
+ """
311
+ if hasattr(resource_content, "text"):
312
+ svg_content = resource_content.text
313
+ return TextBlockParam(type="text", text=f"```xml\n{svg_content}\n```")
314
+ return TextBlockParam(type="text", text="[SVG content could not be extracted]")
315
+
316
+ @staticmethod
317
+ def _create_fallback_text(
318
+ message: str, resource: Union[TextContent, ImageContent, EmbeddedResource]
319
+ ) -> TextBlockParam:
320
+ """
321
+ Create a fallback text block for unsupported resource types.
322
+
323
+ Args:
324
+ message: The fallback message
325
+ resource: The resource that couldn't be converted
326
+
327
+ Returns:
328
+ A TextBlockParam with the fallback message
329
+ """
330
+ if isinstance(resource, EmbeddedResource) and hasattr(resource.resource, "uri"):
331
+ uri = resource.resource.uri
332
+ return TextBlockParam(type="text", text=f"[{message}: {uri._url}]")
333
+
334
+ return TextBlockParam(type="text", text=f"[{message}]")
253
335
 
254
336
  @staticmethod
255
337
  def convert_tool_result_to_anthropic(
@@ -265,20 +347,20 @@ class AnthropicConverter:
265
347
  Returns:
266
348
  An Anthropic ToolResultBlockParam ready to be included in a user message
267
349
  """
268
- # For tool results, we always use documentMode=False to get text blocks instead of document blocks
350
+ # For tool results, always use document_mode=False to get text blocks instead of document blocks
269
351
  anthropic_content = []
270
352
 
271
353
  for item in tool_result.content:
272
354
  if isinstance(item, EmbeddedResource):
273
355
  # For embedded resources, always use text mode in tool results
274
356
  resource_block = AnthropicConverter._convert_embedded_resource(
275
- item, documentMode=False
357
+ item, document_mode=False
276
358
  )
277
359
  anthropic_content.append(resource_block)
278
- else:
279
- # For other types (Text, Image), use standard conversion
360
+ elif isinstance(item, (TextContent, ImageContent)):
361
+ # For text and image, use standard conversion
280
362
  blocks = AnthropicConverter._convert_content_items(
281
- [item], documentMode=False
363
+ [item], document_mode=False
282
364
  )
283
365
  anthropic_content.extend(blocks)
284
366
 
@@ -312,35 +394,57 @@ class AnthropicConverter:
312
394
  content_blocks = []
313
395
 
314
396
  for tool_use_id, result in tool_results:
315
- # Split into text/image content vs other content
316
- tool_content = []
397
+ # Process each tool result
398
+ tool_result_blocks = []
317
399
  separate_blocks = []
318
400
 
401
+ # Process each content item in the result
319
402
  for item in result.content:
320
- # Text and images go in tool results, other resources (PDFs) go as separate blocks
321
403
  if isinstance(item, (TextContent, ImageContent)):
322
- tool_content.append(item)
404
+ blocks = AnthropicConverter._convert_content_items(
405
+ [item], document_mode=False
406
+ )
407
+ tool_result_blocks.extend(blocks)
323
408
  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)
409
+ resource_content = item.resource
410
+
411
+ # Text resources go in tool results, others go as separate blocks
412
+ if isinstance(resource_content, TextResourceContents):
413
+ block = AnthropicConverter._convert_embedded_resource(
414
+ item, document_mode=False
415
+ )
416
+ tool_result_blocks.append(block)
327
417
  else:
328
- # For binary resources like PDFs, convert and add as separate block
418
+ # For binary resources like PDFs, add as separate block
329
419
  block = AnthropicConverter._convert_embedded_resource(
330
- item, documentMode=True
420
+ item, document_mode=True
331
421
  )
332
422
  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
423
+
424
+ # Create the tool result block if we have content
425
+ if tool_result_blocks:
426
+ content_blocks.append(
427
+ ToolResultBlockParam(
428
+ type="tool_result",
429
+ tool_use_id=tool_use_id,
430
+ content=tool_result_blocks,
431
+ is_error=result.isError,
432
+ )
433
+ )
434
+ else:
435
+ # If there's no content, still create a placeholder
436
+ content_blocks.append(
437
+ ToolResultBlockParam(
438
+ type="tool_result",
439
+ tool_use_id=tool_use_id,
440
+ content=[
441
+ TextBlockParam(
442
+ type="text", text="[No content in tool result]"
443
+ )
444
+ ],
445
+ is_error=result.isError,
446
+ )
342
447
  )
343
- )
344
448
 
345
449
  # Add separate blocks directly to the message
346
450
  content_blocks.extend(separate_blocks)