Files
prowler/mcp_server/tests/helpers/assertions.py
T

67 lines
2.6 KiB
Python

"""Assertions for the MCP tool contract every sub-server must honour.
A tool's description and its parameter descriptions are not documentation -- they
are the only thing a model sees when deciding whether and how to call it. A tool
that registers without them is invisible in practice, so these are correctness
assertions rather than style ones.
"""
from mcp.types import Tool
# Mounted namespaces, most specific first so prefix matching is unambiguous.
NAMESPACES = ("prowler_hub_", "prowler_docs_", "prowler_")
def assert_tool_contract(tool: Tool) -> None:
"""Assert the tool and all of its parameters carry a usable description.
Missing and blank are asserted separately because they are different
mistakes: a missing description was never written, a blank one exists but was
left empty. One truthiness check would report both the same way.
"""
assert tool.description is not None, (
f"Tool '{tool.name}' has no description. Its docstring is what the model reads."
)
assert tool.description.strip(), (
f"Tool '{tool.name}' has a blank description. "
"Its docstring is what the model reads."
)
# `inputSchema` is a required field of the MCP Tool type, so it is always a
# dict; a tool that takes no arguments simply has no `properties`.
for parameter, schema in tool.inputSchema.get("properties", {}).items():
description = schema.get("description")
assert description is not None, (
f"Parameter '{parameter}' of tool '{tool.name}' has no description. "
"Declare it with pydantic Field(description=...)."
)
assert description.strip(), (
f"Parameter '{parameter}' of tool '{tool.name}' has a blank description. "
"Declare it with pydantic Field(description=...)."
)
def assert_namespaced(tool: Tool) -> None:
"""Assert the tool is reachable under one of the published namespaces."""
assert tool.name.startswith(NAMESPACES), (
f"Tool '{tool.name}' is outside the published namespaces {NAMESPACES}"
)
def tools_in_namespace(tools: list[Tool], namespace: str) -> list[Tool]:
"""Return the tools in a namespace.
``prowler_`` is a prefix of the other two namespaces, so tools belonging to a
more specific one are excluded rather than counted twice.
"""
more_specific = tuple(
other
for other in NAMESPACES
if other != namespace and other.startswith(namespace)
)
return [
tool
for tool in tools
if tool.name.startswith(namespace) and not tool.name.startswith(more_specific)
]