mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Advertise required tool parameters in MCP Server (#182105)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
eb5024a1f2
commit
fdb882c737
@@ -45,13 +45,17 @@ def _format_tool(
|
||||
) -> types.Tool:
|
||||
"""Format tool specification."""
|
||||
input_schema = to_openapi(tool.parameters, custom_serializer=custom_serializer)
|
||||
mcp_schema: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": input_schema["properties"],
|
||||
}
|
||||
# Omitted by to_openapi when the tool has no required parameters.
|
||||
if required := input_schema.get("required"):
|
||||
mcp_schema["required"] = required
|
||||
return types.Tool(
|
||||
name=tool.name,
|
||||
description=tool.description or "",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": input_schema["properties"],
|
||||
},
|
||||
inputSchema=mcp_schema,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Common fixtures for the Model Context Protocol Server tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass, field
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -16,8 +17,11 @@ from tests.common import MockConfigEntry
|
||||
TEST_LLM_API_ID = "test-api"
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class MockLLMAPI(llm.API):
|
||||
"""Test LLM API that does not expose any tools."""
|
||||
"""Test LLM API that exposes the tools it is created with."""
|
||||
|
||||
tools: list[llm.Tool] = field(default_factory=list)
|
||||
|
||||
async def async_get_api_instance(
|
||||
self, llm_context: llm.LLMContext
|
||||
@@ -27,7 +31,7 @@ class MockLLMAPI(llm.API):
|
||||
api=self,
|
||||
api_prompt="Test prompt",
|
||||
llm_context=llm_context,
|
||||
tools=[],
|
||||
tools=self.tools,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import mcp.client.sse
|
||||
import mcp.client.streamable_http
|
||||
from mcp.shared.exceptions import McpError
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.conversation import DOMAIN as CONVERSATION_DOMAIN
|
||||
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
|
||||
@@ -42,6 +43,7 @@ from homeassistant.helpers import (
|
||||
)
|
||||
from homeassistant.helpers.httpx_client import create_async_httpx_client
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
|
||||
from .conftest import TEST_LLM_API_ID, MockLLMAPI
|
||||
|
||||
@@ -80,6 +82,25 @@ EXPECTED_PROMPT_ENTITY_DEFINITION = """
|
||||
"""
|
||||
|
||||
|
||||
class _StubTool(llm.Tool):
|
||||
"""Minimal tool with a configurable parameter schema."""
|
||||
|
||||
name = "test_tool"
|
||||
|
||||
def __init__(self, parameters: vol.Schema) -> None:
|
||||
"""Initialize the stub tool."""
|
||||
self.parameters = parameters
|
||||
|
||||
async def async_call(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
tool_input: llm.ToolInput,
|
||||
llm_context: llm.LLMContext,
|
||||
) -> JsonObjectType:
|
||||
"""Return an empty result."""
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Set up the config entry."""
|
||||
@@ -612,6 +633,51 @@ async def test_mcp_tools_list(
|
||||
assert properties.get("name") == {"type": "string"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("llm_hass_api", [TEST_LLM_API_ID])
|
||||
@pytest.mark.parametrize(
|
||||
("parameters", "expected_required"),
|
||||
[
|
||||
pytest.param(
|
||||
vol.Schema({vol.Required("name"): str, vol.Optional("area"): str}),
|
||||
["name"],
|
||||
id="required-and-optional",
|
||||
),
|
||||
pytest.param(
|
||||
vol.Schema({vol.Optional("area"): str}),
|
||||
None,
|
||||
id="optional-only",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_mcp_tools_list_required_parameters(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: None,
|
||||
mcp_url: str,
|
||||
mcp_client: MCPClientFactory,
|
||||
hass_supervisor_access_token: str,
|
||||
parameters: vol.Schema,
|
||||
expected_required: list[str] | None,
|
||||
) -> None:
|
||||
"""Test the tools list advertises the required tool parameters."""
|
||||
|
||||
llm.async_register_api(
|
||||
hass,
|
||||
MockLLMAPI(
|
||||
hass=hass,
|
||||
id=TEST_LLM_API_ID,
|
||||
name="Test API",
|
||||
tools=[_StubTool(parameters)],
|
||||
),
|
||||
)
|
||||
|
||||
async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session:
|
||||
result = await session.list_tools()
|
||||
|
||||
tool = next(iter(tool for tool in result.tools if tool.name == "test_tool"))
|
||||
assert tool.inputSchema.get("type") == "object"
|
||||
assert tool.inputSchema.get("required") == expected_required
|
||||
|
||||
|
||||
@pytest.mark.parametrize("llm_hass_api", [llm.LLM_API_ASSIST, STATELESS_LLM_API])
|
||||
async def test_mcp_tool_call(
|
||||
hass: HomeAssistant,
|
||||
|
||||
Reference in New Issue
Block a user