Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions tests/test_mcp_tool_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
"""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")}