Search Examiner Reports
search_examiner_reportsSearch examiner reports to understand what examiners expect, common mistakes, and tips for structuring answers to gain full marks on Cambridge topics.
Instructions
Search Cambridge examiner report commentary for insights on how a topic is examined.
Returns examiner observations including:
What examiners expect in answers on this topic
Common mistakes candidates make
Tips for how to structure answers to gain full marks
Use AFTER searching questions to understand examiner expectations on the same topic. Examiner reports are the most authoritative source for HOW to answer, not WHAT to answer.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | ||
| subject | No | ||
| paper | No | ||
| year | No | ||
| limit | No |
Implementation Reference
- mcp_server.py:1214-1288 (handler)The search_examiner_reports tool handler function. Calls /search/examiner-reports API endpoint, deduplicates results using _clean_examiner_chunk, and returns formatted examiner commentary with year/session/paper metadata.
def search_examiner_reports( query: str, subject: Optional[str] = DEFAULT_SUBJECT, paper: Optional[int] = None, year: Optional[int] = None, limit: int = 5, ) -> ToolResult: """Search Cambridge examiner report commentary for insights on how a topic is examined. Returns examiner observations including: - What examiners expect in answers on this topic - Common mistakes candidates make - Tips for how to structure answers to gain full marks Use AFTER searching questions to understand examiner expectations on the same topic. Examiner reports are the most authoritative source for HOW to answer, not WHAT to answer. """ params: dict[str, Any] = {"q": query, "limit": max(1, min(limit, 20))} if subject: params["subject"] = subject if paper is not None: params["paper"] = paper if year is not None: params["year"] = year try: data = _api_get("/search/examiner-reports", params) except Exception as exc: logger.error("search_examiner_reports failed: %s", exc) error_payload = _error_from_exception(exc, "/search/examiner-reports") raise ToolError(error_payload.get("error", {}).get("message", "ER search failed.")) raw_chunks = data.get("results", []) if isinstance(data, dict) else [] total = data.get("total", 0) if isinstance(data, dict) else 0 # De-duplicate near-identical chunks and strip boilerplate seen_hashes: set[int] = set() curated_chunks: list[dict[str, Any]] = [] for chunk in raw_chunks: if not isinstance(chunk, dict): continue cleaned_text = _clean_examiner_chunk(chunk.get("chunk_text", "")) if not cleaned_text or len(cleaned_text) < 20: continue text_hash = hash(cleaned_text[:200]) if text_hash in seen_hashes: continue seen_hashes.add(text_hash) curated_chunks.append({ "year": chunk.get("year"), "session": chunk.get("session_name"), "paper": chunk.get("paper_number"), "commentary": cleaned_text, "relevance_score": chunk.get("relevance_score"), }) lines = [f"Examiner Report Search: {total} total for '{query}', showing {len(curated_chunks)} unique."] for i, chunk in enumerate(curated_chunks, 1): year_str = chunk.get("year", "?") session = chunk.get("session", "?") paper_num = chunk.get("paper") paper_label = f"Paper {paper_num}" if paper_num else "General" text = _clean_text(chunk.get("commentary", ""), max_len=350) lines.append(f"[{i}] {year_str} {session} | {paper_label}") lines.append(f" {text}") if not curated_chunks: lines.append("No examiner report data found for this query.") payload = { "ok": True, "query": query, "total": total, "returned": len(curated_chunks), "results": curated_chunks, } return ToolResult(content="\n".join(lines), structured_content=payload) - mcp_server.py:1209-1213 (registration)Tool registration via @mcp.tool decorator with title 'Search Examiner Reports', tags 'search' and 'enhanced', and readOnlyHint/idempotentHint annotations.
@mcp.tool( title="Search Examiner Reports", tags={"search", "enhanced"}, annotations={"readOnlyHint": True, "idempotentHint": True}, ) - mcp_server.py:1105-1143 (helper)Helper function that cleans examiner report text by removing generic boilerplate preamble (e.g., 'Comments on specific questions', 'Key messages', 'General comments') and copyright lines.
def _clean_examiner_chunk(text: str) -> str: """Remove generic ER boilerplate preamble, keep only topic-specific commentary.""" if not text: return "" # Common boilerplate sections to skip past skip_markers = [ "Comments on specific questions", "Comment on specific questions", ] for marker in skip_markers: idx = text.find(marker) if idx >= 0: # Start from after the marker line after = text[idx + len(marker):] stripped = after.lstrip("\n\r ") if stripped: text = stripped break # Strip remaining generic preamble phrases that add no value boilerplate_starts = [ "Key messages\n", "General comments\n", "Candidates need to demonstrate a detailed study", "Candidates are advised to answer each question", "Candidates must always make sure", "Candidates are further advised to make use", ] for bp in boilerplate_starts: if text.startswith(bp): # Find next paragraph next_para = text.find("\n\n", len(bp)) if next_para > 0: text = text[next_para:].lstrip("\n\r ") # Clean up copyright lines text = re.sub(r"©\s*\d{4}\s*$", "", text, flags=re.MULTILINE).strip() return text