tool_grade_answer_group
Apply consistent rubric items, point adjustments, and comments to all student submissions within a designated answer group simultaneously.
Instructions
Batch-grade ALL submissions in an answer group at once.
**WARNING**: This grades N students at once. Use with caution.
Args:
course_id: The Gradescope course ID.
question_id: The question ID.
group_id: The answer group ID.
rubric_item_ids: Rubric item IDs to apply.
point_adjustment: Point adjustment.
comment: Grader comment.
confirm_write: Must be True to apply grades.Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| course_id | Yes | ||
| question_id | Yes | ||
| group_id | Yes | ||
| rubric_item_ids | No | ||
| point_adjustment | No | ||
| comment | No | ||
| confirm_write | No |
Implementation Reference
- The handler function `grade_answer_group` which performs batch grading of answer groups.
def grade_answer_group( course_id: str, question_id: str, group_id: str, rubric_item_ids: list[str] | None = None, point_adjustment: float | None = None, comment: str | None = None, confirm_write: bool = False, ) -> str: """Batch-grade all submissions in an answer group at once. This is the most efficient grading method. Instead of grading N submissions individually, you grade one group and the score applies to ALL members via the `save_many_grades` endpoint. **WARNING**: This modifies grades for ALL submissions in the group. Args: course_id: The Gradescope course ID. question_id: The question ID. group_id: The answer group ID. rubric_item_ids: Rubric item IDs to apply. None keeps current. point_adjustment: Submission-specific point adjustment. None keeps current. comment: Grader comment. None keeps current. confirm_write: Must be True to apply grades. """ if not course_id or not question_id or not group_id: return "Error: course_id, question_id, and group_id are required." # Defensive coercion: MCP clients / LLMs sometimes pass a single string # instead of a list. Wrap it so the rest of the logic works. if isinstance(rubric_item_ids, str): rubric_item_ids = [rubric_item_ids] if rubric_item_ids is None and point_adjustment is None and comment is None: return "Error: at least one of rubric_item_ids, point_adjustment, or comment must be provided." try: conn = get_connection() # Get answer groups data for group size info ag_data = _fetch_answer_groups_json(course_id, question_id) group_subs = [ s for s in ag_data.get("submissions", []) if str(s.get("confirmed_group_id")) == str(group_id) or str(s.get("unconfirmed_group_id")) == str(group_id) ] target_group = None for g in ag_data.get("groups", []): if str(g["id"]) == str(group_id): target_group = g break if not target_group: return f"Error: group `{group_id}` not found." # Access the group grading page to get save_many_grades URL + CSRF group_grade_url = ( f"{conn.gradescope_base_url}/courses/{course_id}" f"/questions/{question_id}/answer_groups/{group_id}/grade" ) resp = conn.session.get(group_grade_url) if resp.status_code != 200: return f"Error: Cannot access group grade page (status {resp.status_code})." soup = BeautifulSoup(resp.text, "html.parser") csrf_meta = soup.find("meta", {"name": "csrf-token"}) csrf_token = csrf_meta.get("content", "") if csrf_meta else "" grader = soup.find(attrs={"data-react-class": "SubmissionGrader"}) if not grader: return "Error: SubmissionGrader component not found on group grade page." props = json.loads(grader.get("data-react-props", "{}")) except AuthError as e: return f"Authentication error: {e}" except ValueError as e: return f"Error: {e}" except Exception as e: return f"Error preparing batch grade: {e}" # Confirmation gate if not confirm_write: details = [ f"course_id=`{course_id}`", f"question_id=`{question_id}`", f"group_id=`{group_id}`", f"group_title={target_group.get('title', '?')}", f"group_size={len(group_subs)} submissions", ] if rubric_item_ids is not None: details.append(f"rubric_item_ids={sorted(rubric_item_ids)}") if point_adjustment is not None: details.append(f"point_adjustment={point_adjustment}") if comment is not None: details.append(f"comment={comment}") return write_confirmation_required("grade_answer_group", details) # Find save_many_grades URL save_url = props.get("urls", {}).get("save_grade") if not save_url: return "Error: save_grade URL not found in group grading context." # In group mode, the URL should be save_many_grades # The frontend replaces save_grade with save_many_grades when group_mode=True save_many_url = save_url.replace("/save_grade", "/save_many_grades") if "/save_many_grades" not in save_many_url: # Fallback: construct it match = re.search(r"/submissions/(\d+)", save_url) if match: save_many_url = save_url.replace( f"/submissions/{match.group(1)}/save_grade", f"/submissions/{match.group(1)}/save_many_grades", ) # Build JSON payload matching what the Gradescope frontend sends. rubric_items = props.get("rubric_items", []) current_evals = props.get("rubric_item_evaluations", []) current_eval = props.get("evaluation", {}) if rubric_item_ids is not None: apply_ids = set(str(rid) for rid in rubric_item_ids) else: apply_ids = {str(e["rubric_item_id"]) for e in current_evals if e.get("present")} rubric_items_payload = {} for ri in rubric_items: rid = str(ri["id"]) rubric_items_payload[rid] = { "score": "true" if rid in apply_ids else "false" } resolved_points = ( point_adjustment if point_adjustment is not None else current_eval.get("points") ) resolved_comments = ( comment if comment is not None else current_eval.get("comments") ) json_payload = { "rubric_items": rubric_items_payload, "question_submission_evaluation": { "points": resolved_points, "comments": resolved_comments, }, } headers = { "X-CSRF-Token": csrf_token, "Content-Type": "application/json", "Accept": "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", } try: resp = conn.session.post( f"{conn.gradescope_base_url}{save_many_url}", json=json_payload, headers=headers, ) except Exception as e: return f"Error saving batch grade: {e}" if resp.status_code == 200: try: result = resp.json() return ( f"✅ Batch grade applied to {len(group_subs)} submissions!\n" f"**Group:** {target_group.get('title', group_id)}\n" f"**Rubric items applied:** {sorted(apply_ids)}\n" f"**Point adjustment:** {point_adjustment}\n" f"**Comment:** {comment or '(unchanged)'}" ) except Exception: return f"✅ Batch grade saved (status 200). Response: {resp.text[:200]}" else: return ( f"Error: Batch grade failed (status {resp.status_code}). " f"Response: {resp.text[:300]}" ) - src/gradescope_mcp/server.py:592-617 (registration)Registration of `tool_grade_answer_group` in the MCP server definition.
def tool_grade_answer_group( course_id: str, question_id: str, group_id: str, rubric_item_ids: list[str] | None = None, point_adjustment: float | None = None, comment: str | None = None, confirm_write: bool = False, ) -> str: """Batch-grade ALL submissions in an answer group at once. **WARNING**: This grades N students at once. Use with caution. Args: course_id: The Gradescope course ID. question_id: The question ID. group_id: The answer group ID. rubric_item_ids: Rubric item IDs to apply. point_adjustment: Point adjustment. comment: Grader comment. confirm_write: Must be True to apply grades. """ return grade_answer_group( course_id, question_id, group_id, rubric_item_ids, point_adjustment, comment, confirm_write )