gogcli-mcp 2.30.0 → 3.0.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.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +15416 -18013
  4. package/dist/lib.js +13476 -8827
  5. package/manifest.json +1 -1
  6. package/package.json +3 -3
  7. package/server.json +2 -2
  8. package/src/connector-auth.ts +22 -6
  9. package/src/connector-login.ts +87 -0
  10. package/src/connector-runtime.ts +33 -10
  11. package/src/gmail-dispatch-guard.ts +106 -0
  12. package/src/gmail-results.ts +1 -1
  13. package/src/lib.ts +10 -0
  14. package/src/pagination.ts +1 -1
  15. package/src/tools/api.ts +7 -7
  16. package/src/tools/appscript.ts +15 -15
  17. package/src/tools/auth.ts +11 -11
  18. package/src/tools/calendar.ts +13 -13
  19. package/src/tools/chat.ts +25 -25
  20. package/src/tools/classroom.ts +49 -49
  21. package/src/tools/contacts.ts +9 -9
  22. package/src/tools/docs.ts +13 -13
  23. package/src/tools/drive.ts +22 -22
  24. package/src/tools/gmail.ts +136 -23
  25. package/src/tools/sheets.ts +15 -15
  26. package/src/tools/slides.ts +13 -13
  27. package/src/tools/tasks.ts +13 -13
  28. package/src/tools/utils.ts +2 -3
  29. package/src/worker.ts +52 -64
  30. package/tests/connector-login.test.ts +151 -0
  31. package/tests/connector-runtime.test.ts +21 -1
  32. package/tests/gmail-dispatch-guard.test.ts +132 -0
  33. package/tests/sdk-single-copy.test.ts +18 -36
  34. package/tests/tools/appscript.test.ts +1 -1
  35. package/tests/tools/chat.test.ts +1 -1
  36. package/tests/tools/gmail.test.ts +242 -12
  37. package/tests/tools/sheets.test.ts +1 -1
  38. package/tests/zod-single-copy.test.ts +2 -2
  39. package/tsconfig.json +1 -3
  40. package/vitest.config.ts +3 -5
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { viewParam, resolveView } from '@chrischall/mcp-utils';
4
4
  import { accountParam, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken } from './utils.js';
@@ -82,7 +82,7 @@ export function registerCalendarTools(server: McpServer): void {
82
82
  + 'gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. '
83
83
  + 'A response carrying "truncated": true is an incomplete view — never conclude an event does not exist from one.',
84
84
  annotations: { readOnlyHint: true },
85
- inputSchema: {
85
+ inputSchema: z.object({
86
86
  calendarId: z.string().optional().describe('Calendar ID (default: primary calendar)'),
87
87
  from: z.string().optional().describe('Start time filter (RFC3339, date, or natural language)'),
88
88
  to: z.string().optional().describe('End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days.'),
@@ -105,7 +105,7 @@ export function registerCalendarTools(server: McpServer): void {
105
105
  + 'listing\'s bytes — plus etag/iCalUID/kind. Ask for full when you need a body or a guest list.',
106
106
  }),
107
107
  account: accountParam,
108
- },
108
+ }),
109
109
  }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, view, account }) => {
110
110
  const args = ['calendar', 'events'];
111
111
  if (calendarId) args.push(calendarId);
@@ -134,12 +134,12 @@ export function registerCalendarTools(server: McpServer): void {
134
134
  server.registerTool('gog_calendar_get', {
135
135
  description: 'Get a specific calendar event by ID.',
136
136
  annotations: { readOnlyHint: true },
137
- inputSchema: {
137
+ inputSchema: z.object({
138
138
  calendarId: z.string().describe('Calendar ID'),
139
139
  eventId: z.string().describe('Event ID'),
140
140
  timezone: z.string().optional().describe('Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: the event\'s timezone, then its calendar\'s timezone.'),
141
141
  account: accountParam,
142
- },
142
+ }),
143
143
  }, async ({ calendarId, eventId, timezone, account }) => {
144
144
  const args = ['calendar', 'event', calendarId, eventId];
145
145
  if (timezone) args.push(`--timezone=${timezone}`);
@@ -149,7 +149,7 @@ export function registerCalendarTools(server: McpServer): void {
149
149
  server.registerTool('gog_calendar_create', {
150
150
  description: 'Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description — Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients).',
151
151
  annotations: { destructiveHint: false },
152
- inputSchema: {
152
+ inputSchema: z.object({
153
153
  calendarId: z.string().describe('Calendar ID (use "primary" for the default calendar)'),
154
154
  summary: z.string().describe('Event title'),
155
155
  from: z.string().describe('Start time (RFC3339 or date for all-day events)'),
@@ -162,7 +162,7 @@ export function registerCalendarTools(server: McpServer): void {
162
162
  withZoom: z.boolean().optional().describe('Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)'),
163
163
  ...reminderParams,
164
164
  account: accountParam,
165
- },
165
+ }),
166
166
  }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
167
167
  const args = ['calendar', 'create', calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
168
168
  if (description) args.push(`--description=${description}`);
@@ -178,7 +178,7 @@ export function registerCalendarTools(server: McpServer): void {
178
178
  server.registerTool('gog_calendar_update', {
179
179
  description: 'Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it. removeMeet clears the event\'s Google Meet conference data (e.g. before attaching another provider). Conference flags are independent — use one per call.',
180
180
  annotations: { destructiveHint: false },
181
- inputSchema: {
181
+ inputSchema: z.object({
182
182
  calendarId: z.string().describe('Calendar ID'),
183
183
  eventId: z.string().describe('Event ID'),
184
184
  summary: z.string().optional().describe('New event title'),
@@ -195,7 +195,7 @@ export function registerCalendarTools(server: McpServer): void {
195
195
  removeMeet: z.boolean().optional().describe('Remove the event\'s Google Meet video conference (clears conference data only)'),
196
196
  ...reminderParams,
197
197
  account: accountParam,
198
- },
198
+ }),
199
199
  }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
200
200
  const args = ['calendar', 'update', calendarId, eventId];
201
201
  if (summary !== undefined) args.push(`--summary=${summary}`);
@@ -217,11 +217,11 @@ export function registerCalendarTools(server: McpServer): void {
217
217
  server.registerTool('gog_calendar_delete', {
218
218
  description: 'Delete a calendar event.',
219
219
  annotations: { destructiveHint: true },
220
- inputSchema: {
220
+ inputSchema: z.object({
221
221
  calendarId: z.string().describe('Calendar ID'),
222
222
  eventId: z.string().describe('Event ID'),
223
223
  account: accountParam,
224
- },
224
+ }),
225
225
  }, async ({ calendarId, eventId, account }) => {
226
226
  // gog gates this delete behind a confirmation; the runner injects
227
227
  // --no-input, so without --force it refuses at runtime.
@@ -231,13 +231,13 @@ export function registerCalendarTools(server: McpServer): void {
231
231
  server.registerTool('gog_calendar_respond', {
232
232
  description: 'Respond to a calendar event invitation.',
233
233
  annotations: { destructiveHint: true },
234
- inputSchema: {
234
+ inputSchema: z.object({
235
235
  calendarId: z.string().describe('Calendar ID'),
236
236
  eventId: z.string().describe('Event ID'),
237
237
  status: z.enum(['accepted', 'declined', 'tentative']).describe('Response status'),
238
238
  comment: z.string().optional().describe('Optional comment to include with response'),
239
239
  account: accountParam,
240
- },
240
+ }),
241
241
  }, async ({ calendarId, eventId, status, comment, account }) => {
242
242
  const args = ['calendar', 'respond', calendarId, eventId, `--status=${status}`];
243
243
  if (comment) args.push(`--comment=${comment}`);
package/src/tools/chat.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import {
4
4
  accountParam,
@@ -49,10 +49,10 @@ export function registerChatTools(server: McpServer): void {
49
49
  + 'Start here when you do not yet have a space name; gog_chat_spaces_find is faster when you know the room\'s title.'
50
50
  + workspaceOnlyNote,
51
51
  annotations: { readOnlyHint: true },
52
- inputSchema: {
52
+ inputSchema: z.object({
53
53
  ...paginationParams,
54
54
  account: accountParam,
55
- },
55
+ }),
56
56
  }, async ({ max, pageToken, page, all, account }) => {
57
57
  const args = ['chat', 'spaces', 'list'];
58
58
  pushPaginationFlags(args, { max, pageToken, page, all });
@@ -65,12 +65,12 @@ export function registerChatTools(server: McpServer): void {
65
65
  + 'user names a room approximately ("the launch room"); pass exact=true to require the whole title. DMs have no '
66
66
  + 'display name — use gog_chat_dm_space to reach a person.' + workspaceOnlyNote,
67
67
  annotations: { readOnlyHint: true },
68
- inputSchema: {
68
+ inputSchema: z.object({
69
69
  displayName: z.string().describe('Space display name, or part of one'),
70
70
  exact: z.boolean().optional().describe('Require an exact (still case-insensitive) match on the whole display name'),
71
71
  max: z.number().int().optional().describe('Max results per page'),
72
72
  account: accountParam,
73
- },
73
+ }),
74
74
  }, async ({ displayName, exact, max, account }) => {
75
75
  const args = ['chat', 'spaces', 'find', displayName];
76
76
  if (exact) args.push('--exact');
@@ -82,11 +82,11 @@ export function registerChatTools(server: McpServer): void {
82
82
  description:
83
83
  'Create a named Chat space, optionally seeding its membership. Members are added immediately and are notified — this '
84
84
  + 'is visible to other people the moment it runs, so confirm the member list before calling it.' + workspaceOnlyNote,
85
- inputSchema: {
85
+ inputSchema: z.object({
86
86
  displayName: z.string().describe('Display name for the new space'),
87
87
  members: z.array(z.string()).optional().describe('Initial members, as email addresses or "users/..." resource names'),
88
88
  account: accountParam,
89
- },
89
+ }),
90
90
  }, async ({ displayName, members, account }) => {
91
91
  const args = ['chat', 'spaces', 'create', displayName];
92
92
  if (members) for (const member of members) args.push(`--member=${member}`);
@@ -98,11 +98,11 @@ export function registerChatTools(server: McpServer): void {
98
98
  'List the threads in a space, so a reply can be targeted at an existing conversation rather than starting a new one. '
99
99
  + 'Pass a thread name from here as `thread` to gog_chat_messages_send.' + workspaceOnlyNote,
100
100
  annotations: { readOnlyHint: true },
101
- inputSchema: {
101
+ inputSchema: z.object({
102
102
  space: spaceParam,
103
103
  ...paginationParams,
104
104
  account: accountParam,
105
- },
105
+ }),
106
106
  }, async ({ space, max, pageToken, page, all, account }) => {
107
107
  const args = ['chat', 'threads', 'list', space];
108
108
  pushPaginationFlags(args, { max, pageToken, page, all });
@@ -117,7 +117,7 @@ export function registerChatTools(server: McpServer): void {
117
117
  + 'miss". Newest-first needs an explicit order="createTime desc"; Chat\'s own default is oldest-first.'
118
118
  + workspaceOnlyNote,
119
119
  annotations: { readOnlyHint: true },
120
- inputSchema: {
120
+ inputSchema: z.object({
121
121
  space: spaceParam,
122
122
  thread: threadParam,
123
123
  unread: z.boolean().optional().describe('Only messages posted after the account last read this space'),
@@ -125,7 +125,7 @@ export function registerChatTools(server: McpServer): void {
125
125
  .optional().describe('Sort order (Chat default: "createTime asc", i.e. OLDEST first — ask for "createTime desc" when you want the latest messages)'),
126
126
  ...paginationParams,
127
127
  account: accountParam,
128
- },
128
+ }),
129
129
  }, async ({ space, thread, unread, order, max, pageToken, page, all, account }) => {
130
130
  const args = ['chat', 'messages', 'list', space];
131
131
  if (thread) args.push(`--thread=${thread}`);
@@ -146,7 +146,7 @@ export function registerChatTools(server: McpServer): void {
146
146
  + 'which gog\'s chat scope set does NOT request (re-auth with extraScopes to get it). Missing metadata is OMITTED '
147
147
  + 'rather than defaulted, so an absent `read` means unknown while an explicit false means unread.' + workspaceOnlyNote,
148
148
  annotations: { readOnlyHint: true },
149
- inputSchema: {
149
+ inputSchema: z.object({
150
150
  query: z.string().describe('Google Chat filter-syntax query — keywords, or filters such as "from:alice@example.com budget"'),
151
151
  order: z.enum(['create_time desc', 'relevance desc']).optional().describe(
152
152
  'Sort order. NOTE the snake_case, which differs from gog_chat_messages_list\'s camelCase. "relevance desc" needs '
@@ -160,7 +160,7 @@ export function registerChatTools(server: McpServer): void {
160
160
  ...paginationParams,
161
161
  max: z.number().int().min(1).max(100).optional().describe('Max results per page (1-100; Chat search caps a page at 100)'),
162
162
  account: accountParam,
163
- },
163
+ }),
164
164
  }, async ({ query, order, view, markup, max, pageToken, page, all, account }) => {
165
165
  const args = ['chat', 'messages', 'search', query];
166
166
  if (order) args.push(`--order=${order}`);
@@ -176,7 +176,7 @@ export function registerChatTools(server: McpServer): void {
176
176
  + 'this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing '
177
177
  + 'conversation (from gog_chat_threads_list or a message\'s thread field); omit it to start a new one. Text supports '
178
178
  + 'Chat\'s markdown-ish formatting (*bold*, _italic_, `code`).' + workspaceOnlyNote,
179
- inputSchema: {
179
+ inputSchema: z.object({
180
180
  space: spaceParam,
181
181
  text: z.string().optional().describe('Message text. Optional only when an attachment is supplied.'),
182
182
  thread: threadParam,
@@ -186,7 +186,7 @@ export function registerChatTools(server: McpServer): void {
186
186
  ),
187
187
  attachInline: attachInlineParam,
188
188
  account: accountParam,
189
- },
189
+ }),
190
190
  }, async ({ space, text, thread, attach, attachInline, account }) => {
191
191
  if (text === undefined && !attach?.length && !attachInline?.length) {
192
192
  throw new Error('A Chat message needs text, an attachment, or both.');
@@ -207,12 +207,12 @@ export function registerChatTools(server: McpServer): void {
207
207
  'Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered '
208
208
  + 'immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send.'
209
209
  + workspaceOnlyNote,
210
- inputSchema: {
210
+ inputSchema: z.object({
211
211
  email: z.string().describe('Recipient email address'),
212
212
  text: z.string().describe('Message text'),
213
213
  thread: threadParam,
214
214
  account: accountParam,
215
- },
215
+ }),
216
216
  }, async ({ email, text, thread, account }) => {
217
217
  const args = ['chat', 'dm', 'send', email, `--text=${text}`];
218
218
  if (thread) args.push(`--thread=${thread}`);
@@ -223,10 +223,10 @@ export function registerChatTools(server: McpServer): void {
223
223
  description:
224
224
  'Resolve the DM space for an email address — the bridge from a person to the "spaces/..." name the message tools '
225
225
  + 'want. Creates the space if none exists yet, which is silent: it does not message the person.' + workspaceOnlyNote,
226
- inputSchema: {
226
+ inputSchema: z.object({
227
227
  email: z.string().describe('The other person\'s email address'),
228
228
  account: accountParam,
229
- },
229
+ }),
230
230
  }, async ({ email, account }) => {
231
231
  return runOrDiagnose(['chat', 'dm', 'space', email], { account });
232
232
  });
@@ -237,12 +237,12 @@ export function registerChatTools(server: McpServer): void {
237
237
  + 'SUMMARY per message; come here when you need the individual reactors, or the reaction resource names that '
238
238
  + 'gog_chat_reactions_delete takes.' + workspaceOnlyNote,
239
239
  annotations: { readOnlyHint: true },
240
- inputSchema: {
240
+ inputSchema: z.object({
241
241
  message: z.string().describe('Message resource name ("spaces/AAAA/messages/BBBB"), or a bare message ID together with `space`'),
242
242
  space: z.string().optional().describe('Space resource name — required only when `message` is a bare ID'),
243
243
  ...paginationParams,
244
244
  account: accountParam,
245
- },
245
+ }),
246
246
  }, async ({ message, space, max, pageToken, page, all, account }) => {
247
247
  const args = ['chat', 'messages', 'reactions', 'list', message];
248
248
  if (space) args.push(`--space=${space}`);
@@ -254,12 +254,12 @@ export function registerChatTools(server: McpServer): void {
254
254
  description:
255
255
  'React to a message with an emoji. Visible to the space immediately. Pass the emoji itself ("👍"), not a :shortcode:.'
256
256
  + workspaceOnlyNote,
257
- inputSchema: {
257
+ inputSchema: z.object({
258
258
  message: z.string().describe('Message resource name ("spaces/AAAA/messages/BBBB"), or a bare message ID together with `space`'),
259
259
  emoji: z.string().describe('The emoji character to react with, e.g. "👍"'),
260
260
  space: z.string().optional().describe('Space resource name — required only when `message` is a bare ID'),
261
261
  account: accountParam,
262
- },
262
+ }),
263
263
  }, async ({ message, emoji, space, account }) => {
264
264
  const args = ['chat', 'messages', 'reactions', 'create', message, emoji];
265
265
  if (space) args.push(`--space=${space}`);
@@ -272,10 +272,10 @@ export function registerChatTools(server: McpServer): void {
272
272
  + 'the message\'s and not the emoji — get it from gog_chat_reactions_list. An account can only remove its own reaction.'
273
273
  + workspaceOnlyNote,
274
274
  annotations: { destructiveHint: true },
275
- inputSchema: {
275
+ inputSchema: z.object({
276
276
  reaction: z.string().describe('Reaction resource name, e.g. "spaces/AAAA/messages/BBBB/reactions/CCCC"'),
277
277
  account: accountParam,
278
- },
278
+ }),
279
279
  }, async ({ reaction, account }) => {
280
280
  return runOrDiagnose(['chat', 'messages', 'reactions', 'delete', reaction], { account });
281
281
  });
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { accountParam, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken} from './utils.js';
4
4
 
@@ -6,7 +6,7 @@ export function registerClassroomTools(server: McpServer): void {
6
6
  server.registerTool('gog_classroom_courses_list', {
7
7
  description: 'List Google Classroom courses.',
8
8
  annotations: { readOnlyHint: true },
9
- inputSchema: {
9
+ inputSchema: z.object({
10
10
  state: z.string().optional().describe('Comma-separated course states (ACTIVE, ARCHIVED, PROVISIONED, DECLINED, SUSPENDED)'),
11
11
  teacher: z.string().optional().describe('Filter by teacher user ID'),
12
12
  student: z.string().optional().describe('Filter by student user ID'),
@@ -15,7 +15,7 @@ export function registerClassroomTools(server: McpServer): void {
15
15
  page: pageAliasParam,
16
16
  all: z.boolean().optional().describe('Fetch all pages'),
17
17
  account: accountParam,
18
- },
18
+ }),
19
19
  }, async ({ state, teacher, student, max, pageToken, page, all, account }) => {
20
20
  const args = ['classroom', 'courses', 'list'];
21
21
  if (state) args.push(`--state=${state}`);
@@ -31,10 +31,10 @@ export function registerClassroomTools(server: McpServer): void {
31
31
  server.registerTool('gog_classroom_courses_get', {
32
32
  description: 'Get a single Google Classroom course by ID.',
33
33
  annotations: { readOnlyHint: true },
34
- inputSchema: {
34
+ inputSchema: z.object({
35
35
  courseId: z.string().describe('Course ID'),
36
36
  account: accountParam,
37
- },
37
+ }),
38
38
  }, async ({ courseId, account }) => {
39
39
  return runOrDiagnose(['classroom', 'courses', 'get', courseId], { account });
40
40
  });
@@ -42,14 +42,14 @@ export function registerClassroomTools(server: McpServer): void {
42
42
  server.registerTool('gog_classroom_students_list', {
43
43
  description: 'List students enrolled in a Google Classroom course.',
44
44
  annotations: { readOnlyHint: true },
45
- inputSchema: {
45
+ inputSchema: z.object({
46
46
  courseId: z.string().describe('Course ID'),
47
47
  max: z.number().optional().describe('Max results per page'),
48
48
  pageToken: pageTokenParam,
49
49
  page: pageAliasParam,
50
50
  all: z.boolean().optional().describe('Fetch all pages'),
51
51
  account: accountParam,
52
- },
52
+ }),
53
53
  }, async ({ courseId, max, pageToken, page, all, account }) => {
54
54
  const args = ['classroom', 'students', 'list', courseId];
55
55
  if (max !== undefined) args.push(`--max=${max}`);
@@ -62,11 +62,11 @@ export function registerClassroomTools(server: McpServer): void {
62
62
  server.registerTool('gog_classroom_students_get', {
63
63
  description: 'Get a specific student enrolled in a course.',
64
64
  annotations: { readOnlyHint: true },
65
- inputSchema: {
65
+ inputSchema: z.object({
66
66
  courseId: z.string().describe('Course ID'),
67
67
  userId: z.string().describe('Student user ID'),
68
68
  account: accountParam,
69
- },
69
+ }),
70
70
  }, async ({ courseId, userId, account }) => {
71
71
  return runOrDiagnose(['classroom', 'students', 'get', courseId, userId], { account });
72
72
  });
@@ -74,14 +74,14 @@ export function registerClassroomTools(server: McpServer): void {
74
74
  server.registerTool('gog_classroom_teachers_list', {
75
75
  description: 'List teachers in a Google Classroom course.',
76
76
  annotations: { readOnlyHint: true },
77
- inputSchema: {
77
+ inputSchema: z.object({
78
78
  courseId: z.string().describe('Course ID'),
79
79
  max: z.number().optional().describe('Max results per page'),
80
80
  pageToken: pageTokenParam,
81
81
  page: pageAliasParam,
82
82
  all: z.boolean().optional().describe('Fetch all pages'),
83
83
  account: accountParam,
84
- },
84
+ }),
85
85
  }, async ({ courseId, max, pageToken, page, all, account }) => {
86
86
  const args = ['classroom', 'teachers', 'list', courseId];
87
87
  if (max !== undefined) args.push(`--max=${max}`);
@@ -94,11 +94,11 @@ export function registerClassroomTools(server: McpServer): void {
94
94
  server.registerTool('gog_classroom_teachers_get', {
95
95
  description: 'Get a specific teacher in a course.',
96
96
  annotations: { readOnlyHint: true },
97
- inputSchema: {
97
+ inputSchema: z.object({
98
98
  courseId: z.string().describe('Course ID'),
99
99
  userId: z.string().describe('Teacher user ID'),
100
100
  account: accountParam,
101
- },
101
+ }),
102
102
  }, async ({ courseId, userId, account }) => {
103
103
  return runOrDiagnose(['classroom', 'teachers', 'get', courseId, userId], { account });
104
104
  });
@@ -106,7 +106,7 @@ export function registerClassroomTools(server: McpServer): void {
106
106
  server.registerTool('gog_classroom_roster', {
107
107
  description: 'List the full roster (students and/or teachers) of a Google Classroom course. Omit both flags to return both groups.',
108
108
  annotations: { readOnlyHint: true },
109
- inputSchema: {
109
+ inputSchema: z.object({
110
110
  courseId: z.string().describe('Course ID'),
111
111
  students: z.boolean().optional().describe('Include students only'),
112
112
  teachers: z.boolean().optional().describe('Include teachers only'),
@@ -115,7 +115,7 @@ export function registerClassroomTools(server: McpServer): void {
115
115
  page: pageAliasParam,
116
116
  all: z.boolean().optional().describe('Fetch all pages'),
117
117
  account: accountParam,
118
- },
118
+ }),
119
119
  }, async ({ courseId, students, teachers, max, pageToken, page, all, account }) => {
120
120
  const args = ['classroom', 'roster', courseId];
121
121
  if (students) args.push('--students');
@@ -130,7 +130,7 @@ export function registerClassroomTools(server: McpServer): void {
130
130
  server.registerTool('gog_classroom_coursework_list', {
131
131
  description: 'List coursework (assignments, questions, materials) for a course.',
132
132
  annotations: { readOnlyHint: true },
133
- inputSchema: {
133
+ inputSchema: z.object({
134
134
  courseId: z.string().describe('Course ID'),
135
135
  state: z.string().optional().describe('Filter by coursework state (PUBLISHED, DRAFT, DELETED)'),
136
136
  topic: z.string().optional().describe('Filter by topic ID'),
@@ -141,7 +141,7 @@ export function registerClassroomTools(server: McpServer): void {
141
141
  all: z.boolean().optional().describe('Fetch all pages'),
142
142
  scanPages: z.number().optional().describe('Max pages to scan when filtering'),
143
143
  account: accountParam,
144
- },
144
+ }),
145
145
  }, async ({ courseId, state, topic, orderBy, max, pageToken, page, all, scanPages, account }) => {
146
146
  const args = ['classroom', 'coursework', 'list', courseId];
147
147
  if (state) args.push(`--state=${state}`);
@@ -158,11 +158,11 @@ export function registerClassroomTools(server: McpServer): void {
158
158
  server.registerTool('gog_classroom_coursework_get', {
159
159
  description: 'Get a single coursework item by ID.',
160
160
  annotations: { readOnlyHint: true },
161
- inputSchema: {
161
+ inputSchema: z.object({
162
162
  courseId: z.string().describe('Course ID'),
163
163
  courseworkId: z.string().describe('Coursework ID'),
164
164
  account: accountParam,
165
- },
165
+ }),
166
166
  }, async ({ courseId, courseworkId, account }) => {
167
167
  return runOrDiagnose(['classroom', 'coursework', 'get', courseId, courseworkId], { account });
168
168
  });
@@ -170,7 +170,7 @@ export function registerClassroomTools(server: McpServer): void {
170
170
  server.registerTool('gog_classroom_submissions_list', {
171
171
  description: 'List student submissions for a coursework item.',
172
172
  annotations: { readOnlyHint: true },
173
- inputSchema: {
173
+ inputSchema: z.object({
174
174
  courseId: z.string().describe('Course ID'),
175
175
  courseworkId: z.string().describe('Coursework ID'),
176
176
  state: z.string().optional().describe('Filter by submission state'),
@@ -181,7 +181,7 @@ export function registerClassroomTools(server: McpServer): void {
181
181
  page: pageAliasParam,
182
182
  all: z.boolean().optional().describe('Fetch all pages'),
183
183
  account: accountParam,
184
- },
184
+ }),
185
185
  }, async ({ courseId, courseworkId, state, late, user, max, pageToken, page, all, account }) => {
186
186
  const args = ['classroom', 'submissions', 'list', courseId, courseworkId];
187
187
  if (state) args.push(`--state=${state}`);
@@ -197,12 +197,12 @@ export function registerClassroomTools(server: McpServer): void {
197
197
  server.registerTool('gog_classroom_submissions_get', {
198
198
  description: 'Get a single submission by ID.',
199
199
  annotations: { readOnlyHint: true },
200
- inputSchema: {
200
+ inputSchema: z.object({
201
201
  courseId: z.string().describe('Course ID'),
202
202
  courseworkId: z.string().describe('Coursework ID'),
203
203
  submissionId: z.string().describe('Submission ID'),
204
204
  account: accountParam,
205
- },
205
+ }),
206
206
  }, async ({ courseId, courseworkId, submissionId, account }) => {
207
207
  return runOrDiagnose(['classroom', 'submissions', 'get', courseId, courseworkId, submissionId], { account });
208
208
  });
@@ -210,14 +210,14 @@ export function registerClassroomTools(server: McpServer): void {
210
210
  server.registerTool('gog_classroom_submissions_grade', {
211
211
  description: 'Grade a student submission. Set draft and/or assigned grade values.',
212
212
  annotations: { destructiveHint: true },
213
- inputSchema: {
213
+ inputSchema: z.object({
214
214
  courseId: z.string().describe('Course ID'),
215
215
  courseworkId: z.string().describe('Coursework ID'),
216
216
  submissionId: z.string().describe('Submission ID'),
217
217
  draft: z.string().optional().describe('Draft grade value'),
218
218
  assigned: z.string().optional().describe('Assigned (final) grade value'),
219
219
  account: accountParam,
220
- },
220
+ }),
221
221
  }, async ({ courseId, courseworkId, submissionId, draft, assigned, account }) => {
222
222
  const args = ['classroom', 'submissions', 'grade', courseId, courseworkId, submissionId];
223
223
  if (draft) args.push(`--draft=${draft}`);
@@ -228,12 +228,12 @@ export function registerClassroomTools(server: McpServer): void {
228
228
  server.registerTool('gog_classroom_submissions_return', {
229
229
  description: 'Return a graded submission to the student.',
230
230
  annotations: { destructiveHint: true },
231
- inputSchema: {
231
+ inputSchema: z.object({
232
232
  courseId: z.string().describe('Course ID'),
233
233
  courseworkId: z.string().describe('Coursework ID'),
234
234
  submissionId: z.string().describe('Submission ID'),
235
235
  account: accountParam,
236
- },
236
+ }),
237
237
  }, async ({ courseId, courseworkId, submissionId, account }) => {
238
238
  return runOrDiagnose(['classroom', 'submissions', 'return', courseId, courseworkId, submissionId], { account });
239
239
  });
@@ -241,12 +241,12 @@ export function registerClassroomTools(server: McpServer): void {
241
241
  server.registerTool('gog_classroom_submissions_turn_in', {
242
242
  description: 'Turn in a student submission (student action).',
243
243
  annotations: { destructiveHint: true },
244
- inputSchema: {
244
+ inputSchema: z.object({
245
245
  courseId: z.string().describe('Course ID'),
246
246
  courseworkId: z.string().describe('Coursework ID'),
247
247
  submissionId: z.string().describe('Submission ID'),
248
248
  account: accountParam,
249
- },
249
+ }),
250
250
  }, async ({ courseId, courseworkId, submissionId, account }) => {
251
251
  return runOrDiagnose(['classroom', 'submissions', 'turn-in', courseId, courseworkId, submissionId], { account });
252
252
  });
@@ -254,12 +254,12 @@ export function registerClassroomTools(server: McpServer): void {
254
254
  server.registerTool('gog_classroom_submissions_reclaim', {
255
255
  description: 'Reclaim a turned-in submission (student action to edit a submission).',
256
256
  annotations: { destructiveHint: true },
257
- inputSchema: {
257
+ inputSchema: z.object({
258
258
  courseId: z.string().describe('Course ID'),
259
259
  courseworkId: z.string().describe('Coursework ID'),
260
260
  submissionId: z.string().describe('Submission ID'),
261
261
  account: accountParam,
262
- },
262
+ }),
263
263
  }, async ({ courseId, courseworkId, submissionId, account }) => {
264
264
  return runOrDiagnose(['classroom', 'submissions', 'reclaim', courseId, courseworkId, submissionId], { account });
265
265
  });
@@ -267,7 +267,7 @@ export function registerClassroomTools(server: McpServer): void {
267
267
  server.registerTool('gog_classroom_announcements_list', {
268
268
  description: 'List announcements in a Google Classroom course.',
269
269
  annotations: { readOnlyHint: true },
270
- inputSchema: {
270
+ inputSchema: z.object({
271
271
  courseId: z.string().describe('Course ID'),
272
272
  state: z.string().optional().describe('Filter by announcement state'),
273
273
  orderBy: z.string().optional().describe('Sort order'),
@@ -276,7 +276,7 @@ export function registerClassroomTools(server: McpServer): void {
276
276
  page: pageAliasParam,
277
277
  all: z.boolean().optional().describe('Fetch all pages'),
278
278
  account: accountParam,
279
- },
279
+ }),
280
280
  }, async ({ courseId, state, orderBy, max, pageToken, page, all, account }) => {
281
281
  const args = ['classroom', 'announcements', 'list', courseId];
282
282
  if (state) args.push(`--state=${state}`);
@@ -291,24 +291,24 @@ export function registerClassroomTools(server: McpServer): void {
291
291
  server.registerTool('gog_classroom_announcements_get', {
292
292
  description: 'Get a single announcement by ID.',
293
293
  annotations: { readOnlyHint: true },
294
- inputSchema: {
294
+ inputSchema: z.object({
295
295
  courseId: z.string().describe('Course ID'),
296
296
  announcementId: z.string().describe('Announcement ID'),
297
297
  account: accountParam,
298
- },
298
+ }),
299
299
  }, async ({ courseId, announcementId, account }) => {
300
300
  return runOrDiagnose(['classroom', 'announcements', 'get', courseId, announcementId], { account });
301
301
  });
302
302
 
303
303
  server.registerTool('gog_classroom_announcements_create', {
304
304
  description: 'Create an announcement in a Google Classroom course.',
305
- inputSchema: {
305
+ inputSchema: z.object({
306
306
  courseId: z.string().describe('Course ID'),
307
307
  text: z.string().describe('Announcement text'),
308
308
  state: z.enum(['PUBLISHED', 'DRAFT']).optional().describe('State'),
309
309
  scheduled: z.string().optional().describe('Scheduled publish time'),
310
310
  account: accountParam,
311
- },
311
+ }),
312
312
  }, async ({ courseId, text, state, scheduled, account }) => {
313
313
  const args = ['classroom', 'announcements', 'create', courseId, `--text=${text}`];
314
314
  if (state) args.push(`--state=${state}`);
@@ -319,14 +319,14 @@ export function registerClassroomTools(server: McpServer): void {
319
319
  server.registerTool('gog_classroom_topics_list', {
320
320
  description: 'List topics in a Google Classroom course.',
321
321
  annotations: { readOnlyHint: true },
322
- inputSchema: {
322
+ inputSchema: z.object({
323
323
  courseId: z.string().describe('Course ID'),
324
324
  max: z.number().optional().describe('Max results per page'),
325
325
  pageToken: pageTokenParam,
326
326
  page: pageAliasParam,
327
327
  all: z.boolean().optional().describe('Fetch all pages'),
328
328
  account: accountParam,
329
- },
329
+ }),
330
330
  }, async ({ courseId, max, pageToken, page, all, account }) => {
331
331
  const args = ['classroom', 'topics', 'list', courseId];
332
332
  if (max !== undefined) args.push(`--max=${max}`);
@@ -339,11 +339,11 @@ export function registerClassroomTools(server: McpServer): void {
339
339
  server.registerTool('gog_classroom_topics_get', {
340
340
  description: 'Get a single topic by ID.',
341
341
  annotations: { readOnlyHint: true },
342
- inputSchema: {
342
+ inputSchema: z.object({
343
343
  courseId: z.string().describe('Course ID'),
344
344
  topicId: z.string().describe('Topic ID'),
345
345
  account: accountParam,
346
- },
346
+ }),
347
347
  }, async ({ courseId, topicId, account }) => {
348
348
  return runOrDiagnose(['classroom', 'topics', 'get', courseId, topicId], { account });
349
349
  });
@@ -351,7 +351,7 @@ export function registerClassroomTools(server: McpServer): void {
351
351
  server.registerTool('gog_classroom_invitations_list', {
352
352
  description: 'List Google Classroom invitations.',
353
353
  annotations: { readOnlyHint: true },
354
- inputSchema: {
354
+ inputSchema: z.object({
355
355
  course: z.string().optional().describe('Filter by course ID'),
356
356
  user: z.string().optional().describe('Filter by user ID'),
357
357
  max: z.number().optional().describe('Max results per page'),
@@ -359,7 +359,7 @@ export function registerClassroomTools(server: McpServer): void {
359
359
  page: pageAliasParam,
360
360
  all: z.boolean().optional().describe('Fetch all pages'),
361
361
  account: accountParam,
362
- },
362
+ }),
363
363
  }, async ({ course, user, max, pageToken, page, all, account }) => {
364
364
  const args = ['classroom', 'invitations', 'list'];
365
365
  if (course) args.push(`--course=${course}`);
@@ -374,20 +374,20 @@ export function registerClassroomTools(server: McpServer): void {
374
374
  server.registerTool('gog_classroom_invitations_get', {
375
375
  description: 'Get a single invitation by ID.',
376
376
  annotations: { readOnlyHint: true },
377
- inputSchema: {
377
+ inputSchema: z.object({
378
378
  invitationId: z.string().describe('Invitation ID'),
379
379
  account: accountParam,
380
- },
380
+ }),
381
381
  }, async ({ invitationId, account }) => {
382
382
  return runOrDiagnose(['classroom', 'invitations', 'get', invitationId], { account });
383
383
  });
384
384
 
385
385
  server.registerTool('gog_classroom_invitations_accept', {
386
386
  description: 'Accept a Google Classroom invitation.',
387
- inputSchema: {
387
+ inputSchema: z.object({
388
388
  invitationId: z.string().describe('Invitation ID'),
389
389
  account: accountParam,
390
- },
390
+ }),
391
391
  }, async ({ invitationId, account }) => {
392
392
  return runOrDiagnose(['classroom', 'invitations', 'accept', invitationId], { account });
393
393
  });
@@ -395,10 +395,10 @@ export function registerClassroomTools(server: McpServer): void {
395
395
  server.registerTool('gog_classroom_profile_get', {
396
396
  description: 'Get a Google Classroom user profile. Omit userId to fetch the authenticated user.',
397
397
  annotations: { readOnlyHint: true },
398
- inputSchema: {
398
+ inputSchema: z.object({
399
399
  userId: z.string().optional().describe('User ID (omit for self)'),
400
400
  account: accountParam,
401
- },
401
+ }),
402
402
  }, async ({ userId, account }) => {
403
403
  const args = ['classroom', 'profile', 'get'];
404
404
  if (userId) args.push(userId);