mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
67 lines
2.6 KiB
Python
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)
|
|
]
|