remove submit_doc_bug tool
This commit was merged in pull request #7.
This commit is contained in:
+7
-152
@@ -9,7 +9,7 @@ PLAN.md add or extend pieces of this file:
|
||||
Phase 9 — diff_versions, list_cluster, bundle_changelog
|
||||
Phase 10 — TimedCall wiring (already imported below)
|
||||
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
|
||||
|
||||
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")
|
||||
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.
|
||||
@@ -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(
|
||||
@@ -1038,17 +1034,13 @@ def find_doc_inconsistencies(
|
||||
) -> str:
|
||||
"""Scan a scoped set of HVM docs pages for likely documentation bugs.
|
||||
|
||||
Surfaces concrete candidates for human review — NOT a stream of
|
||||
bugs to auto-submit. Workflow:
|
||||
Surfaces concrete candidates for human review. Workflow:
|
||||
|
||||
1. Run this against a focused scope.
|
||||
2. Review each finding; many will be false positives.
|
||||
3. For real bugs, drill in with `get_page` / `diff_versions`.
|
||||
4. Draft a bug report; show the operator; ask explicitly.
|
||||
5. Only then call `submit_doc_bug`. One bug = one confirmation.
|
||||
|
||||
**Do NOT loop submissions.** Even on "submit them all", confirm each
|
||||
one individually. HPE's docs queue is a shared resource.
|
||||
4. Draft a bug report and file it through the docs portal's own
|
||||
feedback channel — this MCP does not submit upstream.
|
||||
"""
|
||||
with TimedCall("find_doc_inconsistencies", {
|
||||
"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"_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. "
|
||||
"For real bugs, follow up with `get_page` / `diff_versions`, draft the report, "
|
||||
"show the operator, and only call `submit_doc_bug` after explicit confirmation.", "",
|
||||
"For real bugs, follow up with `get_page` / `diff_versions`, draft a report, "
|
||||
"and file it via the docs portal's own feedback channel.", "",
|
||||
]
|
||||
if not total:
|
||||
lines.append("_No findings in this scope._")
|
||||
@@ -1131,146 +1123,9 @@ def find_doc_inconsistencies(
|
||||
elif check == "redirect_chain":
|
||||
lines.append(f"- Body length: {f['body_chars']} chars • Phrase: *\"{f['redirect_phrase']}\"*")
|
||||
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)
|
||||
|
||||
|
||||
# --- 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
|
||||
# ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user