remove submit_doc_bug tool
The submit_doc_bug tool was modeled after the Zerto / Zoomin docs backend pattern (anonymous JSON POST to a known feedback endpoint). HVM docs use a different upstream feedback channel and the team that owns them doesn't consume bug submissions through that pipeline, so the tool would never be wired live here. Removing rather than leaving a permanently-disabled stub keeps the MCP tool surface clean (10 tools instead of 11) and the find_doc_inconsistencies docstring now points operators at the docs portal's own feedback channel for follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,11 +47,6 @@ services:
|
|||||||
# Phase 10 — usage telemetry.
|
# Phase 10 — usage telemetry.
|
||||||
USAGE_LOG_DIR: /app/var/logs
|
USAGE_LOG_DIR: /app/var/logs
|
||||||
USAGE_LOG_KEEP_DAYS: "90"
|
USAGE_LOG_KEEP_DAYS: "90"
|
||||||
|
|
||||||
# Phase 12 — doc-bug submission gate. Off by default; on only
|
|
||||||
# in production after you've verified the endpoint contract.
|
|
||||||
DOC_BUG_SUBMIT_ENABLED: "false"
|
|
||||||
# DOC_BUG_API_URL: "https://docs-be.example.com/api/feedback"
|
|
||||||
volumes:
|
volumes:
|
||||||
# Usage logs persist across container recreates.
|
# Usage logs persist across container recreates.
|
||||||
- ./hvm-docs-mcp-logs:/app/var/logs
|
- ./hvm-docs-mcp-logs:/app/var/logs
|
||||||
|
|||||||
+7
-152
@@ -9,7 +9,7 @@ PLAN.md add or extend pieces of this file:
|
|||||||
Phase 9 — diff_versions, list_cluster, bundle_changelog
|
Phase 9 — diff_versions, list_cluster, bundle_changelog
|
||||||
Phase 10 — TimedCall wiring (already imported below)
|
Phase 10 — TimedCall wiring (already imported below)
|
||||||
Phase 11 — <product>_api_lessons tool
|
Phase 11 — <product>_api_lessons tool
|
||||||
Phase 12 — find_doc_inconsistencies, submit_doc_bug
|
Phase 12 — find_doc_inconsistencies
|
||||||
Phase 13 — weekly_digest + _digest_history reader
|
Phase 13 — weekly_digest + _digest_history reader
|
||||||
|
|
||||||
Every stub below has a docstring + `raise NotImplementedError`. Replace
|
Every stub below has a docstring + `raise NotImplementedError`. Replace
|
||||||
@@ -63,10 +63,6 @@ RERANK_TIMEOUT = float(os.environ.get("RERANK_TIMEOUT", "30"))
|
|||||||
HYBRID_SEARCH = os.environ.get("HYBRID_SEARCH", "").lower() in ("true", "1", "yes", "on")
|
HYBRID_SEARCH = os.environ.get("HYBRID_SEARCH", "").lower() in ("true", "1", "yes", "on")
|
||||||
RRF_K = int(os.environ.get("RRF_K", "60"))
|
RRF_K = int(os.environ.get("RRF_K", "60"))
|
||||||
|
|
||||||
DOC_BUG_SUBMIT_ENABLED = os.environ.get("DOC_BUG_SUBMIT_ENABLED", "").lower() in ("true", "1", "yes", "on")
|
|
||||||
DOC_BUG_API_URL = os.environ.get("DOC_BUG_API_URL", "") # product-specific endpoint
|
|
||||||
DOC_BUG_TIMEOUT = float(os.environ.get("DOC_BUG_TIMEOUT", "15"))
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# FastMCP setup.
|
# FastMCP setup.
|
||||||
@@ -927,7 +923,7 @@ def hvm_api_lessons(
|
|||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Phase 12 — find_doc_inconsistencies + submit_doc_bug
|
# Phase 12 — find_doc_inconsistencies
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
_REDIRECT_PHRASE_RE = re.compile(
|
_REDIRECT_PHRASE_RE = re.compile(
|
||||||
@@ -1038,17 +1034,13 @@ def find_doc_inconsistencies(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Scan a scoped set of HVM docs pages for likely documentation bugs.
|
"""Scan a scoped set of HVM docs pages for likely documentation bugs.
|
||||||
|
|
||||||
Surfaces concrete candidates for human review — NOT a stream of
|
Surfaces concrete candidates for human review. Workflow:
|
||||||
bugs to auto-submit. Workflow:
|
|
||||||
|
|
||||||
1. Run this against a focused scope.
|
1. Run this against a focused scope.
|
||||||
2. Review each finding; many will be false positives.
|
2. Review each finding; many will be false positives.
|
||||||
3. For real bugs, drill in with `get_page` / `diff_versions`.
|
3. For real bugs, drill in with `get_page` / `diff_versions`.
|
||||||
4. Draft a bug report; show the operator; ask explicitly.
|
4. Draft a bug report and file it through the docs portal's own
|
||||||
5. Only then call `submit_doc_bug`. One bug = one confirmation.
|
feedback channel — this MCP does not submit upstream.
|
||||||
|
|
||||||
**Do NOT loop submissions.** Even on "submit them all", confirm each
|
|
||||||
one individually. HPE's docs queue is a shared resource.
|
|
||||||
"""
|
"""
|
||||||
with TimedCall("find_doc_inconsistencies", {
|
with TimedCall("find_doc_inconsistencies", {
|
||||||
"scope_query": scope_query, "version": version, "platform": platform,
|
"scope_query": scope_query, "version": version, "platform": platform,
|
||||||
@@ -1110,8 +1102,8 @@ def find_doc_inconsistencies(
|
|||||||
f"# Doc inconsistency scan — {len(candidates)} pages inspected", "",
|
f"# Doc inconsistency scan — {len(candidates)} pages inspected", "",
|
||||||
f"_Scope_: `{scope_query}` • _Filters_: version={version}, platform={platform}, bundle_id={bundle_id} • _Checks_: {sorted(requested)}", "",
|
f"_Scope_: `{scope_query}` • _Filters_: version={version}, platform={platform}, bundle_id={bundle_id} • _Checks_: {sorted(requested)}", "",
|
||||||
f"**{total} candidate finding{'' if total == 1 else 's'}.** Review each individually. "
|
f"**{total} candidate finding{'' if total == 1 else 's'}.** Review each individually. "
|
||||||
"For real bugs, follow up with `get_page` / `diff_versions`, draft the report, "
|
"For real bugs, follow up with `get_page` / `diff_versions`, draft a report, "
|
||||||
"show the operator, and only call `submit_doc_bug` after explicit confirmation.", "",
|
"and file it via the docs portal's own feedback channel.", "",
|
||||||
]
|
]
|
||||||
if not total:
|
if not total:
|
||||||
lines.append("_No findings in this scope._")
|
lines.append("_No findings in this scope._")
|
||||||
@@ -1131,146 +1123,9 @@ def find_doc_inconsistencies(
|
|||||||
elif check == "redirect_chain":
|
elif check == "redirect_chain":
|
||||||
lines.append(f"- Body length: {f['body_chars']} chars • Phrase: *\"{f['redirect_phrase']}\"*")
|
lines.append(f"- Body length: {f['body_chars']} chars • Phrase: *\"{f['redirect_phrase']}\"*")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines += ["---",
|
|
||||||
"_Reminder: `submit_doc_bug` has a real side effect. Draft → show → confirm → submit, one at a time. Do not loop._"]
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
# --- submit_doc_bug ----------------------------------------------------------
|
|
||||||
# HPE Support DocPortal's "Was this helpful?" widget POSTs to an endpoint
|
|
||||||
# we haven't sniffed yet. Until DOC_BUG_API_URL is set AND
|
|
||||||
# DOC_BUG_SUBMIT_ENABLED=true, this tool refuses submission and tells the
|
|
||||||
# operator to paste manually. When you sniff the endpoint, set both env
|
|
||||||
# vars and verify the payload shape against the schema below.
|
|
||||||
|
|
||||||
_DOC_BUG_ALLOWED_HOSTS = {"support.hpe.com"}
|
|
||||||
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def submit_doc_bug(
|
|
||||||
page_url: Annotated[str, Field(description="Full URL of the support.hpe.com page the bug is about. Must be a support.hpe.com URL.")],
|
|
||||||
content: Annotated[str, Field(description="Body of the bug report. Be specific: what the page says, what's wrong, what it should say. Cite exact passages. The docs team reads it verbatim.")],
|
|
||||||
email: Annotated[str | None, Field(description="OPTIONAL submitter email for follow-up. Omit if anonymous.")] = None,
|
|
||||||
rating: Annotated[int | None, Field(description="OPTIONAL star rating 1-5 (1-2 for serious bugs, 3 unclear, 4-5 only on explicit request).")] = None,
|
|
||||||
like: Annotated[bool | None, Field(description="OPTIONAL thumbs-up/down. False for bugs, True for positive feedback.")] = None,
|
|
||||||
) -> str:
|
|
||||||
"""Submit a documentation bug to HPE's docs feedback channel.
|
|
||||||
|
|
||||||
**⚠️ THIS TOOL HAS A REAL SIDE EFFECT (when enabled). It POSTs to
|
|
||||||
HPE's docs feedback endpoint and the submission lands in their queue.**
|
|
||||||
|
|
||||||
**MANDATORY operator-confirmation workflow:**
|
|
||||||
|
|
||||||
1. Draft the bug content yourself. Show the operator the exact text
|
|
||||||
you intend to submit + the page URL + any rating/email fields.
|
|
||||||
2. Ask explicitly: *"Submit this bug? (yes/no)"*
|
|
||||||
3. Only call submit_doc_bug AFTER they answer yes.
|
|
||||||
4. If they say *"submit them all"*, STILL confirm each one. This
|
|
||||||
tool MUST NOT be called in a loop without per-bug consent.
|
|
||||||
|
|
||||||
**Do not call this autonomously.** Don't preemptively submit while
|
|
||||||
exploring inconsistencies. Don't call inside an agent loop without
|
|
||||||
a human in the loop. Misuse will get this MCP blocked at HPE's WAF.
|
|
||||||
|
|
||||||
**What makes a good bug report:**
|
|
||||||
- Specific page URL. One bug = one page.
|
|
||||||
- Concrete quote of the problem text + version/platform context.
|
|
||||||
- Suggested correction when you have one.
|
|
||||||
- Avoid editorializing — factual bugs and broken links best.
|
|
||||||
"""
|
|
||||||
with TimedCall("submit_doc_bug", {
|
|
||||||
"page_url": page_url, "content_len": len(content or ""),
|
|
||||||
"email_present": bool(email), "rating": rating, "like": like,
|
|
||||||
}) as _call:
|
|
||||||
if not DOC_BUG_SUBMIT_ENABLED:
|
|
||||||
_call.set(error="disabled", outcome="refused_disabled")
|
|
||||||
return (
|
|
||||||
"submit_doc_bug is disabled on this MCP deployment "
|
|
||||||
"(DOC_BUG_SUBMIT_ENABLED is not set). The operator's draft is good — "
|
|
||||||
f"they can paste it into the feedback widget on {page_url} themselves.\n\n"
|
|
||||||
"_(For maintainers: sniff HPE's feedback endpoint, set DOC_BUG_API_URL "
|
|
||||||
"to the POST target, and DOC_BUG_SUBMIT_ENABLED=true to activate.)_"
|
|
||||||
)
|
|
||||||
if not DOC_BUG_API_URL:
|
|
||||||
_call.set(error="no_endpoint", outcome="refused_disabled")
|
|
||||||
return ("submit_doc_bug is enabled but DOC_BUG_API_URL is empty. "
|
|
||||||
f"Operator should paste manually at {page_url}.")
|
|
||||||
if not content or not content.strip():
|
|
||||||
_call.set(error="empty_content", outcome="refused_invalid")
|
|
||||||
return "Refused: empty `content`."
|
|
||||||
if len(content) > 10000:
|
|
||||||
_call.set(error="content_too_long", outcome="refused_invalid")
|
|
||||||
return f"Refused: `content` is {len(content)} chars (cap 10000)."
|
|
||||||
try:
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
parsed = urlparse(page_url)
|
|
||||||
except Exception as e:
|
|
||||||
_call.set(error=f"url_parse: {e}", outcome="refused_invalid")
|
|
||||||
return f"Refused: couldn't parse page_url ({e})."
|
|
||||||
if parsed.scheme not in ("http", "https"):
|
|
||||||
_call.set(error="bad_scheme", outcome="refused_invalid")
|
|
||||||
return f"Refused: scheme must be http(s), got {parsed.scheme!r}."
|
|
||||||
if parsed.hostname not in _DOC_BUG_ALLOWED_HOSTS:
|
|
||||||
_call.set(error=f"bad_host: {parsed.hostname}", outcome="refused_invalid")
|
|
||||||
return (f"Refused: page_url host {parsed.hostname!r} isn't a "
|
|
||||||
f"support.hpe.com URL. submit_doc_bug only accepts bugs against HPE Support pages.")
|
|
||||||
if email is not None and not _EMAIL_RE.match(email):
|
|
||||||
_call.set(error="bad_email", outcome="refused_invalid")
|
|
||||||
return f"Refused: email {email!r} doesn't look valid. Omit if anonymous."
|
|
||||||
if rating is not None and not (1 <= rating <= 5):
|
|
||||||
_call.set(error="bad_rating", outcome="refused_invalid")
|
|
||||||
return f"Refused: rating must be 1-5, got {rating}."
|
|
||||||
|
|
||||||
href = f"{parsed.scheme}://{parsed.hostname}{parsed.path}{('?' + parsed.query) if parsed.query else ''}"
|
|
||||||
payload: dict = {"content": content, "href": href}
|
|
||||||
if email:
|
|
||||||
payload["email"] = email
|
|
||||||
if rating is not None:
|
|
||||||
payload["rating"] = rating
|
|
||||||
if like is not None:
|
|
||||||
payload["like"] = like
|
|
||||||
|
|
||||||
try:
|
|
||||||
import httpx
|
|
||||||
except ImportError:
|
|
||||||
_call.set(error="httpx_missing", outcome="refused_runtime")
|
|
||||||
return "Refused: httpx not available."
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"User-Agent": "hvm-docs-mcp submit_doc_bug",
|
|
||||||
"Origin": "https://support.hpe.com",
|
|
||||||
"Referer": href,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
with httpx.Client(timeout=DOC_BUG_TIMEOUT) as c:
|
|
||||||
r = c.post(DOC_BUG_API_URL, json=payload, headers=headers)
|
|
||||||
except httpx.RequestError as e:
|
|
||||||
_call.set(error=f"transport: {e}", outcome="failed_transport")
|
|
||||||
return f"Submission failed (transport): {e}"
|
|
||||||
|
|
||||||
comment_id: object = None
|
|
||||||
body_summary = ""
|
|
||||||
try:
|
|
||||||
resp_json = r.json()
|
|
||||||
comment_id = resp_json.get("commentId") or resp_json.get("id")
|
|
||||||
body_summary = json.dumps(resp_json)[:300]
|
|
||||||
except (ValueError, json.JSONDecodeError):
|
|
||||||
body_summary = (r.text or "")[:300]
|
|
||||||
_call.set(http_status=r.status_code, comment_id=comment_id,
|
|
||||||
outcome=("submitted" if r.is_success else "rejected_upstream"))
|
|
||||||
if r.is_success:
|
|
||||||
id_note = f" (commentId={comment_id})" if comment_id else ""
|
|
||||||
return f"Submitted. HTTP {r.status_code}{id_note}. HPE docs team will see this for {href}."
|
|
||||||
if r.status_code in (401, 403, 429):
|
|
||||||
return (f"Submission rejected upstream (HTTP {r.status_code}). "
|
|
||||||
"Likely captcha/auth/rate-limit on anonymous POSTs. "
|
|
||||||
f"Operator can paste manually at {href}.\n\nResponse (truncated): {body_summary}")
|
|
||||||
return f"Submission rejected upstream (HTTP {r.status_code}). Response (truncated): {body_summary}"
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Entry point
|
# Entry point
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user