-
Notifications
You must be signed in to change notification settings - Fork 40
Description
Problem Description
Currently, the CrewAIAdapter
in src/mcpadapt/crewai_adapter.py
only returns the first content item from MCP tool results (line 81: return func(filtered_kwargs).content[0].text
). This limitation prevents users from accessing additional content when MCP tools return multiple values.
Current Issue Location:
- File:
src/mcpadapt/crewai_adapter.py
- Line: 81
- Code:
return func(filtered_kwargs).content[0].text
Motivation
Multi-part responses: Some MCP tools return structured responses with multiple content items. For example, I have a self-made MCP server, its function is to fetch context from specific url, it would return 4 values (e.g., {page_id, html content, plain text content, attachment name list}), since it's behind the company firewall, I cannot share the detail of MCP server here (sorry).
Proposed Solution
Enhance the CrewAIAdapter
with configurable return value selection:
class CrewAIAdapter(ToolAdapter):
def __init__(
self,
return_mode: Literal["first", "all", "index", "slice"] = "first",
return_index: Optional[int] = None,
return_slice: Optional[tuple[Optional[int], Optional[int]]] = None,
separator: str = "\n"
):
"""
Args:
return_mode: How to handle multiple content items
- "first": Return first content (current behavior, default)
- "all": Return all content joined by separator
- "index": Return content at specific index
- "slice": Return content slice
return_index: Index when return_mode="index"
return_slice: Slice range when return_mode="slice"
separator: Join separator for multiple content items
"""
🔧 Implementation Details
Core Changes
- Constructor parameters: Add configuration options to
CrewAIAdapter.__init__()
- Result processing: Extract content processing logic into a separate method
- Safe indexing: Add bounds checking and meaningful error messages
- Flexible joining: Support custom separators for multi-content responses
Example Usage
# Default behavior (backward compatible)
adapter = CrewAIAdapter()
# Return all content joined with newlines
adapter = CrewAIAdapter(return_mode="all")
# Return second content item
adapter = CrewAIAdapter(return_mode="index", return_index=1)
# Return last content item
adapter = CrewAIAdapter(return_mode="index", return_index=-1)
# Return first 3 content items
adapter = CrewAIAdapter(return_mode="slice", return_slice=(0, 3))
Error Handling
- Index out of bounds: Clear error messages with valid range information
- Empty content: Graceful handling with appropriate defaults
- Invalid configuration: Parameter validation at initialization
Benefits
- Backward Compatibility: Default behavior remains unchanged
- Flexibility: Support various content selection strategies
- Safety: Comprehensive bounds checking and error handling
- Usability: Clear error messages and intuitive configuration
Implementation Example
Here's a complete implementation example:
from typing import Any, Callable, Coroutine, Type, Literal, Optional
import jsonref
import mcp
from crewai.tools import BaseTool
from pydantic import BaseModel
from mcpadapt.core import ToolAdapter
from mcpadapt.utils.modeling import (
create_model_from_json_schema,
resolve_refs_and_remove_defs,
)
class CrewAIAdapter(ToolAdapter):
"""Enhanced CrewAI adapter with configurable return value selection."""
def __init__(
self,
return_mode: Literal["first", "all", "index", "slice"] = "first",
return_index: Optional[int] = None,
return_slice: Optional[tuple[Optional[int], Optional[int]]] = None,
separator: str = "\n",
safe_mode: bool = True
):
"""
Initialize CrewAI adapter with flexible return value configuration.
Args:
return_mode: How to handle multiple content items
return_index: Index when return_mode="index"
return_slice: Slice range when return_mode="slice"
separator: Join separator for multiple content items
safe_mode: Enable strict error checking
"""
self.return_mode = return_mode
self.return_index = return_index
self.return_slice = return_slice
self.separator = separator
self.safe_mode = safe_mode
self._validate_config()
def _validate_config(self):
"""Validate configuration parameters."""
if self.return_mode == "index" and self.return_index is None:
raise ValueError("return_index must be specified when return_mode='index'")
if self.return_mode == "slice" and self.return_slice is None:
raise ValueError("return_slice must be specified when return_mode='slice'")
def _process_result(self, result) -> str:
"""Process MCP tool result based on configuration."""
contents = result.content
content_count = len(contents)
if content_count == 0:
if self.safe_mode and self.return_mode in ["index", "slice"]:
raise ValueError("Tool returned no content, but specific index/slice was requested")
return ""
if self.return_mode == "first":
return contents[0].text
elif self.return_mode == "all":
texts = [content.text for content in contents if hasattr(content, 'text')]
return self.separator.join(texts)
elif self.return_mode == "index":
if self.return_index >= content_count or self.return_index < -content_count:
raise IndexError(
f"Index {self.return_index} out of range. "
f"Tool returned {content_count} content(s), "
f"valid indices are 0 to {content_count-1} or "
f"-{content_count} to -1"
)
return contents[self.return_index].text
elif self.return_mode == "slice":
start, end = self.return_slice
sliced_contents = contents[start:end]
if not sliced_contents and self.safe_mode:
raise IndexError(f"Slice {self.return_slice} returned no content")
texts = [content.text for content in sliced_contents if hasattr(content, 'text')]
return self.separator.join(texts)
def adapt(
self,
func: Callable[[dict | None], mcp.types.CallToolResult],
mcp_tool: mcp.types.Tool,
) -> BaseTool:
"""Adapt MCP tool to CrewAI tool with enhanced return value handling."""
mcp_tool.inputSchema = resolve_refs_and_remove_defs(mcp_tool.inputSchema)
ToolInput = create_model_from_json_schema(mcp_tool.inputSchema)
adapter_self = self
class EnhancedCrewAIMCPTool(BaseTool):
name: str = mcp_tool.name
description: str = mcp_tool.description or ""
args_schema: Type[BaseModel] = ToolInput
def _run(self, *args: Any, **kwargs: Any) -> Any:
# ... existing parameter filtering logic ...
filtered_kwargs: dict[str, Any] = {}
schema_properties = mcp_tool.inputSchema.get("properties", {})
for key, value in kwargs.items():
if value is None and key in schema_properties:
prop_schema = schema_properties[key]
if isinstance(prop_schema.get("type"), list):
if "null" in prop_schema["type"]:
filtered_kwargs[key] = value
elif "anyOf" in prop_schema:
if any(opt.get("type") == "null" for opt in prop_schema["anyOf"]):
filtered_kwargs[key] = value
else:
filtered_kwargs[key] = value
result = func(filtered_kwargs)
return adapter_self._process_result(result)
def _generate_description(self):
args_schema = {
k: v for k, v in jsonref.replace_refs(
self.args_schema.model_json_schema()
).items() if k != "$defs"
}
self.description = f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nTool Description: {self.description}"
return EnhancedCrewAIMCPTool()
Can I contribute this feature? I'm happy to implement this enhancement and submit a pull request if you agree and we align on the approach.
Thank you