Skip to content

Feature Request: Configurable Return Value Selection for CrewAI Adapter #55

@Viewer-HX

Description

@Viewer-HX

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

  1. Constructor parameters: Add configuration options to CrewAIAdapter.__init__()
  2. Result processing: Extract content processing logic into a separate method
  3. Safe indexing: Add bounds checking and meaningful error messages
  4. 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

  1. Backward Compatibility: Default behavior remains unchanged
  2. Flexibility: Support various content selection strategies
  3. Safety: Comprehensive bounds checking and error handling
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions