gogcli-mcp 4.2.5 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +1340 -262
- package/dist/lib.js +1381 -260
- package/manifest.json +9 -2
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/arg-guard.ts +68 -0
- package/src/argv.ts +27 -0
- package/src/attachment-root.ts +111 -0
- package/src/attachments.ts +1 -0
- package/src/blob-upload.ts +4 -2
- package/src/dispatch-confirmation.ts +277 -0
- package/src/file-roots.ts +66 -0
- package/src/gmail-dispatch-guard.ts +70 -30
- package/src/gmail-results.ts +21 -2
- package/src/lib.ts +15 -0
- package/src/run-path-guard.ts +118 -0
- package/src/runner.ts +86 -18
- package/src/send-confirm-token.ts +167 -0
- package/src/tools/api.ts +60 -7
- package/src/tools/appscript.ts +12 -8
- package/src/tools/auth.ts +13 -3
- package/src/tools/calendar.ts +178 -15
- package/src/tools/chat.ts +94 -18
- package/src/tools/classroom.ts +110 -28
- package/src/tools/contacts.ts +5 -3
- package/src/tools/docs.ts +8 -6
- package/src/tools/drive.ts +96 -20
- package/src/tools/gmail.ts +175 -24
- package/src/tools/sheets.ts +10 -8
- package/src/tools/slides.ts +14 -9
- package/src/tools/tasks.ts +7 -5
- package/src/tools/utils.ts +52 -6
- package/tests/arg-guard.test.ts +80 -0
- package/tests/attachment-root.test.ts +130 -0
- package/tests/attachments.test.ts +8 -0
- package/tests/blob-upload.test.ts +4 -3
- package/tests/file-roots.test.ts +101 -0
- package/tests/gmail-dispatch-guard.test.ts +270 -11
- package/tests/gmail-results.test.ts +35 -1
- package/tests/run-path-guard.test.ts +142 -0
- package/tests/runner.test.ts +136 -0
- package/tests/send-confirm-token.test.ts +200 -0
- package/tests/tools/api.test.ts +74 -8
- package/tests/tools/appscript.test.ts +34 -8
- package/tests/tools/auth.test.ts +44 -17
- package/tests/tools/calendar.test.ts +33 -28
- package/tests/tools/chat.test.ts +50 -21
- package/tests/tools/classroom.test.ts +43 -39
- package/tests/tools/contacts.test.ts +3 -2
- package/tests/tools/dispatch-gates.test.ts +396 -0
- package/tests/tools/docs.test.ts +44 -15
- package/tests/tools/drive.test.ts +110 -20
- package/tests/tools/gmail-confirm-token.test.ts +274 -0
- package/tests/tools/gmail.test.ts +227 -29
- package/tests/tools/run-tool-examples.test.ts +69 -0
- package/tests/tools/run-vets.test.ts +131 -0
- package/tests/tools/sheets.test.ts +16 -15
- package/tests/tools/slides.test.ts +47 -11
- package/tests/tools/tasks.test.ts +7 -6
- package/tests/tools/utils.test.ts +32 -31
- package/vitest.config.ts +5 -0
package/src/tools/classroom.ts
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
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';
|
|
6
|
+
import { bodyPreview, CONFIRM_FALLBACK_DESCRIPTION, confirmTokenParam, gatedElsewhere, hasCommandWord, requireDispatchConfirmation, resultText } from '../dispatch-confirmation.js';
|
|
7
|
+
|
|
8
|
+
// gog's spellings (internal/cmd/classroom.go, classroom_announcements.go, classroom_invitations.go).
|
|
9
|
+
const CLASSROOM_CREATE_WORDS = new Set(['create', 'add', 'new']);
|
|
10
|
+
const CLASSROOM_GATED: Record<string, { does: string; tool: string }> = {
|
|
11
|
+
announcements: { does: 'posts to a class', tool: 'gog_classroom_announcements_create' },
|
|
12
|
+
announcement: { does: 'posts to a class', tool: 'gog_classroom_announcements_create' },
|
|
13
|
+
ann: { does: 'posts to a class', tool: 'gog_classroom_announcements_create' },
|
|
14
|
+
invitations: { does: 'invites someone to a class', tool: 'gog_classroom_invitations_create' },
|
|
15
|
+
invitation: { does: 'invites someone to a class', tool: 'gog_classroom_invitations_create' },
|
|
16
|
+
invites: { does: 'invites someone to a class', tool: 'gog_classroom_invitations_create' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** gog_classroom_run must not post or invite what the dedicated tools would ask about. */
|
|
20
|
+
export function vetClassroomRun(subcommand: string, args: readonly string[]): string | undefined {
|
|
21
|
+
const sub = subcommand.toLowerCase();
|
|
22
|
+
const gated = Object.hasOwn(CLASSROOM_GATED, sub) ? CLASSROOM_GATED[sub] : undefined;
|
|
23
|
+
if (!gated) return undefined;
|
|
24
|
+
const word = hasCommandWord(args, CLASSROOM_CREATE_WORDS);
|
|
25
|
+
return word ? gatedElsewhere(`gog classroom ${sub} ${word.toLowerCase()}`, 'gog_classroom_run', gated.does, gated.tool) : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The course a Classroom dispatch reaches, for its confirmation prompt: a user
|
|
30
|
+
* approving "post to 123456789" cannot tell which class that is. Read on every
|
|
31
|
+
* call, so it is also the token fallback's phase-2 re-read. Unreadable output
|
|
32
|
+
* names nothing rather than throwing.
|
|
33
|
+
*/
|
|
34
|
+
export async function readCourse(
|
|
35
|
+
courseId: string,
|
|
36
|
+
account: string | undefined,
|
|
37
|
+
// A sub-package passes the runOrDiagnose it imported from lib.js, so its own
|
|
38
|
+
// tests' mock of that seam covers this read too.
|
|
39
|
+
runner: typeof runOrDiagnose = runOrDiagnose,
|
|
40
|
+
) {
|
|
41
|
+
const got = await runner(['classroom', 'courses', 'get', pos(courseId)], { account });
|
|
42
|
+
if (got.isError) return { error: got };
|
|
43
|
+
let course: { name?: unknown; section?: unknown } | undefined;
|
|
44
|
+
try {
|
|
45
|
+
course = (JSON.parse(resultText(got)) as { course?: typeof course } | null)?.course;
|
|
46
|
+
} catch {
|
|
47
|
+
course = undefined;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
course: {
|
|
51
|
+
id: courseId,
|
|
52
|
+
...(typeof course?.name === 'string' ? { name: course.name } : {}),
|
|
53
|
+
...(typeof course?.section === 'string' ? { section: course.section } : {}),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
4
57
|
|
|
5
58
|
export function registerClassroomTools(server: McpServer): void {
|
|
6
59
|
server.registerTool('gog_classroom_courses_list', {
|
|
@@ -17,7 +70,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
17
70
|
account: accountParam,
|
|
18
71
|
}),
|
|
19
72
|
}, async ({ state, teacher, student, max, pageToken, page, all, account }) => {
|
|
20
|
-
const args = ['classroom', 'courses', 'list'];
|
|
73
|
+
const args: GogArg[] = ['classroom', 'courses', 'list'];
|
|
21
74
|
if (state) args.push(`--state=${state}`);
|
|
22
75
|
if (teacher) args.push(`--teacher=${teacher}`);
|
|
23
76
|
if (student) args.push(`--student=${student}`);
|
|
@@ -36,7 +89,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
36
89
|
account: accountParam,
|
|
37
90
|
}),
|
|
38
91
|
}, async ({ courseId, account }) => {
|
|
39
|
-
return runOrDiagnose(['classroom', 'courses', 'get', courseId], { account });
|
|
92
|
+
return runOrDiagnose(['classroom', 'courses', 'get', pos(courseId)], { account });
|
|
40
93
|
});
|
|
41
94
|
|
|
42
95
|
server.registerTool('gog_classroom_students_list', {
|
|
@@ -51,7 +104,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
51
104
|
account: accountParam,
|
|
52
105
|
}),
|
|
53
106
|
}, async ({ courseId, max, pageToken, page, all, account }) => {
|
|
54
|
-
const args = ['classroom', 'students', 'list', courseId];
|
|
107
|
+
const args: GogArg[] = ['classroom', 'students', 'list', pos(courseId)];
|
|
55
108
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
56
109
|
const token = resolvePageToken({ pageToken, page });
|
|
57
110
|
if (token) args.push(`--page=${token}`);
|
|
@@ -68,7 +121,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
68
121
|
account: accountParam,
|
|
69
122
|
}),
|
|
70
123
|
}, async ({ courseId, userId, account }) => {
|
|
71
|
-
return runOrDiagnose(['classroom', 'students', 'get', courseId, userId], { account });
|
|
124
|
+
return runOrDiagnose(['classroom', 'students', 'get', pos(courseId), pos(userId)], { account });
|
|
72
125
|
});
|
|
73
126
|
|
|
74
127
|
server.registerTool('gog_classroom_teachers_list', {
|
|
@@ -83,7 +136,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
83
136
|
account: accountParam,
|
|
84
137
|
}),
|
|
85
138
|
}, async ({ courseId, max, pageToken, page, all, account }) => {
|
|
86
|
-
const args = ['classroom', 'teachers', 'list', courseId];
|
|
139
|
+
const args: GogArg[] = ['classroom', 'teachers', 'list', pos(courseId)];
|
|
87
140
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
88
141
|
const token = resolvePageToken({ pageToken, page });
|
|
89
142
|
if (token) args.push(`--page=${token}`);
|
|
@@ -100,7 +153,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
100
153
|
account: accountParam,
|
|
101
154
|
}),
|
|
102
155
|
}, async ({ courseId, userId, account }) => {
|
|
103
|
-
return runOrDiagnose(['classroom', 'teachers', 'get', courseId, userId], { account });
|
|
156
|
+
return runOrDiagnose(['classroom', 'teachers', 'get', pos(courseId), pos(userId)], { account });
|
|
104
157
|
});
|
|
105
158
|
|
|
106
159
|
server.registerTool('gog_classroom_roster', {
|
|
@@ -117,7 +170,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
117
170
|
account: accountParam,
|
|
118
171
|
}),
|
|
119
172
|
}, async ({ courseId, students, teachers, max, pageToken, page, all, account }) => {
|
|
120
|
-
const args = ['classroom', 'roster', courseId];
|
|
173
|
+
const args: GogArg[] = ['classroom', 'roster', pos(courseId)];
|
|
121
174
|
if (students) args.push('--students');
|
|
122
175
|
if (teachers) args.push('--teachers');
|
|
123
176
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
@@ -143,7 +196,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
143
196
|
account: accountParam,
|
|
144
197
|
}),
|
|
145
198
|
}, async ({ courseId, state, topic, orderBy, max, pageToken, page, all, scanPages, account }) => {
|
|
146
|
-
const args = ['classroom', 'coursework', 'list', courseId];
|
|
199
|
+
const args: GogArg[] = ['classroom', 'coursework', 'list', pos(courseId)];
|
|
147
200
|
if (state) args.push(`--state=${state}`);
|
|
148
201
|
if (topic) args.push(`--topic=${topic}`);
|
|
149
202
|
if (orderBy) args.push(`--order-by=${orderBy}`);
|
|
@@ -164,7 +217,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
164
217
|
account: accountParam,
|
|
165
218
|
}),
|
|
166
219
|
}, async ({ courseId, courseworkId, account }) => {
|
|
167
|
-
return runOrDiagnose(['classroom', 'coursework', 'get', courseId, courseworkId], { account });
|
|
220
|
+
return runOrDiagnose(['classroom', 'coursework', 'get', pos(courseId), pos(courseworkId)], { account });
|
|
168
221
|
});
|
|
169
222
|
|
|
170
223
|
server.registerTool('gog_classroom_submissions_list', {
|
|
@@ -183,7 +236,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
183
236
|
account: accountParam,
|
|
184
237
|
}),
|
|
185
238
|
}, async ({ courseId, courseworkId, state, late, user, max, pageToken, page, all, account }) => {
|
|
186
|
-
const args = ['classroom', 'submissions', 'list', courseId, courseworkId];
|
|
239
|
+
const args: GogArg[] = ['classroom', 'submissions', 'list', pos(courseId), pos(courseworkId)];
|
|
187
240
|
if (state) args.push(`--state=${state}`);
|
|
188
241
|
if (late) args.push(`--late=${late}`);
|
|
189
242
|
if (user) args.push(`--user=${user}`);
|
|
@@ -204,7 +257,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
204
257
|
account: accountParam,
|
|
205
258
|
}),
|
|
206
259
|
}, async ({ courseId, courseworkId, submissionId, account }) => {
|
|
207
|
-
return runOrDiagnose(['classroom', 'submissions', 'get', courseId, courseworkId, submissionId], { account });
|
|
260
|
+
return runOrDiagnose(['classroom', 'submissions', 'get', pos(courseId), pos(courseworkId), pos(submissionId)], { account });
|
|
208
261
|
});
|
|
209
262
|
|
|
210
263
|
server.registerTool('gog_classroom_submissions_grade', {
|
|
@@ -219,7 +272,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
219
272
|
account: accountParam,
|
|
220
273
|
}),
|
|
221
274
|
}, async ({ courseId, courseworkId, submissionId, draft, assigned, account }) => {
|
|
222
|
-
const args = ['classroom', 'submissions', 'grade', courseId, courseworkId, submissionId];
|
|
275
|
+
const args: GogArg[] = ['classroom', 'submissions', 'grade', pos(courseId), pos(courseworkId), pos(submissionId)];
|
|
223
276
|
if (draft) args.push(`--draft=${draft}`);
|
|
224
277
|
if (assigned) args.push(`--assigned=${assigned}`);
|
|
225
278
|
return runOrDiagnose(args, { account });
|
|
@@ -235,7 +288,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
235
288
|
account: accountParam,
|
|
236
289
|
}),
|
|
237
290
|
}, async ({ courseId, courseworkId, submissionId, account }) => {
|
|
238
|
-
return runOrDiagnose(['classroom', 'submissions', 'return', courseId, courseworkId, submissionId], { account });
|
|
291
|
+
return runOrDiagnose(['classroom', 'submissions', 'return', pos(courseId), pos(courseworkId), pos(submissionId)], { account });
|
|
239
292
|
});
|
|
240
293
|
|
|
241
294
|
server.registerTool('gog_classroom_submissions_turn_in', {
|
|
@@ -248,7 +301,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
248
301
|
account: accountParam,
|
|
249
302
|
}),
|
|
250
303
|
}, async ({ courseId, courseworkId, submissionId, account }) => {
|
|
251
|
-
return runOrDiagnose(['classroom', 'submissions', 'turn-in', courseId, courseworkId, submissionId], { account });
|
|
304
|
+
return runOrDiagnose(['classroom', 'submissions', 'turn-in', pos(courseId), pos(courseworkId), pos(submissionId)], { account });
|
|
252
305
|
});
|
|
253
306
|
|
|
254
307
|
server.registerTool('gog_classroom_submissions_reclaim', {
|
|
@@ -261,7 +314,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
261
314
|
account: accountParam,
|
|
262
315
|
}),
|
|
263
316
|
}, async ({ courseId, courseworkId, submissionId, account }) => {
|
|
264
|
-
return runOrDiagnose(['classroom', 'submissions', 'reclaim', courseId, courseworkId, submissionId], { account });
|
|
317
|
+
return runOrDiagnose(['classroom', 'submissions', 'reclaim', pos(courseId), pos(courseworkId), pos(submissionId)], { account });
|
|
265
318
|
});
|
|
266
319
|
|
|
267
320
|
server.registerTool('gog_classroom_announcements_list', {
|
|
@@ -278,7 +331,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
278
331
|
account: accountParam,
|
|
279
332
|
}),
|
|
280
333
|
}, async ({ courseId, state, orderBy, max, pageToken, page, all, account }) => {
|
|
281
|
-
const args = ['classroom', 'announcements', 'list', courseId];
|
|
334
|
+
const args: GogArg[] = ['classroom', 'announcements', 'list', pos(courseId)];
|
|
282
335
|
if (state) args.push(`--state=${state}`);
|
|
283
336
|
if (orderBy) args.push(`--order-by=${orderBy}`);
|
|
284
337
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
@@ -297,23 +350,51 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
297
350
|
account: accountParam,
|
|
298
351
|
}),
|
|
299
352
|
}, async ({ courseId, announcementId, account }) => {
|
|
300
|
-
return runOrDiagnose(['classroom', 'announcements', 'get', courseId, announcementId], { account });
|
|
353
|
+
return runOrDiagnose(['classroom', 'announcements', 'get', pos(courseId), pos(announcementId)], { account });
|
|
301
354
|
});
|
|
302
355
|
|
|
303
356
|
server.registerTool('gog_classroom_announcements_create', {
|
|
304
|
-
description: 'Create an announcement in a Google Classroom course.'
|
|
357
|
+
description: 'Create an announcement in a Google Classroom course. Unless state is DRAFT (which students cannot '
|
|
358
|
+
+ 'see), this reads the course and asks the MCP host to show the user a confirmation prompt with the class, the '
|
|
359
|
+
+ 'full text and when it publishes; nothing is posted unless they accept. To stage one without asking, pass '
|
|
360
|
+
+ 'state DRAFT.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
305
361
|
annotations: { destructiveHint: true },
|
|
306
362
|
inputSchema: z.object({
|
|
307
363
|
courseId: z.string().describe('Course ID'),
|
|
308
364
|
text: z.string().describe('Announcement text'),
|
|
309
|
-
state: z.enum(['PUBLISHED', 'DRAFT']).optional().describe('State'),
|
|
365
|
+
state: z.enum(['PUBLISHED', 'DRAFT']).optional().describe('State (DRAFT is visible only to teachers and needs no confirmation)'),
|
|
310
366
|
scheduled: z.string().optional().describe('Scheduled publish time'),
|
|
311
367
|
account: accountParam,
|
|
368
|
+
confirmToken: confirmTokenParam,
|
|
312
369
|
}),
|
|
313
|
-
}, async ({ courseId, text, state, scheduled, account }) => {
|
|
314
|
-
const args = ['classroom', 'announcements', 'create', courseId, `--text=${text}`];
|
|
370
|
+
}, async ({ courseId, text, state, scheduled, account, confirmToken }, ctx) => {
|
|
371
|
+
const args: GogArg[] = ['classroom', 'announcements', 'create', pos(courseId), `--text=${text}`];
|
|
315
372
|
if (state) args.push(`--state=${state}`);
|
|
316
373
|
if (scheduled) args.push(`--scheduled=${scheduled}`);
|
|
374
|
+
// A draft reaches nobody until a teacher publishes it: it is this tool's
|
|
375
|
+
// own staging twin, so it needs no confirmation.
|
|
376
|
+
if (state !== 'DRAFT') {
|
|
377
|
+
const read = await readCourse(courseId, account);
|
|
378
|
+
if (read.error) return read.error;
|
|
379
|
+
const publishes = scheduled ? `at ${scheduled}` : 'immediately';
|
|
380
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
381
|
+
action: 'classroom.announcement-create',
|
|
382
|
+
message: 'Review and confirm this Classroom announcement:',
|
|
383
|
+
confirmationLabel: 'Confirm that this announcement should be posted to the class.',
|
|
384
|
+
details: { course: read.course, publishes, textPreview: bodyPreview(text) },
|
|
385
|
+
unsupportedNote: 'Create it with state DRAFT instead; the user can review and post it from Classroom.',
|
|
386
|
+
fallback: {
|
|
387
|
+
tool: 'gog_classroom_announcements_create',
|
|
388
|
+
account,
|
|
389
|
+
confirmToken,
|
|
390
|
+
subject: () => {
|
|
391
|
+
const view = { course: read.course, publishes, text };
|
|
392
|
+
return { target: courseId, payload: view, preview: view };
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
if (confirmation) return confirmation;
|
|
397
|
+
}
|
|
317
398
|
return runOrDiagnose(args, { account });
|
|
318
399
|
});
|
|
319
400
|
|
|
@@ -329,7 +410,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
329
410
|
account: accountParam,
|
|
330
411
|
}),
|
|
331
412
|
}, async ({ courseId, max, pageToken, page, all, account }) => {
|
|
332
|
-
const args = ['classroom', 'topics', 'list', courseId];
|
|
413
|
+
const args: GogArg[] = ['classroom', 'topics', 'list', pos(courseId)];
|
|
333
414
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
334
415
|
const token = resolvePageToken({ pageToken, page });
|
|
335
416
|
if (token) args.push(`--page=${token}`);
|
|
@@ -346,7 +427,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
346
427
|
account: accountParam,
|
|
347
428
|
}),
|
|
348
429
|
}, async ({ courseId, topicId, account }) => {
|
|
349
|
-
return runOrDiagnose(['classroom', 'topics', 'get', courseId, topicId], { account });
|
|
430
|
+
return runOrDiagnose(['classroom', 'topics', 'get', pos(courseId), pos(topicId)], { account });
|
|
350
431
|
});
|
|
351
432
|
|
|
352
433
|
server.registerTool('gog_classroom_invitations_list', {
|
|
@@ -362,7 +443,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
362
443
|
account: accountParam,
|
|
363
444
|
}),
|
|
364
445
|
}, async ({ course, user, max, pageToken, page, all, account }) => {
|
|
365
|
-
const args = ['classroom', 'invitations', 'list'];
|
|
446
|
+
const args: GogArg[] = ['classroom', 'invitations', 'list'];
|
|
366
447
|
if (course) args.push(`--course=${course}`);
|
|
367
448
|
if (user) args.push(`--user=${user}`);
|
|
368
449
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
@@ -380,7 +461,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
380
461
|
account: accountParam,
|
|
381
462
|
}),
|
|
382
463
|
}, async ({ invitationId, account }) => {
|
|
383
|
-
return runOrDiagnose(['classroom', 'invitations', 'get', invitationId], { account });
|
|
464
|
+
return runOrDiagnose(['classroom', 'invitations', 'get', pos(invitationId)], { account });
|
|
384
465
|
});
|
|
385
466
|
|
|
386
467
|
server.registerTool('gog_classroom_invitations_accept', {
|
|
@@ -391,7 +472,7 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
391
472
|
account: accountParam,
|
|
392
473
|
}),
|
|
393
474
|
}, async ({ invitationId, account }) => {
|
|
394
|
-
return runOrDiagnose(['classroom', 'invitations', 'accept', invitationId], { account });
|
|
475
|
+
return runOrDiagnose(['classroom', 'invitations', 'accept', pos(invitationId)], { account });
|
|
395
476
|
});
|
|
396
477
|
|
|
397
478
|
server.registerTool('gog_classroom_profile_get', {
|
|
@@ -402,14 +483,15 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
402
483
|
account: accountParam,
|
|
403
484
|
}),
|
|
404
485
|
}, async ({ userId, account }) => {
|
|
405
|
-
const args = ['classroom', 'profile', 'get'];
|
|
406
|
-
if (userId) args.push(userId);
|
|
486
|
+
const args: GogArg[] = ['classroom', 'profile', 'get'];
|
|
487
|
+
if (userId) args.push(pos(userId));
|
|
407
488
|
return runOrDiagnose(args, { account });
|
|
408
489
|
});
|
|
409
490
|
|
|
410
491
|
registerRunTool(server, {
|
|
411
492
|
service: 'classroom',
|
|
412
493
|
examples: '"guardians", "materials", "guardian-invitations"',
|
|
494
|
+
vet: vetClassroomRun,
|
|
413
495
|
note: 'Covers anything not wrapped by the dedicated tools (guardians, guardian-invitations, materials, coursework assignees, announcement assignees, etc.).',
|
|
414
496
|
});
|
|
415
497
|
}
|
package/src/tools/contacts.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 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"' });
|
package/src/tools/drive.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
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';
|
|
11
|
+
import { CONFIRM_FALLBACK_DESCRIPTION, confirmTokenParam, gatedElsewhere, requireDispatchConfirmation, resultText } from '../dispatch-confirmation.js';
|
|
12
|
+
|
|
13
|
+
/** gog_drive_run must not grant access that gog_drive_share would ask about. */
|
|
14
|
+
export function vetDriveRun(subcommand: string, _args: readonly string[]): string | undefined {
|
|
15
|
+
return subcommand.toLowerCase() === 'share'
|
|
16
|
+
? gatedElsewhere('gog drive share', 'gog_drive_run', 'grants access to a file', 'gog_drive_share')
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
8
19
|
|
|
9
20
|
// A native Google Doc exports to text directly; anything else (PDF, image,
|
|
10
21
|
// docx, …) is first copied WITH conversion to this type, which makes Drive run
|
|
@@ -13,12 +24,31 @@ const GOOGLE_DOC_MIME = 'application/vnd.google-apps.document';
|
|
|
13
24
|
|
|
14
25
|
// Parse `gog drive get` JSON into { name, mimeType }. gog nests the payload
|
|
15
26
|
// under `file`; fall back to the top level if that ever changes.
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
type DriveMeta = { name?: string; mimeType?: string; size?: string | number };
|
|
28
|
+
|
|
29
|
+
/** fileMeta for a preview: unreadable output names nothing rather than throwing. */
|
|
30
|
+
export function shareTargetMeta(raw: string): { name?: string; mimeType?: string } {
|
|
31
|
+
try {
|
|
32
|
+
const { name, mimeType } = fileMeta(raw);
|
|
33
|
+
return { name, mimeType };
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fileMeta(raw: string): { name?: string; mimeType?: string; size?: number } {
|
|
40
|
+
const parsed = JSON.parse(raw) as { file?: DriveMeta } & DriveMeta;
|
|
18
41
|
const f = parsed.file ?? parsed;
|
|
19
|
-
|
|
42
|
+
// Drive reports int64 fields as strings; native Google formats carry none.
|
|
43
|
+
const size = f.size === undefined ? undefined : Number(f.size);
|
|
44
|
+
return { name: f.name, mimeType: f.mimeType, size: Number.isFinite(size) ? size : undefined };
|
|
20
45
|
}
|
|
21
46
|
|
|
47
|
+
// The most gog_drive_read_bytes returns inline — the same 8 MiB per-file cap
|
|
48
|
+
// the attachment path enforces. The bytes travel base64-encoded (4/3 larger)
|
|
49
|
+
// inside one JSON-RPC message, and past this no MCP client accepts the result.
|
|
50
|
+
export const MAX_DRIVE_READ_BYTES = MAX_INLINE_ATTACHMENT_BYTES;
|
|
51
|
+
|
|
22
52
|
|
|
23
53
|
// The `compact` rung for gog_drive_ls, as a Google Drive field mask.
|
|
24
54
|
//
|
|
@@ -52,7 +82,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
52
82
|
account: accountParam,
|
|
53
83
|
}),
|
|
54
84
|
}, async ({ folderId, max, pageToken, page, query, allDrives, view, account }) => {
|
|
55
|
-
const args = ['drive', 'ls'];
|
|
85
|
+
const args: GogArg[] = ['drive', 'ls'];
|
|
56
86
|
if (folderId) args.push(`--parent=${folderId}`);
|
|
57
87
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
58
88
|
const token = resolvePageToken({ pageToken, page });
|
|
@@ -78,7 +108,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
78
108
|
account: accountParam,
|
|
79
109
|
}),
|
|
80
110
|
}, async ({ query, view, account }) => {
|
|
81
|
-
return runOrDiagnose(['drive', 'search', query], {
|
|
111
|
+
return runOrDiagnose(['drive', 'search', pos(query)], {
|
|
82
112
|
account,
|
|
83
113
|
stripMedia: resolveView(view, ['compact', 'full']) === 'compact',
|
|
84
114
|
});
|
|
@@ -99,7 +129,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
99
129
|
account: accountParam,
|
|
100
130
|
}),
|
|
101
131
|
}, async ({ fileId, view, account }) => {
|
|
102
|
-
return runOrDiagnose(['drive', 'get', fileId], {
|
|
132
|
+
return runOrDiagnose(['drive', 'get', pos(fileId)], {
|
|
103
133
|
account,
|
|
104
134
|
stripMedia: resolveView(view, ['compact', 'full']) === 'compact',
|
|
105
135
|
});
|
|
@@ -113,7 +143,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
113
143
|
account: accountParam,
|
|
114
144
|
}),
|
|
115
145
|
}, async ({ name, account }) => {
|
|
116
|
-
return runOrDiagnose(['drive', 'mkdir', name], { account });
|
|
146
|
+
return runOrDiagnose(['drive', 'mkdir', pos(name)], { account });
|
|
117
147
|
});
|
|
118
148
|
|
|
119
149
|
server.registerTool('gog_drive_rename', {
|
|
@@ -125,7 +155,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
125
155
|
account: accountParam,
|
|
126
156
|
}),
|
|
127
157
|
}, async ({ fileId, newName, account }) => {
|
|
128
|
-
return runOrDiagnose(['drive', 'rename', fileId, newName], { account });
|
|
158
|
+
return runOrDiagnose(['drive', 'rename', pos(fileId), pos(newName)], { account });
|
|
129
159
|
});
|
|
130
160
|
|
|
131
161
|
server.registerTool('gog_drive_move', {
|
|
@@ -137,7 +167,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
137
167
|
account: accountParam,
|
|
138
168
|
}),
|
|
139
169
|
}, async ({ fileId, parentId, account }) => {
|
|
140
|
-
return runOrDiagnose(['drive', 'move', fileId, `--parent=${parentId}`], { account });
|
|
170
|
+
return runOrDiagnose(['drive', 'move', pos(fileId), `--parent=${parentId}`], { account });
|
|
141
171
|
});
|
|
142
172
|
|
|
143
173
|
server.registerTool('gog_drive_delete', {
|
|
@@ -149,7 +179,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
149
179
|
account: accountParam,
|
|
150
180
|
}),
|
|
151
181
|
}, async ({ fileId, permanent, account }) => {
|
|
152
|
-
const args = ['drive', 'delete', fileId];
|
|
182
|
+
const args: GogArg[] = ['drive', 'delete', pos(fileId)];
|
|
153
183
|
if (permanent) args.push('--permanent');
|
|
154
184
|
// gog gates drive delete behind a confirmation; the runner injects
|
|
155
185
|
// --no-input, so without --force it refuses at runtime.
|
|
@@ -158,7 +188,10 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
158
188
|
});
|
|
159
189
|
|
|
160
190
|
server.registerTool('gog_drive_share', {
|
|
161
|
-
description: 'Share a Google Drive file or folder.'
|
|
191
|
+
description: 'Share a Google Drive file or folder. Granting access is the risk even though no email is sent (gog '
|
|
192
|
+
+ 'does not notify by default), so this reads the file and asks the MCP host to show the user a confirmation '
|
|
193
|
+
+ 'prompt naming it, who gets access and with what role; nothing is shared unless they accept.'
|
|
194
|
+
+ CONFIRM_FALLBACK_DESCRIPTION,
|
|
162
195
|
annotations: { destructiveHint: true },
|
|
163
196
|
inputSchema: z.object({
|
|
164
197
|
fileId: z.string().describe('File or folder ID'),
|
|
@@ -167,9 +200,40 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
167
200
|
domain: z.string().optional().describe('Domain (required when to=domain)'),
|
|
168
201
|
role: z.enum(['reader', 'writer']).optional().describe('Permission role (default: reader)'),
|
|
169
202
|
account: accountParam,
|
|
203
|
+
confirmToken: confirmTokenParam,
|
|
170
204
|
}),
|
|
171
|
-
}, async ({ fileId, to, email, domain, role, account }) => {
|
|
172
|
-
|
|
205
|
+
}, async ({ fileId, to, email, domain, role, account, confirmToken }, ctx) => {
|
|
206
|
+
// Read first, on every call: a prompt that shows only an opaque id asks the
|
|
207
|
+
// user to approve something they cannot recognise, and on the token
|
|
208
|
+
// fallback's phase 2 this is the re-read the token is checked against.
|
|
209
|
+
const got = await runOrDiagnose(['drive', 'get', pos(fileId)], { account });
|
|
210
|
+
if (got.isError) return got;
|
|
211
|
+
const file = { id: fileId, ...shareTargetMeta(resultText(got)) };
|
|
212
|
+
const grant = {
|
|
213
|
+
file,
|
|
214
|
+
to,
|
|
215
|
+
...(email ? { email } : {}),
|
|
216
|
+
...(domain ? { domain } : {}),
|
|
217
|
+
role: role ?? 'reader',
|
|
218
|
+
publicLink: to === 'anyone',
|
|
219
|
+
};
|
|
220
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
221
|
+
action: 'drive.share',
|
|
222
|
+
message: to === 'anyone'
|
|
223
|
+
? 'Review and confirm making this file available to ANYONE with the link:'
|
|
224
|
+
: 'Review and confirm this Drive share:',
|
|
225
|
+
confirmationLabel: 'Confirm that this access should be granted now.',
|
|
226
|
+
details: grant,
|
|
227
|
+
unsupportedNote: 'Ask the user to share it themselves from Google Drive.',
|
|
228
|
+
fallback: {
|
|
229
|
+
tool: 'gog_drive_share',
|
|
230
|
+
account,
|
|
231
|
+
confirmToken,
|
|
232
|
+
subject: () => ({ target: fileId, payload: grant, preview: grant }),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
if (confirmation) return confirmation;
|
|
236
|
+
const args: GogArg[] = ['drive', 'share', pos(fileId), `--to=${to}`];
|
|
173
237
|
if (email) args.push(`--email=${email}`);
|
|
174
238
|
if (domain) args.push(`--domain=${domain}`);
|
|
175
239
|
if (role) args.push(`--role=${role}`);
|
|
@@ -201,7 +265,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
201
265
|
}, async ({ fileId, ocrLanguage, offset = 0, maxChars, account }) => {
|
|
202
266
|
let tempDocId: string | undefined;
|
|
203
267
|
try {
|
|
204
|
-
const { name, mimeType } = fileMeta(await run(['drive', 'get', fileId], { account }));
|
|
268
|
+
const { name, mimeType } = fileMeta(await run(['drive', 'get', pos(fileId)], { account }));
|
|
205
269
|
|
|
206
270
|
// Native Docs export straight to text; everything else is OCR-converted first.
|
|
207
271
|
let sourceId = fileId;
|
|
@@ -242,7 +306,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
242
306
|
} finally {
|
|
243
307
|
// Always remove the temp Doc — on success, and on a mid-extraction failure.
|
|
244
308
|
if (tempDocId) {
|
|
245
|
-
await run(['drive', 'delete', tempDocId, '--permanent', '--force'], { account }).catch(() => {});
|
|
309
|
+
await run(['drive', 'delete', pos(tempDocId), '--permanent', '--force'], { account }).catch(() => {});
|
|
246
310
|
}
|
|
247
311
|
}
|
|
248
312
|
});
|
|
@@ -251,7 +315,7 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
251
315
|
description:
|
|
252
316
|
'Fetch a Drive file\'s raw bytes and return them base64-encoded as an embedded resource — the ' +
|
|
253
317
|
'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.',
|
|
318
|
+
'text. Files over 8 MiB are refused (checked before downloading). For readable text from a PDF, prefer gog_drive_extract_text.',
|
|
255
319
|
annotations: { readOnlyHint: true },
|
|
256
320
|
inputSchema: z.object({
|
|
257
321
|
fileId: z.string().describe('Drive file ID'),
|
|
@@ -259,9 +323,21 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
259
323
|
}),
|
|
260
324
|
}, async ({ fileId, account }): Promise<CallToolResult> => {
|
|
261
325
|
try {
|
|
262
|
-
const { name, mimeType } = fileMeta(await run(['drive', 'get', fileId], { account }));
|
|
326
|
+
const { name, mimeType, size } = fileMeta(await run(['drive', 'get', pos(fileId)], { account }));
|
|
327
|
+
// Refuse an oversized file BEFORE fetching it (audit BUG-2): buffering a
|
|
328
|
+
// multi-GB file plus its base64 copy can take down the whole server.
|
|
329
|
+
if (size !== undefined && size > MAX_DRIVE_READ_BYTES) {
|
|
330
|
+
return errorResult(
|
|
331
|
+
`${name ?? fileId} is ${size} bytes, over the 8 MiB this tool returns inline. `
|
|
332
|
+
+ 'Use gog_drive_extract_text for its text, or share its webViewLink (gog_drive_get) instead.',
|
|
333
|
+
);
|
|
334
|
+
}
|
|
263
335
|
const params = JSON.stringify({ fileId, alt: 'media' });
|
|
264
|
-
|
|
336
|
+
// The cap also bounds the fetch itself, for files Drive reports no size for.
|
|
337
|
+
const blob = await runBinary(
|
|
338
|
+
['api', 'call', 'drive', 'v3', 'files.get', `--params=${params}`],
|
|
339
|
+
{ account, maxOutputBytes: MAX_DRIVE_READ_BYTES },
|
|
340
|
+
);
|
|
265
341
|
const type = mimeType ?? 'application/octet-stream';
|
|
266
342
|
// Several hosts (claude.ai among them) render embedded IMAGE resources
|
|
267
343
|
// and reject every other type outright — "Resources of type
|
|
@@ -290,5 +366,5 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
290
366
|
}
|
|
291
367
|
});
|
|
292
368
|
|
|
293
|
-
registerRunTool(server, { service: 'drive', examples: '"copy", "
|
|
369
|
+
registerRunTool(server, { service: 'drive', examples: '"copy", "download", "permissions"', vet: vetDriveRun });
|
|
294
370
|
}
|