gogcli-mcp-classroom 2.7.1 → 2.18.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": "2.7.1",
6
+ "version": "2.18.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",
@@ -62,6 +62,18 @@
62
62
  "name": "gog_auth_add",
63
63
  "description": "Authorize a Google account via browser-based OAuth"
64
64
  },
65
+ {
66
+ "name": "gog_auth_health",
67
+ "description": "Live per-account token health: validity, age, and pre-expiry warnings (detects invalid_grant)"
68
+ },
69
+ {
70
+ "name": "gog_auth_add_url",
71
+ "description": "Begin remote/headless OAuth (step 1): return a sign-in URL to open in any browser"
72
+ },
73
+ {
74
+ "name": "gog_auth_add_complete",
75
+ "description": "Complete remote/headless OAuth (step 2): exchange the pasted redirect URL and store the token"
76
+ },
65
77
  {
66
78
  "name": "gog_auth_list",
67
79
  "description": "List all Google accounts stored in gogcli"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-classroom",
3
- "version": "2.7.1",
3
+ "version": "2.18.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>",
@@ -19,11 +19,12 @@
19
19
  "build": "tsc --noEmit && npm run bundle",
20
20
  "bundle": "node ../../scripts/bundle.js src/index.ts dist/index.js",
21
21
  "typecheck": "tsc --noEmit",
22
- "test": "vitest run",
22
+ "test": "vitest run --coverage",
23
23
  "test:watch": "vitest",
24
24
  "test:coverage": "vitest run --coverage"
25
25
  },
26
26
  "dependencies": {
27
+ "@chrischall/mcp-utils": "^0.13.3",
27
28
  "@modelcontextprotocol/sdk": "^1.29.0",
28
29
  "zod": "^4.4.3"
29
30
  },
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp-classroom"
9
9
  },
10
- "version": "2.7.1",
10
+ "version": "2.18.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-classroom",
15
- "version": "2.7.1",
15
+ "version": "2.18.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
package/src/index.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { createServer, registerAuthTools, registerClassroomTools } from '../../gogcli-mcp/src/lib.js';
2
+ import { runMcp } from '@chrischall/mcp-utils';
3
+ import { VERSION, authToolsFor, registerClassroomTools } from '../../gogcli-mcp/src/lib.js';
4
4
  import { registerExtraClassroomTools } from './tools/classroom-extra.js';
5
5
 
6
- const server = createServer({ name: 'gogcli-classroom' });
7
- registerAuthTools(server);
8
- registerClassroomTools(server);
9
- registerExtraClassroomTools(server);
10
-
11
- const transport = new StdioServerTransport();
12
- await server.connect(transport);
6
+ await runMcp({
7
+ name: 'gogcli-classroom',
8
+ version: VERSION,
9
+ tools: [authToolsFor('classroom'), registerClassroomTools, registerExtraClassroomTools],
10
+ });
@@ -79,7 +79,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
79
79
  account: accountParam,
80
80
  },
81
81
  }, async ({ courseId, account }) => {
82
- return runOrDiagnose(['classroom', 'courses', 'delete', courseId], { account });
82
+ return runOrDiagnose(['classroom', 'courses', 'delete', courseId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
83
83
  });
84
84
 
85
85
  server.registerTool('gog_classroom_courses_archive', {
@@ -126,7 +126,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
126
126
  account: accountParam,
127
127
  },
128
128
  }, async ({ courseId, userId, account }) => {
129
- return runOrDiagnose(['classroom', 'students', 'remove', courseId, userId], { account });
129
+ return runOrDiagnose(['classroom', 'students', 'remove', courseId, userId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
130
130
  });
131
131
 
132
132
  server.registerTool('gog_classroom_teachers_add', {
@@ -149,7 +149,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
149
149
  account: accountParam,
150
150
  },
151
151
  }, async ({ courseId, userId, account }) => {
152
- return runOrDiagnose(['classroom', 'teachers', 'remove', courseId, userId], { account });
152
+ return runOrDiagnose(['classroom', 'teachers', 'remove', courseId, userId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
153
153
  });
154
154
 
155
155
  server.registerTool('gog_classroom_coursework_create', {
@@ -208,7 +208,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
208
208
  account: accountParam,
209
209
  },
210
210
  }, async ({ courseId, courseworkId, account }) => {
211
- return runOrDiagnose(['classroom', 'coursework', 'delete', courseId, courseworkId], { account });
211
+ return runOrDiagnose(['classroom', 'coursework', 'delete', courseId, courseworkId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
212
212
  });
213
213
 
214
214
  server.registerTool('gog_classroom_announcements_update', {
@@ -239,7 +239,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
239
239
  account: accountParam,
240
240
  },
241
241
  }, async ({ courseId, announcementId, account }) => {
242
- return runOrDiagnose(['classroom', 'announcements', 'delete', courseId, announcementId], { account });
242
+ return runOrDiagnose(['classroom', 'announcements', 'delete', courseId, announcementId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
243
243
  });
244
244
 
245
245
  server.registerTool('gog_classroom_topics_create', {
@@ -275,7 +275,7 @@ export function registerExtraClassroomTools(server: McpServer): void {
275
275
  account: accountParam,
276
276
  },
277
277
  }, async ({ courseId, topicId, account }) => {
278
- return runOrDiagnose(['classroom', 'topics', 'delete', courseId, topicId], { account });
278
+ return runOrDiagnose(['classroom', 'topics', 'delete', courseId, topicId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
279
279
  });
280
280
 
281
281
  server.registerTool('gog_classroom_invitations_create', {
@@ -298,6 +298,6 @@ export function registerExtraClassroomTools(server: McpServer): void {
298
298
  account: accountParam,
299
299
  },
300
300
  }, async ({ invitationId, account }) => {
301
- return runOrDiagnose(['classroom', 'invitations', 'delete', invitationId], { account });
301
+ return runOrDiagnose(['classroom', 'invitations', 'delete', invitationId, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
302
302
  });
303
303
  }
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { registerExtraClassroomTools } from '../../src/tools/classroom-extra.js';
3
3
  import * as lib from '../../../gogcli-mcp/src/lib.js';
4
- import { setupHandlers, toText, type ToolHandler } from '../../../gogcli-mcp/tests/helpers/test-harness.js';
4
+ import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
5
+ import { rawTextResult } from '@chrischall/mcp-utils';
5
6
 
6
7
  vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
7
8
  const actual = await importOriginal<typeof lib>();
@@ -11,17 +12,17 @@ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
11
12
  };
12
13
  });
13
14
 
14
- let handlers: Map<string, ToolHandler>;
15
+ let harness: TestHarness;
15
16
 
16
- beforeEach(() => {
17
+ beforeEach(async () => {
17
18
  vi.clearAllMocks();
18
- vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
19
- handlers = setupHandlers(registerExtraClassroomTools);
19
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
20
+ harness = await createTestHarness(registerExtraClassroomTools);
20
21
  });
21
22
 
22
23
  describe('gog_classroom_courses_create', () => {
23
24
  it('calls runOrDiagnose with required name only', async () => {
24
- await handlers.get('gog_classroom_courses_create')!({ name: 'Math 101' });
25
+ await harness.callTool('gog_classroom_courses_create', { name: 'Math 101' });
25
26
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
26
27
  ['classroom', 'courses', 'create', '--name=Math 101'],
27
28
  { account: undefined },
@@ -29,7 +30,7 @@ describe('gog_classroom_courses_create', () => {
29
30
  });
30
31
 
31
32
  it('passes all optional flags', async () => {
32
- await handlers.get('gog_classroom_courses_create')!({
33
+ await harness.callTool('gog_classroom_courses_create', {
33
34
  name: 'Math 101',
34
35
  owner: 'me',
35
36
  section: 'Section A',
@@ -47,12 +48,12 @@ describe('gog_classroom_courses_create', () => {
47
48
 
48
49
  describe('gog_classroom_courses_update', () => {
49
50
  it('calls runOrDiagnose with courseId only', async () => {
50
- await handlers.get('gog_classroom_courses_update')!({ courseId: 'c1' });
51
+ await harness.callTool('gog_classroom_courses_update', { courseId: 'c1' });
51
52
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'update', 'c1'], { account: undefined });
52
53
  });
53
54
 
54
55
  it('passes all optional flags', async () => {
55
- await handlers.get('gog_classroom_courses_update')!({
56
+ await harness.callTool('gog_classroom_courses_update', {
56
57
  courseId: 'c1',
57
58
  name: 'New Name',
58
59
  owner: 'me',
@@ -71,33 +72,33 @@ describe('gog_classroom_courses_update', () => {
71
72
 
72
73
  describe('gog_classroom_courses_delete', () => {
73
74
  it('calls runOrDiagnose with courseId', async () => {
74
- await handlers.get('gog_classroom_courses_delete')!({ courseId: 'c1' });
75
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'delete', 'c1'], { account: undefined });
75
+ await harness.callTool('gog_classroom_courses_delete', { courseId: 'c1' });
76
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'delete', 'c1', '--force'], { account: undefined });
76
77
  });
77
78
  });
78
79
 
79
80
  describe('gog_classroom_courses_archive', () => {
80
81
  it('calls runOrDiagnose with courseId', async () => {
81
- await handlers.get('gog_classroom_courses_archive')!({ courseId: 'c1' });
82
+ await harness.callTool('gog_classroom_courses_archive', { courseId: 'c1' });
82
83
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'archive', 'c1'], { account: undefined });
83
84
  });
84
85
  });
85
86
 
86
87
  describe('gog_classroom_courses_unarchive', () => {
87
88
  it('calls runOrDiagnose with courseId', async () => {
88
- await handlers.get('gog_classroom_courses_unarchive')!({ courseId: 'c1' });
89
+ await harness.callTool('gog_classroom_courses_unarchive', { courseId: 'c1' });
89
90
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'courses', 'unarchive', 'c1'], { account: undefined });
90
91
  });
91
92
  });
92
93
 
93
94
  describe('gog_classroom_students_add', () => {
94
95
  it('calls runOrDiagnose with courseId and userId', async () => {
95
- await handlers.get('gog_classroom_students_add')!({ courseId: 'c1', userId: 'u1' });
96
+ await harness.callTool('gog_classroom_students_add', { courseId: 'c1', userId: 'u1' });
96
97
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'add', 'c1', 'u1'], { account: undefined });
97
98
  });
98
99
 
99
100
  it('passes --enrollment-code when provided', async () => {
100
- await handlers.get('gog_classroom_students_add')!({ courseId: 'c1', userId: 'u1', enrollmentCode: 'abc123' });
101
+ await harness.callTool('gog_classroom_students_add', { courseId: 'c1', userId: 'u1', enrollmentCode: 'abc123' });
101
102
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
102
103
  ['classroom', 'students', 'add', 'c1', 'u1', '--enrollment-code=abc123'],
103
104
  { account: undefined },
@@ -107,28 +108,28 @@ describe('gog_classroom_students_add', () => {
107
108
 
108
109
  describe('gog_classroom_students_remove', () => {
109
110
  it('calls runOrDiagnose with courseId and userId', async () => {
110
- await handlers.get('gog_classroom_students_remove')!({ courseId: 'c1', userId: 'u1' });
111
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'remove', 'c1', 'u1'], { account: undefined });
111
+ await harness.callTool('gog_classroom_students_remove', { courseId: 'c1', userId: 'u1' });
112
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'students', 'remove', 'c1', 'u1', '--force'], { account: undefined });
112
113
  });
113
114
  });
114
115
 
115
116
  describe('gog_classroom_teachers_add', () => {
116
117
  it('calls runOrDiagnose with courseId and userId', async () => {
117
- await handlers.get('gog_classroom_teachers_add')!({ courseId: 'c1', userId: 'u1' });
118
+ await harness.callTool('gog_classroom_teachers_add', { courseId: 'c1', userId: 'u1' });
118
119
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'add', 'c1', 'u1'], { account: undefined });
119
120
  });
120
121
  });
121
122
 
122
123
  describe('gog_classroom_teachers_remove', () => {
123
124
  it('calls runOrDiagnose with courseId and userId', async () => {
124
- await handlers.get('gog_classroom_teachers_remove')!({ courseId: 'c1', userId: 'u1' });
125
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'remove', 'c1', 'u1'], { account: undefined });
125
+ await harness.callTool('gog_classroom_teachers_remove', { courseId: 'c1', userId: 'u1' });
126
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'teachers', 'remove', 'c1', 'u1', '--force'], { account: undefined });
126
127
  });
127
128
  });
128
129
 
129
130
  describe('gog_classroom_coursework_create', () => {
130
131
  it('calls runOrDiagnose with required title only', async () => {
131
- await handlers.get('gog_classroom_coursework_create')!({ courseId: 'c1', title: 'HW1' });
132
+ await harness.callTool('gog_classroom_coursework_create', { courseId: 'c1', title: 'HW1' });
132
133
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
133
134
  ['classroom', 'coursework', 'create', 'c1', '--title=HW1'],
134
135
  { account: undefined },
@@ -136,7 +137,7 @@ describe('gog_classroom_coursework_create', () => {
136
137
  });
137
138
 
138
139
  it('passes all optional flags', async () => {
139
- await handlers.get('gog_classroom_coursework_create')!({
140
+ await harness.callTool('gog_classroom_coursework_create', {
140
141
  courseId: 'c1',
141
142
  title: 'HW1',
142
143
  description: 'Chapter 1',
@@ -158,12 +159,12 @@ describe('gog_classroom_coursework_create', () => {
158
159
 
159
160
  describe('gog_classroom_coursework_update', () => {
160
161
  it('calls runOrDiagnose with ids only', async () => {
161
- await handlers.get('gog_classroom_coursework_update')!({ courseId: 'c1', courseworkId: 'w1' });
162
+ await harness.callTool('gog_classroom_coursework_update', { courseId: 'c1', courseworkId: 'w1' });
162
163
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'update', 'c1', 'w1'], { account: undefined });
163
164
  });
164
165
 
165
166
  it('passes all optional flags', async () => {
166
- await handlers.get('gog_classroom_coursework_update')!({
167
+ await harness.callTool('gog_classroom_coursework_update', {
167
168
  courseId: 'c1',
168
169
  courseworkId: 'w1',
169
170
  title: 'New Title',
@@ -186,19 +187,19 @@ describe('gog_classroom_coursework_update', () => {
186
187
 
187
188
  describe('gog_classroom_coursework_delete', () => {
188
189
  it('calls runOrDiagnose with ids', async () => {
189
- await handlers.get('gog_classroom_coursework_delete')!({ courseId: 'c1', courseworkId: 'w1' });
190
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'delete', 'c1', 'w1'], { account: undefined });
190
+ await harness.callTool('gog_classroom_coursework_delete', { courseId: 'c1', courseworkId: 'w1' });
191
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'coursework', 'delete', 'c1', 'w1', '--force'], { account: undefined });
191
192
  });
192
193
  });
193
194
 
194
195
  describe('gog_classroom_announcements_update', () => {
195
196
  it('calls runOrDiagnose with ids only', async () => {
196
- await handlers.get('gog_classroom_announcements_update')!({ courseId: 'c1', announcementId: 'a1' });
197
+ await harness.callTool('gog_classroom_announcements_update', { courseId: 'c1', announcementId: 'a1' });
197
198
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'update', 'c1', 'a1'], { account: undefined });
198
199
  });
199
200
 
200
201
  it('passes all optional flags', async () => {
201
- await handlers.get('gog_classroom_announcements_update')!({
202
+ await harness.callTool('gog_classroom_announcements_update', {
202
203
  courseId: 'c1',
203
204
  announcementId: 'a1',
204
205
  text: 'edited',
@@ -214,14 +215,14 @@ describe('gog_classroom_announcements_update', () => {
214
215
 
215
216
  describe('gog_classroom_announcements_delete', () => {
216
217
  it('calls runOrDiagnose with ids', async () => {
217
- await handlers.get('gog_classroom_announcements_delete')!({ courseId: 'c1', announcementId: 'a1' });
218
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'delete', 'c1', 'a1'], { account: undefined });
218
+ await harness.callTool('gog_classroom_announcements_delete', { courseId: 'c1', announcementId: 'a1' });
219
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'announcements', 'delete', 'c1', 'a1', '--force'], { account: undefined });
219
220
  });
220
221
  });
221
222
 
222
223
  describe('gog_classroom_topics_create', () => {
223
224
  it('calls runOrDiagnose with courseId and name', async () => {
224
- await handlers.get('gog_classroom_topics_create')!({ courseId: 'c1', name: 'Week 1' });
225
+ await harness.callTool('gog_classroom_topics_create', { courseId: 'c1', name: 'Week 1' });
225
226
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
226
227
  ['classroom', 'topics', 'create', 'c1', '--name=Week 1'],
227
228
  { account: undefined },
@@ -231,7 +232,7 @@ describe('gog_classroom_topics_create', () => {
231
232
 
232
233
  describe('gog_classroom_topics_update', () => {
233
234
  it('calls runOrDiagnose with ids and name', async () => {
234
- await handlers.get('gog_classroom_topics_update')!({ courseId: 'c1', topicId: 't1', name: 'Week 2' });
235
+ await harness.callTool('gog_classroom_topics_update', { courseId: 'c1', topicId: 't1', name: 'Week 2' });
235
236
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
236
237
  ['classroom', 'topics', 'update', 'c1', 't1', '--name=Week 2'],
237
238
  { account: undefined },
@@ -241,14 +242,14 @@ describe('gog_classroom_topics_update', () => {
241
242
 
242
243
  describe('gog_classroom_topics_delete', () => {
243
244
  it('calls runOrDiagnose with ids', async () => {
244
- await handlers.get('gog_classroom_topics_delete')!({ courseId: 'c1', topicId: 't1' });
245
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'topics', 'delete', 'c1', 't1'], { account: undefined });
245
+ await harness.callTool('gog_classroom_topics_delete', { courseId: 'c1', topicId: 't1' });
246
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'topics', 'delete', 'c1', 't1', '--force'], { account: undefined });
246
247
  });
247
248
  });
248
249
 
249
250
  describe('gog_classroom_invitations_create', () => {
250
251
  it('calls runOrDiagnose with courseId, userId, role', async () => {
251
- await handlers.get('gog_classroom_invitations_create')!({ courseId: 'c1', userId: 'u1', role: 'STUDENT' });
252
+ await harness.callTool('gog_classroom_invitations_create', { courseId: 'c1', userId: 'u1', role: 'STUDENT' });
252
253
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
253
254
  ['classroom', 'invitations', 'create', 'c1', 'u1', '--role=STUDENT'],
254
255
  { account: undefined },
@@ -258,7 +259,7 @@ describe('gog_classroom_invitations_create', () => {
258
259
 
259
260
  describe('gog_classroom_invitations_delete', () => {
260
261
  it('calls runOrDiagnose with invitationId', async () => {
261
- await handlers.get('gog_classroom_invitations_delete')!({ invitationId: 'i1' });
262
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'invitations', 'delete', 'i1'], { account: undefined });
262
+ await harness.callTool('gog_classroom_invitations_delete', { invitationId: 'i1' });
263
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['classroom', 'invitations', 'delete', 'i1', '--force'], { account: undefined });
263
264
  });
264
265
  });
package/tsconfig.json CHANGED
@@ -5,5 +5,5 @@
5
5
  "rootDir": "../"
6
6
  },
7
7
  "include": ["src/**/*", "../gogcli-mcp/src/**/*"],
8
- "exclude": ["node_modules", "dist"]
8
+ "exclude": ["node_modules", "dist", "../gogcli-mcp/src/worker.ts"]
9
9
  }