Skip to content
Open
Show file tree
Hide file tree
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
40 changes: 26 additions & 14 deletions qiskit-docs-mcp-server/src/qiskit_docs_mcp_server/data_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,21 +324,31 @@ def _normalize_result_url(url_val: Any, base: str) -> Any:


def _build_result_entry(item: dict[str, Any], query: str, detail: str, base: str) -> dict[str, Any]:
"""Build a single search-result entry with a snippet (or full text)."""
entry: dict[str, Any] = {
key: item[key] for key in _RESULT_META_FIELDS if item.get(key) is not None
}
"""Build a single search-result entry.

In ``snippet`` mode (default) the entry is trimmed to a small set of
navigation fields plus a short ``snippet`` — token-economical for agent use.
In ``full`` mode the original upstream fields are preserved (backwards
compatible with the pre-snippet response) and the full page body is returned
as ``text``. Either way the upstream ``item`` is not mutated (dict/comprehension
copies), so cached search responses stay intact.
"""
if detail == "full":
# Preserve all upstream fields for backwards compatibility.
entry: dict[str, Any] = dict(item)
raw_text = item.get("text")
if isinstance(raw_text, str):
entry["text"] = _strip_html_tags(raw_text)
else:
entry = {key: item[key] for key in _RESULT_META_FIELDS if item.get(key) is not None}
raw_text = item.get("text")
clean_text = _strip_html_tags(raw_text) if isinstance(raw_text, str) else ""
entry["snippet"] = _make_snippet(clean_text, query)

if isinstance(entry.get("title"), str):
entry["title"] = _strip_html_tags(entry["title"])
if "url" in entry:
entry["url"] = _normalize_result_url(entry["url"], base)

raw_text = item.get("text")
clean_text = _strip_html_tags(raw_text) if isinstance(raw_text, str) else ""
if detail == "full":
entry["text"] = clean_text
else:
entry["snippet"] = _make_snippet(clean_text, query)
return entry


Expand All @@ -360,9 +370,11 @@ async def search_qiskit_docs(
'all', 'documentation', 'api', 'learning', 'tutorials'
top_k: Maximum number of results to return (clamped to
[1, MAX_SEARCH_TOP_K]). Defaults to DEFAULT_SEARCH_TOP_K.
detail: 'snippet' (default) returns a short excerpt per result;
'full' returns each result's full page body (heavier — prefer
get_page for full content).
detail: 'snippet' (default) returns a short excerpt per result and a
trimmed field set; 'full' returns each result's full page body as
'text' and preserves the original upstream fields (backwards
compatible with the pre-snippet response — heavier, so prefer
get_page for a single page's full content).

Returns:
Search results with matching entries, counts, and metadata.
Expand Down
5 changes: 3 additions & 2 deletions qiskit-docs-mcp-server/src/qiskit_docs_mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ async def search_docs_tool(
'tutorials' — Tutorial content only
top_k: Maximum number of results to return (default: 5, capped at 10).
detail: Per-result content level. 'snippet' (default) returns a short
excerpt; 'full' returns each result's full page body — heavier, so
prefer get_page_tool when you only need one page in full.
excerpt and a trimmed field set; 'full' returns each result's full
page body as 'text' and preserves all original fields (backwards
compatible) — heavier, so prefer get_page_tool for a single page.

Returns:
Matching documentation entries (id, url, title, pageTitle, module,
Expand Down
22 changes: 22 additions & 0 deletions qiskit-docs-mcp-server/tests/test_data_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,28 @@ async def test_detail_full_returns_full_body(self, mock_fetch):
assert "snippet" not in entry
assert len(entry["text"]) > SNIPPET_MAX_CHARS

@patch("qiskit_docs_mcp_server.data_fetcher.fetch_text_json")
async def test_detail_full_preserves_upstream_fields(self, mock_fetch):
"""detail='full' is backwards compatible: original fields pass through."""
mock_fetch.return_value = [
{
"url": "/docs/api/qiskit/circuit",
"title": "circuit",
"text": "body",
"language": "en",
"package": "qiskit",
"package-version": "current",
},
]
result = await search_qiskit_docs("circuit", detail="full")
entry = result["results"][0]
# Fields dropped in snippet mode are retained in full mode.
assert entry["language"] == "en"
assert entry["package"] == "qiskit"
assert entry["package-version"] == "current"
# URL is still normalized to an absolute URL.
assert entry["url"].startswith("https://")

@patch("qiskit_docs_mcp_server.data_fetcher.fetch_text_json")
async def test_detail_full_strips_html(self, mock_fetch):
"""detail='full' still strips HTML tags from the body."""
Expand Down
Loading