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

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

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
        )
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden and does well by disclosing key behavioral traits: it's a batch operation affecting multiple students, includes a safety warning, and specifies a confirm_write parameter as a safeguard. It doesn't detail permissions, rate limits, or exact effects on grades, but covers essential mutation context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose and warning, followed by a structured Args section. Every sentence earns its place: the first states the action, the second warns, and the parameter list is necessary given low schema coverage. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given a complex mutation tool with 7 parameters, 0% schema coverage, no annotations, but an output schema exists, the description is mostly complete. It covers purpose, caution, and parameter meanings, but could benefit from more on prerequisites or error handling. The output schema reduces need for return value details.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It lists all 7 parameters with brief explanations (e.g., 'Rubric item IDs to apply,' 'Point adjustment,' 'Grader comment'), adding meaningful semantics beyond the schema's titles. However, it lacks details on formats or constraints for IDs and adjustments.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('batch-grade ALL submissions'), the target resource ('answer group'), and scope ('at once'). It distinguishes from sibling tools like tool_apply_grade (which likely grades individual submissions) by emphasizing the batch nature and N students scope.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context with the 'WARNING' about grading N students at once and 'Use with caution,' which implicitly guides when to use this tool versus individual grading alternatives. However, it doesn't explicitly name alternative tools or state when-not-to-use scenarios beyond the caution.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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