gogcli-mcp 4.2.5 → 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.
Files changed (57) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +714 -216
  4. package/dist/lib.js +750 -216
  5. package/manifest.json +9 -2
  6. package/package.json +2 -2
  7. package/server.json +2 -2
  8. package/src/arg-guard.ts +68 -0
  9. package/src/argv.ts +27 -0
  10. package/src/attachment-root.ts +111 -0
  11. package/src/attachments.ts +1 -0
  12. package/src/blob-upload.ts +4 -2
  13. package/src/file-roots.ts +66 -0
  14. package/src/gmail-dispatch-guard.ts +56 -16
  15. package/src/gmail-results.ts +21 -2
  16. package/src/lib.ts +6 -0
  17. package/src/run-path-guard.ts +118 -0
  18. package/src/runner.ts +86 -18
  19. package/src/tools/api.ts +39 -7
  20. package/src/tools/appscript.ts +12 -8
  21. package/src/tools/auth.ts +13 -3
  22. package/src/tools/calendar.ts +10 -8
  23. package/src/tools/chat.ts +16 -13
  24. package/src/tools/classroom.ts +27 -25
  25. package/src/tools/contacts.ts +5 -3
  26. package/src/tools/docs.ts +8 -6
  27. package/src/tools/drive.ts +42 -18
  28. package/src/tools/gmail.ts +71 -11
  29. package/src/tools/sheets.ts +10 -8
  30. package/src/tools/slides.ts +14 -9
  31. package/src/tools/tasks.ts +7 -5
  32. package/src/tools/utils.ts +52 -6
  33. package/tests/arg-guard.test.ts +80 -0
  34. package/tests/attachment-root.test.ts +130 -0
  35. package/tests/attachments.test.ts +8 -0
  36. package/tests/blob-upload.test.ts +4 -3
  37. package/tests/file-roots.test.ts +101 -0
  38. package/tests/gmail-dispatch-guard.test.ts +46 -0
  39. package/tests/gmail-results.test.ts +35 -1
  40. package/tests/run-path-guard.test.ts +142 -0
  41. package/tests/runner.test.ts +136 -0
  42. package/tests/tools/api.test.ts +74 -8
  43. package/tests/tools/appscript.test.ts +34 -8
  44. package/tests/tools/auth.test.ts +44 -17
  45. package/tests/tools/calendar.test.ts +29 -27
  46. package/tests/tools/chat.test.ts +46 -20
  47. package/tests/tools/classroom.test.ts +39 -38
  48. package/tests/tools/contacts.test.ts +3 -2
  49. package/tests/tools/docs.test.ts +44 -15
  50. package/tests/tools/drive.test.ts +106 -19
  51. package/tests/tools/gmail.test.ts +227 -29
  52. package/tests/tools/run-tool-examples.test.ts +69 -0
  53. package/tests/tools/sheets.test.ts +16 -15
  54. package/tests/tools/slides.test.ts +47 -11
  55. package/tests/tools/tasks.test.ts +7 -6
  56. package/tests/tools/utils.test.ts +32 -31
  57. package/vitest.config.ts +5 -0
package/src/tools/chat.ts CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  } from './utils.js';
10
10
  import type { GogArg } from '../runner.js';
11
11
  import { attachInlineParam, inlineAttachmentArgs } from '../attachments.js';
12
+ import { pos } from '../argv.js';
13
+ import { confinePaths } from '../file-roots.js';
12
14
 
13
15
  // Google Chat (gog >= 0.38.0 for the mention/reaction metadata in
14
16
  // `messages list`, >= 0.39.0 for `messages search`; the rest is older).
@@ -54,7 +56,7 @@ export function registerChatTools(server: McpServer): void {
54
56
  account: accountParam,
55
57
  }),
56
58
  }, async ({ max, pageToken, page, all, account }) => {
57
- const args = ['chat', 'spaces', 'list'];
59
+ const args: GogArg[] = ['chat', 'spaces', 'list'];
58
60
  pushPaginationFlags(args, { max, pageToken, page, all });
59
61
  return runOrDiagnose(args, { account });
60
62
  });
@@ -72,7 +74,7 @@ export function registerChatTools(server: McpServer): void {
72
74
  account: accountParam,
73
75
  }),
74
76
  }, async ({ displayName, exact, max, account }) => {
75
- const args = ['chat', 'spaces', 'find', displayName];
77
+ const args: GogArg[] = ['chat', 'spaces', 'find', pos(displayName)];
76
78
  if (exact) args.push('--exact');
77
79
  if (max !== undefined) args.push(`--max=${max}`);
78
80
  return runOrDiagnose(args, { account });
@@ -89,7 +91,7 @@ export function registerChatTools(server: McpServer): void {
89
91
  account: accountParam,
90
92
  }),
91
93
  }, async ({ displayName, members, account }) => {
92
- const args = ['chat', 'spaces', 'create', displayName];
94
+ const args: GogArg[] = ['chat', 'spaces', 'create', pos(displayName)];
93
95
  if (members) for (const member of members) args.push(`--member=${member}`);
94
96
  return runOrDiagnose(args, { account });
95
97
  });
@@ -105,7 +107,7 @@ export function registerChatTools(server: McpServer): void {
105
107
  account: accountParam,
106
108
  }),
107
109
  }, async ({ space, max, pageToken, page, all, account }) => {
108
- const args = ['chat', 'threads', 'list', space];
110
+ const args: GogArg[] = ['chat', 'threads', 'list', pos(space)];
109
111
  pushPaginationFlags(args, { max, pageToken, page, all });
110
112
  return runOrDiagnose(args, { account });
111
113
  });
@@ -128,7 +130,7 @@ export function registerChatTools(server: McpServer): void {
128
130
  account: accountParam,
129
131
  }),
130
132
  }, async ({ space, thread, unread, order, max, pageToken, page, all, account }) => {
131
- const args = ['chat', 'messages', 'list', space];
133
+ const args: GogArg[] = ['chat', 'messages', 'list', pos(space)];
132
134
  if (thread) args.push(`--thread=${thread}`);
133
135
  if (unread) args.push('--unread');
134
136
  if (order) args.push(`--order=${order}`);
@@ -163,7 +165,7 @@ export function registerChatTools(server: McpServer): void {
163
165
  account: accountParam,
164
166
  }),
165
167
  }, async ({ query, order, view, markup, max, pageToken, page, all, account }) => {
166
- const args = ['chat', 'messages', 'search', query];
168
+ const args: GogArg[] = ['chat', 'messages', 'search', pos(query)];
167
169
  if (order) args.push(`--order=${order}`);
168
170
  if (view) args.push(`--view=${view}`);
169
171
  if (markup) args.push(`--markup=${markup}`);
@@ -184,16 +186,17 @@ export function registerChatTools(server: McpServer): void {
184
186
  thread: threadParam,
185
187
  attach: z.array(z.string()).optional().describe(
186
188
  'Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine — use '
187
- + 'attachInline there instead.',
189
+ + 'attachInline there instead. Must be inside the server\'s GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files).',
188
190
  ),
189
191
  attachInline: attachInlineParam,
190
192
  account: accountParam,
191
193
  }),
192
194
  }, async ({ space, text, thread, attach, attachInline, account }) => {
195
+ confinePaths(attach, 'attach');
193
196
  if (text === undefined && !attach?.length && !attachInline?.length) {
194
197
  throw new Error('A Chat message needs text, an attachment, or both.');
195
198
  }
196
- const args: GogArg[] = ['chat', 'messages', 'send', space];
199
+ const args: GogArg[] = ['chat', 'messages', 'send', pos(space)];
197
200
  if (text !== undefined) args.push(`--text=${text}`);
198
201
  if (thread) args.push(`--thread=${thread}`);
199
202
  if (attach) for (const path of attach) args.push(`--attach=${path}`);
@@ -217,7 +220,7 @@ export function registerChatTools(server: McpServer): void {
217
220
  account: accountParam,
218
221
  }),
219
222
  }, async ({ email, text, thread, account }) => {
220
- const args = ['chat', 'dm', 'send', email, `--text=${text}`];
223
+ const args: GogArg[] = ['chat', 'dm', 'send', pos(email), `--text=${text}`];
221
224
  if (thread) args.push(`--thread=${thread}`);
222
225
  return runOrDiagnose(args, { account });
223
226
  });
@@ -232,7 +235,7 @@ export function registerChatTools(server: McpServer): void {
232
235
  account: accountParam,
233
236
  }),
234
237
  }, async ({ email, account }) => {
235
- return runOrDiagnose(['chat', 'dm', 'space', email], { account });
238
+ return runOrDiagnose(['chat', 'dm', 'space', pos(email)], { account });
236
239
  });
237
240
 
238
241
  server.registerTool('gog_chat_reactions_list', {
@@ -248,7 +251,7 @@ export function registerChatTools(server: McpServer): void {
248
251
  account: accountParam,
249
252
  }),
250
253
  }, async ({ message, space, max, pageToken, page, all, account }) => {
251
- const args = ['chat', 'messages', 'reactions', 'list', message];
254
+ const args: GogArg[] = ['chat', 'messages', 'reactions', 'list', pos(message)];
252
255
  if (space) args.push(`--space=${space}`);
253
256
  pushPaginationFlags(args, { max, pageToken, page, all });
254
257
  return runOrDiagnose(args, { account });
@@ -266,7 +269,7 @@ export function registerChatTools(server: McpServer): void {
266
269
  account: accountParam,
267
270
  }),
268
271
  }, async ({ message, emoji, space, account }) => {
269
- const args = ['chat', 'messages', 'reactions', 'create', message, emoji];
272
+ const args: GogArg[] = ['chat', 'messages', 'reactions', 'create', pos(message), pos(emoji)];
270
273
  if (space) args.push(`--space=${space}`);
271
274
  return runOrDiagnose(args, { account });
272
275
  });
@@ -282,7 +285,7 @@ export function registerChatTools(server: McpServer): void {
282
285
  account: accountParam,
283
286
  }),
284
287
  }, async ({ reaction, account }) => {
285
- return runOrDiagnose(['chat', 'messages', 'reactions', 'delete', reaction], { account });
288
+ return runOrDiagnose(['chat', 'messages', 'reactions', 'delete', pos(reaction)], { account });
286
289
  });
287
290
 
288
291
  registerRunTool(server, {
@@ -1,6 +1,8 @@
1
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
+ import { pos } from '../argv.js';
5
+ import type { GogArg } from '../runner.js';
4
6
 
5
7
  export function registerClassroomTools(server: McpServer): void {
6
8
  server.registerTool('gog_classroom_courses_list', {
@@ -17,7 +19,7 @@ export function registerClassroomTools(server: McpServer): void {
17
19
  account: accountParam,
18
20
  }),
19
21
  }, async ({ state, teacher, student, max, pageToken, page, all, account }) => {
20
- const args = ['classroom', 'courses', 'list'];
22
+ const args: GogArg[] = ['classroom', 'courses', 'list'];
21
23
  if (state) args.push(`--state=${state}`);
22
24
  if (teacher) args.push(`--teacher=${teacher}`);
23
25
  if (student) args.push(`--student=${student}`);
@@ -36,7 +38,7 @@ export function registerClassroomTools(server: McpServer): void {
36
38
  account: accountParam,
37
39
  }),
38
40
  }, async ({ courseId, account }) => {
39
- return runOrDiagnose(['classroom', 'courses', 'get', courseId], { account });
41
+ return runOrDiagnose(['classroom', 'courses', 'get', pos(courseId)], { account });
40
42
  });
41
43
 
42
44
  server.registerTool('gog_classroom_students_list', {
@@ -51,7 +53,7 @@ export function registerClassroomTools(server: McpServer): void {
51
53
  account: accountParam,
52
54
  }),
53
55
  }, async ({ courseId, max, pageToken, page, all, account }) => {
54
- const args = ['classroom', 'students', 'list', courseId];
56
+ const args: GogArg[] = ['classroom', 'students', 'list', pos(courseId)];
55
57
  if (max !== undefined) args.push(`--max=${max}`);
56
58
  const token = resolvePageToken({ pageToken, page });
57
59
  if (token) args.push(`--page=${token}`);
@@ -68,7 +70,7 @@ export function registerClassroomTools(server: McpServer): void {
68
70
  account: accountParam,
69
71
  }),
70
72
  }, async ({ courseId, userId, account }) => {
71
- return runOrDiagnose(['classroom', 'students', 'get', courseId, userId], { account });
73
+ return runOrDiagnose(['classroom', 'students', 'get', pos(courseId), pos(userId)], { account });
72
74
  });
73
75
 
74
76
  server.registerTool('gog_classroom_teachers_list', {
@@ -83,7 +85,7 @@ export function registerClassroomTools(server: McpServer): void {
83
85
  account: accountParam,
84
86
  }),
85
87
  }, async ({ courseId, max, pageToken, page, all, account }) => {
86
- const args = ['classroom', 'teachers', 'list', courseId];
88
+ const args: GogArg[] = ['classroom', 'teachers', 'list', pos(courseId)];
87
89
  if (max !== undefined) args.push(`--max=${max}`);
88
90
  const token = resolvePageToken({ pageToken, page });
89
91
  if (token) args.push(`--page=${token}`);
@@ -100,7 +102,7 @@ export function registerClassroomTools(server: McpServer): void {
100
102
  account: accountParam,
101
103
  }),
102
104
  }, async ({ courseId, userId, account }) => {
103
- return runOrDiagnose(['classroom', 'teachers', 'get', courseId, userId], { account });
105
+ return runOrDiagnose(['classroom', 'teachers', 'get', pos(courseId), pos(userId)], { account });
104
106
  });
105
107
 
106
108
  server.registerTool('gog_classroom_roster', {
@@ -117,7 +119,7 @@ export function registerClassroomTools(server: McpServer): void {
117
119
  account: accountParam,
118
120
  }),
119
121
  }, async ({ courseId, students, teachers, max, pageToken, page, all, account }) => {
120
- const args = ['classroom', 'roster', courseId];
122
+ const args: GogArg[] = ['classroom', 'roster', pos(courseId)];
121
123
  if (students) args.push('--students');
122
124
  if (teachers) args.push('--teachers');
123
125
  if (max !== undefined) args.push(`--max=${max}`);
@@ -143,7 +145,7 @@ export function registerClassroomTools(server: McpServer): void {
143
145
  account: accountParam,
144
146
  }),
145
147
  }, async ({ courseId, state, topic, orderBy, max, pageToken, page, all, scanPages, account }) => {
146
- const args = ['classroom', 'coursework', 'list', courseId];
148
+ const args: GogArg[] = ['classroom', 'coursework', 'list', pos(courseId)];
147
149
  if (state) args.push(`--state=${state}`);
148
150
  if (topic) args.push(`--topic=${topic}`);
149
151
  if (orderBy) args.push(`--order-by=${orderBy}`);
@@ -164,7 +166,7 @@ export function registerClassroomTools(server: McpServer): void {
164
166
  account: accountParam,
165
167
  }),
166
168
  }, async ({ courseId, courseworkId, account }) => {
167
- return runOrDiagnose(['classroom', 'coursework', 'get', courseId, courseworkId], { account });
169
+ return runOrDiagnose(['classroom', 'coursework', 'get', pos(courseId), pos(courseworkId)], { account });
168
170
  });
169
171
 
170
172
  server.registerTool('gog_classroom_submissions_list', {
@@ -183,7 +185,7 @@ export function registerClassroomTools(server: McpServer): void {
183
185
  account: accountParam,
184
186
  }),
185
187
  }, async ({ courseId, courseworkId, state, late, user, max, pageToken, page, all, account }) => {
186
- const args = ['classroom', 'submissions', 'list', courseId, courseworkId];
188
+ const args: GogArg[] = ['classroom', 'submissions', 'list', pos(courseId), pos(courseworkId)];
187
189
  if (state) args.push(`--state=${state}`);
188
190
  if (late) args.push(`--late=${late}`);
189
191
  if (user) args.push(`--user=${user}`);
@@ -204,7 +206,7 @@ export function registerClassroomTools(server: McpServer): void {
204
206
  account: accountParam,
205
207
  }),
206
208
  }, async ({ courseId, courseworkId, submissionId, account }) => {
207
- return runOrDiagnose(['classroom', 'submissions', 'get', courseId, courseworkId, submissionId], { account });
209
+ return runOrDiagnose(['classroom', 'submissions', 'get', pos(courseId), pos(courseworkId), pos(submissionId)], { account });
208
210
  });
209
211
 
210
212
  server.registerTool('gog_classroom_submissions_grade', {
@@ -219,7 +221,7 @@ export function registerClassroomTools(server: McpServer): void {
219
221
  account: accountParam,
220
222
  }),
221
223
  }, async ({ courseId, courseworkId, submissionId, draft, assigned, account }) => {
222
- const args = ['classroom', 'submissions', 'grade', courseId, courseworkId, submissionId];
224
+ const args: GogArg[] = ['classroom', 'submissions', 'grade', pos(courseId), pos(courseworkId), pos(submissionId)];
223
225
  if (draft) args.push(`--draft=${draft}`);
224
226
  if (assigned) args.push(`--assigned=${assigned}`);
225
227
  return runOrDiagnose(args, { account });
@@ -235,7 +237,7 @@ export function registerClassroomTools(server: McpServer): void {
235
237
  account: accountParam,
236
238
  }),
237
239
  }, async ({ courseId, courseworkId, submissionId, account }) => {
238
- return runOrDiagnose(['classroom', 'submissions', 'return', courseId, courseworkId, submissionId], { account });
240
+ return runOrDiagnose(['classroom', 'submissions', 'return', pos(courseId), pos(courseworkId), pos(submissionId)], { account });
239
241
  });
240
242
 
241
243
  server.registerTool('gog_classroom_submissions_turn_in', {
@@ -248,7 +250,7 @@ export function registerClassroomTools(server: McpServer): void {
248
250
  account: accountParam,
249
251
  }),
250
252
  }, async ({ courseId, courseworkId, submissionId, account }) => {
251
- return runOrDiagnose(['classroom', 'submissions', 'turn-in', courseId, courseworkId, submissionId], { account });
253
+ return runOrDiagnose(['classroom', 'submissions', 'turn-in', pos(courseId), pos(courseworkId), pos(submissionId)], { account });
252
254
  });
253
255
 
254
256
  server.registerTool('gog_classroom_submissions_reclaim', {
@@ -261,7 +263,7 @@ export function registerClassroomTools(server: McpServer): void {
261
263
  account: accountParam,
262
264
  }),
263
265
  }, async ({ courseId, courseworkId, submissionId, account }) => {
264
- return runOrDiagnose(['classroom', 'submissions', 'reclaim', courseId, courseworkId, submissionId], { account });
266
+ return runOrDiagnose(['classroom', 'submissions', 'reclaim', pos(courseId), pos(courseworkId), pos(submissionId)], { account });
265
267
  });
266
268
 
267
269
  server.registerTool('gog_classroom_announcements_list', {
@@ -278,7 +280,7 @@ export function registerClassroomTools(server: McpServer): void {
278
280
  account: accountParam,
279
281
  }),
280
282
  }, async ({ courseId, state, orderBy, max, pageToken, page, all, account }) => {
281
- const args = ['classroom', 'announcements', 'list', courseId];
283
+ const args: GogArg[] = ['classroom', 'announcements', 'list', pos(courseId)];
282
284
  if (state) args.push(`--state=${state}`);
283
285
  if (orderBy) args.push(`--order-by=${orderBy}`);
284
286
  if (max !== undefined) args.push(`--max=${max}`);
@@ -297,7 +299,7 @@ export function registerClassroomTools(server: McpServer): void {
297
299
  account: accountParam,
298
300
  }),
299
301
  }, async ({ courseId, announcementId, account }) => {
300
- return runOrDiagnose(['classroom', 'announcements', 'get', courseId, announcementId], { account });
302
+ return runOrDiagnose(['classroom', 'announcements', 'get', pos(courseId), pos(announcementId)], { account });
301
303
  });
302
304
 
303
305
  server.registerTool('gog_classroom_announcements_create', {
@@ -311,7 +313,7 @@ export function registerClassroomTools(server: McpServer): void {
311
313
  account: accountParam,
312
314
  }),
313
315
  }, async ({ courseId, text, state, scheduled, account }) => {
314
- const args = ['classroom', 'announcements', 'create', courseId, `--text=${text}`];
316
+ const args: GogArg[] = ['classroom', 'announcements', 'create', pos(courseId), `--text=${text}`];
315
317
  if (state) args.push(`--state=${state}`);
316
318
  if (scheduled) args.push(`--scheduled=${scheduled}`);
317
319
  return runOrDiagnose(args, { account });
@@ -329,7 +331,7 @@ export function registerClassroomTools(server: McpServer): void {
329
331
  account: accountParam,
330
332
  }),
331
333
  }, async ({ courseId, max, pageToken, page, all, account }) => {
332
- const args = ['classroom', 'topics', 'list', courseId];
334
+ const args: GogArg[] = ['classroom', 'topics', 'list', pos(courseId)];
333
335
  if (max !== undefined) args.push(`--max=${max}`);
334
336
  const token = resolvePageToken({ pageToken, page });
335
337
  if (token) args.push(`--page=${token}`);
@@ -346,7 +348,7 @@ export function registerClassroomTools(server: McpServer): void {
346
348
  account: accountParam,
347
349
  }),
348
350
  }, async ({ courseId, topicId, account }) => {
349
- return runOrDiagnose(['classroom', 'topics', 'get', courseId, topicId], { account });
351
+ return runOrDiagnose(['classroom', 'topics', 'get', pos(courseId), pos(topicId)], { account });
350
352
  });
351
353
 
352
354
  server.registerTool('gog_classroom_invitations_list', {
@@ -362,7 +364,7 @@ export function registerClassroomTools(server: McpServer): void {
362
364
  account: accountParam,
363
365
  }),
364
366
  }, async ({ course, user, max, pageToken, page, all, account }) => {
365
- const args = ['classroom', 'invitations', 'list'];
367
+ const args: GogArg[] = ['classroom', 'invitations', 'list'];
366
368
  if (course) args.push(`--course=${course}`);
367
369
  if (user) args.push(`--user=${user}`);
368
370
  if (max !== undefined) args.push(`--max=${max}`);
@@ -380,7 +382,7 @@ export function registerClassroomTools(server: McpServer): void {
380
382
  account: accountParam,
381
383
  }),
382
384
  }, async ({ invitationId, account }) => {
383
- return runOrDiagnose(['classroom', 'invitations', 'get', invitationId], { account });
385
+ return runOrDiagnose(['classroom', 'invitations', 'get', pos(invitationId)], { account });
384
386
  });
385
387
 
386
388
  server.registerTool('gog_classroom_invitations_accept', {
@@ -391,7 +393,7 @@ export function registerClassroomTools(server: McpServer): void {
391
393
  account: accountParam,
392
394
  }),
393
395
  }, async ({ invitationId, account }) => {
394
- return runOrDiagnose(['classroom', 'invitations', 'accept', invitationId], { account });
396
+ return runOrDiagnose(['classroom', 'invitations', 'accept', pos(invitationId)], { account });
395
397
  });
396
398
 
397
399
  server.registerTool('gog_classroom_profile_get', {
@@ -402,8 +404,8 @@ export function registerClassroomTools(server: McpServer): void {
402
404
  account: accountParam,
403
405
  }),
404
406
  }, async ({ userId, account }) => {
405
- const args = ['classroom', 'profile', 'get'];
406
- if (userId) args.push(userId);
407
+ const args: GogArg[] = ['classroom', 'profile', 'get'];
408
+ if (userId) args.push(pos(userId));
407
409
  return runOrDiagnose(args, { account });
408
410
  });
409
411
 
@@ -1,6 +1,8 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
+ import { pos } from '../argv.js';
5
+ import type { GogArg } from '../runner.js';
4
6
 
5
7
  export function registerContactsTools(server: McpServer): void {
6
8
  server.registerTool('gog_contacts_search', {
@@ -11,7 +13,7 @@ export function registerContactsTools(server: McpServer): void {
11
13
  account: accountParam,
12
14
  }),
13
15
  }, async ({ query, account }) => {
14
- return runOrDiagnose(['contacts', 'search', query], { account });
16
+ return runOrDiagnose(['contacts', 'search', pos(query)], { account });
15
17
  });
16
18
 
17
19
  server.registerTool('gog_contacts_list', {
@@ -32,7 +34,7 @@ export function registerContactsTools(server: McpServer): void {
32
34
  account: accountParam,
33
35
  }),
34
36
  }, async ({ resourceName, account }) => {
35
- return runOrDiagnose(['contacts', 'get', resourceName], { account });
37
+ return runOrDiagnose(['contacts', 'get', pos(resourceName)], { account });
36
38
  });
37
39
 
38
40
  server.registerTool('gog_contacts_create', {
@@ -48,7 +50,7 @@ export function registerContactsTools(server: McpServer): void {
48
50
  account: accountParam,
49
51
  }),
50
52
  }, async ({ givenName, familyName, email, phone, org, title, account }) => {
51
- const args = ['contacts', 'create', `--given=${givenName}`];
53
+ const args: GogArg[] = ['contacts', 'create', `--given=${givenName}`];
52
54
  if (familyName) args.push(`--family=${familyName}`);
53
55
  if (email) args.push(`--email=${email}`);
54
56
  if (phone) args.push(`--phone=${phone}`);
package/src/tools/docs.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
4
+ import { pos } from '../argv.js';
5
+ import type { GogArg } from '../runner.js';
4
6
 
5
7
  export function registerDocsTools(server: McpServer): void {
6
8
  server.registerTool('gog_docs_info', {
@@ -11,7 +13,7 @@ export function registerDocsTools(server: McpServer): void {
11
13
  account: accountParam,
12
14
  }),
13
15
  }, async ({ docId, account }) => {
14
- return runOrDiagnose(['docs', 'info', docId], { account });
16
+ return runOrDiagnose(['docs', 'info', pos(docId)], { account });
15
17
  });
16
18
 
17
19
  server.registerTool('gog_docs_cat', {
@@ -23,7 +25,7 @@ export function registerDocsTools(server: McpServer): void {
23
25
  account: accountParam,
24
26
  }),
25
27
  }, async ({ docId, chips, account }) => {
26
- const args = ['docs', 'cat', docId];
28
+ const args: GogArg[] = ['docs', 'cat', pos(docId)];
27
29
  if (chips) args.push('--chips');
28
30
  return runOrDiagnose(args, { account });
29
31
  });
@@ -36,7 +38,7 @@ export function registerDocsTools(server: McpServer): void {
36
38
  account: accountParam,
37
39
  }),
38
40
  }, async ({ title, account }) => {
39
- return runOrDiagnose(['docs', 'create', title], { account });
41
+ return runOrDiagnose(['docs', 'create', pos(title)], { account });
40
42
  });
41
43
 
42
44
  server.registerTool('gog_docs_write', {
@@ -62,7 +64,7 @@ export function registerDocsTools(server: McpServer): void {
62
64
  account: accountParam,
63
65
  }),
64
66
  }, async ({ docId, text, append, checkOrphans, bullets, bulletPreset, ordered, noBullets, indentStart, indentEnd, indentFirstLine, spaceAbove, spaceBelow, keepLinesTogether, keepWithNext, batch, account }) => {
65
- const args = ['docs', 'write', docId, `--text=${text}`];
67
+ const args: GogArg[] = ['docs', 'write', pos(docId), `--text=${text}`];
66
68
  if (append) args.push('--append');
67
69
  if (checkOrphans) args.push('--check-orphans');
68
70
  if (bullets) args.push('--bullets');
@@ -90,7 +92,7 @@ export function registerDocsTools(server: McpServer): void {
90
92
  account: accountParam,
91
93
  }),
92
94
  }, async ({ docId, find, replace, account }) => {
93
- return runOrDiagnose(['docs', 'find-replace', docId, find, replace], { account });
95
+ return runOrDiagnose(['docs', 'find-replace', pos(docId), pos(find), pos(replace)], { account });
94
96
  });
95
97
 
96
98
  server.registerTool('gog_docs_structure', {
@@ -101,7 +103,7 @@ export function registerDocsTools(server: McpServer): void {
101
103
  account: accountParam,
102
104
  }),
103
105
  }, async ({ docId, account }) => {
104
- return runOrDiagnose(['docs', 'structure', docId], { account });
106
+ return runOrDiagnose(['docs', 'structure', pos(docId)], { account });
105
107
  });
106
108
 
107
109
  registerRunTool(server, { service: 'docs', examples: '"copy", "clear", "insert", "sed", "export"' });
@@ -1,10 +1,13 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import type { CallToolResult } from '@modelcontextprotocol/server';
3
3
  import { z } from 'zod';
4
- import { rawTextResult, viewParam, resolveView } from '@chrischall/mcp-utils';
4
+ import { errorResult, rawTextResult, viewParam, resolveView } from '@chrischall/mcp-utils';
5
+ import { MAX_INLINE_ATTACHMENT_BYTES } from '../attachments.js';
5
6
 
6
7
  import { run, runBinary } from '../runner.js';
7
8
  import { accountParam, diagnose, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken} from './utils.js';
9
+ import { pos } from '../argv.js';
10
+ import type { GogArg } from '../runner.js';
8
11
 
9
12
  // A native Google Doc exports to text directly; anything else (PDF, image,
10
13
  // docx, …) is first copied WITH conversion to this type, which makes Drive run
@@ -13,12 +16,21 @@ const GOOGLE_DOC_MIME = 'application/vnd.google-apps.document';
13
16
 
14
17
  // Parse `gog drive get` JSON into { name, mimeType }. gog nests the payload
15
18
  // under `file`; fall back to the top level if that ever changes.
16
- function fileMeta(raw: string): { name?: string; mimeType?: string } {
17
- const parsed = JSON.parse(raw) as { file?: { name?: string; mimeType?: string }; name?: string; mimeType?: string };
19
+ type DriveMeta = { name?: string; mimeType?: string; size?: string | number };
20
+
21
+ function fileMeta(raw: string): { name?: string; mimeType?: string; size?: number } {
22
+ const parsed = JSON.parse(raw) as { file?: DriveMeta } & DriveMeta;
18
23
  const f = parsed.file ?? parsed;
19
- return { name: f.name, mimeType: f.mimeType };
24
+ // Drive reports int64 fields as strings; native Google formats carry none.
25
+ const size = f.size === undefined ? undefined : Number(f.size);
26
+ return { name: f.name, mimeType: f.mimeType, size: Number.isFinite(size) ? size : undefined };
20
27
  }
21
28
 
29
+ // The most gog_drive_read_bytes returns inline — the same 8 MiB per-file cap
30
+ // the attachment path enforces. The bytes travel base64-encoded (4/3 larger)
31
+ // inside one JSON-RPC message, and past this no MCP client accepts the result.
32
+ export const MAX_DRIVE_READ_BYTES = MAX_INLINE_ATTACHMENT_BYTES;
33
+
22
34
 
23
35
  // The `compact` rung for gog_drive_ls, as a Google Drive field mask.
24
36
  //
@@ -52,7 +64,7 @@ export function registerDriveTools(server: McpServer): void {
52
64
  account: accountParam,
53
65
  }),
54
66
  }, async ({ folderId, max, pageToken, page, query, allDrives, view, account }) => {
55
- const args = ['drive', 'ls'];
67
+ const args: GogArg[] = ['drive', 'ls'];
56
68
  if (folderId) args.push(`--parent=${folderId}`);
57
69
  if (max !== undefined) args.push(`--max=${max}`);
58
70
  const token = resolvePageToken({ pageToken, page });
@@ -78,7 +90,7 @@ export function registerDriveTools(server: McpServer): void {
78
90
  account: accountParam,
79
91
  }),
80
92
  }, async ({ query, view, account }) => {
81
- return runOrDiagnose(['drive', 'search', query], {
93
+ return runOrDiagnose(['drive', 'search', pos(query)], {
82
94
  account,
83
95
  stripMedia: resolveView(view, ['compact', 'full']) === 'compact',
84
96
  });
@@ -99,7 +111,7 @@ export function registerDriveTools(server: McpServer): void {
99
111
  account: accountParam,
100
112
  }),
101
113
  }, async ({ fileId, view, account }) => {
102
- return runOrDiagnose(['drive', 'get', fileId], {
114
+ return runOrDiagnose(['drive', 'get', pos(fileId)], {
103
115
  account,
104
116
  stripMedia: resolveView(view, ['compact', 'full']) === 'compact',
105
117
  });
@@ -113,7 +125,7 @@ export function registerDriveTools(server: McpServer): void {
113
125
  account: accountParam,
114
126
  }),
115
127
  }, async ({ name, account }) => {
116
- return runOrDiagnose(['drive', 'mkdir', name], { account });
128
+ return runOrDiagnose(['drive', 'mkdir', pos(name)], { account });
117
129
  });
118
130
 
119
131
  server.registerTool('gog_drive_rename', {
@@ -125,7 +137,7 @@ export function registerDriveTools(server: McpServer): void {
125
137
  account: accountParam,
126
138
  }),
127
139
  }, async ({ fileId, newName, account }) => {
128
- return runOrDiagnose(['drive', 'rename', fileId, newName], { account });
140
+ return runOrDiagnose(['drive', 'rename', pos(fileId), pos(newName)], { account });
129
141
  });
130
142
 
131
143
  server.registerTool('gog_drive_move', {
@@ -137,7 +149,7 @@ export function registerDriveTools(server: McpServer): void {
137
149
  account: accountParam,
138
150
  }),
139
151
  }, async ({ fileId, parentId, account }) => {
140
- return runOrDiagnose(['drive', 'move', fileId, `--parent=${parentId}`], { account });
152
+ return runOrDiagnose(['drive', 'move', pos(fileId), `--parent=${parentId}`], { account });
141
153
  });
142
154
 
143
155
  server.registerTool('gog_drive_delete', {
@@ -149,7 +161,7 @@ export function registerDriveTools(server: McpServer): void {
149
161
  account: accountParam,
150
162
  }),
151
163
  }, async ({ fileId, permanent, account }) => {
152
- const args = ['drive', 'delete', fileId];
164
+ const args: GogArg[] = ['drive', 'delete', pos(fileId)];
153
165
  if (permanent) args.push('--permanent');
154
166
  // gog gates drive delete behind a confirmation; the runner injects
155
167
  // --no-input, so without --force it refuses at runtime.
@@ -169,7 +181,7 @@ export function registerDriveTools(server: McpServer): void {
169
181
  account: accountParam,
170
182
  }),
171
183
  }, async ({ fileId, to, email, domain, role, account }) => {
172
- const args = ['drive', 'share', fileId, `--to=${to}`];
184
+ const args: GogArg[] = ['drive', 'share', pos(fileId), `--to=${to}`];
173
185
  if (email) args.push(`--email=${email}`);
174
186
  if (domain) args.push(`--domain=${domain}`);
175
187
  if (role) args.push(`--role=${role}`);
@@ -201,7 +213,7 @@ export function registerDriveTools(server: McpServer): void {
201
213
  }, async ({ fileId, ocrLanguage, offset = 0, maxChars, account }) => {
202
214
  let tempDocId: string | undefined;
203
215
  try {
204
- const { name, mimeType } = fileMeta(await run(['drive', 'get', fileId], { account }));
216
+ const { name, mimeType } = fileMeta(await run(['drive', 'get', pos(fileId)], { account }));
205
217
 
206
218
  // Native Docs export straight to text; everything else is OCR-converted first.
207
219
  let sourceId = fileId;
@@ -242,7 +254,7 @@ export function registerDriveTools(server: McpServer): void {
242
254
  } finally {
243
255
  // Always remove the temp Doc — on success, and on a mid-extraction failure.
244
256
  if (tempDocId) {
245
- await run(['drive', 'delete', tempDocId, '--permanent', '--force'], { account }).catch(() => {});
257
+ await run(['drive', 'delete', pos(tempDocId), '--permanent', '--force'], { account }).catch(() => {});
246
258
  }
247
259
  }
248
260
  });
@@ -251,7 +263,7 @@ export function registerDriveTools(server: McpServer): void {
251
263
  description:
252
264
  'Fetch a Drive file\'s raw bytes and return them base64-encoded as an embedded resource — the ' +
253
265
  'generic fallback for callers that want the file itself (to parse locally) rather than extracted ' +
254
- 'text. For readable text from a PDF, prefer gog_drive_extract_text.',
266
+ 'text. Files over 8 MiB are refused (checked before downloading). For readable text from a PDF, prefer gog_drive_extract_text.',
255
267
  annotations: { readOnlyHint: true },
256
268
  inputSchema: z.object({
257
269
  fileId: z.string().describe('Drive file ID'),
@@ -259,9 +271,21 @@ export function registerDriveTools(server: McpServer): void {
259
271
  }),
260
272
  }, async ({ fileId, account }): Promise<CallToolResult> => {
261
273
  try {
262
- const { name, mimeType } = fileMeta(await run(['drive', 'get', fileId], { account }));
274
+ const { name, mimeType, size } = fileMeta(await run(['drive', 'get', pos(fileId)], { account }));
275
+ // Refuse an oversized file BEFORE fetching it (audit BUG-2): buffering a
276
+ // multi-GB file plus its base64 copy can take down the whole server.
277
+ if (size !== undefined && size > MAX_DRIVE_READ_BYTES) {
278
+ return errorResult(
279
+ `${name ?? fileId} is ${size} bytes, over the 8 MiB this tool returns inline. `
280
+ + 'Use gog_drive_extract_text for its text, or share its webViewLink (gog_drive_get) instead.',
281
+ );
282
+ }
263
283
  const params = JSON.stringify({ fileId, alt: 'media' });
264
- const blob = await runBinary(['api', 'call', 'drive', 'v3', 'files.get', `--params=${params}`], { account });
284
+ // The cap also bounds the fetch itself, for files Drive reports no size for.
285
+ const blob = await runBinary(
286
+ ['api', 'call', 'drive', 'v3', 'files.get', `--params=${params}`],
287
+ { account, maxOutputBytes: MAX_DRIVE_READ_BYTES },
288
+ );
265
289
  const type = mimeType ?? 'application/octet-stream';
266
290
  // Several hosts (claude.ai among them) render embedded IMAGE resources
267
291
  // and reject every other type outright — "Resources of type
@@ -290,5 +314,5 @@ export function registerDriveTools(server: McpServer): void {
290
314
  }
291
315
  });
292
316
 
293
- registerRunTool(server, { service: 'drive', examples: '"copy", "upload", "download", "permissions"' });
317
+ registerRunTool(server, { service: 'drive', examples: '"copy", "download", "permissions"' });
294
318
  }