fix(mcp): call the Mintlify search endpoint the documentation moved to (#12578)

This commit is contained in:
Rubén De la Torre Vico
2026-08-27 16:48:38 +02:00
committed by GitHub
parent db298c1d48
commit 2b81fdcc04
4 changed files with 158 additions and 67 deletions
@@ -0,0 +1,109 @@
"""Tests for the Prowler documentation search tool.
Mintlify moved the docs search to a new endpoint that answers with page
sections, so a result is a part of a page and has to read as one.
"""
import json
from fastmcp import Client
SEARCH = "/api/search/prowler"
def search_match(
path: str = "getting-started/installation",
*,
header: str = "Requirements",
breadcrumbs: tuple[str, ...] = ("Get Started", "Installation"),
anchor: str | None = "requirements",
score: float = 4.9,
):
"""One match as Mintlify answers with it: a section of a page, not the page."""
return {
"page": path,
"header": header,
"content": "Prowler runs on Python 3.9 or later.",
"metadata": {
"title": header,
"breadcrumbs": list(breadcrumbs),
"icon": "",
"hash": anchor,
"openapi": "",
},
"score": score,
}
def stub_search_hit(docs_router, *matches):
"""Serve the search endpoint, with one default match when none are given."""
if not matches:
matches = (search_match(),)
return docs_router.add("POST", SEARCH, json={"results": list(matches)})
async def test_search_returns_the_matching_sections(mcp_root_server, docs_router):
"""Every field of a result, since the shape of one changed with the endpoint."""
stub_search_hit(docs_router)
async with Client(mcp_root_server) as client:
result = await client.call_tool("prowler_docs_search", {"term": "install"})
match = result.data[0]
assert match["path"] == "getting-started/installation"
# The page's title, so a result reads as more than the heading it matched.
assert match["title"] == "Installation"
assert match["section"] == "Requirements"
assert match["breadcrumbs"] == ["Get Started", "Installation"]
assert match["excerpt"] == "Prowler runs on Python 3.9 or later."
assert match["score"] == 4.9
# Anchored: a match is a section, and the page it is on can be a long one.
assert match["url"] == (
"https://docs.prowler.com/getting-started/installation#requirements"
)
async def test_the_search_query_is_sent_as_the_api_expects_it(
mcp_root_server, docs_router
):
"""The endpoint takes a POST body, not the payload the old one took."""
stub_search_hit(docs_router)
async with Client(mcp_root_server) as client:
await client.call_tool("prowler_docs_search", {"term": "install"})
request = docs_router.request_for("POST", SEARCH)
assert json.loads(request.content) == {"query": "install", "filters": {}}
async def test_a_section_with_no_anchor_links_to_the_page(mcp_root_server, docs_router):
"""The API sends "" for a page's first section and null for pages without anchors."""
stub_search_hit(
docs_router,
search_match(anchor=""),
search_match(path="getting-started/requirements", anchor=None),
)
async with Client(mcp_root_server) as client:
result = await client.call_tool("prowler_docs_search", {"term": "install"})
assert result.data[0]["url"] == (
"https://docs.prowler.com/getting-started/installation"
)
assert result.data[1]["url"] == (
"https://docs.prowler.com/getting-started/requirements"
)
async def test_page_size_caps_a_response_the_api_did_not_size(
mcp_root_server, docs_router
):
"""The endpoint takes no size argument, so the cap has to be applied here."""
stub_search_hit(docs_router, *(search_match() for _ in range(6)))
async with Client(mcp_root_server) as client:
result = await client.call_tool(
"prowler_docs_search", {"term": "install", "page_size": 2}
)
assert len(result.data) == 2