gogcli-mcp-classroom 4.4.0 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/dist/index.js +736 -285
- package/manifest.json +9 -9
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/tools/classroom-extra.ts +247 -19
- package/tests/tools/classroom-extra-gates.test.ts +211 -0
- package/tests/tools/classroom-extra.test.ts +240 -2
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-classroom",
|
|
5
5
|
"display_name": "gogcli (Classroom)",
|
|
6
|
-
"version": "4.
|
|
6
|
+
"version": "4.5.1",
|
|
7
7
|
"description": "Extended Google Classroom for Claude via gogcli — auth + full Classroom support (courses, rosters, coursework, submissions, announcements, topics, invitations)",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
"name": "gog_classroom_courses_delete",
|
|
118
|
-
"description": "Delete a course"
|
|
118
|
+
"description": "Delete a course (asks the user to confirm)"
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
"name": "gog_classroom_courses_archive",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
"name": "gog_classroom_students_add",
|
|
138
|
-
"description": "Add a student to a course"
|
|
138
|
+
"description": "Add a student to a course (asks the user to confirm unless adding yourself)"
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
"name": "gog_classroom_students_remove",
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"name": "gog_classroom_teachers_add",
|
|
154
|
-
"description": "Add a teacher to a course"
|
|
154
|
+
"description": "Add a teacher to a course (asks the user to confirm)"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
"name": "gog_classroom_teachers_remove",
|
|
@@ -171,15 +171,15 @@
|
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"name": "gog_classroom_coursework_create",
|
|
174
|
-
"description": "Create coursework (assignment/question)"
|
|
174
|
+
"description": "Create coursework (assignment/question) (asks the user to confirm unless an unscheduled DRAFT)"
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
"name": "gog_classroom_coursework_update",
|
|
178
|
-
"description": "Update existing coursework"
|
|
178
|
+
"description": "Update existing coursework (publishing asks the user to confirm)"
|
|
179
179
|
},
|
|
180
180
|
{
|
|
181
181
|
"name": "gog_classroom_coursework_delete",
|
|
182
|
-
"description": "Delete coursework"
|
|
182
|
+
"description": "Delete coursework (asks the user to confirm)"
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"name": "gog_classroom_submissions_list",
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
},
|
|
196
196
|
{
|
|
197
197
|
"name": "gog_classroom_submissions_return",
|
|
198
|
-
"description": "Return a graded submission"
|
|
198
|
+
"description": "Return a graded submission (asks the user to confirm)"
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
201
|
"name": "gog_classroom_submissions_turn_in",
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
},
|
|
220
220
|
{
|
|
221
221
|
"name": "gog_classroom_announcements_update",
|
|
222
|
-
"description": "Update an announcement"
|
|
222
|
+
"description": "Update an announcement (publishing asks the user to confirm)"
|
|
223
223
|
},
|
|
224
224
|
{
|
|
225
225
|
"name": "gog_classroom_announcements_delete",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-classroom",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.1",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-classroom",
|
|
5
5
|
"description": "Extended Google Classroom MCP server via gogcli — auth + full Classroom support",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test:coverage": "vitest run --coverage"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@chrischall/mcp-utils": "^2.
|
|
27
|
+
"@chrischall/mcp-utils": "^2.6.0",
|
|
28
28
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
29
29
|
"zod": "^4.6.1"
|
|
30
30
|
},
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp-classroom"
|
|
9
9
|
},
|
|
10
|
-
"version": "4.
|
|
10
|
+
"version": "4.5.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-classroom",
|
|
15
|
-
"version": "4.
|
|
15
|
+
"version": "4.5.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
accountParam,
|
|
5
|
+
runOrDiagnose,
|
|
6
|
+
pos,
|
|
7
|
+
readCourse,
|
|
8
|
+
readCoursework,
|
|
9
|
+
readClassroomWork,
|
|
10
|
+
requireDispatchConfirmation,
|
|
11
|
+
bodyPreview,
|
|
12
|
+
CONFIRM_FALLBACK_DESCRIPTION,
|
|
13
|
+
confirmTokenParam,
|
|
14
|
+
} from '../../../gogcli-mcp/src/lib.js';
|
|
4
15
|
import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
|
|
5
16
|
|
|
6
17
|
const courseState = z.enum(['ACTIVE', 'ARCHIVED', 'PROVISIONED', 'DECLINED', 'SUSPENDED']);
|
|
7
18
|
const workState = z.enum(['PUBLISHED', 'DRAFT']);
|
|
8
19
|
const workType = z.enum(['ASSIGNMENT', 'SHORT_ANSWER_QUESTION', 'MULTIPLE_CHOICE_QUESTION']);
|
|
9
20
|
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// PUBLISHING THROUGH UPDATE is on the dispatch rail (SEC-3, fleet-audit #932).
|
|
23
|
+
// gog_classroom_announcements_create asks unless the state is DRAFT — the
|
|
24
|
+
// draft path is documented as the way through on a client that cannot be
|
|
25
|
+
// prompted — so create-as-DRAFT then update-to-PUBLISHED was the same two-step
|
|
26
|
+
// around a gate that closed for Gmail drafts. An update that publishes (state
|
|
27
|
+
// PUBLISHED, or a schedule: a scheduled item is a draft that publishes itself)
|
|
28
|
+
// reads the course and the item and asks, with the text or title the class
|
|
29
|
+
// will actually see; a text edit, a due date or a return to DRAFT does not.
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/** When an update makes work visible to students, in the words the prompt uses; undefined when it does not. */
|
|
33
|
+
function publishesWhen(state: string | undefined, scheduled: string | undefined): string | undefined {
|
|
34
|
+
if (scheduled) return `at ${scheduled}`;
|
|
35
|
+
return state === 'PUBLISHED' ? 'immediately' : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PUBLISH_DESCRIPTION = ' Publishing it (state PUBLISHED, or a scheduled time) reads the course and the item and asks the MCP host to '
|
|
39
|
+
+ 'show the user a confirmation prompt with the class and what students will see; nothing is published unless they '
|
|
40
|
+
+ 'accept. Other edits, and a return to DRAFT, need no confirmation.' + CONFIRM_FALLBACK_DESCRIPTION;
|
|
41
|
+
|
|
10
42
|
// Fields shared by courses_create and courses_update. `name` is required on
|
|
11
43
|
// create, optional on update — keep it out of this fragment so each tool can
|
|
12
44
|
// declare its own rule.
|
|
@@ -74,13 +106,33 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
74
106
|
});
|
|
75
107
|
|
|
76
108
|
server.registerTool('gog_classroom_courses_delete', {
|
|
77
|
-
description: 'Delete a Google Classroom course.'
|
|
109
|
+
description: 'Delete a Google Classroom course — its coursework, submissions and grades go with it, for good. Reads '
|
|
110
|
+
+ 'the course and asks the MCP host to show the user a confirmation prompt naming it first; nothing is deleted '
|
|
111
|
+
+ 'unless they accept. gog_classroom_courses_archive is the reversible alternative.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
78
112
|
annotations: { destructiveHint: true },
|
|
79
113
|
inputSchema: z.object({
|
|
80
114
|
courseId: z.string().describe('Course ID'),
|
|
81
115
|
account: accountParam,
|
|
116
|
+
confirmToken: confirmTokenParam,
|
|
82
117
|
}),
|
|
83
|
-
}, async ({ courseId, account }) => {
|
|
118
|
+
}, async ({ courseId, account, confirmToken }, ctx) => {
|
|
119
|
+
const read = await readCourse(courseId, account, runOrDiagnose);
|
|
120
|
+
if (read.error) return read.error;
|
|
121
|
+
const view = { course: read.course, deletes: 'the course with all of its coursework, submissions and grades, for good' };
|
|
122
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
123
|
+
action: 'classroom.course-delete',
|
|
124
|
+
message: 'Review and confirm PERMANENTLY deleting this course:',
|
|
125
|
+
confirmationLabel: 'Confirm that this course should be deleted now.',
|
|
126
|
+
details: view,
|
|
127
|
+
unsupportedNote: 'Archive it with gog_classroom_courses_archive instead, or ask the user to delete it from Classroom.',
|
|
128
|
+
fallback: {
|
|
129
|
+
tool: 'gog_classroom_courses_delete',
|
|
130
|
+
account,
|
|
131
|
+
confirmToken,
|
|
132
|
+
subject: () => ({ target: courseId, payload: view, preview: view }),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
if (confirmation) return confirmation;
|
|
84
136
|
return runOrDiagnose(['classroom', 'courses', 'delete', pos(courseId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
85
137
|
});
|
|
86
138
|
|
|
@@ -107,17 +159,39 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
107
159
|
});
|
|
108
160
|
|
|
109
161
|
server.registerTool('gog_classroom_students_add', {
|
|
110
|
-
description: 'Add a student to a Google Classroom course.'
|
|
162
|
+
description: 'Add a student to a Google Classroom course. Adding someone else gives them the class stream and '
|
|
163
|
+
+ 'materials, so this reads the course and asks the MCP host to show the user a confirmation prompt with the class '
|
|
164
|
+
+ 'and the student first; enrolling yourself ("me") does not ask.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
111
165
|
annotations: { destructiveHint: true },
|
|
112
166
|
inputSchema: z.object({
|
|
113
167
|
courseId: z.string().describe('Course ID'),
|
|
114
|
-
userId: z.string().describe('Student user ID (or "me")'),
|
|
168
|
+
userId: z.string().describe('Student user ID or email (or "me")'),
|
|
115
169
|
enrollmentCode: z.string().optional().describe('Enrollment code (required if adding self via code)'),
|
|
116
170
|
account: accountParam,
|
|
171
|
+
confirmToken: confirmTokenParam,
|
|
117
172
|
}),
|
|
118
|
-
}, async ({ courseId, userId, enrollmentCode, account }) => {
|
|
173
|
+
}, async ({ courseId, userId, enrollmentCode, account, confirmToken }, ctx) => {
|
|
119
174
|
const args: GogArg[] = ['classroom', 'students', 'add', pos(courseId), pos(userId)];
|
|
120
175
|
if (enrollmentCode) args.push(`--enrollment-code=${enrollmentCode}`);
|
|
176
|
+
if (userId.toLowerCase() !== 'me') {
|
|
177
|
+
const read = await readCourse(courseId, account, runOrDiagnose);
|
|
178
|
+
if (read.error) return read.error;
|
|
179
|
+
const view = { course: read.course, student: userId };
|
|
180
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
181
|
+
action: 'classroom.student-add',
|
|
182
|
+
message: 'Review and confirm adding this student to the class:',
|
|
183
|
+
confirmationLabel: 'Confirm that this person should be added to the class now.',
|
|
184
|
+
details: view,
|
|
185
|
+
unsupportedNote: 'Ask the user to add them from Classroom, or invite them with gog_classroom_invitations_create.',
|
|
186
|
+
fallback: {
|
|
187
|
+
tool: 'gog_classroom_students_add',
|
|
188
|
+
account,
|
|
189
|
+
confirmToken,
|
|
190
|
+
subject: () => ({ target: `${courseId}/${userId}`, payload: view, preview: view }),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
if (confirmation) return confirmation;
|
|
194
|
+
}
|
|
121
195
|
return runOrDiagnose(args, { account });
|
|
122
196
|
});
|
|
123
197
|
|
|
@@ -134,14 +208,34 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
134
208
|
});
|
|
135
209
|
|
|
136
210
|
server.registerTool('gog_classroom_teachers_add', {
|
|
137
|
-
description: 'Add a teacher to a Google Classroom course.',
|
|
211
|
+
description: 'Add a teacher to a Google Classroom course. A co-teacher sees every student\'s work and grades, so '
|
|
212
|
+
+ 'this reads the course and asks the MCP host to show the user a confirmation prompt with the class and the '
|
|
213
|
+
+ 'teacher first; nothing changes unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
138
214
|
annotations: { destructiveHint: true },
|
|
139
215
|
inputSchema: z.object({
|
|
140
216
|
courseId: z.string().describe('Course ID'),
|
|
141
|
-
userId: z.string().describe('Teacher user ID'),
|
|
217
|
+
userId: z.string().describe('Teacher user ID or email'),
|
|
142
218
|
account: accountParam,
|
|
219
|
+
confirmToken: confirmTokenParam,
|
|
143
220
|
}),
|
|
144
|
-
}, async ({ courseId, userId, account }) => {
|
|
221
|
+
}, async ({ courseId, userId, account, confirmToken }, ctx) => {
|
|
222
|
+
const read = await readCourse(courseId, account, runOrDiagnose);
|
|
223
|
+
if (read.error) return read.error;
|
|
224
|
+
const view = { course: read.course, teacher: userId, grants: 'teacher access: every student\'s work, grades and roster' };
|
|
225
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
226
|
+
action: 'classroom.teacher-add',
|
|
227
|
+
message: 'Review and confirm giving this person teacher access to the class:',
|
|
228
|
+
confirmationLabel: 'Confirm that this person should become a teacher of the class now.',
|
|
229
|
+
details: view,
|
|
230
|
+
unsupportedNote: 'Ask the user to add them from Classroom, or invite them with gog_classroom_invitations_create.',
|
|
231
|
+
fallback: {
|
|
232
|
+
tool: 'gog_classroom_teachers_add',
|
|
233
|
+
account,
|
|
234
|
+
confirmToken,
|
|
235
|
+
subject: () => ({ target: `${courseId}/${userId}`, payload: view, preview: view }),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
if (confirmation) return confirmation;
|
|
145
239
|
return runOrDiagnose(['classroom', 'teachers', 'add', pos(courseId), pos(userId)], { account });
|
|
146
240
|
});
|
|
147
241
|
|
|
@@ -158,15 +252,20 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
158
252
|
});
|
|
159
253
|
|
|
160
254
|
server.registerTool('gog_classroom_coursework_create', {
|
|
161
|
-
description: 'Create a new coursework item (assignment, question, etc.) in a course.'
|
|
255
|
+
description: 'Create a new coursework item (assignment, question, etc.) in a course. Unless state is DRAFT with no '
|
|
256
|
+
+ 'scheduled time (which students cannot see; a scheduled draft publishes itself), the class is notified, so this '
|
|
257
|
+
+ 'reads the course and asks the MCP host to show the user a confirmation prompt with the class, the title, the '
|
|
258
|
+
+ 'text, the due date and when it publishes; nothing is posted unless they accept. To stage one without asking, '
|
|
259
|
+
+ 'pass state DRAFT and no scheduled time.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
162
260
|
annotations: { destructiveHint: true },
|
|
163
261
|
inputSchema: z.object({
|
|
164
262
|
courseId: z.string().describe('Course ID'),
|
|
165
263
|
title: z.string().describe('Coursework title'),
|
|
166
264
|
...courseworkSharedFields,
|
|
167
265
|
account: accountParam,
|
|
266
|
+
confirmToken: confirmTokenParam,
|
|
168
267
|
}),
|
|
169
|
-
}, async ({ courseId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account }) => {
|
|
268
|
+
}, async ({ courseId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account, confirmToken }, ctx) => {
|
|
170
269
|
const args: GogArg[] = ['classroom', 'coursework', 'create', pos(courseId), `--title=${title}`];
|
|
171
270
|
if (description) args.push(`--description=${description}`);
|
|
172
271
|
if (type) args.push(`--type=${type}`);
|
|
@@ -177,11 +276,42 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
177
276
|
if (dueTime) args.push(`--due-time=${dueTime}`);
|
|
178
277
|
if (scheduled) args.push(`--scheduled=${scheduled}`);
|
|
179
278
|
if (topic) args.push(`--topic=${topic}`);
|
|
279
|
+
// An unscheduled draft reaches nobody until a teacher publishes it; a
|
|
280
|
+
// scheduled one publishes itself.
|
|
281
|
+
if (state !== 'DRAFT' || scheduled) {
|
|
282
|
+
const read = await readCourse(courseId, account, runOrDiagnose);
|
|
283
|
+
if (read.error) return read.error;
|
|
284
|
+
const work = {
|
|
285
|
+
course: read.course,
|
|
286
|
+
title,
|
|
287
|
+
type: type ?? 'ASSIGNMENT',
|
|
288
|
+
publishes: scheduled ? `at ${scheduled}` : 'immediately',
|
|
289
|
+
due: due ?? ([dueDate, dueTime].filter(Boolean).join(' ') || undefined),
|
|
290
|
+
maxPoints,
|
|
291
|
+
};
|
|
292
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
293
|
+
action: 'classroom.coursework-create',
|
|
294
|
+
message: 'Review and confirm posting this coursework to the class:',
|
|
295
|
+
confirmationLabel: 'Confirm that this coursework should be posted to the class.',
|
|
296
|
+
details: { ...work, descriptionPreview: bodyPreview(description) },
|
|
297
|
+
unsupportedNote: 'Create it with state DRAFT instead; the user can review and post it from Classroom.',
|
|
298
|
+
fallback: {
|
|
299
|
+
tool: 'gog_classroom_coursework_create',
|
|
300
|
+
account,
|
|
301
|
+
confirmToken,
|
|
302
|
+
subject: () => {
|
|
303
|
+
const view = { ...work, description };
|
|
304
|
+
return { target: courseId, payload: view, preview: view };
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
if (confirmation) return confirmation;
|
|
309
|
+
}
|
|
180
310
|
return runOrDiagnose(args, { account });
|
|
181
311
|
});
|
|
182
312
|
|
|
183
313
|
server.registerTool('gog_classroom_coursework_update', {
|
|
184
|
-
description: 'Update an existing coursework item.',
|
|
314
|
+
description: 'Update an existing coursework item.' + PUBLISH_DESCRIPTION,
|
|
185
315
|
annotations: { destructiveHint: true },
|
|
186
316
|
inputSchema: z.object({
|
|
187
317
|
courseId: z.string().describe('Course ID'),
|
|
@@ -189,8 +319,9 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
189
319
|
title: z.string().optional().describe('New title'),
|
|
190
320
|
...courseworkSharedFields,
|
|
191
321
|
account: accountParam,
|
|
322
|
+
confirmToken: confirmTokenParam,
|
|
192
323
|
}),
|
|
193
|
-
}, async ({ courseId, courseworkId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account }) => {
|
|
324
|
+
}, async ({ courseId, courseworkId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account, confirmToken }, ctx) => {
|
|
194
325
|
const args: GogArg[] = ['classroom', 'coursework', 'update', pos(courseId), pos(courseworkId)];
|
|
195
326
|
if (title) args.push(`--title=${title}`);
|
|
196
327
|
if (description) args.push(`--description=${description}`);
|
|
@@ -202,37 +333,134 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
202
333
|
if (dueTime) args.push(`--due-time=${dueTime}`);
|
|
203
334
|
if (scheduled) args.push(`--scheduled=${scheduled}`);
|
|
204
335
|
if (topic) args.push(`--topic=${topic}`);
|
|
336
|
+
const publishes = publishesWhen(state, scheduled);
|
|
337
|
+
if (publishes) {
|
|
338
|
+
const read = await readCourse(courseId, account, runOrDiagnose);
|
|
339
|
+
if (read.error) return read.error;
|
|
340
|
+
const current = await readClassroomWork('coursework', courseId, courseworkId, account, runOrDiagnose);
|
|
341
|
+
if (current.error) return current.error;
|
|
342
|
+
// What the class will see: the new title/description when this call
|
|
343
|
+
// replaces them, else what the draft says now. `||`, not `??`: an empty
|
|
344
|
+
// string is never sent (the argv guards above are truthy checks), so gog
|
|
345
|
+
// keeps the current value and the prompt must show that one.
|
|
346
|
+
const published = description || current.work.description;
|
|
347
|
+
const publishedTitle = title || current.work.title;
|
|
348
|
+
const view = {
|
|
349
|
+
course: read.course,
|
|
350
|
+
coursework: { id: courseworkId, title: publishedTitle, state: current.work.state },
|
|
351
|
+
publishes,
|
|
352
|
+
descriptionPreview: bodyPreview(published),
|
|
353
|
+
};
|
|
354
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
355
|
+
action: 'classroom.coursework-publish',
|
|
356
|
+
message: 'Review and confirm publishing this Classroom coursework to students:',
|
|
357
|
+
confirmationLabel: 'Confirm that this coursework should be published to the class.',
|
|
358
|
+
details: view,
|
|
359
|
+
unsupportedNote: 'Leave it as a DRAFT; the user can review and publish it from Classroom.',
|
|
360
|
+
fallback: {
|
|
361
|
+
tool: 'gog_classroom_coursework_update',
|
|
362
|
+
account,
|
|
363
|
+
confirmToken,
|
|
364
|
+
subject: () => ({
|
|
365
|
+
target: `${courseId}/${courseworkId}`,
|
|
366
|
+
revision: current.work.updateTime,
|
|
367
|
+
payload: { course: read.course, courseworkId, title: publishedTitle, description: published, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic },
|
|
368
|
+
preview: view,
|
|
369
|
+
}),
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
if (confirmation) return confirmation;
|
|
373
|
+
}
|
|
205
374
|
return runOrDiagnose(args, { account });
|
|
206
375
|
});
|
|
207
376
|
|
|
208
377
|
server.registerTool('gog_classroom_coursework_delete', {
|
|
209
|
-
description: 'Delete a coursework item.'
|
|
378
|
+
description: 'Delete a coursework item — every student submission to it goes too, for good. Reads the course and '
|
|
379
|
+
+ 'the coursework and asks the MCP host to show the user a confirmation prompt naming both first; nothing is '
|
|
380
|
+
+ 'deleted unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
210
381
|
annotations: { destructiveHint: true },
|
|
211
382
|
inputSchema: z.object({
|
|
212
383
|
courseId: z.string().describe('Course ID'),
|
|
213
384
|
courseworkId: z.string().describe('Coursework ID'),
|
|
214
385
|
account: accountParam,
|
|
386
|
+
confirmToken: confirmTokenParam,
|
|
215
387
|
}),
|
|
216
|
-
}, async ({ courseId, courseworkId, account }) => {
|
|
388
|
+
}, async ({ courseId, courseworkId, account, confirmToken }, ctx) => {
|
|
389
|
+
const course = await readCourse(courseId, account, runOrDiagnose);
|
|
390
|
+
if (course.error) return course.error;
|
|
391
|
+
const work = await readCoursework(courseId, courseworkId, account, runOrDiagnose);
|
|
392
|
+
if (work.error) return work.error;
|
|
393
|
+
const view = { course: course.course, coursework: work.coursework, deletes: 'the coursework and every student submission to it, for good' };
|
|
394
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
395
|
+
action: 'classroom.coursework-delete',
|
|
396
|
+
message: 'Review and confirm PERMANENTLY deleting this coursework and its submissions:',
|
|
397
|
+
confirmationLabel: 'Confirm that this coursework should be deleted now.',
|
|
398
|
+
details: view,
|
|
399
|
+
unsupportedNote: 'Ask the user to delete it from Classroom.',
|
|
400
|
+
fallback: {
|
|
401
|
+
tool: 'gog_classroom_coursework_delete',
|
|
402
|
+
account,
|
|
403
|
+
confirmToken,
|
|
404
|
+
subject: () => ({ target: `${courseId}/${courseworkId}`, payload: view, preview: view }),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
if (confirmation) return confirmation;
|
|
217
408
|
return runOrDiagnose(['classroom', 'coursework', 'delete', pos(courseId), pos(courseworkId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
218
409
|
});
|
|
219
410
|
|
|
220
411
|
server.registerTool('gog_classroom_announcements_update', {
|
|
221
|
-
description: 'Update an existing announcement.',
|
|
412
|
+
description: 'Update an existing announcement.' + PUBLISH_DESCRIPTION,
|
|
222
413
|
annotations: { destructiveHint: true },
|
|
223
414
|
inputSchema: z.object({
|
|
224
415
|
courseId: z.string().describe('Course ID'),
|
|
225
416
|
announcementId: z.string().describe('Announcement ID'),
|
|
226
417
|
text: z.string().optional().describe('New text'),
|
|
227
|
-
state: workState.optional().describe('State'),
|
|
228
|
-
scheduled: z.string().optional().describe('Scheduled publish time'),
|
|
418
|
+
state: workState.optional().describe('State (PUBLISHED asks the user to confirm; DRAFT does not)'),
|
|
419
|
+
scheduled: z.string().optional().describe('Scheduled publish time (asks the user to confirm)'),
|
|
229
420
|
account: accountParam,
|
|
421
|
+
confirmToken: confirmTokenParam,
|
|
230
422
|
}),
|
|
231
|
-
}, async ({ courseId, announcementId, text, state, scheduled, account }) => {
|
|
423
|
+
}, async ({ courseId, announcementId, text, state, scheduled, account, confirmToken }, ctx) => {
|
|
232
424
|
const args: GogArg[] = ['classroom', 'announcements', 'update', pos(courseId), pos(announcementId)];
|
|
233
425
|
if (text) args.push(`--text=${text}`);
|
|
234
426
|
if (state) args.push(`--state=${state}`);
|
|
235
427
|
if (scheduled) args.push(`--scheduled=${scheduled}`);
|
|
428
|
+
const publishes = publishesWhen(state, scheduled);
|
|
429
|
+
if (publishes) {
|
|
430
|
+
const read = await readCourse(courseId, account, runOrDiagnose);
|
|
431
|
+
if (read.error) return read.error;
|
|
432
|
+
const current = await readClassroomWork('announcements', courseId, announcementId, account, runOrDiagnose);
|
|
433
|
+
if (current.error) return current.error;
|
|
434
|
+
// The text the class will see: the new one when this call replaces it,
|
|
435
|
+
// else what the draft says now. `||`, not `??`: an empty --text is never
|
|
436
|
+
// sent, so gog keeps the current text.
|
|
437
|
+
const published = text || current.work.text;
|
|
438
|
+
const view = {
|
|
439
|
+
course: read.course,
|
|
440
|
+
announcement: { id: announcementId, state: current.work.state },
|
|
441
|
+
publishes,
|
|
442
|
+
textPreview: bodyPreview(published),
|
|
443
|
+
};
|
|
444
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
445
|
+
action: 'classroom.announcement-publish',
|
|
446
|
+
message: 'Review and confirm publishing this Classroom announcement to the class:',
|
|
447
|
+
confirmationLabel: 'Confirm that this announcement should be published to the class.',
|
|
448
|
+
details: view,
|
|
449
|
+
unsupportedNote: 'Leave it as a DRAFT; the user can review and publish it from Classroom.',
|
|
450
|
+
fallback: {
|
|
451
|
+
tool: 'gog_classroom_announcements_update',
|
|
452
|
+
account,
|
|
453
|
+
confirmToken,
|
|
454
|
+
subject: () => ({
|
|
455
|
+
target: `${courseId}/${announcementId}`,
|
|
456
|
+
revision: current.work.updateTime,
|
|
457
|
+
payload: { course: read.course, announcementId, text: published, state, scheduled },
|
|
458
|
+
preview: view,
|
|
459
|
+
}),
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
if (confirmation) return confirmation;
|
|
463
|
+
}
|
|
236
464
|
return runOrDiagnose(args, { account });
|
|
237
465
|
});
|
|
238
466
|
|