gogcli-mcp-classroom 4.2.4 → 4.3.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/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.2.4",
6
+ "version": "4.3.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/mint.yaml CHANGED
@@ -92,7 +92,7 @@ dependencies:
92
92
  # bumping these nine pins, and the pin stored on each mcp-host registration.
93
93
  - kind: github-release
94
94
  repo: openclaw/gogcli
95
- tag: v0.40.0
95
+ tag: v0.41.0
96
96
  asset: "gogcli_*_linux_amd64.tar.gz"
97
97
  bin: [gog]
98
98
  state:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-classroom",
3
- "version": "4.2.4",
3
+ "version": "4.3.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.1.0",
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.2.4",
10
+ "version": "4.3.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-classroom",
15
- "version": "4.2.4",
15
+ "version": "4.3.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 } 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,7 +282,7 @@ 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', {
@@ -294,7 +295,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
294
295
  account: accountParam,
295
296
  }),
296
297
  }, async ({ courseId, userId, role, account }) => {
297
- return runOrDiagnose(['classroom', 'invitations', 'create', courseId, userId, `--role=${role}`], { account });
298
+ return runOrDiagnose(['classroom', 'invitations', 'create', pos(courseId), pos(userId), `--role=${role}`], { account });
298
299
  });
299
300
 
300
301
  server.registerTool('gog_classroom_invitations_delete', {
@@ -305,6 +306,6 @@ export function registerExtraClassroomTools(server: McpServer): void {
305
306
  account: accountParam,
306
307
  }),
307
308
  }, 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
309
+ return runOrDiagnose(['classroom', 'invitations', 'delete', pos(invitationId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
309
310
  });
310
311
  }
@@ -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>();
@@ -49,7 +50,7 @@ describe('gog_classroom_courses_create', () => {
49
50
  describe('gog_classroom_courses_update', () => {
50
51
  it('calls runOrDiagnose with courseId only', async () => {
51
52
  await harness.callTool('gog_classroom_courses_update', { courseId: 'c1' });
52
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'update', 'c1'], { account: undefined });
53
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'update', pos('c1')], { account: undefined });
53
54
  });
54
55
 
55
56
  it('passes all optional flags', async () => {
@@ -64,7 +65,7 @@ describe('gog_classroom_courses_update', () => {
64
65
  state: 'ARCHIVED',
65
66
  });
66
67
  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'],
68
+ ['classroom', 'courses', 'update', pos('c1'), '--name=New Name', '--owner=me', '--section=B', '--description-heading=Heading', '--description=Desc', '--room=R2', '--state=ARCHIVED'],
68
69
  { account: undefined },
69
70
  );
70
71
  });
@@ -73,34 +74,34 @@ describe('gog_classroom_courses_update', () => {
73
74
  describe('gog_classroom_courses_delete', () => {
74
75
  it('calls runOrDiagnose with courseId', async () => {
75
76
  await harness.callTool('gog_classroom_courses_delete', { courseId: 'c1' });
76
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'delete', 'c1', '--force'], { account: undefined });
77
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'delete', pos('c1'), '--force'], { account: undefined });
77
78
  });
78
79
  });
79
80
 
80
81
  describe('gog_classroom_courses_archive', () => {
81
82
  it('calls runOrDiagnose with courseId', async () => {
82
83
  await harness.callTool('gog_classroom_courses_archive', { courseId: 'c1' });
83
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'archive', 'c1'], { account: undefined });
84
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'archive', pos('c1')], { account: undefined });
84
85
  });
85
86
  });
86
87
 
87
88
  describe('gog_classroom_courses_unarchive', () => {
88
89
  it('calls runOrDiagnose with courseId', async () => {
89
90
  await harness.callTool('gog_classroom_courses_unarchive', { courseId: 'c1' });
90
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'unarchive', 'c1'], { account: undefined });
91
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'unarchive', pos('c1')], { account: undefined });
91
92
  });
92
93
  });
93
94
 
94
95
  describe('gog_classroom_students_add', () => {
95
96
  it('calls runOrDiagnose with courseId and userId', async () => {
96
97
  await harness.callTool('gog_classroom_students_add', { courseId: 'c1', userId: 'u1' });
97
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'add', 'c1', 'u1'], { account: undefined });
98
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'add', pos('c1'), pos('u1')], { account: undefined });
98
99
  });
99
100
 
100
101
  it('passes --enrollment-code when provided', async () => {
101
102
  await harness.callTool('gog_classroom_students_add', { courseId: 'c1', userId: 'u1', enrollmentCode: 'abc123' });
102
103
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
103
- ['classroom', 'students', 'add', 'c1', 'u1', '--enrollment-code=abc123'],
104
+ ['classroom', 'students', 'add', pos('c1'), pos('u1'), '--enrollment-code=abc123'],
104
105
  { account: undefined },
105
106
  );
106
107
  });
@@ -109,21 +110,21 @@ describe('gog_classroom_students_add', () => {
109
110
  describe('gog_classroom_students_remove', () => {
110
111
  it('calls runOrDiagnose with courseId and userId', async () => {
111
112
  await harness.callTool('gog_classroom_students_remove', { courseId: 'c1', userId: 'u1' });
112
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'remove', 'c1', 'u1', '--force'], { account: undefined });
113
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'remove', pos('c1'), pos('u1'), '--force'], { account: undefined });
113
114
  });
114
115
  });
115
116
 
116
117
  describe('gog_classroom_teachers_add', () => {
117
118
  it('calls runOrDiagnose with courseId and userId', async () => {
118
119
  await harness.callTool('gog_classroom_teachers_add', { courseId: 'c1', userId: 'u1' });
119
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'add', 'c1', 'u1'], { account: undefined });
120
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'add', pos('c1'), pos('u1')], { account: undefined });
120
121
  });
121
122
  });
122
123
 
123
124
  describe('gog_classroom_teachers_remove', () => {
124
125
  it('calls runOrDiagnose with courseId and userId', async () => {
125
126
  await harness.callTool('gog_classroom_teachers_remove', { courseId: 'c1', userId: 'u1' });
126
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'remove', 'c1', 'u1', '--force'], { account: undefined });
127
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'remove', pos('c1'), pos('u1'), '--force'], { account: undefined });
127
128
  });
128
129
  });
129
130
 
@@ -131,7 +132,7 @@ describe('gog_classroom_coursework_create', () => {
131
132
  it('calls runOrDiagnose with required title only', async () => {
132
133
  await harness.callTool('gog_classroom_coursework_create', { courseId: 'c1', title: 'HW1' });
133
134
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
134
- ['classroom', 'coursework', 'create', 'c1', '--title=HW1'],
135
+ ['classroom', 'coursework', 'create', pos('c1'), '--title=HW1'],
135
136
  { account: undefined },
136
137
  );
137
138
  });
@@ -151,7 +152,7 @@ describe('gog_classroom_coursework_create', () => {
151
152
  topic: 't1',
152
153
  });
153
154
  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'],
155
+ ['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
156
  { account: undefined },
156
157
  );
157
158
  });
@@ -160,7 +161,7 @@ describe('gog_classroom_coursework_create', () => {
160
161
  describe('gog_classroom_coursework_update', () => {
161
162
  it('calls runOrDiagnose with ids only', async () => {
162
163
  await harness.callTool('gog_classroom_coursework_update', { courseId: 'c1', courseworkId: 'w1' });
163
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'update', 'c1', 'w1'], { account: undefined });
164
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'update', pos('c1'), pos('w1')], { account: undefined });
164
165
  });
165
166
 
166
167
  it('passes all optional flags', async () => {
@@ -179,7 +180,7 @@ describe('gog_classroom_coursework_update', () => {
179
180
  topic: 't1',
180
181
  });
181
182
  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'],
183
+ ['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
184
  { account: undefined },
184
185
  );
185
186
  });
@@ -188,14 +189,14 @@ describe('gog_classroom_coursework_update', () => {
188
189
  describe('gog_classroom_coursework_delete', () => {
189
190
  it('calls runOrDiagnose with ids', async () => {
190
191
  await harness.callTool('gog_classroom_coursework_delete', { courseId: 'c1', courseworkId: 'w1' });
191
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'delete', 'c1', 'w1', '--force'], { account: undefined });
192
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'delete', pos('c1'), pos('w1'), '--force'], { account: undefined });
192
193
  });
193
194
  });
194
195
 
195
196
  describe('gog_classroom_announcements_update', () => {
196
197
  it('calls runOrDiagnose with ids only', async () => {
197
198
  await harness.callTool('gog_classroom_announcements_update', { courseId: 'c1', announcementId: 'a1' });
198
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'update', 'c1', 'a1'], { account: undefined });
199
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'update', pos('c1'), pos('a1')], { account: undefined });
199
200
  });
200
201
 
201
202
  it('passes all optional flags', async () => {
@@ -207,7 +208,7 @@ describe('gog_classroom_announcements_update', () => {
207
208
  scheduled: '2026-05-01T12:00',
208
209
  });
209
210
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
210
- ['classroom', 'announcements', 'update', 'c1', 'a1', '--text=edited', '--state=PUBLISHED', '--scheduled=2026-05-01T12:00'],
211
+ ['classroom', 'announcements', 'update', pos('c1'), pos('a1'), '--text=edited', '--state=PUBLISHED', '--scheduled=2026-05-01T12:00'],
211
212
  { account: undefined },
212
213
  );
213
214
  });
@@ -216,7 +217,7 @@ describe('gog_classroom_announcements_update', () => {
216
217
  describe('gog_classroom_announcements_delete', () => {
217
218
  it('calls runOrDiagnose with ids', async () => {
218
219
  await harness.callTool('gog_classroom_announcements_delete', { courseId: 'c1', announcementId: 'a1' });
219
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'delete', 'c1', 'a1', '--force'], { account: undefined });
220
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'delete', pos('c1'), pos('a1'), '--force'], { account: undefined });
220
221
  });
221
222
  });
222
223
 
@@ -224,7 +225,7 @@ describe('gog_classroom_topics_create', () => {
224
225
  it('calls runOrDiagnose with courseId and name', async () => {
225
226
  await harness.callTool('gog_classroom_topics_create', { courseId: 'c1', name: 'Week 1' });
226
227
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
227
- ['classroom', 'topics', 'create', 'c1', '--name=Week 1'],
228
+ ['classroom', 'topics', 'create', pos('c1'), '--name=Week 1'],
228
229
  { account: undefined },
229
230
  );
230
231
  });
@@ -234,7 +235,7 @@ describe('gog_classroom_topics_update', () => {
234
235
  it('calls runOrDiagnose with ids and name', async () => {
235
236
  await harness.callTool('gog_classroom_topics_update', { courseId: 'c1', topicId: 't1', name: 'Week 2' });
236
237
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
237
- ['classroom', 'topics', 'update', 'c1', 't1', '--name=Week 2'],
238
+ ['classroom', 'topics', 'update', pos('c1'), pos('t1'), '--name=Week 2'],
238
239
  { account: undefined },
239
240
  );
240
241
  });
@@ -243,7 +244,7 @@ describe('gog_classroom_topics_update', () => {
243
244
  describe('gog_classroom_topics_delete', () => {
244
245
  it('calls runOrDiagnose with ids', async () => {
245
246
  await harness.callTool('gog_classroom_topics_delete', { courseId: 'c1', topicId: 't1' });
246
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'topics', 'delete', 'c1', 't1', '--force'], { account: undefined });
247
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'topics', 'delete', pos('c1'), pos('t1'), '--force'], { account: undefined });
247
248
  });
248
249
  });
249
250
 
@@ -251,7 +252,7 @@ describe('gog_classroom_invitations_create', () => {
251
252
  it('calls runOrDiagnose with courseId, userId, role', async () => {
252
253
  await harness.callTool('gog_classroom_invitations_create', { courseId: 'c1', userId: 'u1', role: 'STUDENT' });
253
254
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
254
- ['classroom', 'invitations', 'create', 'c1', 'u1', '--role=STUDENT'],
255
+ ['classroom', 'invitations', 'create', pos('c1'), pos('u1'), '--role=STUDENT'],
255
256
  { account: undefined },
256
257
  );
257
258
  });
@@ -260,6 +261,6 @@ describe('gog_classroom_invitations_create', () => {
260
261
  describe('gog_classroom_invitations_delete', () => {
261
262
  it('calls runOrDiagnose with invitationId', async () => {
262
263
  await harness.callTool('gog_classroom_invitations_delete', { invitationId: 'i1' });
263
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'invitations', 'delete', 'i1', '--force'], { account: undefined });
264
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'invitations', 'delete', pos('i1'), '--force'], { account: undefined });
264
265
  });
265
266
  });
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'],