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.
Files changed (126) hide show
  1. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/METADATA +1 -1
  2. fast_agent_mcp-0.1.13.dist-info/RECORD +164 -0
  3. mcp_agent/agents/agent.py +37 -79
  4. mcp_agent/app.py +16 -22
  5. mcp_agent/cli/commands/bootstrap.py +22 -52
  6. mcp_agent/cli/commands/config.py +4 -4
  7. mcp_agent/cli/commands/setup.py +11 -26
  8. mcp_agent/cli/main.py +6 -9
  9. mcp_agent/cli/terminal.py +2 -2
  10. mcp_agent/config.py +1 -5
  11. mcp_agent/context.py +13 -24
  12. mcp_agent/context_dependent.py +3 -7
  13. mcp_agent/core/agent_app.py +45 -121
  14. mcp_agent/core/agent_utils.py +3 -5
  15. mcp_agent/core/decorators.py +5 -12
  16. mcp_agent/core/enhanced_prompt.py +25 -52
  17. mcp_agent/core/exceptions.py +8 -8
  18. mcp_agent/core/factory.py +29 -70
  19. mcp_agent/core/fastagent.py +48 -88
  20. mcp_agent/core/mcp_content.py +8 -16
  21. mcp_agent/core/prompt.py +8 -15
  22. mcp_agent/core/proxies.py +34 -25
  23. mcp_agent/core/request_params.py +6 -3
  24. mcp_agent/core/types.py +4 -6
  25. mcp_agent/core/validation.py +4 -3
  26. mcp_agent/executor/decorator_registry.py +11 -23
  27. mcp_agent/executor/executor.py +8 -17
  28. mcp_agent/executor/task_registry.py +2 -4
  29. mcp_agent/executor/temporal.py +28 -74
  30. mcp_agent/executor/workflow.py +3 -5
  31. mcp_agent/executor/workflow_signal.py +17 -29
  32. mcp_agent/human_input/handler.py +4 -9
  33. mcp_agent/human_input/types.py +2 -3
  34. mcp_agent/logging/events.py +1 -5
  35. mcp_agent/logging/json_serializer.py +7 -6
  36. mcp_agent/logging/listeners.py +20 -23
  37. mcp_agent/logging/logger.py +15 -17
  38. mcp_agent/logging/rich_progress.py +10 -8
  39. mcp_agent/logging/tracing.py +4 -6
  40. mcp_agent/logging/transport.py +22 -22
  41. mcp_agent/mcp/gen_client.py +4 -12
  42. mcp_agent/mcp/interfaces.py +71 -86
  43. mcp_agent/mcp/mcp_agent_client_session.py +11 -19
  44. mcp_agent/mcp/mcp_agent_server.py +8 -10
  45. mcp_agent/mcp/mcp_aggregator.py +45 -117
  46. mcp_agent/mcp/mcp_connection_manager.py +16 -37
  47. mcp_agent/mcp/prompt_message_multipart.py +12 -18
  48. mcp_agent/mcp/prompt_serialization.py +13 -38
  49. mcp_agent/mcp/prompts/prompt_load.py +99 -0
  50. mcp_agent/mcp/prompts/prompt_server.py +21 -128
  51. mcp_agent/mcp/prompts/prompt_template.py +20 -42
  52. mcp_agent/mcp/resource_utils.py +8 -17
  53. mcp_agent/mcp/sampling.py +5 -14
  54. mcp_agent/mcp/stdio.py +11 -8
  55. mcp_agent/mcp_server/agent_server.py +10 -17
  56. mcp_agent/mcp_server_registry.py +13 -35
  57. mcp_agent/resources/examples/data-analysis/analysis-campaign.py +1 -1
  58. mcp_agent/resources/examples/data-analysis/analysis.py +1 -1
  59. mcp_agent/resources/examples/data-analysis/slides.py +110 -0
  60. mcp_agent/resources/examples/internal/agent.py +2 -1
  61. mcp_agent/resources/examples/internal/job.py +2 -1
  62. mcp_agent/resources/examples/internal/prompt_category.py +1 -1
  63. mcp_agent/resources/examples/internal/prompt_sizing.py +3 -5
  64. mcp_agent/resources/examples/internal/sizer.py +2 -1
  65. mcp_agent/resources/examples/internal/social.py +2 -1
  66. mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +1 -1
  67. mcp_agent/resources/examples/prompting/agent.py +2 -1
  68. mcp_agent/resources/examples/prompting/image_server.py +5 -11
  69. mcp_agent/resources/examples/researcher/researcher-eval.py +1 -1
  70. mcp_agent/resources/examples/researcher/researcher-imp.py +3 -4
  71. mcp_agent/resources/examples/researcher/researcher.py +2 -1
  72. mcp_agent/resources/examples/workflows/agent_build.py +2 -1
  73. mcp_agent/resources/examples/workflows/chaining.py +2 -1
  74. mcp_agent/resources/examples/workflows/evaluator.py +2 -1
  75. mcp_agent/resources/examples/workflows/human_input.py +2 -1
  76. mcp_agent/resources/examples/workflows/orchestrator.py +2 -1
  77. mcp_agent/resources/examples/workflows/parallel.py +2 -1
  78. mcp_agent/resources/examples/workflows/router.py +2 -1
  79. mcp_agent/resources/examples/workflows/sse.py +1 -1
  80. mcp_agent/telemetry/usage_tracking.py +2 -1
  81. mcp_agent/ui/console_display.py +15 -39
  82. mcp_agent/workflows/embedding/embedding_base.py +1 -4
  83. mcp_agent/workflows/embedding/embedding_cohere.py +2 -2
  84. mcp_agent/workflows/embedding/embedding_openai.py +4 -13
  85. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +23 -57
  86. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +5 -8
  87. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +7 -11
  88. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +4 -8
  89. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +4 -8
  90. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +11 -22
  91. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +3 -3
  92. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +4 -6
  93. mcp_agent/workflows/llm/anthropic_utils.py +8 -29
  94. mcp_agent/workflows/llm/augmented_llm.py +69 -247
  95. mcp_agent/workflows/llm/augmented_llm_anthropic.py +39 -73
  96. mcp_agent/workflows/llm/augmented_llm_openai.py +42 -97
  97. mcp_agent/workflows/llm/augmented_llm_passthrough.py +13 -20
  98. mcp_agent/workflows/llm/augmented_llm_playback.py +8 -6
  99. mcp_agent/workflows/llm/memory.py +103 -0
  100. mcp_agent/workflows/llm/model_factory.py +8 -20
  101. mcp_agent/workflows/llm/openai_utils.py +1 -1
  102. mcp_agent/workflows/llm/prompt_utils.py +1 -3
  103. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +47 -89
  104. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +20 -55
  105. mcp_agent/workflows/llm/providers/openai_multipart.py +19 -61
  106. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +10 -12
  107. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +7 -11
  108. mcp_agent/workflows/llm/sampling_converter.py +4 -11
  109. mcp_agent/workflows/llm/sampling_format_converter.py +12 -12
  110. mcp_agent/workflows/orchestrator/orchestrator.py +24 -67
  111. mcp_agent/workflows/orchestrator/orchestrator_models.py +14 -40
  112. mcp_agent/workflows/parallel/fan_in.py +17 -47
  113. mcp_agent/workflows/parallel/fan_out.py +6 -12
  114. mcp_agent/workflows/parallel/parallel_llm.py +9 -26
  115. mcp_agent/workflows/router/router_base.py +19 -49
  116. mcp_agent/workflows/router/router_embedding.py +11 -25
  117. mcp_agent/workflows/router/router_embedding_cohere.py +2 -2
  118. mcp_agent/workflows/router/router_embedding_openai.py +2 -2
  119. mcp_agent/workflows/router/router_llm.py +12 -28
  120. mcp_agent/workflows/swarm/swarm.py +20 -48
  121. mcp_agent/workflows/swarm/swarm_anthropic.py +2 -2
  122. mcp_agent/workflows/swarm/swarm_openai.py +2 -2
  123. fast_agent_mcp-0.1.12.dist-info/RECORD +0 -161
  124. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/WHEEL +0 -0
  125. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
  126. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,13 @@
1
- from typing import List, Optional, Union
1
+ from typing import TYPE_CHECKING, List, Optional, Union
2
+
2
3
  from mcp import GetPromptResult
3
- from mcp.types import PromptMessage
4
+
4
5
  from mcp_agent.workflows.llm.augmented_llm import MessageParamT, RequestParams
5
6
  from mcp_agent.workflows.llm.augmented_llm_passthrough import PassthroughLLM
6
7
 
8
+ if TYPE_CHECKING:
9
+ from mcp.types import PromptMessage
10
+
7
11
 
8
12
  # TODO -- support tool calling
9
13
  class PlaybackLLM(PassthroughLLM):
@@ -19,7 +23,7 @@ class PlaybackLLM(PassthroughLLM):
19
23
  been played back, it returns a message indicating that messages are exhausted.
20
24
  """
21
25
 
22
- def __init__(self, name: str = "Playback", **kwargs):
26
+ def __init__(self, name: str = "Playback", **kwargs) -> None:
23
27
  super().__init__(name=name, **kwargs)
24
28
  self._messages: List[PromptMessage] = []
25
29
  self._current_index = 0
@@ -70,9 +74,7 @@ class PlaybackLLM(PassthroughLLM):
70
74
  # If we get here, we've run out of assistant messages
71
75
  return f"MESSAGES EXHAUSTED (list size {len(self._messages)})"
72
76
 
73
- async def apply_prompt_template(
74
- self, prompt_result: GetPromptResult, prompt_name: str
75
- ) -> str:
77
+ async def apply_prompt_template(self, prompt_result: GetPromptResult, prompt_name: str) -> str:
76
78
  """
77
79
  Apply a prompt template by adding its messages to the playback queue.
78
80
 
@@ -0,0 +1,103 @@
1
+ from typing import Generic, List, Protocol, TypeVar
2
+
3
+ # Define our own type variable for implementation use
4
+ MessageParamT = TypeVar("MessageParamT")
5
+
6
+
7
+ class Memory(Protocol, Generic[MessageParamT]):
8
+ """
9
+ Simple memory management for storing past interactions in-memory.
10
+ """
11
+
12
+ # TODO: saqadri - add checkpointing and other advanced memory capabilities
13
+
14
+ def __init__(self) -> None: ...
15
+
16
+ def extend(self, messages: List[MessageParamT], is_prompt: bool = False) -> None: ...
17
+
18
+ def set(self, messages: List[MessageParamT], is_prompt: bool = False) -> None: ...
19
+
20
+ def append(self, message: MessageParamT, is_prompt: bool = False) -> None: ...
21
+
22
+ def get(self, include_history: bool = True) -> List[MessageParamT]: ...
23
+
24
+ def clear(self, clear_prompts: bool = False) -> None: ...
25
+
26
+
27
+ class SimpleMemory(Memory, Generic[MessageParamT]):
28
+ """
29
+ Simple memory management for storing past interactions in-memory.
30
+
31
+ Maintains both prompt messages (which are always included) and
32
+ generated conversation history (which is included based on use_history setting).
33
+ """
34
+
35
+ def __init__(self) -> None:
36
+ self.history: List[MessageParamT] = []
37
+ self.prompt_messages: List[MessageParamT] = [] # Always included
38
+
39
+ def extend(self, messages: List[MessageParamT], is_prompt: bool = False) -> None:
40
+ """
41
+ Add multiple messages to history.
42
+
43
+ Args:
44
+ messages: Messages to add
45
+ is_prompt: If True, add to prompt_messages instead of regular history
46
+ """
47
+ if is_prompt:
48
+ self.prompt_messages.extend(messages)
49
+ else:
50
+ self.history.extend(messages)
51
+
52
+ def set(self, messages: List[MessageParamT], is_prompt: bool = False) -> None:
53
+ """
54
+ Replace messages in history.
55
+
56
+ Args:
57
+ messages: Messages to set
58
+ is_prompt: If True, replace prompt_messages instead of regular history
59
+ """
60
+ if is_prompt:
61
+ self.prompt_messages = messages.copy()
62
+ else:
63
+ self.history = messages.copy()
64
+
65
+ def append(self, message: MessageParamT, is_prompt: bool = False) -> None:
66
+ """
67
+ Add a single message to history.
68
+
69
+ Args:
70
+ message: Message to add
71
+ is_prompt: If True, add to prompt_messages instead of regular history
72
+ """
73
+ if is_prompt:
74
+ self.prompt_messages.append(message)
75
+ else:
76
+ self.history.append(message)
77
+
78
+ def get(self, include_history: bool = True) -> List[MessageParamT]:
79
+ """
80
+ Get all messages in memory.
81
+
82
+ Args:
83
+ include_history: If True, include regular history messages
84
+ If False, only return prompt messages
85
+
86
+ Returns:
87
+ Combined list of prompt messages and optionally history messages
88
+ """
89
+ if include_history:
90
+ return self.prompt_messages + self.history
91
+ else:
92
+ return self.prompt_messages.copy()
93
+
94
+ def clear(self, clear_prompts: bool = False) -> None:
95
+ """
96
+ Clear history and optionally prompt messages.
97
+
98
+ Args:
99
+ clear_prompts: If True, also clear prompt messages
100
+ """
101
+ self.history = []
102
+ if clear_prompts:
103
+ self.prompt_messages = []
@@ -1,16 +1,16 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum, auto
3
- from typing import Optional, Type, Dict, Union, Callable
3
+ from typing import Callable, Dict, Optional, Type, Union
4
4
 
5
5
  from mcp_agent.agents.agent import Agent
6
6
  from mcp_agent.core.exceptions import ModelConfigError
7
7
  from mcp_agent.core.request_params import RequestParams
8
+ from mcp_agent.mcp.interfaces import AugmentedLLMProtocol
8
9
  from mcp_agent.workflows.llm.augmented_llm_anthropic import AnthropicAugmentedLLM
9
10
  from mcp_agent.workflows.llm.augmented_llm_openai import OpenAIAugmentedLLM
10
11
  from mcp_agent.workflows.llm.augmented_llm_passthrough import PassthroughLLM
11
12
  from mcp_agent.workflows.llm.augmented_llm_playback import PlaybackLLM
12
13
 
13
-
14
14
  # Type alias for LLM classes
15
15
  LLMClass = Union[
16
16
  Type[AnthropicAugmentedLLM],
@@ -145,14 +145,10 @@ class ModelFactory:
145
145
  if provider is None:
146
146
  raise ModelConfigError(f"Unknown model: {model_name}")
147
147
 
148
- return ModelConfig(
149
- provider=provider, model_name=model_name, reasoning_effort=reasoning_effort
150
- )
148
+ return ModelConfig(provider=provider, model_name=model_name, reasoning_effort=reasoning_effort)
151
149
 
152
150
  @classmethod
153
- def create_factory(
154
- cls, model_string: str, request_params: Optional[RequestParams] = None
155
- ) -> Callable[..., LLMClass]:
151
+ def create_factory(cls, model_string: str, request_params: Optional[RequestParams] = None) -> Callable[..., AugmentedLLMProtocol]:
156
152
  """
157
153
  Creates a factory function that follows the attach_llm protocol.
158
154
 
@@ -173,23 +169,15 @@ class ModelFactory:
173
169
  # Create a factory function matching the attach_llm protocol
174
170
  def factory(agent: Agent, **kwargs) -> LLMClass:
175
171
  # Create merged params with parsed model name
176
- factory_params = (
177
- request_params.model_copy() if request_params else RequestParams()
178
- )
179
- factory_params.model = (
180
- config.model_name
181
- ) # Use the parsed model name, not the alias
172
+ factory_params = request_params.model_copy() if request_params else RequestParams()
173
+ factory_params.model = config.model_name # Use the parsed model name, not the alias
182
174
 
183
175
  # Merge with any provided default_request_params
184
176
  if "default_request_params" in kwargs and kwargs["default_request_params"]:
185
177
  params_dict = factory_params.model_dump()
186
- params_dict.update(
187
- kwargs["default_request_params"].model_dump(exclude_unset=True)
188
- )
178
+ params_dict.update(kwargs["default_request_params"].model_dump(exclude_unset=True))
189
179
  factory_params = RequestParams(**params_dict)
190
- factory_params.model = (
191
- config.model_name
192
- ) # Ensure parsed model name isn't overwritten
180
+ factory_params.model = config.model_name # Ensure parsed model name isn't overwritten
193
181
 
194
182
  # Forward all keyword arguments to LLM constructor
195
183
  llm_args = {
@@ -5,7 +5,7 @@ This file provides backward compatibility with the existing API while
5
5
  delegating to the proper implementations in the providers/ directory.
6
6
  """
7
7
 
8
- from typing import Dict, Any, Union
8
+ from typing import Any, Dict, Union
9
9
 
10
10
  from openai.types.chat import (
11
11
  ChatCompletionMessage,
@@ -97,9 +97,7 @@ def format_server_info(
97
97
 
98
98
  # Combine all components
99
99
  server_content = "\n".join(components)
100
- return format_fastagent_tag(
101
- "server", f"\n{server_content}\n", {"name": server_name}
102
- )
100
+ return format_fastagent_tag("server", f"\n{server_content}\n", {"name": server_name})
103
101
 
104
102
 
105
103
  def format_agent_info(
@@ -1,38 +1,40 @@
1
- from typing import List, Union, Sequence, Optional
1
+ from typing import TYPE_CHECKING, List, Optional, Sequence, Union
2
2
 
3
+ from anthropic.types import (
4
+ Base64ImageSourceParam,
5
+ Base64PDFSourceParam,
6
+ ContentBlockParam,
7
+ DocumentBlockParam,
8
+ ImageBlockParam,
9
+ MessageParam,
10
+ PlainTextSourceParam,
11
+ TextBlockParam,
12
+ ToolResultBlockParam,
13
+ URLImageSourceParam,
14
+ URLPDFSourceParam,
15
+ )
3
16
  from mcp.types import (
4
- TextContent,
5
- ImageContent,
6
- EmbeddedResource,
7
- CallToolResult,
8
- TextResourceContents,
9
17
  BlobResourceContents,
18
+ CallToolResult,
19
+ EmbeddedResource,
20
+ ImageContent,
10
21
  PromptMessage,
22
+ TextContent,
23
+ TextResourceContents,
11
24
  )
12
- from pydantic import AnyUrl
13
- from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
25
+
26
+ from mcp_agent.logging.logger import get_logger
14
27
  from mcp_agent.mcp.mime_utils import (
15
28
  guess_mime_type,
16
- is_text_mime_type,
17
29
  is_image_mime_type,
30
+ is_text_mime_type,
18
31
  )
19
-
20
- from anthropic.types import (
21
- MessageParam,
22
- TextBlockParam,
23
- ImageBlockParam,
24
- DocumentBlockParam,
25
- Base64ImageSourceParam,
26
- URLImageSourceParam,
27
- Base64PDFSourceParam,
28
- URLPDFSourceParam,
29
- PlainTextSourceParam,
30
- ToolResultBlockParam,
31
- ContentBlockParam,
32
- )
33
- from mcp_agent.logging.logger import get_logger
32
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
34
33
  from mcp_agent.mcp.resource_utils import extract_title_from_uri
35
34
 
35
+ if TYPE_CHECKING:
36
+ from pydantic import AnyUrl
37
+
36
38
  _logger = get_logger("multipart_converter_anthropic")
37
39
 
38
40
  # List of image MIME types supported by Anthropic API
@@ -72,9 +74,7 @@ class AnthropicConverter:
72
74
  return MessageParam(role=role, content=[])
73
75
 
74
76
  # Convert content blocks
75
- anthropic_blocks = AnthropicConverter._convert_content_items(
76
- multipart_msg.content, document_mode=True
77
- )
77
+ anthropic_blocks = AnthropicConverter._convert_content_items(multipart_msg.content, document_mode=True)
78
78
 
79
79
  # Filter blocks based on role (assistant can only have text blocks)
80
80
  if role == "assistant":
@@ -83,9 +83,7 @@ class AnthropicConverter:
83
83
  if block.get("type") == "text":
84
84
  text_blocks.append(block)
85
85
  else:
86
- _logger.warning(
87
- f"Removing non-text block from assistant message: {block.get('type')}"
88
- )
86
+ _logger.warning(f"Removing non-text block from assistant message: {block.get('type')}")
89
87
  anthropic_blocks = text_blocks
90
88
 
91
89
  # Create the Anthropic message
@@ -127,15 +125,11 @@ class AnthropicConverter:
127
125
 
128
126
  for content_item in content_items:
129
127
  if isinstance(content_item, TextContent):
130
- anthropic_blocks.append(
131
- TextBlockParam(type="text", text=content_item.text)
132
- )
128
+ anthropic_blocks.append(TextBlockParam(type="text", text=content_item.text))
133
129
 
134
130
  elif isinstance(content_item, ImageContent):
135
131
  # Check if image MIME type is supported
136
- if not AnthropicConverter._is_supported_image_type(
137
- content_item.mimeType
138
- ):
132
+ if not AnthropicConverter._is_supported_image_type(content_item.mimeType):
139
133
  anthropic_blocks.append(
140
134
  TextBlockParam(
141
135
  type="text",
@@ -155,9 +149,7 @@ class AnthropicConverter:
155
149
  )
156
150
 
157
151
  elif isinstance(content_item, EmbeddedResource):
158
- block = AnthropicConverter._convert_embedded_resource(
159
- content_item, document_mode
160
- )
152
+ block = AnthropicConverter._convert_embedded_resource(content_item, document_mode)
161
153
  anthropic_blocks.append(block)
162
154
 
163
155
  return anthropic_blocks
@@ -193,24 +185,16 @@ class AnthropicConverter:
193
185
 
194
186
  elif is_image_mime_type(mime_type):
195
187
  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
- )
188
+ return AnthropicConverter._create_fallback_text(f"Image with unsupported format '{mime_type}'", resource)
199
189
 
200
190
  if is_url:
201
- return ImageBlockParam(
202
- type="image", source=URLImageSourceParam(type="url", url=str(uri))
203
- )
191
+ return ImageBlockParam(type="image", source=URLImageSourceParam(type="url", url=str(uri)))
204
192
  elif hasattr(resource_content, "blob"):
205
193
  return ImageBlockParam(
206
194
  type="image",
207
- source=Base64ImageSourceParam(
208
- type="base64", media_type=mime_type, data=resource_content.blob
209
- ),
195
+ source=Base64ImageSourceParam(type="base64", media_type=mime_type, data=resource_content.blob),
210
196
  )
211
- return AnthropicConverter._create_fallback_text(
212
- "Image missing data", resource
213
- )
197
+ return AnthropicConverter._create_fallback_text("Image missing data", resource)
214
198
 
215
199
  elif mime_type == "application/pdf":
216
200
  if is_url:
@@ -229,9 +213,7 @@ class AnthropicConverter:
229
213
  data=resource_content.blob,
230
214
  ),
231
215
  )
232
- return TextBlockParam(
233
- type="text", text=f"[PDF resource missing data: {title}]"
234
- )
216
+ return TextBlockParam(type="text", text=f"[PDF resource missing data: {title}]")
235
217
 
236
218
  elif is_text_mime_type(mime_type):
237
219
  if not hasattr(resource_content, "text"):
@@ -260,18 +242,14 @@ class AnthropicConverter:
260
242
  return TextBlockParam(type="text", text=resource_content.text)
261
243
 
262
244
  # 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
- ):
245
+ if isinstance(resource.resource, BlobResourceContents) and hasattr(resource.resource, "blob"):
266
246
  blob_length = len(resource.resource.blob)
267
247
  return TextBlockParam(
268
248
  type="text",
269
249
  text=f"Embedded Resource {uri._url} with unsupported format {mime_type} ({blob_length} characters)",
270
250
  )
271
251
 
272
- return AnthropicConverter._create_fallback_text(
273
- f"Unsupported resource ({mime_type})", resource
274
- )
252
+ return AnthropicConverter._create_fallback_text(f"Unsupported resource ({mime_type})", resource)
275
253
 
276
254
  @staticmethod
277
255
  def _determine_mime_type(
@@ -314,9 +292,7 @@ class AnthropicConverter:
314
292
  return TextBlockParam(type="text", text="[SVG content could not be extracted]")
315
293
 
316
294
  @staticmethod
317
- def _create_fallback_text(
318
- message: str, resource: Union[TextContent, ImageContent, EmbeddedResource]
319
- ) -> TextBlockParam:
295
+ def _create_fallback_text(message: str, resource: Union[TextContent, ImageContent, EmbeddedResource]) -> TextBlockParam:
320
296
  """
321
297
  Create a fallback text block for unsupported resource types.
322
298
 
@@ -334,9 +310,7 @@ class AnthropicConverter:
334
310
  return TextBlockParam(type="text", text=f"[{message}]")
335
311
 
336
312
  @staticmethod
337
- def convert_tool_result_to_anthropic(
338
- tool_result: CallToolResult, tool_use_id: str
339
- ) -> ToolResultBlockParam:
313
+ def convert_tool_result_to_anthropic(tool_result: CallToolResult, tool_use_id: str) -> ToolResultBlockParam:
340
314
  """
341
315
  Convert an MCP CallToolResult to an Anthropic ToolResultBlockParam.
342
316
 
@@ -353,22 +327,16 @@ class AnthropicConverter:
353
327
  for item in tool_result.content:
354
328
  if isinstance(item, EmbeddedResource):
355
329
  # For embedded resources, always use text mode in tool results
356
- resource_block = AnthropicConverter._convert_embedded_resource(
357
- item, document_mode=False
358
- )
330
+ resource_block = AnthropicConverter._convert_embedded_resource(item, document_mode=False)
359
331
  anthropic_content.append(resource_block)
360
332
  elif isinstance(item, (TextContent, ImageContent)):
361
333
  # For text and image, use standard conversion
362
- blocks = AnthropicConverter._convert_content_items(
363
- [item], document_mode=False
364
- )
334
+ blocks = AnthropicConverter._convert_content_items([item], document_mode=False)
365
335
  anthropic_content.extend(blocks)
366
336
 
367
337
  # If we ended up with no valid content blocks, create a placeholder
368
338
  if not anthropic_content:
369
- anthropic_content = [
370
- TextBlockParam(type="text", text="[No content in tool result]")
371
- ]
339
+ anthropic_content = [TextBlockParam(type="text", text="[No content in tool result]")]
372
340
 
373
341
  # Create the tool result block
374
342
  return ToolResultBlockParam(
@@ -401,24 +369,18 @@ class AnthropicConverter:
401
369
  # Process each content item in the result
402
370
  for item in result.content:
403
371
  if isinstance(item, (TextContent, ImageContent)):
404
- blocks = AnthropicConverter._convert_content_items(
405
- [item], document_mode=False
406
- )
372
+ blocks = AnthropicConverter._convert_content_items([item], document_mode=False)
407
373
  tool_result_blocks.extend(blocks)
408
374
  elif isinstance(item, EmbeddedResource):
409
375
  resource_content = item.resource
410
376
 
411
377
  # Text resources go in tool results, others go as separate blocks
412
378
  if isinstance(resource_content, TextResourceContents):
413
- block = AnthropicConverter._convert_embedded_resource(
414
- item, document_mode=False
415
- )
379
+ block = AnthropicConverter._convert_embedded_resource(item, document_mode=False)
416
380
  tool_result_blocks.append(block)
417
381
  else:
418
382
  # For binary resources like PDFs, add as separate block
419
- block = AnthropicConverter._convert_embedded_resource(
420
- item, document_mode=True
421
- )
383
+ block = AnthropicConverter._convert_embedded_resource(item, document_mode=True)
422
384
  separate_blocks.append(block)
423
385
 
424
386
  # Create the tool result block if we have content
@@ -437,11 +399,7 @@ class AnthropicConverter:
437
399
  ToolResultBlockParam(
438
400
  type="tool_result",
439
401
  tool_use_id=tool_use_id,
440
- content=[
441
- TextBlockParam(
442
- type="text", text="[No content in tool result]"
443
- )
444
- ],
402
+ content=[TextBlockParam(type="text", text="[No content in tool result]")],
445
403
  is_error=result.isError,
446
404
  )
447
405
  )
@@ -1,23 +1,22 @@
1
- from typing import List, Union, Optional, Tuple, Dict, Any
1
+ from typing import Any, Dict, List, Optional, Tuple, Union
2
2
 
3
3
  from mcp.types import (
4
- TextContent,
5
- ImageContent,
6
- EmbeddedResource,
7
4
  CallToolResult,
5
+ EmbeddedResource,
6
+ ImageContent,
8
7
  PromptMessage,
8
+ TextContent,
9
9
  )
10
- from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
10
+
11
+ from mcp_agent.logging.logger import get_logger
11
12
  from mcp_agent.mcp.mime_utils import (
12
13
  guess_mime_type,
13
- is_text_mime_type,
14
14
  is_image_mime_type,
15
+ is_text_mime_type,
15
16
  )
17
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
16
18
  from mcp_agent.mcp.resource_utils import extract_title_from_uri
17
19
 
18
-
19
- from mcp_agent.logging.logger import get_logger
20
-
21
20
  _logger = get_logger("multipart_converter_openai")
22
21
 
23
22
  # Define type aliases for content blocks
@@ -39,16 +38,10 @@ class OpenAIConverter:
39
38
  Returns:
40
39
  True if the MIME type is generally supported, False otherwise
41
40
  """
42
- return (
43
- mime_type is not None
44
- and is_image_mime_type(mime_type)
45
- and mime_type != "image/svg+xml"
46
- )
41
+ return mime_type is not None and is_image_mime_type(mime_type) and mime_type != "image/svg+xml"
47
42
 
48
43
  @staticmethod
49
- def convert_to_openai(
50
- multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False
51
- ) -> OpenAIMessage:
44
+ def convert_to_openai(multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False) -> OpenAIMessage:
52
45
  """
53
46
  Convert a PromptMessageMultipart message to OpenAI API format.
54
47
 
@@ -104,9 +97,7 @@ class OpenAIConverter:
104
97
 
105
98
  # Handle input_audio if implemented
106
99
  elif hasattr(item, "type") and getattr(item, "type") == "input_audio":
107
- _logger.warning(
108
- "Input audio content not supported in standard OpenAI types"
109
- )
100
+ _logger.warning("Input audio content not supported in standard OpenAI types")
110
101
  fallback_text = "[Audio content not directly supported]"
111
102
  content_blocks.append({"type": "text", "text": fallback_text})
112
103
 
@@ -127,11 +118,7 @@ class OpenAIConverter:
127
118
  return {"role": role, "content": ""}
128
119
 
129
120
  # If we only have one text content and it's empty, return an empty string for content
130
- if (
131
- len(content_blocks) == 1
132
- and content_blocks[0]["type"] == "text"
133
- and not content_blocks[0]["text"]
134
- ):
121
+ if len(content_blocks) == 1 and content_blocks[0]["type"] == "text" and not content_blocks[0]["text"]:
135
122
  return {"role": role, "content": ""}
136
123
 
137
124
  # If concatenate_text_blocks is True, combine adjacent text blocks
@@ -180,9 +167,7 @@ class OpenAIConverter:
180
167
  return combined_blocks
181
168
 
182
169
  @staticmethod
183
- def convert_prompt_message_to_openai(
184
- message: PromptMessage, concatenate_text_blocks: bool = False
185
- ) -> OpenAIMessage:
170
+ def convert_prompt_message_to_openai(message: PromptMessage, concatenate_text_blocks: bool = False) -> OpenAIMessage:
186
171
  """
187
172
  Convert a standard PromptMessage to OpenAI API format.
188
173
 
@@ -265,9 +250,7 @@ class OpenAIConverter:
265
250
  elif hasattr(resource_content, "blob"):
266
251
  return {
267
252
  "type": "image_url",
268
- "image_url": {
269
- "url": f"data:{mime_type};base64,{resource_content.blob}"
270
- },
253
+ "image_url": {"url": f"data:{mime_type};base64,{resource_content.blob}"},
271
254
  }
272
255
  else:
273
256
  return {"type": "text", "text": f"[Image missing data: {title}]"}
@@ -291,20 +274,12 @@ class OpenAIConverter:
291
274
 
292
275
  # Handle SVG (convert to text)
293
276
  elif mime_type == "image/svg+xml" and hasattr(resource_content, "text"):
294
- file_text = (
295
- f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
296
- f"{resource_content.text}\n"
297
- f"</fastagent:file>"
298
- )
277
+ file_text = f'<fastagent:file title="{title}" mimetype="{mime_type}">\n' f"{resource_content.text}\n" f"</fastagent:file>"
299
278
  return {"type": "text", "text": file_text}
300
279
 
301
280
  # Handle text files
302
281
  elif is_text_mime_type(mime_type) and hasattr(resource_content, "text"):
303
- file_text = (
304
- f'<fastagent:file title="{title}" mimetype="{mime_type}">\n'
305
- f"{resource_content.text}\n"
306
- f"</fastagent:file>"
307
- )
282
+ file_text = f'<fastagent:file title="{title}" mimetype="{mime_type}">\n' f"{resource_content.text}\n" f"</fastagent:file>"
308
283
  return {"type": "text", "text": file_text}
309
284
 
310
285
  # Default fallback for text resources
@@ -349,11 +324,7 @@ class OpenAIConverter:
349
324
  if block.get("type") == "text":
350
325
  text_parts.append(block.get("text", ""))
351
326
 
352
- return (
353
- " ".join(text_parts)
354
- if text_parts
355
- else "[Complex content converted to text]"
356
- )
327
+ return " ".join(text_parts) if text_parts else "[Complex content converted to text]"
357
328
 
358
329
  @staticmethod
359
330
  def convert_tool_result_to_openai(
@@ -399,14 +370,10 @@ class OpenAIConverter:
399
370
  if text_content:
400
371
  # Convert text content to OpenAI format
401
372
  temp_multipart = PromptMessageMultipart(role="user", content=text_content)
402
- converted = OpenAIConverter.convert_to_openai(
403
- temp_multipart, concatenate_text_blocks=concatenate_text_blocks
404
- )
373
+ converted = OpenAIConverter.convert_to_openai(temp_multipart, concatenate_text_blocks=concatenate_text_blocks)
405
374
 
406
375
  # Extract text from content blocks
407
- tool_message_content = OpenAIConverter._extract_text_from_content_blocks(
408
- converted.get("content", "")
409
- )
376
+ tool_message_content = OpenAIConverter._extract_text_from_content_blocks(converted.get("content", ""))
410
377
 
411
378
  if not tool_message_content:
412
379
  tool_message_content = "[Tool returned non-text content]"
@@ -423,9 +390,7 @@ class OpenAIConverter:
423
390
  return tool_message
424
391
 
425
392
  # Process non-text content as a separate user message
426
- non_text_multipart = PromptMessageMultipart(
427
- role="user", content=non_text_content
428
- )
393
+ non_text_multipart = PromptMessageMultipart(role="user", content=non_text_content)
429
394
 
430
395
  # Convert to OpenAI format
431
396
  user_message = OpenAIConverter.convert_to_openai(non_text_multipart)