
/**
* Google Classroom MCP Server + Gemini 3.8 Flash Agent Runner
* Configured to test “Sample Delete” -> “Figurative Language Project” (6 criteria rubric)
*/
function doPost(e) {
const request = JSON.parse(e.postData.contents);
const method = request.method;
const id = request.id;
// 1. Tell Gemini what Google Classroom MCP tools are available
if (method === “tools/list”) {
return sendJsonRpcResponse(id, {
tools: [
{
name: “list_courses”,
description: “List the 20 most recent active Google Classroom courses for the teacher.”,
inputSchema: {
type: “object”,
properties: {}
}
},
{
name: “list_coursework”,
description: “List assignments (coursework) in a Google Classroom course so you can find the courseWorkId.”,
inputSchema: {
type: “object”,
properties: {
courseId: { type: “string”, description: “The ID of the course” }
},
required: [“courseId”]
}
},
{
name: “create_student_rubric_report”,
description: “Extracts a student’s 6-criteria rubric results on an assignment and creates an individual Google Doc growth report. If studentName is omitted, it automatically picks a student who has rubric grades (or the first student on the roster).”,
inputSchema: {
type: “object”,
properties: {
courseId: { type: “string”, description: “The ID of the course” },
courseWorkId: { type: “string”, description: “The ID of the assignment (courseWork)” },
studentName: { type: “string”, description: “Optional name of the student. Leave blank to auto-select a student with rubric scores.” },
teacherFeedbackNote: { type: “string”, description: “Encouraging note or next steps from the teacher to include at the top of the report” }
},
required: [“courseId”, “courseWorkId”]
}
}
]
});
}
// 2. Execute the requested MCP tool when Gemini calls it
if (method === “tools/call”) {
const toolName = request.params.name;
const args = request.params.arguments || {};
if (toolName === “list_courses”) {
const response = Classroom.Courses.list({
courseStates: [“ACTIVE”],
pageSize: 20
});
const courses = (response.courses || []).map(c => ({
id: c.id,
name: c.name,
section: c.section || “No section”
}));
return sendJsonRpcResponse(id, {
content: [{ type: “text”, text: JSON.stringify(courses, null, 2) }]
});
}
if (toolName === “list_coursework”) {
const response = Classroom.Courses.CourseWork.list(args.courseId, { pageSize: 20 });
const work = (response.courseWork || []).map(w => ({
courseWorkId: w.id,
title: w.title
}));
return sendJsonRpcResponse(id, {
content: [{ type: “text”, text: JSON.stringify(work, null, 2) }]
});
}
if (toolName === “create_student_rubric_report”) {
const reportResult = buildRubricReportDoc(
args.courseId,
args.courseWorkId,
args.studentName || “”,
args.teacherFeedbackNote || “”
);
return sendJsonRpcResponse(id, {
content: [{ type: “text”, text: JSON.stringify(reportResult, null, 2) }]
});
}
}
return sendJsonRpcResponse(id, { message: “Method not supported: ” + method });
}
function sendJsonRpcResponse(id, result) {
const payload = { jsonrpc: “2.0”, id: id, result: result };
return ContentService.createTextOutput(JSON.stringify(payload))
.setMimeType(ContentService.MimeType.JSON);
}
/**
* Pulls the rubric from Figurative Language Project, finds a student submission,
* and creates a Google Doc report in Drive.
*/
function buildRubricReportDoc(courseId, courseWorkId, studentNameSearch, teacherNote) {
const courseWork = Classroom.Courses.CourseWork.get(courseId, courseWorkId);
// 1. Fetch the 6-criteria rubric attached to Figurative Language Project
const rubricsResponse = Classroom.Courses.CourseWork.Rubrics.list(courseId, courseWorkId);
const rubrics = rubricsResponse.rubrics || [];
if (rubrics.length === 0) {
return { error: “No rubric was found attached to assignment: ” + courseWork.title };
}
const rubric = rubrics[0];
// 2. Pull the student roster and submissions
const studentsResponse = Classroom.Courses.Students.list(courseId);
const students = studentsResponse.students || [];
if (students.length === 0) {
return { error: “No students are enrolled in this course.” };
}
const subsResponse = Classroom.Courses.CourseWork.StudentSubmissions.list(courseId, courseWorkId);
const submissions = subsResponse.studentSubmissions || [];
let targetStudent = null;
let targetSub = null;
// If a student name was specified, match by name
if (studentNameSearch && studentNameSearch.trim() !== “”) {
targetStudent = students.find(s =>
s.profile.name.fullName.toLowerCase().includes(studentNameSearch.toLowerCase())
);
if (targetStudent) {
targetSub = submissions.find(sub => sub.userId === targetStudent.userId);
}
}
// Otherwise, automatically find the first submission that has assigned or draft rubric grades!
if (!targetSub) {
targetSub = submissions.find(sub =>
(sub.assignedRubricGrades && Object.keys(sub.assignedRubricGrades).length > 0) ||
(sub.draftRubricGrades && Object.keys(sub.draftRubricGrades).length > 0)
) || submissions[0];
if (targetSub) {
targetStudent = students.find(s => s.userId === targetSub.userId);
}
}
const studentFullName = targetStudent
? targetStudent.profile.name.fullName
: “Sample Student (User ID ” + (targetSub ? targetSub.userId : “Unknown”) + “)”;
const rubricGrades = targetSub
? (targetSub.assignedRubricGrades || targetSub.draftRubricGrades || {})
: {};
// 3. Create the Google Doc Rubric Growth Report
const docTitle = studentFullName + ” – Rubric Report – ” + courseWork.title;
const doc = DocumentApp.create(docTitle);
const body = doc.getBody();
body.appendParagraph(docTitle)
.setHeading(DocumentApp.ParagraphHeading.HEADING1);
if (teacherNote) {
body.appendParagraph(“Teacher Note: ” + teacherNote)
.setHeading(DocumentApp.ParagraphHeading.HEADING3);
}
body.appendParagraph(“Here is your rubric breakdown across all ” + (rubric.criteria || []).length + ” criteria for ” + courseWork.title + “:”);
const tableRows = [[“Criterion”, “Current Level”, “What This Means & How to Grow”]];
(rubric.criteria || []).forEach(criterion => {
const critTitle = criterion.title || “Untitled Criterion”;
const critDesc = criterion.description || “”;
const studentGradeObj = rubricGrades[criterion.id];
let achievedLevelTitle = “Not Yet Assessed”;
let achievedLevelDesc = “Review the criterion levels below to see the target expectation.”;
if (studentGradeObj && studentGradeObj.levelId) {
const matchedLevel = (criterion.levels || []).find(lvl => lvl.id === studentGradeObj.levelId);
if (matchedLevel) {
achievedLevelTitle = matchedLevel.title || (“Points: ” + matchedLevel.points);
achievedLevelDesc = matchedLevel.description || “”;
}
} else if (criterion.levels && criterion.levels.length > 0) {
// Show all available levels if the teacher hasn’t clicked a rubric box for this student yet
achievedLevelDesc = “Target Levels:\n” + criterion.levels
.map(l => “• ” + (l.title || l.points + ” pts”) + “: ” + (l.description || “”))
.join(“\n”);
}
const detailText = (critDesc ? “Skill Goal: ” + critDesc + “\n\n” : “”) + achievedLevelDesc;
tableRows.push([critTitle, achievedLevelTitle, detailText.trim()]);
});
const table = body.appendTable(tableRows);
table.getRow(0).editAsText().setBold(true);
doc.saveAndClose();
return {
status: “Success”,
student: studentFullName,
assignment: courseWork.title,
criteriaCount: (rubric.criteria || []).length,
hadScoredRubricBoxes: Object.keys(rubricGrades).length > 0,
documentUrl: doc.getUrl()
};
}
/**
* Run this function! It pulls GEMINI_API_KEY from Script Properties and runs the 3-step MCP loop.
*/
function askClassroomAgent() {
const apiKey = PropertiesService.getScriptProperties().getProperty(“GEMINI_API_KEY”);
if (!apiKey) {
throw new Error(“Missing GEMINI_API_KEY in Script Properties!”);
}
const myPrompt = “Find my ‘Sample Delete’ course, look up the ‘Figurative Language Project’ assignment, and create a student rubric report with an encouraging note about using vivid metaphors.”;
const toolsList = JSON.parse(doPost({
postData: { contents: JSON.stringify({ jsonrpc: “2.0”, id: 1, method: “tools/list” }) }
}).getContent()).result.tools;
let contents = [{ role: “user”, parts: [{ text: myPrompt }] }];
for (let step = 0; step < 5; step++) {
const aiResponse = callGeminiWithMcp(apiKey, contents, toolsList);
if (aiResponse.error) {
Logger.log(“❌ API Error: ” + JSON.stringify(aiResponse.error, null, 2));
return;
}
const modelContent = aiResponse.candidates[0].content;
contents.push(modelContent);
const callPart = modelContent.parts.find(p => p.functionCall);
if (callPart) {
const fnCall = callPart.functionCall;
Logger.log(“🤖 Step ” + (step + 1) + ” – Gemini 3.8 Flash called MCP tool: ” + fnCall.name);
Logger.log(“📦 Arguments: ” + JSON.stringify(fnCall.args));
const mcpResult = JSON.parse(doPost({
postData: {
contents: JSON.stringify({
jsonrpc: “2.0”,
id: step + 2,
method: “tools/call”,
params: { name: fnCall.name, arguments: fnCall.args || {} }
})
}
}).getContent()).result.content[0].text;
Logger.log(“🔧 MCP Server Output: ” + mcpResult);
contents.push({
role: “user”,
parts: [{
functionResponse: {
name: fnCall.name,
response: { result: mcpResult }
}
}]
});
} else {
const textPart = modelContent.parts.find(p => p.text && !p.thought);
Logger.log(“✅ Done! Gemini 3.8 Flash says:\n” + (textPart ? textPart.text : JSON.stringify(modelContent.parts)));
break;
}
}
}
function callGeminiWithMcp(apiKey, contents, mcpTools) {
const url = “https://generativelanguage.googleapis.com/v1beta/models/gemini-3.8-flash:generateContent?key=” + apiKey;
const functionDeclarations = mcpTools.map(t => {
const decl = { name: t.name, description: t.description };
if (t.inputSchema && t.inputSchema.properties && Object.keys(t.inputSchema.properties).length > 0) {
decl.parameters = t.inputSchema;
}
return decl;
});
const payload = {
contents: contents,
tools: [{ functionDeclarations: functionDeclarations }]
};
const res = UrlFetchApp.fetch(url, {
method: “post”,
contentType: “application/json”,
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
return JSON.parse(res.getContentText());
}