801 lines
36 KiB
Python
801 lines
36 KiB
Python
"""
|
|
Test MCP tool schema validation integration.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from letta.functions.mcp_client.types import MCPTool, MCPToolHealth
|
|
from letta.functions.schema_generator import generate_tool_schema_for_mcp
|
|
from letta.functions.schema_validator import SchemaHealth, validate_complete_json_schema
|
|
from letta.server.rest_api.dependencies import HeaderParams
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_tools_get_health_status():
|
|
"""Test that MCP tools receive health status when listed."""
|
|
from letta.server.server import SyncServer
|
|
|
|
# Create mock tools with different schema types
|
|
mock_tools = [
|
|
# Strict compliant tool
|
|
MCPTool(
|
|
name="strict_tool",
|
|
inputSchema={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"], "additionalProperties": False},
|
|
),
|
|
# Non-strict tool (free-form object)
|
|
MCPTool(
|
|
name="non_strict_tool",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {"message": {"type": "object", "additionalProperties": {}}}, # Free-form object
|
|
"required": ["message"],
|
|
"additionalProperties": False,
|
|
},
|
|
),
|
|
# Invalid tool (missing type)
|
|
MCPTool(name="invalid_tool", inputSchema={"properties": {"data": {"type": "string"}}, "required": ["data"]}),
|
|
]
|
|
|
|
# Mock the server and client
|
|
mock_client = AsyncMock()
|
|
mock_client.list_tools = AsyncMock(return_value=mock_tools)
|
|
|
|
# Call the method directly
|
|
actual_server = SyncServer.__new__(SyncServer)
|
|
actual_server.mcp_clients = {"test_server": mock_client}
|
|
|
|
tools = await actual_server.get_tools_from_mcp_server("test_server")
|
|
|
|
# Verify health status was added
|
|
assert len(tools) == 3
|
|
|
|
# Check strict tool
|
|
strict_tool = tools[0]
|
|
assert strict_tool.name == "strict_tool"
|
|
assert strict_tool.health is not None
|
|
assert strict_tool.health.status == SchemaHealth.STRICT_COMPLIANT.value
|
|
assert strict_tool.health.reasons == []
|
|
|
|
# Check non-strict tool
|
|
non_strict_tool = tools[1]
|
|
assert non_strict_tool.name == "non_strict_tool"
|
|
assert non_strict_tool.health is not None
|
|
assert non_strict_tool.health.status == SchemaHealth.NON_STRICT_ONLY.value
|
|
assert len(non_strict_tool.health.reasons) > 0
|
|
assert any("additionalProperties" in reason for reason in non_strict_tool.health.reasons)
|
|
|
|
# Check invalid tool
|
|
invalid_tool = tools[2]
|
|
assert invalid_tool.name == "invalid_tool"
|
|
assert invalid_tool.health is not None
|
|
assert invalid_tool.health.status == SchemaHealth.INVALID.value
|
|
assert len(invalid_tool.health.reasons) > 0
|
|
assert any("type" in reason for reason in invalid_tool.health.reasons)
|
|
|
|
|
|
def test_empty_object_in_required_marked_invalid():
|
|
"""Test that required properties allowing empty objects are marked INVALID."""
|
|
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"config": {"type": "object", "properties": {}, "required": [], "additionalProperties": False} # Empty object schema
|
|
},
|
|
"required": ["config"], # Required but allows empty object
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
status, reasons = validate_complete_json_schema(schema)
|
|
|
|
assert status == SchemaHealth.INVALID
|
|
assert any("empty object" in reason for reason in reasons)
|
|
assert any("config" in reason for reason in reasons)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_mcp_tool_accepts_non_strict_schemas():
|
|
"""Test that adding MCP tools with non-strict schemas is allowed."""
|
|
from letta.server.rest_api.routers.v1.tools import add_mcp_tool
|
|
from letta.settings import tool_settings
|
|
|
|
# Mock a non-strict tool
|
|
non_strict_tool = MCPTool(
|
|
name="test_tool",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {"message": {"type": "object"}}, # Missing additionalProperties: false
|
|
"required": ["message"],
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
non_strict_tool.health = MCPToolHealth(status=SchemaHealth.NON_STRICT_ONLY.value, reasons=["Missing additionalProperties for message"])
|
|
|
|
# Mock server response
|
|
with patch("letta.server.rest_api.routers.v1.tools.get_letta_server") as mock_get_server:
|
|
with patch.object(tool_settings, "mcp_read_from_config", True): # Ensure we're using config path
|
|
mock_server = AsyncMock()
|
|
mock_server.get_tools_from_mcp_server = AsyncMock(return_value=[non_strict_tool])
|
|
mock_server.user_manager.get_user_or_default = MagicMock()
|
|
mock_server.tool_manager.create_mcp_tool_async = AsyncMock(return_value=non_strict_tool)
|
|
mock_get_server.return_value = mock_server
|
|
|
|
# Should accept non-strict schema without raising an exception
|
|
headers = HeaderParams(actor_id="test_user")
|
|
result = await add_mcp_tool(mcp_server_name="test_server", mcp_tool_name="test_tool", server=mock_server, headers=headers)
|
|
|
|
# Verify the tool was added successfully
|
|
assert result is not None
|
|
|
|
# Verify create_mcp_tool_async was called with the right parameters
|
|
mock_server.tool_manager.create_mcp_tool_async.assert_called_once()
|
|
call_args = mock_server.tool_manager.create_mcp_tool_async.call_args
|
|
assert call_args.kwargs["mcp_server_name"] == "test_server"
|
|
|
|
|
|
@pytest.mark.skip(reason="Allowing invalid schemas to be attached")
|
|
@pytest.mark.asyncio
|
|
async def test_add_mcp_tool_rejects_invalid_schemas():
|
|
"""Test that adding MCP tools with invalid schemas is rejected."""
|
|
from fastapi import HTTPException
|
|
|
|
from letta.server.rest_api.routers.v1.tools import add_mcp_tool
|
|
from letta.settings import tool_settings
|
|
|
|
# Mock an invalid tool
|
|
invalid_tool = MCPTool(
|
|
name="test_tool",
|
|
inputSchema={
|
|
"properties": {"data": {"type": "string"}},
|
|
"required": ["data"],
|
|
# Missing "type": "object"
|
|
},
|
|
)
|
|
invalid_tool.health = MCPToolHealth(status=SchemaHealth.INVALID.value, reasons=["Missing 'type' at root level"])
|
|
|
|
# Mock server response
|
|
with patch("letta.server.rest_api.routers.v1.tools.get_letta_server") as mock_get_server:
|
|
with patch.object(tool_settings, "mcp_read_from_config", True): # Ensure we're using config path
|
|
mock_server = AsyncMock()
|
|
mock_server.get_tools_from_mcp_server = AsyncMock(return_value=[invalid_tool])
|
|
mock_server.user_manager.get_user_or_default = MagicMock()
|
|
mock_get_server.return_value = mock_server
|
|
|
|
# Should raise HTTPException for invalid schema
|
|
headers = HeaderParams(actor_id="test_user")
|
|
from letta.errors import LettaInvalidMCPSchemaError
|
|
|
|
with pytest.raises(LettaInvalidMCPSchemaError) as exc_info:
|
|
await add_mcp_tool(mcp_server_name="test_server", mcp_tool_name="test_tool", server=mock_server, headers=headers)
|
|
|
|
assert "invalid schema" in exc_info.value.message.lower()
|
|
assert exc_info.value.details["mcp_tool_name"] == "test_tool"
|
|
assert exc_info.value.details["reasons"] == ["Missing 'type' at root level"]
|
|
|
|
|
|
def test_mcp_schema_healing_for_optional_fields():
|
|
"""Test that optional fields in MCP schemas are healed only in strict mode."""
|
|
# Create an MCP tool with optional field 'b'
|
|
mcp_tool = MCPTool(
|
|
name="test_tool",
|
|
description="A test tool",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"a": {"type": "integer", "description": "Required field"},
|
|
"b": {"type": "integer", "description": "Optional field"},
|
|
},
|
|
"required": ["a"], # Only 'a' is required
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
|
|
# Generate schema without strict mode - should NOT heal optional fields
|
|
non_strict_schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=False)
|
|
assert "a" in non_strict_schema["parameters"]["required"]
|
|
assert "b" not in non_strict_schema["parameters"]["required"] # Should remain optional
|
|
assert non_strict_schema["parameters"]["properties"]["b"]["type"] == "integer" # No null added
|
|
|
|
# Validate non-strict schema - should still be STRICT_COMPLIANT because validator is relaxed
|
|
status, _ = validate_complete_json_schema(non_strict_schema["parameters"])
|
|
assert status == SchemaHealth.STRICT_COMPLIANT
|
|
|
|
# Generate schema with strict mode - should heal optional fields
|
|
strict_schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=True)
|
|
assert strict_schema["strict"] is True
|
|
assert "a" in strict_schema["parameters"]["required"]
|
|
assert "b" in strict_schema["parameters"]["required"] # Now required
|
|
assert set(strict_schema["parameters"]["properties"]["b"]["type"]) == {"integer", "null"} # Now accepts null
|
|
|
|
# Validate strict schema
|
|
status, _ = validate_complete_json_schema(strict_schema["parameters"])
|
|
assert status == SchemaHealth.STRICT_COMPLIANT # Should pass strict mode
|
|
|
|
|
|
def test_mcp_schema_healing_with_anyof():
|
|
"""Test schema healing for fields with anyOf that include optional types."""
|
|
mcp_tool = MCPTool(
|
|
name="test_tool",
|
|
description="A test tool",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"a": {"type": "string", "description": "Required field"},
|
|
"b": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"description": "Optional field with anyOf",
|
|
},
|
|
},
|
|
"required": ["a"], # Only 'a' is required
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
|
|
# Generate strict schema
|
|
strict_schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=True)
|
|
assert strict_schema["strict"] is True
|
|
assert "a" in strict_schema["parameters"]["required"]
|
|
assert "b" in strict_schema["parameters"]["required"] # Now required
|
|
# Type should be flattened array with deduplication
|
|
assert set(strict_schema["parameters"]["properties"]["b"]["type"]) == {"integer", "null"}
|
|
|
|
# Validate strict schema
|
|
status, _ = validate_complete_json_schema(strict_schema["parameters"])
|
|
assert status == SchemaHealth.STRICT_COMPLIANT
|
|
|
|
|
|
def test_mcp_schema_type_deduplication():
|
|
"""Test that duplicate types are deduplicated in schema generation."""
|
|
mcp_tool = MCPTool(
|
|
name="test_tool",
|
|
description="A test tool",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"field": {
|
|
"anyOf": [
|
|
{"type": "string"},
|
|
{"type": "string"}, # Duplicate
|
|
{"type": "null"},
|
|
],
|
|
"description": "Field with duplicate types",
|
|
},
|
|
},
|
|
"required": [],
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
|
|
# Generate strict schema
|
|
strict_schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=True)
|
|
|
|
# Check that duplicates were removed
|
|
field_types = strict_schema["parameters"]["properties"]["field"]["type"]
|
|
assert len(field_types) == len(set(field_types)) # No duplicates
|
|
assert set(field_types) == {"string", "null"}
|
|
|
|
|
|
def test_mcp_schema_healing_preserves_existing_null():
|
|
"""Test that schema healing doesn't add duplicate null when it already exists."""
|
|
mcp_tool = MCPTool(
|
|
name="test_tool",
|
|
description="A test tool",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"field": {
|
|
"type": ["string", "null"], # Already has null
|
|
"description": "Field that already accepts null",
|
|
},
|
|
},
|
|
"required": [], # Optional
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
|
|
# Generate strict schema
|
|
strict_schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=True)
|
|
|
|
# Check that null wasn't duplicated
|
|
field_types = strict_schema["parameters"]["properties"]["field"]["type"]
|
|
null_count = field_types.count("null")
|
|
assert null_count == 1 # Should only have one null
|
|
|
|
|
|
def test_mcp_schema_healing_all_fields_already_required():
|
|
"""Test that schema healing works correctly when all fields are already required."""
|
|
mcp_tool = MCPTool(
|
|
name="test_tool",
|
|
description="A test tool",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"a": {"type": "string", "description": "Field A"},
|
|
"b": {"type": "integer", "description": "Field B"},
|
|
},
|
|
"required": ["a", "b"], # All fields already required
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
|
|
# Generate strict schema
|
|
strict_schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=True)
|
|
|
|
# Check that fields remain as-is
|
|
assert set(strict_schema["parameters"]["required"]) == {"a", "b"}
|
|
assert strict_schema["parameters"]["properties"]["a"]["type"] == "string"
|
|
assert strict_schema["parameters"]["properties"]["b"]["type"] == "integer"
|
|
|
|
# Should be strict compliant
|
|
status, _ = validate_complete_json_schema(strict_schema["parameters"])
|
|
assert status == SchemaHealth.STRICT_COMPLIANT
|
|
|
|
|
|
def test_mcp_schema_with_uuid_format():
|
|
"""Test handling of UUID format in anyOf schemas (root cause of duplicate string types)."""
|
|
mcp_tool = MCPTool(
|
|
name="test_tool",
|
|
description="A test tool with UUID formatted field",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"session_id": {
|
|
"anyOf": [{"type": "string"}, {"format": "uuid", "type": "string"}, {"type": "null"}],
|
|
"description": "Session ID that can be a string, UUID, or null",
|
|
},
|
|
},
|
|
"required": [],
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
|
|
# Generate strict schema
|
|
strict_schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=True)
|
|
|
|
# Check that string type is not duplicated
|
|
session_props = strict_schema["parameters"]["properties"]["session_id"]
|
|
assert set(session_props["type"]) == {"string", "null"} # No duplicate strings
|
|
# Format should NOT be preserved because field is optional (has null type)
|
|
assert "format" not in session_props
|
|
|
|
# Should be in required array (healed)
|
|
assert "session_id" in strict_schema["parameters"]["required"]
|
|
|
|
# Should be strict compliant
|
|
status, _ = validate_complete_json_schema(strict_schema["parameters"])
|
|
assert status == SchemaHealth.STRICT_COMPLIANT
|
|
|
|
|
|
def test_mcp_schema_healing_only_in_strict_mode():
|
|
"""Test that schema healing only happens in strict mode."""
|
|
mcp_tool = MCPTool(
|
|
name="test_tool",
|
|
description="Test that healing only happens in strict mode",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"required_field": {"type": "string", "description": "Already required"},
|
|
"optional_field1": {"type": "integer", "description": "Optional 1"},
|
|
"optional_field2": {"type": "boolean", "description": "Optional 2"},
|
|
},
|
|
"required": ["required_field"],
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
|
|
# Test with strict=False - no healing
|
|
non_strict = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=False)
|
|
assert "strict" not in non_strict # strict flag not set
|
|
assert non_strict["parameters"]["required"] == ["required_field"] # Only originally required field
|
|
assert non_strict["parameters"]["properties"]["required_field"]["type"] == "string"
|
|
assert non_strict["parameters"]["properties"]["optional_field1"]["type"] == "integer" # No null
|
|
assert non_strict["parameters"]["properties"]["optional_field2"]["type"] == "boolean" # No null
|
|
|
|
# Test with strict=True - healing happens
|
|
strict = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=True)
|
|
assert strict["strict"] is True # strict flag is set
|
|
assert set(strict["parameters"]["required"]) == {"required_field", "optional_field1", "optional_field2"}
|
|
assert strict["parameters"]["properties"]["required_field"]["type"] == "string"
|
|
assert set(strict["parameters"]["properties"]["optional_field1"]["type"]) == {"integer", "null"}
|
|
assert set(strict["parameters"]["properties"]["optional_field2"]["type"]) == {"boolean", "null"}
|
|
|
|
# Both should be strict compliant (validator is relaxed)
|
|
status1, _ = validate_complete_json_schema(non_strict["parameters"])
|
|
status2, _ = validate_complete_json_schema(strict["parameters"])
|
|
assert status1 == SchemaHealth.STRICT_COMPLIANT
|
|
assert status2 == SchemaHealth.STRICT_COMPLIANT
|
|
|
|
|
|
def test_mcp_schema_with_uuid_format_required_field():
|
|
"""Test that UUID format is preserved for required fields that don't have null type."""
|
|
mcp_tool = MCPTool(
|
|
name="test_tool",
|
|
description="A test tool with required UUID formatted field",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"session_id": {
|
|
"anyOf": [{"type": "string"}, {"format": "uuid", "type": "string"}],
|
|
"description": "Session ID that must be a string with UUID format",
|
|
},
|
|
},
|
|
"required": ["session_id"], # Required field
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
|
|
# Generate strict schema
|
|
strict_schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=False, strict=True)
|
|
|
|
# Check that string type is not duplicated and format IS preserved
|
|
session_props = strict_schema["parameters"]["properties"]["session_id"]
|
|
assert session_props["type"] == ["string"] # No null, no duplicates
|
|
assert "format" in session_props
|
|
assert session_props["format"] == "uuid" # Format should be preserved for non-optional field
|
|
|
|
# Should be in required array
|
|
assert "session_id" in strict_schema["parameters"]["required"]
|
|
|
|
# Should be strict compliant
|
|
status, _ = validate_complete_json_schema(strict_schema["parameters"])
|
|
assert status == SchemaHealth.STRICT_COMPLIANT
|
|
|
|
|
|
def test_mcp_schema_complex_nested_with_defs():
|
|
"""Test generating exact schema with nested Pydantic-like models using $defs."""
|
|
import json
|
|
|
|
from letta.functions.mcp_client.types import MCPToolHealth
|
|
|
|
mcp_tool = MCPTool(
|
|
name="get_vehicle_configuration",
|
|
description="Get vehicle configuration details for a given model type and optional dealer info and customization options.\n\nArgs:\n model_type (VehicleModel): The vehicle model type selection.\n dealer_location (str | None): Dealer location identifier from registration system, if available.\n customization_options (CustomizationData | None): Customization preferences for the vehicle from user selections, if available.\n\nReturns:\n str: The vehicle configuration details.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"model_type": {
|
|
"$ref": "#/$defs/VehicleModel",
|
|
"description": "The vehicle model type selection.",
|
|
"title": "Model Type",
|
|
},
|
|
"dealer_location": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"default": None,
|
|
"description": "Dealer location identifier from registration system, if available.",
|
|
"title": "Dealer Location",
|
|
},
|
|
"customization_options": {
|
|
"anyOf": [{"$ref": "#/$defs/CustomizationData"}, {"type": "null"}],
|
|
"default": None,
|
|
"description": "Customization preferences for the vehicle from user selections, if available.",
|
|
"title": "Customization Options",
|
|
},
|
|
},
|
|
"required": ["model_type"],
|
|
"additionalProperties": False,
|
|
"$defs": {
|
|
"VehicleModel": {
|
|
"type": "string",
|
|
"enum": [
|
|
"sedan",
|
|
"suv",
|
|
"truck",
|
|
"coupe",
|
|
"hatchback",
|
|
"minivan",
|
|
"wagon",
|
|
"convertible",
|
|
"sports",
|
|
"luxury",
|
|
"electric",
|
|
"hybrid",
|
|
"compact",
|
|
"crossover",
|
|
"other",
|
|
"unknown",
|
|
],
|
|
"title": "VehicleModel",
|
|
},
|
|
"Feature": {
|
|
"type": "object",
|
|
"properties": {
|
|
"feature_id": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Feature ID",
|
|
},
|
|
"category_code": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Category Code",
|
|
},
|
|
"variant_code": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Variant Code",
|
|
},
|
|
"package_level": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Package Level",
|
|
},
|
|
},
|
|
"title": "Feature",
|
|
"additionalProperties": False,
|
|
},
|
|
"CustomizationData": {
|
|
"type": "object",
|
|
"properties": {
|
|
"has_premium_package": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Has Premium Package",
|
|
},
|
|
"has_multiple_trims": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Has Multiple Trims",
|
|
},
|
|
"selected_features": {
|
|
"anyOf": [
|
|
{"type": "array", "items": {"$ref": "#/$defs/Feature"}},
|
|
{"type": "null"},
|
|
],
|
|
"default": None,
|
|
"title": "Selected Features",
|
|
},
|
|
},
|
|
"title": "CustomizationData",
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
# Initialize health status to simulate what happens in the server
|
|
mcp_tool.health = MCPToolHealth(status=SchemaHealth.STRICT_COMPLIANT.value, reasons=[])
|
|
|
|
# Generate schema with heartbeat
|
|
schema = generate_tool_schema_for_mcp(mcp_tool, append_heartbeat=True, strict=False)
|
|
|
|
# Add metadata fields (these are normally added by ToolCreate.from_mcp)
|
|
from letta.schemas.tool import MCP_TOOL_METADATA_SCHEMA_STATUS, MCP_TOOL_METADATA_SCHEMA_WARNINGS
|
|
|
|
schema[MCP_TOOL_METADATA_SCHEMA_STATUS] = mcp_tool.health.status
|
|
schema[MCP_TOOL_METADATA_SCHEMA_WARNINGS] = mcp_tool.health.reasons
|
|
|
|
# Expected schema
|
|
expected_schema = {
|
|
"name": "get_vehicle_configuration",
|
|
"description": "Get vehicle configuration details for a given model type and optional dealer info and customization options.\n\nArgs:\n model_type (VehicleModel): The vehicle model type selection.\n dealer_location (str | None): Dealer location identifier from registration system, if available.\n customization_options (CustomizationData | None): Customization preferences for the vehicle from user selections, if available.\n\nReturns:\n str: The vehicle configuration details.",
|
|
"parameters": {
|
|
"$defs": {
|
|
"Feature": {
|
|
"properties": {
|
|
"feature_id": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Feature ID",
|
|
},
|
|
"category_code": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Category Code",
|
|
},
|
|
"variant_code": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Variant Code",
|
|
},
|
|
"package_level": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Package Level",
|
|
},
|
|
},
|
|
"title": "Feature",
|
|
"type": "object",
|
|
"additionalProperties": False,
|
|
},
|
|
"CustomizationData": {
|
|
"properties": {
|
|
"has_premium_package": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Has Premium Package",
|
|
},
|
|
"has_multiple_trims": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Has Multiple Trims",
|
|
},
|
|
"selected_features": {
|
|
"anyOf": [
|
|
{"items": {"$ref": "#/$defs/Feature"}, "type": "array"},
|
|
{"type": "null"},
|
|
],
|
|
"default": None,
|
|
"title": "Selected Features",
|
|
},
|
|
},
|
|
"title": "CustomizationData",
|
|
"type": "object",
|
|
"additionalProperties": False,
|
|
},
|
|
"VehicleModel": {
|
|
"enum": [
|
|
"sedan",
|
|
"suv",
|
|
"truck",
|
|
"coupe",
|
|
"hatchback",
|
|
"minivan",
|
|
"wagon",
|
|
"convertible",
|
|
"sports",
|
|
"luxury",
|
|
"electric",
|
|
"hybrid",
|
|
"compact",
|
|
"crossover",
|
|
"other",
|
|
"unknown",
|
|
],
|
|
"title": "VehicleModel",
|
|
"type": "string",
|
|
},
|
|
},
|
|
"properties": {
|
|
"model_type": {
|
|
"$ref": "#/$defs/VehicleModel",
|
|
"description": "The vehicle model type selection.",
|
|
"title": "Model Type",
|
|
"type": "string",
|
|
},
|
|
"dealer_location": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"default": None,
|
|
"description": "Dealer location identifier from registration system, if available.",
|
|
"title": "Dealer Location",
|
|
},
|
|
"customization_options": {
|
|
"anyOf": [
|
|
{
|
|
"type": "object",
|
|
"title": "CustomizationData",
|
|
"additionalProperties": False,
|
|
"properties": {
|
|
"has_premium_package": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Has Premium Package",
|
|
},
|
|
"has_multiple_trims": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Has Multiple Trims",
|
|
},
|
|
"selected_features": {
|
|
"anyOf": [
|
|
{
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"title": "Feature",
|
|
"additionalProperties": False,
|
|
"properties": {
|
|
"feature_id": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Feature ID",
|
|
},
|
|
"category_code": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Category Code",
|
|
},
|
|
"variant_code": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Variant Code",
|
|
},
|
|
"package_level": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"default": None,
|
|
"title": "Package Level",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{"type": "null"},
|
|
],
|
|
"default": None,
|
|
"title": "Selected Features",
|
|
},
|
|
},
|
|
},
|
|
{"type": "null"},
|
|
],
|
|
"default": None,
|
|
"description": "Customization preferences for the vehicle from user selections, if available.",
|
|
"title": "Customization Options",
|
|
},
|
|
"request_heartbeat": {
|
|
"type": "boolean",
|
|
"description": "Request an immediate heartbeat after function execution. You MUST set this value to `True` if you want to send a follow-up message or run a follow-up tool call (chain multiple tools together). If set to `False` (the default), then the chain of execution will end immediately after this function call.",
|
|
},
|
|
},
|
|
"required": ["model_type", "request_heartbeat"],
|
|
"type": "object",
|
|
"additionalProperties": False,
|
|
},
|
|
"mcp:SCHEMA_STATUS": "STRICT_COMPLIANT",
|
|
"mcp:SCHEMA_WARNINGS": [],
|
|
}
|
|
|
|
# Compare key components
|
|
assert schema["name"] == expected_schema["name"]
|
|
assert schema["description"] == expected_schema["description"]
|
|
assert schema["parameters"]["type"] == expected_schema["parameters"]["type"]
|
|
assert schema["parameters"]["additionalProperties"] == expected_schema["parameters"]["additionalProperties"]
|
|
assert set(schema["parameters"]["required"]) == set(expected_schema["parameters"]["required"])
|
|
|
|
# Check $defs
|
|
assert "$defs" in schema["parameters"]
|
|
assert set(schema["parameters"]["$defs"].keys()) == set(expected_schema["parameters"]["$defs"].keys())
|
|
|
|
# Check properties
|
|
assert "model_type" in schema["parameters"]["properties"]
|
|
assert "dealer_location" in schema["parameters"]["properties"]
|
|
assert "customization_options" in schema["parameters"]["properties"]
|
|
assert "request_heartbeat" in schema["parameters"]["properties"]
|
|
|
|
# Verify model_type property ($ref is inlined)
|
|
model_prop = schema["parameters"]["properties"]["model_type"]
|
|
assert model_prop["type"] == "string"
|
|
assert "enum" in model_prop, "$ref should be inlined with enum values"
|
|
assert model_prop["description"] == "The vehicle model type selection."
|
|
|
|
# Verify dealer_location property (anyOf preserved)
|
|
dl_prop = schema["parameters"]["properties"]["dealer_location"]
|
|
assert "anyOf" in dl_prop, "anyOf should be preserved for optional primitives"
|
|
assert len(dl_prop["anyOf"]) == 2
|
|
types_in_anyof = {opt.get("type") for opt in dl_prop["anyOf"]}
|
|
assert types_in_anyof == {"string", "null"}
|
|
assert dl_prop["description"] == "Dealer location identifier from registration system, if available."
|
|
|
|
# Verify customization_options property (anyOf with fully inlined $refs)
|
|
co_prop = schema["parameters"]["properties"]["customization_options"]
|
|
assert "anyOf" in co_prop, "Should use anyOf structure"
|
|
assert len(co_prop["anyOf"]) == 2, "Should have object and null options"
|
|
|
|
# Find the object option in anyOf
|
|
object_option = next((opt for opt in co_prop["anyOf"] if opt.get("type") == "object"), None)
|
|
assert object_option is not None, "Should have object type in anyOf"
|
|
assert object_option["additionalProperties"] is False, "Object must have additionalProperties: false"
|
|
assert "properties" in object_option, "$ref should be fully inlined with properties"
|
|
|
|
# Verify the inlined properties are present
|
|
assert "has_premium_package" in object_option["properties"]
|
|
assert "has_multiple_trims" in object_option["properties"]
|
|
assert "selected_features" in object_option["properties"]
|
|
|
|
# Verify nested selected_features array has inlined Feature objects
|
|
features_prop = object_option["properties"]["selected_features"]
|
|
assert "anyOf" in features_prop, "selected_features should have anyOf"
|
|
array_option = next((opt for opt in features_prop["anyOf"] if opt.get("type") == "array"), None)
|
|
assert array_option is not None
|
|
assert "items" in array_option
|
|
assert array_option["items"]["type"] == "object"
|
|
assert array_option["items"]["additionalProperties"] is False
|
|
assert "feature_id" in array_option["items"]["properties"]
|
|
assert "category_code" in array_option["items"]["properties"]
|
|
|
|
# Verify metadata fields
|
|
assert schema[MCP_TOOL_METADATA_SCHEMA_STATUS] == "STRICT_COMPLIANT"
|
|
assert schema[MCP_TOOL_METADATA_SCHEMA_WARNINGS] == []
|
|
|
|
# Should be strict compliant
|
|
status, _ = validate_complete_json_schema(schema["parameters"])
|
|
assert status == SchemaHealth.STRICT_COMPLIANT
|