211 lines
7.3 KiB
Python
211 lines
7.3 KiB
Python
"""Unit tests for src/agents/_mcp_tool_metadata.py pure helpers.
|
|
|
|
The module resolves MCP tool display metadata (title / description) from
|
|
either dict payloads or attribute-bearing objects. It feeds hosted-MCP
|
|
tool definitions into the model and into traces, but had no direct
|
|
test file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from agents._mcp_tool_metadata import (
|
|
MCPToolMetadata,
|
|
collect_mcp_list_tools_metadata,
|
|
extract_mcp_tool_metadata,
|
|
resolve_mcp_tool_description,
|
|
resolve_mcp_tool_description_for_model,
|
|
resolve_mcp_tool_title,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class _ToolObj:
|
|
"""Tiny attribute-bearing stand-in for an MCP tool object."""
|
|
|
|
name: str | None = None
|
|
title: str | None = None
|
|
description: str | None = None
|
|
annotations: Any = None
|
|
|
|
|
|
@dataclass
|
|
class _Annotations:
|
|
title: str | None = None
|
|
|
|
|
|
class TestResolveMCPToolTitle:
|
|
def test_explicit_title_wins(self) -> None:
|
|
tool = {"title": "Explicit", "annotations": {"title": "Annotated"}}
|
|
assert resolve_mcp_tool_title(tool) == "Explicit"
|
|
|
|
def test_falls_back_to_annotations_title(self) -> None:
|
|
tool = {"annotations": {"title": "Annotated"}}
|
|
assert resolve_mcp_tool_title(tool) == "Annotated"
|
|
|
|
def test_returns_none_when_neither_present(self) -> None:
|
|
assert resolve_mcp_tool_title({}) is None
|
|
|
|
def test_skips_empty_explicit_title(self) -> None:
|
|
tool = {"title": "", "annotations": {"title": "Annotated"}}
|
|
assert resolve_mcp_tool_title(tool) == "Annotated"
|
|
|
|
def test_skips_non_string_explicit_title(self) -> None:
|
|
tool = {"title": 123, "annotations": {"title": "Annotated"}}
|
|
assert resolve_mcp_tool_title(tool) == "Annotated"
|
|
|
|
def test_works_with_attribute_objects(self) -> None:
|
|
tool = _ToolObj(title="Explicit")
|
|
assert resolve_mcp_tool_title(tool) == "Explicit"
|
|
|
|
def test_works_with_attribute_annotations(self) -> None:
|
|
tool = _ToolObj(annotations=_Annotations(title="Annotated"))
|
|
assert resolve_mcp_tool_title(tool) == "Annotated"
|
|
|
|
def test_handles_missing_annotations_attribute(self) -> None:
|
|
tool = _ToolObj()
|
|
assert resolve_mcp_tool_title(tool) is None
|
|
|
|
|
|
class TestResolveMCPToolDescription:
|
|
def test_returns_description(self) -> None:
|
|
assert resolve_mcp_tool_description({"description": "Long form"}) == "Long form"
|
|
|
|
def test_returns_none_when_empty(self) -> None:
|
|
assert resolve_mcp_tool_description({"description": ""}) is None
|
|
|
|
def test_returns_none_when_missing(self) -> None:
|
|
assert resolve_mcp_tool_description({}) is None
|
|
|
|
def test_returns_none_when_non_string(self) -> None:
|
|
assert resolve_mcp_tool_description({"description": 123}) is None
|
|
|
|
def test_works_with_attribute_object(self) -> None:
|
|
assert resolve_mcp_tool_description(_ToolObj(description="Long form")) == "Long form"
|
|
|
|
|
|
class TestResolveMCPToolDescriptionForModel:
|
|
def test_uses_description_when_present(self) -> None:
|
|
tool = {"description": "Long form", "title": "Short"}
|
|
assert resolve_mcp_tool_description_for_model(tool) == "Long form"
|
|
|
|
def test_falls_back_to_title_when_description_missing(self) -> None:
|
|
assert resolve_mcp_tool_description_for_model({"title": "Short"}) == "Short"
|
|
|
|
def test_falls_back_to_annotations_title(self) -> None:
|
|
tool = {"annotations": {"title": "Annotated"}}
|
|
assert resolve_mcp_tool_description_for_model(tool) == "Annotated"
|
|
|
|
def test_returns_empty_string_when_nothing_resolvable(self) -> None:
|
|
assert resolve_mcp_tool_description_for_model({}) == ""
|
|
|
|
|
|
class TestExtractMCPToolMetadata:
|
|
def test_collects_both_fields(self) -> None:
|
|
tool = {"description": "Long form", "title": "Short"}
|
|
assert extract_mcp_tool_metadata(tool) == MCPToolMetadata(
|
|
description="Long form", title="Short"
|
|
)
|
|
|
|
def test_returns_empty_metadata_when_nothing_present(self) -> None:
|
|
assert extract_mcp_tool_metadata({}) == MCPToolMetadata()
|
|
|
|
|
|
class TestCollectMCPListToolsMetadata:
|
|
def test_collects_from_raw_payload(self) -> None:
|
|
items = [
|
|
{
|
|
"type": "mcp_list_tools",
|
|
"server_label": "github",
|
|
"tools": [
|
|
{"name": "search", "description": "Search repos", "title": "Search"},
|
|
{"name": "create", "description": "Create issue"},
|
|
],
|
|
}
|
|
]
|
|
result = collect_mcp_list_tools_metadata(items)
|
|
assert result == {
|
|
("github", "search"): MCPToolMetadata(description="Search repos", title="Search"),
|
|
("github", "create"): MCPToolMetadata(description="Create issue"),
|
|
}
|
|
|
|
def test_unwraps_run_item_with_raw_item(self) -> None:
|
|
@dataclass
|
|
class _RunItem:
|
|
raw_item: Any
|
|
|
|
run_item = _RunItem(
|
|
raw_item={
|
|
"type": "mcp_list_tools",
|
|
"server_label": "internal",
|
|
"tools": [{"name": "ping", "description": "Ping"}],
|
|
}
|
|
)
|
|
result = collect_mcp_list_tools_metadata([run_item])
|
|
assert result == {("internal", "ping"): MCPToolMetadata(description="Ping")}
|
|
|
|
def test_skips_items_without_correct_type(self) -> None:
|
|
items = [
|
|
{
|
|
"type": "mcp_call",
|
|
"server_label": "github",
|
|
"tools": [{"name": "ignored"}],
|
|
}
|
|
]
|
|
assert collect_mcp_list_tools_metadata(items) == {}
|
|
|
|
def test_skips_items_without_server_label(self) -> None:
|
|
items = [
|
|
{
|
|
"type": "mcp_list_tools",
|
|
"tools": [{"name": "search"}],
|
|
}
|
|
]
|
|
assert collect_mcp_list_tools_metadata(items) == {}
|
|
|
|
def test_skips_items_with_non_list_tools(self) -> None:
|
|
items = [
|
|
{
|
|
"type": "mcp_list_tools",
|
|
"server_label": "github",
|
|
"tools": "not-a-list",
|
|
}
|
|
]
|
|
assert collect_mcp_list_tools_metadata(items) == {}
|
|
|
|
def test_skips_tools_without_name(self) -> None:
|
|
items = [
|
|
{
|
|
"type": "mcp_list_tools",
|
|
"server_label": "github",
|
|
"tools": [
|
|
{"description": "no name"},
|
|
{"name": "", "description": "empty name"},
|
|
{"name": "good", "description": "kept"},
|
|
],
|
|
}
|
|
]
|
|
result = collect_mcp_list_tools_metadata(items)
|
|
assert result == {("github", "good"): MCPToolMetadata(description="kept")}
|
|
|
|
def test_returns_empty_for_empty_input(self) -> None:
|
|
assert collect_mcp_list_tools_metadata([]) == {}
|
|
|
|
def test_later_entry_for_same_key_wins(self) -> None:
|
|
items = [
|
|
{
|
|
"type": "mcp_list_tools",
|
|
"server_label": "github",
|
|
"tools": [{"name": "search", "description": "first"}],
|
|
},
|
|
{
|
|
"type": "mcp_list_tools",
|
|
"server_label": "github",
|
|
"tools": [{"name": "search", "description": "second"}],
|
|
},
|
|
]
|
|
result = collect_mcp_list_tools_metadata(items)
|
|
assert result == {("github", "search"): MCPToolMetadata(description="second")}
|