Skip to content

Tool.to_mcp_schema() includes 'kind' field in properties which should not be sent to LLMs #640

@xingyaoww

Description

@xingyaoww

Problem

The Tool.to_mcp_schema() method outputs a MCP schema where the "properties" field contains a kind field. This kind field should NOT be included in tool schemas sent to LLMs, as it's an internal discriminator field used by Pydantic for polymorphism.

Current Behavior

When calling Action.to_mcp_schema() on any Action subclass, the resulting schema includes kind in its properties:

from openhands.sdk.tool.schema import Action
from pydantic import Field

class TestAction(Action):
    """Test action class."""
    command: str = Field(description="Command to execute")

schema = TestAction.to_mcp_schema()
print(schema["properties"].keys())
# Output: dict_keys(['kind', 'command'])

Full schema output:

{
  "type": "object",
  "description": "Test action class.",
  "properties": {
    "kind": {
      "type": "string"
    },
    "command": {
      "type": "string",
      "description": "Command to execute"
    }
  },
  "required": [
    "command"
  ]
}

The kind field should not be present in the schema sent to LLMs.

Root Cause

  1. Action inherits from Schema and DiscriminatedUnionMixin
  2. DiscriminatedUnionMixin defines a kind field as a Pydantic field: kind: str = Field(default="")
  3. The to_mcp_schema() method uses model_json_schema() which includes all Pydantic fields, including kind
  4. The _process_schema_node() function processes the schema but doesn't filter out the kind field

Proposed Solution

We should modify the schema generation to exclude the kind field from the MCP schema output. The recommended approach involves two changes:

1. Update Schema.to_mcp_schema() to exclude kind

In openhands/sdk/tool/schema.py, modify the to_mcp_schema() method to filter out the kind field:

@classmethod
def to_mcp_schema(cls) -> dict[str, Any]:
    """Convert to JSON schema format compatible with MCP."""
    full_schema = cls.model_json_schema()
    # Process the schema to simplify and resolve $ref
    result = _process_schema_node(full_schema, full_schema.get("$defs", {}))
    
    # Remove 'kind' from properties if present (discriminator field, not for LLM)
    if "properties" in result and "kind" in result["properties"]:
        result["properties"].pop("kind")
        # Also remove from required if present
        if "required" in result and "kind" in result["required"]:
            result["required"].remove("kind")
    
    return result

2. Refactor Schema to directly inherit from DiscriminatedUnionMixin

Currently, Action and Observation inherit from both Schema and DiscriminatedUnionMixin. We could simplify this by having Schema directly inherit from DiscriminatedUnionMixin:

class Schema(DiscriminatedUnionMixin):
    """Base schema for input action / output observation."""
    
    model_config = ConfigDict(extra="forbid", frozen=True)
    
    # ... rest of the implementation

This would make it clearer that all schemas have the kind discriminator, and it would be natural to exclude it in the to_mcp_schema() method within the same class.

Benefits

  1. Cleaner LLM schemas: LLMs won't see the internal kind discriminator field
  2. Better separation of concerns: Keep kind as a Pydantic field for internal use, but exclude it from external schemas
  3. Simpler inheritance hierarchy: Having Schema inherit from DiscriminatedUnionMixin makes the relationship explicit

Testing

After implementation, verify:

  1. The kind field is excluded from all MCP schemas generated by to_mcp_schema()
  2. The kind field still works correctly for Pydantic validation and discriminated unions
  3. All existing tests pass
  4. Tool schemas sent to LLMs (via to_openai_tool() and to_responses_tool()) don't include kind

Files to Modify

  • openhands/sdk/tool/schema.py - Update Schema class and to_mcp_schema() method
  • Tests in tests/sdk/tool/ - Add tests to verify kind is excluded from MCP schemas

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingopenhandsSolving the issue with OpenHands.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions