Skip to main content
Glama

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
NameRequiredDescriptionDefault
course_idYes
question_idYes
group_idYes
rubric_item_idsNo
point_adjustmentNo
commentNo
confirm_writeNo

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]}"
            )
  • 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
        )

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Yuanpeng-Li/gradescope-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server