gogcli-mcp-classroom 4.2.5 → 4.4.0
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/dist/index.js +903 -139
- package/manifest.json +9 -2
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/tools/classroom-extra.ts +45 -22
- package/tests/tools/classroom-extra.test.ts +76 -24
- package/vitest.config.ts +5 -0
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.4.0",
|
|
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",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
],
|
|
40
40
|
"env": {
|
|
41
41
|
"GOG_ACCOUNT": "${user_config.gog_account}",
|
|
42
|
-
"GOG_PATH": "${user_config.gog_path}"
|
|
42
|
+
"GOG_PATH": "${user_config.gog_path}",
|
|
43
|
+
"GOG_FILE_ROOTS": "${user_config.gog_file_roots}"
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
},
|
|
@@ -55,6 +56,12 @@
|
|
|
55
56
|
"title": "gog Executable Path",
|
|
56
57
|
"description": "Path to the gog executable (optional — defaults to 'gog' on your PATH)",
|
|
57
58
|
"required": false
|
|
59
|
+
},
|
|
60
|
+
"gog_file_roots": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"title": "Allowed file directories",
|
|
63
|
+
"description": "Directories tools may read files from (attachments, uploads) or write files to (exports, downloads), separated by ':' (';' on Windows). Optional — defaults to ~/gogcli-mcp-files",
|
|
64
|
+
"required": false
|
|
58
65
|
}
|
|
59
66
|
},
|
|
60
67
|
"tools": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-classroom",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
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.4.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.4.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-classroom",
|
|
15
|
-
"version": "4.
|
|
15
|
+
"version": "4.4.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { accountParam, runOrDiagnose } from '../../../gogcli-mcp/src/lib.js';
|
|
3
|
+
import { accountParam, runOrDiagnose, pos, readCourse, requireDispatchConfirmation, CONFIRM_FALLBACK_DESCRIPTION, confirmTokenParam } from '../../../gogcli-mcp/src/lib.js';
|
|
4
|
+
import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
|
|
4
5
|
|
|
5
6
|
const courseState = z.enum(['ACTIVE', 'ARCHIVED', 'PROVISIONED', 'DECLINED', 'SUSPENDED']);
|
|
6
7
|
const workState = z.enum(['PUBLISHED', 'DRAFT']);
|
|
@@ -41,7 +42,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
41
42
|
account: accountParam,
|
|
42
43
|
}),
|
|
43
44
|
}, async ({ name, owner, section, descriptionHeading, description, room, state, account }) => {
|
|
44
|
-
const args = ['classroom', 'courses', 'create', `--name=${name}`];
|
|
45
|
+
const args: GogArg[] = ['classroom', 'courses', 'create', `--name=${name}`];
|
|
45
46
|
if (owner) args.push(`--owner=${owner}`);
|
|
46
47
|
if (section) args.push(`--section=${section}`);
|
|
47
48
|
if (descriptionHeading) args.push(`--description-heading=${descriptionHeading}`);
|
|
@@ -61,7 +62,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
61
62
|
account: accountParam,
|
|
62
63
|
}),
|
|
63
64
|
}, async ({ courseId, name, owner, section, descriptionHeading, description, room, state, account }) => {
|
|
64
|
-
const args = ['classroom', 'courses', 'update', courseId];
|
|
65
|
+
const args: GogArg[] = ['classroom', 'courses', 'update', pos(courseId)];
|
|
65
66
|
if (name) args.push(`--name=${name}`);
|
|
66
67
|
if (owner) args.push(`--owner=${owner}`);
|
|
67
68
|
if (section) args.push(`--section=${section}`);
|
|
@@ -80,7 +81,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
80
81
|
account: accountParam,
|
|
81
82
|
}),
|
|
82
83
|
}, async ({ courseId, account }) => {
|
|
83
|
-
return runOrDiagnose(['classroom', 'courses', 'delete', courseId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
84
|
+
return runOrDiagnose(['classroom', 'courses', 'delete', pos(courseId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
server.registerTool('gog_classroom_courses_archive', {
|
|
@@ -91,7 +92,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
91
92
|
account: accountParam,
|
|
92
93
|
}),
|
|
93
94
|
}, async ({ courseId, account }) => {
|
|
94
|
-
return runOrDiagnose(['classroom', 'courses', 'archive', courseId], { account });
|
|
95
|
+
return runOrDiagnose(['classroom', 'courses', 'archive', pos(courseId)], { account });
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
server.registerTool('gog_classroom_courses_unarchive', {
|
|
@@ -102,7 +103,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
102
103
|
account: accountParam,
|
|
103
104
|
}),
|
|
104
105
|
}, async ({ courseId, account }) => {
|
|
105
|
-
return runOrDiagnose(['classroom', 'courses', 'unarchive', courseId], { account });
|
|
106
|
+
return runOrDiagnose(['classroom', 'courses', 'unarchive', pos(courseId)], { account });
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
server.registerTool('gog_classroom_students_add', {
|
|
@@ -115,7 +116,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
115
116
|
account: accountParam,
|
|
116
117
|
}),
|
|
117
118
|
}, async ({ courseId, userId, enrollmentCode, account }) => {
|
|
118
|
-
const args = ['classroom', 'students', 'add', courseId, userId];
|
|
119
|
+
const args: GogArg[] = ['classroom', 'students', 'add', pos(courseId), pos(userId)];
|
|
119
120
|
if (enrollmentCode) args.push(`--enrollment-code=${enrollmentCode}`);
|
|
120
121
|
return runOrDiagnose(args, { account });
|
|
121
122
|
});
|
|
@@ -129,7 +130,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
129
130
|
account: accountParam,
|
|
130
131
|
}),
|
|
131
132
|
}, async ({ courseId, userId, account }) => {
|
|
132
|
-
return runOrDiagnose(['classroom', 'students', 'remove', courseId, userId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
133
|
+
return runOrDiagnose(['classroom', 'students', 'remove', pos(courseId), pos(userId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
133
134
|
});
|
|
134
135
|
|
|
135
136
|
server.registerTool('gog_classroom_teachers_add', {
|
|
@@ -141,7 +142,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
141
142
|
account: accountParam,
|
|
142
143
|
}),
|
|
143
144
|
}, async ({ courseId, userId, account }) => {
|
|
144
|
-
return runOrDiagnose(['classroom', 'teachers', 'add', courseId, userId], { account });
|
|
145
|
+
return runOrDiagnose(['classroom', 'teachers', 'add', pos(courseId), pos(userId)], { account });
|
|
145
146
|
});
|
|
146
147
|
|
|
147
148
|
server.registerTool('gog_classroom_teachers_remove', {
|
|
@@ -153,7 +154,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
153
154
|
account: accountParam,
|
|
154
155
|
}),
|
|
155
156
|
}, async ({ courseId, userId, account }) => {
|
|
156
|
-
return runOrDiagnose(['classroom', 'teachers', 'remove', courseId, userId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
157
|
+
return runOrDiagnose(['classroom', 'teachers', 'remove', pos(courseId), pos(userId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
server.registerTool('gog_classroom_coursework_create', {
|
|
@@ -166,7 +167,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
166
167
|
account: accountParam,
|
|
167
168
|
}),
|
|
168
169
|
}, async ({ courseId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account }) => {
|
|
169
|
-
const args = ['classroom', 'coursework', 'create', courseId, `--title=${title}`];
|
|
170
|
+
const args: GogArg[] = ['classroom', 'coursework', 'create', pos(courseId), `--title=${title}`];
|
|
170
171
|
if (description) args.push(`--description=${description}`);
|
|
171
172
|
if (type) args.push(`--type=${type}`);
|
|
172
173
|
if (state) args.push(`--state=${state}`);
|
|
@@ -190,7 +191,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
190
191
|
account: accountParam,
|
|
191
192
|
}),
|
|
192
193
|
}, async ({ courseId, courseworkId, title, description, type, state, maxPoints, due, dueDate, dueTime, scheduled, topic, account }) => {
|
|
193
|
-
const args = ['classroom', 'coursework', 'update', courseId, courseworkId];
|
|
194
|
+
const args: GogArg[] = ['classroom', 'coursework', 'update', pos(courseId), pos(courseworkId)];
|
|
194
195
|
if (title) args.push(`--title=${title}`);
|
|
195
196
|
if (description) args.push(`--description=${description}`);
|
|
196
197
|
if (type) args.push(`--type=${type}`);
|
|
@@ -213,7 +214,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
213
214
|
account: accountParam,
|
|
214
215
|
}),
|
|
215
216
|
}, async ({ courseId, courseworkId, account }) => {
|
|
216
|
-
return runOrDiagnose(['classroom', 'coursework', 'delete', courseId, courseworkId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
217
|
+
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
|
|
217
218
|
});
|
|
218
219
|
|
|
219
220
|
server.registerTool('gog_classroom_announcements_update', {
|
|
@@ -228,7 +229,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
228
229
|
account: accountParam,
|
|
229
230
|
}),
|
|
230
231
|
}, async ({ courseId, announcementId, text, state, scheduled, account }) => {
|
|
231
|
-
const args = ['classroom', 'announcements', 'update', courseId, announcementId];
|
|
232
|
+
const args: GogArg[] = ['classroom', 'announcements', 'update', pos(courseId), pos(announcementId)];
|
|
232
233
|
if (text) args.push(`--text=${text}`);
|
|
233
234
|
if (state) args.push(`--state=${state}`);
|
|
234
235
|
if (scheduled) args.push(`--scheduled=${scheduled}`);
|
|
@@ -244,7 +245,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
244
245
|
account: accountParam,
|
|
245
246
|
}),
|
|
246
247
|
}, async ({ courseId, announcementId, account }) => {
|
|
247
|
-
return runOrDiagnose(['classroom', 'announcements', 'delete', courseId, announcementId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
248
|
+
return runOrDiagnose(['classroom', 'announcements', 'delete', pos(courseId), pos(announcementId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
248
249
|
});
|
|
249
250
|
|
|
250
251
|
server.registerTool('gog_classroom_topics_create', {
|
|
@@ -256,7 +257,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
256
257
|
account: accountParam,
|
|
257
258
|
}),
|
|
258
259
|
}, async ({ courseId, name, account }) => {
|
|
259
|
-
return runOrDiagnose(['classroom', 'topics', 'create', courseId, `--name=${name}`], { account });
|
|
260
|
+
return runOrDiagnose(['classroom', 'topics', 'create', pos(courseId), `--name=${name}`], { account });
|
|
260
261
|
});
|
|
261
262
|
|
|
262
263
|
server.registerTool('gog_classroom_topics_update', {
|
|
@@ -269,7 +270,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
269
270
|
account: accountParam,
|
|
270
271
|
}),
|
|
271
272
|
}, async ({ courseId, topicId, name, account }) => {
|
|
272
|
-
return runOrDiagnose(['classroom', 'topics', 'update', courseId, topicId, `--name=${name}`], { account });
|
|
273
|
+
return runOrDiagnose(['classroom', 'topics', 'update', pos(courseId), pos(topicId), `--name=${name}`], { account });
|
|
273
274
|
});
|
|
274
275
|
|
|
275
276
|
server.registerTool('gog_classroom_topics_delete', {
|
|
@@ -281,20 +282,42 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
281
282
|
account: accountParam,
|
|
282
283
|
}),
|
|
283
284
|
}, async ({ courseId, topicId, account }) => {
|
|
284
|
-
return runOrDiagnose(['classroom', 'topics', 'delete', courseId, topicId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
285
|
+
return runOrDiagnose(['classroom', 'topics', 'delete', pos(courseId), pos(topicId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
285
286
|
});
|
|
286
287
|
|
|
287
288
|
server.registerTool('gog_classroom_invitations_create', {
|
|
288
|
-
description: 'Create an invitation to a Google Classroom course.'
|
|
289
|
+
description: 'Create an invitation to a Google Classroom course — Classroom emails the invitee. Reads the course '
|
|
290
|
+
+ 'and asks the MCP host to show the user a confirmation prompt with the class, the invitee and the role; '
|
|
291
|
+
+ 'nothing is sent unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
289
292
|
annotations: { destructiveHint: true },
|
|
290
293
|
inputSchema: z.object({
|
|
291
294
|
courseId: z.string().describe('Course ID'),
|
|
292
295
|
userId: z.string().describe('User ID to invite'),
|
|
293
296
|
role: z.enum(['STUDENT', 'TEACHER', 'OWNER']).describe('Role for the invited user'),
|
|
294
297
|
account: accountParam,
|
|
298
|
+
confirmToken: confirmTokenParam,
|
|
295
299
|
}),
|
|
296
|
-
}, async ({ courseId, userId, role, account }) => {
|
|
297
|
-
|
|
300
|
+
}, async ({ courseId, userId, role, account, confirmToken }, ctx) => {
|
|
301
|
+
const read = await readCourse(courseId, account, runOrDiagnose);
|
|
302
|
+
if (read.error) return read.error;
|
|
303
|
+
const invitation = { course: read.course, invitee: userId, role };
|
|
304
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
305
|
+
action: 'classroom.invitation-create',
|
|
306
|
+
message: role === 'OWNER'
|
|
307
|
+
? 'Review and confirm this Classroom invitation — it offers OWNERSHIP of the course:'
|
|
308
|
+
: 'Review and confirm this Classroom invitation:',
|
|
309
|
+
confirmationLabel: 'Confirm that this invitation should be sent now.',
|
|
310
|
+
details: invitation,
|
|
311
|
+
unsupportedNote: 'Ask the user to invite them from Classroom.',
|
|
312
|
+
fallback: {
|
|
313
|
+
tool: 'gog_classroom_invitations_create',
|
|
314
|
+
account,
|
|
315
|
+
confirmToken,
|
|
316
|
+
subject: () => ({ target: `${courseId}/${userId}`, payload: invitation, preview: invitation }),
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
if (confirmation) return confirmation;
|
|
320
|
+
return runOrDiagnose(['classroom', 'invitations', 'create', pos(courseId), pos(userId), `--role=${role}`], { account });
|
|
298
321
|
});
|
|
299
322
|
|
|
300
323
|
server.registerTool('gog_classroom_invitations_delete', {
|
|
@@ -305,6 +328,6 @@ export function registerExtraClassroomTools(server: McpServer): void {
|
|
|
305
328
|
account: accountParam,
|
|
306
329
|
}),
|
|
307
330
|
}, async ({ invitationId, account }) => {
|
|
308
|
-
return runOrDiagnose(['classroom', 'invitations', 'delete', invitationId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
331
|
+
return runOrDiagnose(['classroom', 'invitations', 'delete', pos(invitationId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
309
332
|
});
|
|
310
333
|
}
|
|
@@ -3,6 +3,7 @@ import { registerExtraClassroomTools } from '../../src/tools/classroom-extra.js'
|
|
|
3
3
|
import * as lib from '../../../gogcli-mcp/src/lib.js';
|
|
4
4
|
import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
|
|
5
5
|
import { rawTextResult } from '@chrischall/mcp-utils';
|
|
6
|
+
import { pos } from '../../../gogcli-mcp/src/argv.js';
|
|
6
7
|
|
|
7
8
|
vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
8
9
|
const actual = await importOriginal<typeof lib>();
|
|
@@ -17,7 +18,9 @@ let harness: TestHarness;
|
|
|
17
18
|
beforeEach(async () => {
|
|
18
19
|
vi.clearAllMocks();
|
|
19
20
|
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
20
|
-
harness = await createTestHarness(registerExtraClassroomTools
|
|
21
|
+
harness = await createTestHarness(registerExtraClassroomTools, {
|
|
22
|
+
elicitation: async () => ({ action: 'accept', content: { confirmed: true } }),
|
|
23
|
+
});
|
|
21
24
|
});
|
|
22
25
|
|
|
23
26
|
describe('gog_classroom_courses_create', () => {
|
|
@@ -49,7 +52,7 @@ describe('gog_classroom_courses_create', () => {
|
|
|
49
52
|
describe('gog_classroom_courses_update', () => {
|
|
50
53
|
it('calls runOrDiagnose with courseId only', async () => {
|
|
51
54
|
await harness.callTool('gog_classroom_courses_update', { courseId: 'c1' });
|
|
52
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'update', 'c1'], { account: undefined });
|
|
55
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'update', pos('c1')], { account: undefined });
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
it('passes all optional flags', async () => {
|
|
@@ -64,7 +67,7 @@ describe('gog_classroom_courses_update', () => {
|
|
|
64
67
|
state: 'ARCHIVED',
|
|
65
68
|
});
|
|
66
69
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
67
|
-
['classroom', 'courses', 'update', 'c1', '--name=New Name', '--owner=me', '--section=B', '--description-heading=Heading', '--description=Desc', '--room=R2', '--state=ARCHIVED'],
|
|
70
|
+
['classroom', 'courses', 'update', pos('c1'), '--name=New Name', '--owner=me', '--section=B', '--description-heading=Heading', '--description=Desc', '--room=R2', '--state=ARCHIVED'],
|
|
68
71
|
{ account: undefined },
|
|
69
72
|
);
|
|
70
73
|
});
|
|
@@ -73,34 +76,34 @@ describe('gog_classroom_courses_update', () => {
|
|
|
73
76
|
describe('gog_classroom_courses_delete', () => {
|
|
74
77
|
it('calls runOrDiagnose with courseId', async () => {
|
|
75
78
|
await harness.callTool('gog_classroom_courses_delete', { courseId: 'c1' });
|
|
76
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'delete', 'c1', '--force'], { account: undefined });
|
|
79
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'delete', pos('c1'), '--force'], { account: undefined });
|
|
77
80
|
});
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
describe('gog_classroom_courses_archive', () => {
|
|
81
84
|
it('calls runOrDiagnose with courseId', async () => {
|
|
82
85
|
await harness.callTool('gog_classroom_courses_archive', { courseId: 'c1' });
|
|
83
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'archive', 'c1'], { account: undefined });
|
|
86
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'archive', pos('c1')], { account: undefined });
|
|
84
87
|
});
|
|
85
88
|
});
|
|
86
89
|
|
|
87
90
|
describe('gog_classroom_courses_unarchive', () => {
|
|
88
91
|
it('calls runOrDiagnose with courseId', async () => {
|
|
89
92
|
await harness.callTool('gog_classroom_courses_unarchive', { courseId: 'c1' });
|
|
90
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'unarchive', 'c1'], { account: undefined });
|
|
93
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'unarchive', pos('c1')], { account: undefined });
|
|
91
94
|
});
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
describe('gog_classroom_students_add', () => {
|
|
95
98
|
it('calls runOrDiagnose with courseId and userId', async () => {
|
|
96
99
|
await harness.callTool('gog_classroom_students_add', { courseId: 'c1', userId: 'u1' });
|
|
97
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'add', 'c1', 'u1'], { account: undefined });
|
|
100
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'add', pos('c1'), pos('u1')], { account: undefined });
|
|
98
101
|
});
|
|
99
102
|
|
|
100
103
|
it('passes --enrollment-code when provided', async () => {
|
|
101
104
|
await harness.callTool('gog_classroom_students_add', { courseId: 'c1', userId: 'u1', enrollmentCode: 'abc123' });
|
|
102
105
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
103
|
-
['classroom', 'students', 'add', 'c1', 'u1', '--enrollment-code=abc123'],
|
|
106
|
+
['classroom', 'students', 'add', pos('c1'), pos('u1'), '--enrollment-code=abc123'],
|
|
104
107
|
{ account: undefined },
|
|
105
108
|
);
|
|
106
109
|
});
|
|
@@ -109,21 +112,21 @@ describe('gog_classroom_students_add', () => {
|
|
|
109
112
|
describe('gog_classroom_students_remove', () => {
|
|
110
113
|
it('calls runOrDiagnose with courseId and userId', async () => {
|
|
111
114
|
await harness.callTool('gog_classroom_students_remove', { courseId: 'c1', userId: 'u1' });
|
|
112
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'remove', 'c1', 'u1', '--force'], { account: undefined });
|
|
115
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'remove', pos('c1'), pos('u1'), '--force'], { account: undefined });
|
|
113
116
|
});
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
describe('gog_classroom_teachers_add', () => {
|
|
117
120
|
it('calls runOrDiagnose with courseId and userId', async () => {
|
|
118
121
|
await harness.callTool('gog_classroom_teachers_add', { courseId: 'c1', userId: 'u1' });
|
|
119
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'add', 'c1', 'u1'], { account: undefined });
|
|
122
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'add', pos('c1'), pos('u1')], { account: undefined });
|
|
120
123
|
});
|
|
121
124
|
});
|
|
122
125
|
|
|
123
126
|
describe('gog_classroom_teachers_remove', () => {
|
|
124
127
|
it('calls runOrDiagnose with courseId and userId', async () => {
|
|
125
128
|
await harness.callTool('gog_classroom_teachers_remove', { courseId: 'c1', userId: 'u1' });
|
|
126
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'remove', 'c1', 'u1', '--force'], { account: undefined });
|
|
129
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'remove', pos('c1'), pos('u1'), '--force'], { account: undefined });
|
|
127
130
|
});
|
|
128
131
|
});
|
|
129
132
|
|
|
@@ -131,7 +134,7 @@ describe('gog_classroom_coursework_create', () => {
|
|
|
131
134
|
it('calls runOrDiagnose with required title only', async () => {
|
|
132
135
|
await harness.callTool('gog_classroom_coursework_create', { courseId: 'c1', title: 'HW1' });
|
|
133
136
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
134
|
-
['classroom', 'coursework', 'create', 'c1', '--title=HW1'],
|
|
137
|
+
['classroom', 'coursework', 'create', pos('c1'), '--title=HW1'],
|
|
135
138
|
{ account: undefined },
|
|
136
139
|
);
|
|
137
140
|
});
|
|
@@ -151,7 +154,7 @@ describe('gog_classroom_coursework_create', () => {
|
|
|
151
154
|
topic: 't1',
|
|
152
155
|
});
|
|
153
156
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
154
|
-
['classroom', 'coursework', 'create', 'c1', '--title=HW1', '--description=Chapter 1', '--type=ASSIGNMENT', '--state=PUBLISHED', '--max-points=100', '--due=2026-05-01T23:59', '--due-date=2026-05-01', '--due-time=23:59', '--scheduled=2026-04-30T12:00', '--topic=t1'],
|
|
157
|
+
['classroom', 'coursework', 'create', pos('c1'), '--title=HW1', '--description=Chapter 1', '--type=ASSIGNMENT', '--state=PUBLISHED', '--max-points=100', '--due=2026-05-01T23:59', '--due-date=2026-05-01', '--due-time=23:59', '--scheduled=2026-04-30T12:00', '--topic=t1'],
|
|
155
158
|
{ account: undefined },
|
|
156
159
|
);
|
|
157
160
|
});
|
|
@@ -160,7 +163,7 @@ describe('gog_classroom_coursework_create', () => {
|
|
|
160
163
|
describe('gog_classroom_coursework_update', () => {
|
|
161
164
|
it('calls runOrDiagnose with ids only', async () => {
|
|
162
165
|
await harness.callTool('gog_classroom_coursework_update', { courseId: 'c1', courseworkId: 'w1' });
|
|
163
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'update', 'c1', 'w1'], { account: undefined });
|
|
166
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'update', pos('c1'), pos('w1')], { account: undefined });
|
|
164
167
|
});
|
|
165
168
|
|
|
166
169
|
it('passes all optional flags', async () => {
|
|
@@ -179,7 +182,7 @@ describe('gog_classroom_coursework_update', () => {
|
|
|
179
182
|
topic: 't1',
|
|
180
183
|
});
|
|
181
184
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
182
|
-
['classroom', 'coursework', 'update', 'c1', 'w1', '--title=New Title', '--description=Desc', '--type=SHORT_ANSWER_QUESTION', '--state=DRAFT', '--max-points=50', '--due=2026-05-01T23:59', '--due-date=2026-05-01', '--due-time=23:59', '--scheduled=2026-04-30T12:00', '--topic=t1'],
|
|
185
|
+
['classroom', 'coursework', 'update', pos('c1'), pos('w1'), '--title=New Title', '--description=Desc', '--type=SHORT_ANSWER_QUESTION', '--state=DRAFT', '--max-points=50', '--due=2026-05-01T23:59', '--due-date=2026-05-01', '--due-time=23:59', '--scheduled=2026-04-30T12:00', '--topic=t1'],
|
|
183
186
|
{ account: undefined },
|
|
184
187
|
);
|
|
185
188
|
});
|
|
@@ -188,14 +191,14 @@ describe('gog_classroom_coursework_update', () => {
|
|
|
188
191
|
describe('gog_classroom_coursework_delete', () => {
|
|
189
192
|
it('calls runOrDiagnose with ids', async () => {
|
|
190
193
|
await harness.callTool('gog_classroom_coursework_delete', { courseId: 'c1', courseworkId: 'w1' });
|
|
191
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'delete', 'c1', 'w1', '--force'], { account: undefined });
|
|
194
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'delete', pos('c1'), pos('w1'), '--force'], { account: undefined });
|
|
192
195
|
});
|
|
193
196
|
});
|
|
194
197
|
|
|
195
198
|
describe('gog_classroom_announcements_update', () => {
|
|
196
199
|
it('calls runOrDiagnose with ids only', async () => {
|
|
197
200
|
await harness.callTool('gog_classroom_announcements_update', { courseId: 'c1', announcementId: 'a1' });
|
|
198
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'update', 'c1', 'a1'], { account: undefined });
|
|
201
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'update', pos('c1'), pos('a1')], { account: undefined });
|
|
199
202
|
});
|
|
200
203
|
|
|
201
204
|
it('passes all optional flags', async () => {
|
|
@@ -207,7 +210,7 @@ describe('gog_classroom_announcements_update', () => {
|
|
|
207
210
|
scheduled: '2026-05-01T12:00',
|
|
208
211
|
});
|
|
209
212
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
210
|
-
['classroom', 'announcements', 'update', 'c1', 'a1', '--text=edited', '--state=PUBLISHED', '--scheduled=2026-05-01T12:00'],
|
|
213
|
+
['classroom', 'announcements', 'update', pos('c1'), pos('a1'), '--text=edited', '--state=PUBLISHED', '--scheduled=2026-05-01T12:00'],
|
|
211
214
|
{ account: undefined },
|
|
212
215
|
);
|
|
213
216
|
});
|
|
@@ -216,7 +219,7 @@ describe('gog_classroom_announcements_update', () => {
|
|
|
216
219
|
describe('gog_classroom_announcements_delete', () => {
|
|
217
220
|
it('calls runOrDiagnose with ids', async () => {
|
|
218
221
|
await harness.callTool('gog_classroom_announcements_delete', { courseId: 'c1', announcementId: 'a1' });
|
|
219
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'delete', 'c1', 'a1', '--force'], { account: undefined });
|
|
222
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'delete', pos('c1'), pos('a1'), '--force'], { account: undefined });
|
|
220
223
|
});
|
|
221
224
|
});
|
|
222
225
|
|
|
@@ -224,7 +227,7 @@ describe('gog_classroom_topics_create', () => {
|
|
|
224
227
|
it('calls runOrDiagnose with courseId and name', async () => {
|
|
225
228
|
await harness.callTool('gog_classroom_topics_create', { courseId: 'c1', name: 'Week 1' });
|
|
226
229
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
227
|
-
['classroom', 'topics', 'create', 'c1', '--name=Week 1'],
|
|
230
|
+
['classroom', 'topics', 'create', pos('c1'), '--name=Week 1'],
|
|
228
231
|
{ account: undefined },
|
|
229
232
|
);
|
|
230
233
|
});
|
|
@@ -234,7 +237,7 @@ describe('gog_classroom_topics_update', () => {
|
|
|
234
237
|
it('calls runOrDiagnose with ids and name', async () => {
|
|
235
238
|
await harness.callTool('gog_classroom_topics_update', { courseId: 'c1', topicId: 't1', name: 'Week 2' });
|
|
236
239
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
237
|
-
['classroom', 'topics', 'update', 'c1', 't1', '--name=Week 2'],
|
|
240
|
+
['classroom', 'topics', 'update', pos('c1'), pos('t1'), '--name=Week 2'],
|
|
238
241
|
{ account: undefined },
|
|
239
242
|
);
|
|
240
243
|
});
|
|
@@ -243,15 +246,64 @@ describe('gog_classroom_topics_update', () => {
|
|
|
243
246
|
describe('gog_classroom_topics_delete', () => {
|
|
244
247
|
it('calls runOrDiagnose with ids', async () => {
|
|
245
248
|
await harness.callTool('gog_classroom_topics_delete', { courseId: 'c1', topicId: 't1' });
|
|
246
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'topics', 'delete', 'c1', 't1', '--force'], { account: undefined });
|
|
249
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'topics', 'delete', pos('c1'), pos('t1'), '--force'], { account: undefined });
|
|
247
250
|
});
|
|
248
251
|
});
|
|
249
252
|
|
|
250
253
|
describe('gog_classroom_invitations_create', () => {
|
|
254
|
+
const COURSE = rawTextResult(JSON.stringify({ course: { id: 'c1', name: 'Algebra II' } }));
|
|
255
|
+
|
|
256
|
+
it('reads the course and prompts with the class, invitee and role', async () => {
|
|
257
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(COURSE);
|
|
258
|
+
let message = '';
|
|
259
|
+
const h = await createTestHarness(registerExtraClassroomTools, {
|
|
260
|
+
elicitation: async (r) => { message = r.params.message; return { action: 'decline' }; },
|
|
261
|
+
});
|
|
262
|
+
await h.callTool('gog_classroom_invitations_create', { courseId: 'c1', userId: 'kid@example.com', role: 'OWNER' });
|
|
263
|
+
expect(message).toMatch(/OWNERSHIP/);
|
|
264
|
+
expect(JSON.parse(message.split('\n').slice(1).join('\n')).details)
|
|
265
|
+
.toEqual({ course: { id: 'c1', name: 'Algebra II' }, invitee: 'kid@example.com', role: 'OWNER' });
|
|
266
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'get', pos('c1')], { account: undefined });
|
|
267
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalledWith(
|
|
268
|
+
['classroom', 'invitations', 'create', pos('c1'), pos('kid@example.com'), '--role=OWNER'], { account: undefined });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('refuses a client that cannot be prompted', async () => {
|
|
272
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(COURSE);
|
|
273
|
+
const h = await createTestHarness(registerExtraClassroomTools);
|
|
274
|
+
const r = JSON.parse((await h.callTool('gog_classroom_invitations_create', { courseId: 'c1', userId: 'u1', role: 'STUDENT' })).content[0]!.text as string);
|
|
275
|
+
expect(r).toMatchObject({ reason: 'confirmation-unsupported', action: 'classroom.invitation-create' });
|
|
276
|
+
expect(r.note).toMatch(/invite them from Classroom/);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('token fallback: phase 1 previews, phase 2 invites', async () => {
|
|
280
|
+
const ORIGINAL = { ...process.env };
|
|
281
|
+
process.env.GOG_SEND_CONFIRM_FALLBACK = 'token';
|
|
282
|
+
try {
|
|
283
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(COURSE);
|
|
284
|
+
const h = await createTestHarness(registerExtraClassroomTools);
|
|
285
|
+
const args = { courseId: 'c1', userId: 'u1', role: 'STUDENT' as const };
|
|
286
|
+
const p1 = JSON.parse((await h.callTool('gog_classroom_invitations_create', args)).content[0]!.text as string);
|
|
287
|
+
expect(p1.preview).toEqual({ course: { id: 'c1', name: 'Algebra II' }, invitee: 'u1', role: 'STUDENT' });
|
|
288
|
+
await h.callTool('gog_classroom_invitations_create', { ...args, confirmToken: p1.confirmToken });
|
|
289
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
290
|
+
['classroom', 'invitations', 'create', pos('c1'), pos('u1'), '--role=STUDENT'], { account: undefined });
|
|
291
|
+
} finally {
|
|
292
|
+
process.env = ORIGINAL;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('returns a failed course read without inviting', async () => {
|
|
297
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue({ content: [{ type: 'text', text: 'Error: not found' }], isError: true });
|
|
298
|
+
const r = await harness.callTool('gog_classroom_invitations_create', { courseId: 'c1', userId: 'u1', role: 'STUDENT' });
|
|
299
|
+
expect(r.isError).toBe(true);
|
|
300
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
301
|
+
});
|
|
302
|
+
|
|
251
303
|
it('calls runOrDiagnose with courseId, userId, role', async () => {
|
|
252
304
|
await harness.callTool('gog_classroom_invitations_create', { courseId: 'c1', userId: 'u1', role: 'STUDENT' });
|
|
253
305
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
254
|
-
['classroom', 'invitations', 'create', 'c1', 'u1', '--role=STUDENT'],
|
|
306
|
+
['classroom', 'invitations', 'create', pos('c1'), pos('u1'), '--role=STUDENT'],
|
|
255
307
|
{ account: undefined },
|
|
256
308
|
);
|
|
257
309
|
});
|
|
@@ -260,6 +312,6 @@ describe('gog_classroom_invitations_create', () => {
|
|
|
260
312
|
describe('gog_classroom_invitations_delete', () => {
|
|
261
313
|
it('calls runOrDiagnose with invitationId', async () => {
|
|
262
314
|
await harness.callTool('gog_classroom_invitations_delete', { invitationId: 'i1' });
|
|
263
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'invitations', 'delete', 'i1', '--force'], { account: undefined });
|
|
315
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'invitations', 'delete', pos('i1'), '--force'], { account: undefined });
|
|
264
316
|
});
|
|
265
317
|
});
|
package/vitest.config.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { defineConfig } from 'vitest/config';
|
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
test: {
|
|
5
|
+
// The arg-shape tests pass arbitrary server paths; confinement is exercised
|
|
6
|
+
// explicitly by the tests that narrow GOG_FILE_ROOTS themselves.
|
|
7
|
+
env: {
|
|
8
|
+
GOG_FILE_ROOTS: '/',
|
|
9
|
+
},
|
|
5
10
|
coverage: {
|
|
6
11
|
provider: 'v8',
|
|
7
12
|
include: ['src/**/*.ts'],
|