-
Notifications
You must be signed in to change notification settings - Fork 153
Description
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
Actioninherits fromSchemaandDiscriminatedUnionMixinDiscriminatedUnionMixindefines akindfield as a Pydantic field:kind: str = Field(default="")- The
to_mcp_schema()method usesmodel_json_schema()which includes all Pydantic fields, includingkind - The
_process_schema_node()function processes the schema but doesn't filter out thekindfield
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 result2. 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 implementationThis 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
- Cleaner LLM schemas: LLMs won't see the internal
kinddiscriminator field - Better separation of concerns: Keep
kindas a Pydantic field for internal use, but exclude it from external schemas - Simpler inheritance hierarchy: Having
Schemainherit fromDiscriminatedUnionMixinmakes the relationship explicit
Testing
After implementation, verify:
- The
kindfield is excluded from all MCP schemas generated byto_mcp_schema() - The
kindfield still works correctly for Pydantic validation and discriminated unions - All existing tests pass
- Tool schemas sent to LLMs (via
to_openai_tool()andto_responses_tool()) don't includekind
Files to Modify
openhands/sdk/tool/schema.py- UpdateSchemaclass andto_mcp_schema()method- Tests in
tests/sdk/tool/- Add tests to verifykindis excluded from MCP schemas