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/calendar.ts
CHANGED
|
@@ -3,6 +3,25 @@ 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';
|
|
5
5
|
import { annotateTruncatedList } from '../pagination.js';
|
|
6
|
+
import { pos } from '../argv.js';
|
|
7
|
+
import type { GogArg } from '../runner.js';
|
|
8
|
+
import { CONFIRM_FALLBACK_DESCRIPTION, confirmTokenParam, gatedElsewhere, requireDispatchConfirmation, resultText } from '../dispatch-confirmation.js';
|
|
9
|
+
|
|
10
|
+
// gog's spellings (internal/cmd/calendar.go). The run tool cannot tell whether
|
|
11
|
+
// an event has guests, so it refuses these outright; the dedicated tools ask
|
|
12
|
+
// only when someone else would see the change.
|
|
13
|
+
const CALENDAR_GATED: Record<string, string> = {
|
|
14
|
+
create: 'gog_calendar_create', add: 'gog_calendar_create', new: 'gog_calendar_create',
|
|
15
|
+
update: 'gog_calendar_update', edit: 'gog_calendar_update', set: 'gog_calendar_update',
|
|
16
|
+
respond: 'gog_calendar_respond', rsvp: 'gog_calendar_respond', reply: 'gog_calendar_respond',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** gog_calendar_run must not make the changes gog_calendar_create/update/respond would ask about. */
|
|
20
|
+
export function vetCalendarRun(subcommand: string, _args: readonly string[]): string | undefined {
|
|
21
|
+
const sub = subcommand.toLowerCase();
|
|
22
|
+
const tool = Object.hasOwn(CALENDAR_GATED, sub) ? CALENDAR_GATED[sub] : undefined;
|
|
23
|
+
return tool ? gatedElsewhere(`gog calendar ${sub}`, 'gog_calendar_run', 'can change what guests see', tool) : undefined;
|
|
24
|
+
}
|
|
6
25
|
|
|
7
26
|
// Reminder params, shared by create and update (gog >= 0.38.0 for
|
|
8
27
|
// --no-reminders). An event's reminders are one of THREE states, and the two
|
|
@@ -18,6 +37,72 @@ import { annotateTruncatedList } from '../pagination.js';
|
|
|
18
37
|
// the flag (leave whatever the event already has) and from --no-reminders
|
|
19
38
|
// (override the calendar with silence). An empty array is how a JSON caller
|
|
20
39
|
// says it, since there is no way to send a bare flag with no value.
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// GUEST-VISIBLE CHANGES go through the dispatch rail. gog sends no invitation
|
|
42
|
+
// email here (its --send-updates defaults to none and these tools never pass
|
|
43
|
+
// it), but a guest still sees the event appear, move or change on their own
|
|
44
|
+
// calendar, and an organizer sees a response. A change nobody else can see — a
|
|
45
|
+
// guest-free event, or a reminder, which is per-user — is not gated.
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/** The emails in an attendees flag value (`a@x.com;optional,b@y.com;resource`), resources excluded. */
|
|
49
|
+
export function parseAttendees(value: string | undefined): string[] {
|
|
50
|
+
if (!value) return [];
|
|
51
|
+
return value.split(',')
|
|
52
|
+
.map((entry) => entry.split(';'))
|
|
53
|
+
.filter((parts) => !parts.slice(1).some((m) => m.trim().toLowerCase() === 'resource'))
|
|
54
|
+
.map((parts) => parts[0]!.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type EventAttendee = { email?: unknown; self?: unknown; resource?: unknown };
|
|
59
|
+
type EventTime = { dateTime?: unknown; date?: unknown };
|
|
60
|
+
|
|
61
|
+
function timeOf(t: EventTime | undefined): string | undefined {
|
|
62
|
+
const v = t?.dateTime ?? t?.date;
|
|
63
|
+
return typeof v === 'string' ? v : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What a calendar dispatch's prompt shows about the event as it stands now. */
|
|
67
|
+
export function eventSnapshot(raw: string): {
|
|
68
|
+
summary?: string;
|
|
69
|
+
start?: string;
|
|
70
|
+
end?: string;
|
|
71
|
+
organizer?: string;
|
|
72
|
+
guests: string[];
|
|
73
|
+
etag?: string;
|
|
74
|
+
} {
|
|
75
|
+
let event: {
|
|
76
|
+
summary?: unknown; start?: EventTime; end?: EventTime; etag?: unknown;
|
|
77
|
+
organizer?: { email?: unknown }; attendees?: EventAttendee[];
|
|
78
|
+
} | undefined;
|
|
79
|
+
try {
|
|
80
|
+
event = (JSON.parse(raw) as { event?: typeof event } | null)?.event;
|
|
81
|
+
} catch {
|
|
82
|
+
event = undefined;
|
|
83
|
+
}
|
|
84
|
+
const str = (v: unknown) => (typeof v === 'string' ? v : undefined);
|
|
85
|
+
return {
|
|
86
|
+
summary: str(event?.summary),
|
|
87
|
+
start: timeOf(event?.start),
|
|
88
|
+
end: timeOf(event?.end),
|
|
89
|
+
organizer: str(event?.organizer?.email),
|
|
90
|
+
// Guests are the OTHER people: not this account's own attendee row, not rooms.
|
|
91
|
+
guests: (Array.isArray(event?.attendees) ? event.attendees : [])
|
|
92
|
+
.filter((a) => a.self !== true && a.resource !== true && typeof a.email === 'string')
|
|
93
|
+
.map((a) => a.email as string),
|
|
94
|
+
etag: str(event?.etag),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readEvent(calendarId: string, eventId: string, account: string | undefined) {
|
|
99
|
+
const got = await runOrDiagnose(['calendar', 'event', pos(calendarId), pos(eventId)], { account });
|
|
100
|
+
if (got.isError) return { error: got };
|
|
101
|
+
return { event: eventSnapshot(resultText(got)) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const CALENDAR_CONFIRM_LABEL = 'Confirm that this change, which other people will see, should be made now.';
|
|
105
|
+
|
|
21
106
|
const reminderParams = {
|
|
22
107
|
reminders: z.array(z.string()).max(5).optional().describe(
|
|
23
108
|
'Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d '
|
|
@@ -33,7 +118,7 @@ const reminderParams = {
|
|
|
33
118
|
// The one place the three states become argv. Kept together so create and
|
|
34
119
|
// update cannot drift apart on the empty-array case.
|
|
35
120
|
function pushReminderFlags(
|
|
36
|
-
args:
|
|
121
|
+
args: GogArg[],
|
|
37
122
|
p: { reminders?: string[]; noReminders?: boolean },
|
|
38
123
|
): void {
|
|
39
124
|
if (p.noReminders) {
|
|
@@ -107,8 +192,8 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
107
192
|
account: accountParam,
|
|
108
193
|
}),
|
|
109
194
|
}, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, view, account }) => {
|
|
110
|
-
const args = ['calendar', 'events'];
|
|
111
|
-
if (calendarId) args.push(calendarId);
|
|
195
|
+
const args: GogArg[] = ['calendar', 'events'];
|
|
196
|
+
if (calendarId) args.push(pos(calendarId));
|
|
112
197
|
if (from) args.push(`--from=${from}`);
|
|
113
198
|
if (to) args.push(`--to=${to}`);
|
|
114
199
|
if (days !== undefined) args.push(`--days=${days}`);
|
|
@@ -141,13 +226,16 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
141
226
|
account: accountParam,
|
|
142
227
|
}),
|
|
143
228
|
}, async ({ calendarId, eventId, timezone, account }) => {
|
|
144
|
-
const args = ['calendar', 'event', calendarId, eventId];
|
|
229
|
+
const args: GogArg[] = ['calendar', 'event', pos(calendarId), pos(eventId)];
|
|
145
230
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
146
231
|
return runOrDiagnose(args, { account });
|
|
147
232
|
});
|
|
148
233
|
|
|
149
234
|
server.registerTool('gog_calendar_create', {
|
|
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).'
|
|
235
|
+
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). '
|
|
236
|
+
+ 'With attendees, the event lands on other people\'s calendars (no invitation email is sent), so the MCP host is '
|
|
237
|
+
+ 'asked to show the user a confirmation prompt with the guests, time and details first; a guest-free event is '
|
|
238
|
+
+ 'created without asking.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
151
239
|
annotations: { destructiveHint: false },
|
|
152
240
|
inputSchema: z.object({
|
|
153
241
|
calendarId: z.string().describe('Calendar ID (use "primary" for the default calendar)'),
|
|
@@ -162,9 +250,10 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
162
250
|
withZoom: z.boolean().optional().describe('Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)'),
|
|
163
251
|
...reminderParams,
|
|
164
252
|
account: accountParam,
|
|
253
|
+
confirmToken: confirmTokenParam,
|
|
165
254
|
}),
|
|
166
|
-
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
|
|
167
|
-
const args = ['calendar', 'create', calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
|
|
255
|
+
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account, confirmToken }, ctx) => {
|
|
256
|
+
const args: GogArg[] = ['calendar', 'create', pos(calendarId), `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
|
|
168
257
|
if (description) args.push(`--description=${description}`);
|
|
169
258
|
if (location) args.push(`--location=${location}`);
|
|
170
259
|
if (attendees) args.push(`--attendees=${attendees}`);
|
|
@@ -172,11 +261,33 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
172
261
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
173
262
|
if (withZoom) args.push('--with-zoom');
|
|
174
263
|
pushReminderFlags(args, { reminders, noReminders });
|
|
264
|
+
const guests = parseAttendees(attendees);
|
|
265
|
+
if (guests.length > 0) {
|
|
266
|
+
const event = { calendarId, summary, from, to, allDay: Boolean(allDay), timezone, location, description, guests, attendees, withZoom: Boolean(withZoom) };
|
|
267
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
268
|
+
action: 'calendar.create',
|
|
269
|
+
message: 'Review and confirm this event, which will appear on the guests\' calendars:',
|
|
270
|
+
confirmationLabel: CALENDAR_CONFIRM_LABEL,
|
|
271
|
+
details: event,
|
|
272
|
+
unsupportedNote: 'Create it without attendees instead; the user can add the guests in Google Calendar.',
|
|
273
|
+
fallback: {
|
|
274
|
+
tool: 'gog_calendar_create',
|
|
275
|
+
account,
|
|
276
|
+
confirmToken,
|
|
277
|
+
subject: () => ({ target: calendarId, payload: event, preview: event }),
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
if (confirmation) return confirmation;
|
|
281
|
+
}
|
|
175
282
|
return runOrDiagnose(args, { account });
|
|
176
283
|
});
|
|
177
284
|
|
|
178
285
|
server.registerTool('gog_calendar_update', {
|
|
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.'
|
|
286
|
+
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. '
|
|
287
|
+
+ 'A change guests can see (time, title, place, description, attendees, attachments, conferencing) on an event '
|
|
288
|
+
+ 'that has guests — or gains them — reads the event and asks the MCP host to show the user a confirmation prompt '
|
|
289
|
+
+ 'with the event as it stands and the change; no invitation email is sent. Reminder-only changes and guest-free '
|
|
290
|
+
+ 'events are updated without asking.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
180
291
|
annotations: { destructiveHint: false },
|
|
181
292
|
inputSchema: z.object({
|
|
182
293
|
calendarId: z.string().describe('Calendar ID'),
|
|
@@ -195,9 +306,10 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
195
306
|
removeMeet: z.boolean().optional().describe('Remove the event\'s Google Meet video conference (clears conference data only)'),
|
|
196
307
|
...reminderParams,
|
|
197
308
|
account: accountParam,
|
|
309
|
+
confirmToken: confirmTokenParam,
|
|
198
310
|
}),
|
|
199
|
-
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
|
|
200
|
-
const args = ['calendar', 'update', calendarId, eventId];
|
|
311
|
+
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account, confirmToken }, ctx) => {
|
|
312
|
+
const args: GogArg[] = ['calendar', 'update', pos(calendarId), pos(eventId)];
|
|
201
313
|
if (summary !== undefined) args.push(`--summary=${summary}`);
|
|
202
314
|
if (from !== undefined) args.push(`--from=${from}`);
|
|
203
315
|
if (to !== undefined) args.push(`--to=${to}`);
|
|
@@ -211,6 +323,36 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
211
323
|
if (removeZoom) args.push('--remove-zoom');
|
|
212
324
|
if (removeMeet) args.push('--remove-meet');
|
|
213
325
|
pushReminderFlags(args, { reminders, noReminders });
|
|
326
|
+
// Every field guests can see; reminders are per-user and are not on it.
|
|
327
|
+
const changes = Object.fromEntries(Object.entries({
|
|
328
|
+
summary, from, to, description, location, attendees, addAttendees, attachments,
|
|
329
|
+
withZoom, regenerateZoom, removeZoom, removeMeet,
|
|
330
|
+
}).filter(([, v]) => v !== undefined && v !== false));
|
|
331
|
+
if (Object.keys(changes).length > 0) {
|
|
332
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
333
|
+
if (read.error) return read.error;
|
|
334
|
+
const { etag, ...current } = read.event;
|
|
335
|
+
const added = [...parseAttendees(attendees), ...parseAttendees(addAttendees)];
|
|
336
|
+
if (current.guests.length > 0 || added.length > 0) {
|
|
337
|
+
const view = { calendarId, eventId, current, changes };
|
|
338
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
339
|
+
action: 'calendar.update',
|
|
340
|
+
message: 'Review and confirm this change to an event other people are on:',
|
|
341
|
+
confirmationLabel: CALENDAR_CONFIRM_LABEL,
|
|
342
|
+
details: view,
|
|
343
|
+
unsupportedNote: 'Ask the user to make this change in Google Calendar.',
|
|
344
|
+
fallback: {
|
|
345
|
+
tool: 'gog_calendar_update',
|
|
346
|
+
account,
|
|
347
|
+
confirmToken,
|
|
348
|
+
// The etag rotates on any edit, so an event changed elsewhere between
|
|
349
|
+
// the two phases is DRAFT_CHANGED rather than edited over.
|
|
350
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view }),
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
if (confirmation) return confirmation;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
214
356
|
return runOrDiagnose(args, { account });
|
|
215
357
|
});
|
|
216
358
|
|
|
@@ -225,11 +367,13 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
225
367
|
}, async ({ calendarId, eventId, account }) => {
|
|
226
368
|
// gog gates this delete behind a confirmation; the runner injects
|
|
227
369
|
// --no-input, so without --force it refuses at runtime.
|
|
228
|
-
return runOrDiagnose(['calendar', 'delete', calendarId, eventId, '--force'], { account });
|
|
370
|
+
return runOrDiagnose(['calendar', 'delete', pos(calendarId), pos(eventId), '--force'], { account });
|
|
229
371
|
});
|
|
230
372
|
|
|
231
373
|
server.registerTool('gog_calendar_respond', {
|
|
232
|
-
description: 'Respond to a calendar event invitation.'
|
|
374
|
+
description: 'Respond to a calendar event invitation. The organizer sees the response, so this reads the event and '
|
|
375
|
+
+ 'asks the MCP host to show the user a confirmation prompt with the event, organizer, response and comment; '
|
|
376
|
+
+ 'nothing is recorded unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
233
377
|
annotations: { destructiveHint: true },
|
|
234
378
|
inputSchema: z.object({
|
|
235
379
|
calendarId: z.string().describe('Calendar ID'),
|
|
@@ -237,12 +381,31 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
237
381
|
status: z.enum(['accepted', 'declined', 'tentative']).describe('Response status'),
|
|
238
382
|
comment: z.string().optional().describe('Optional comment to include with response'),
|
|
239
383
|
account: accountParam,
|
|
384
|
+
confirmToken: confirmTokenParam,
|
|
240
385
|
}),
|
|
241
|
-
}, async ({ calendarId, eventId, status, comment, account }) => {
|
|
242
|
-
const args = ['calendar', 'respond', calendarId, eventId, `--status=${status}`];
|
|
386
|
+
}, async ({ calendarId, eventId, status, comment, account, confirmToken }, ctx) => {
|
|
387
|
+
const args: GogArg[] = ['calendar', 'respond', pos(calendarId), pos(eventId), `--status=${status}`];
|
|
243
388
|
if (comment) args.push(`--comment=${comment}`);
|
|
389
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
390
|
+
if (read.error) return read.error;
|
|
391
|
+
const { etag, ...event } = read.event;
|
|
392
|
+
const view = { calendarId, eventId, event, response: status, ...(comment ? { comment } : {}) };
|
|
393
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
394
|
+
action: 'calendar.respond',
|
|
395
|
+
message: 'Review and confirm this response, which the organizer will see:',
|
|
396
|
+
confirmationLabel: 'Confirm that this response should be recorded now.',
|
|
397
|
+
details: view,
|
|
398
|
+
unsupportedNote: 'Ask the user to respond from Google Calendar.',
|
|
399
|
+
fallback: {
|
|
400
|
+
tool: 'gog_calendar_respond',
|
|
401
|
+
account,
|
|
402
|
+
confirmToken,
|
|
403
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view }),
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
if (confirmation) return confirmation;
|
|
244
407
|
return runOrDiagnose(args, { account });
|
|
245
408
|
});
|
|
246
409
|
|
|
247
|
-
registerRunTool(server, { service: 'calendar', examples: '"calendars", "freebusy"' });
|
|
410
|
+
registerRunTool(server, { service: 'calendar', examples: '"calendars", "freebusy"', vet: vetCalendarRun });
|
|
248
411
|
}
|
package/src/tools/chat.ts
CHANGED
|
@@ -9,6 +9,40 @@ 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';
|
|
14
|
+
import {
|
|
15
|
+
attachmentDetails,
|
|
16
|
+
attachmentNames,
|
|
17
|
+
attachmentPreview,
|
|
18
|
+
bodyPreview,
|
|
19
|
+
CONFIRM_FALLBACK_DESCRIPTION,
|
|
20
|
+
confirmTokenParam,
|
|
21
|
+
gatedElsewhere,
|
|
22
|
+
hasCommandWord,
|
|
23
|
+
requireDispatchConfirmation,
|
|
24
|
+
senderPreview,
|
|
25
|
+
} from '../dispatch-confirmation.js';
|
|
26
|
+
|
|
27
|
+
// gog's spellings of `send` under `messages` and `dm` (internal/cmd/chat_messages.go, chat_dm.go).
|
|
28
|
+
const CHAT_SEND_WORDS = new Set(['send', 'create', 'post']);
|
|
29
|
+
|
|
30
|
+
/** gog_chat_run must not post what gog_chat_messages_send / gog_chat_dm_send would ask about. */
|
|
31
|
+
export function vetChatRun(subcommand: string, args: readonly string[]): string | undefined {
|
|
32
|
+
const sub = subcommand.toLowerCase();
|
|
33
|
+
if (sub !== 'messages' && sub !== 'dm') return undefined;
|
|
34
|
+
const word = hasCommandWord(args, CHAT_SEND_WORDS);
|
|
35
|
+
if (!word) return undefined;
|
|
36
|
+
return gatedElsewhere(`gog chat ${sub} ${word.toLowerCase()}`, 'gog_chat_run', 'posts a message',
|
|
37
|
+
sub === 'dm' ? 'gog_chat_dm_send' : 'gog_chat_messages_send');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A Chat post lands in other people's view the moment it is sent and cannot be
|
|
41
|
+
// unsent here, so both send tools go through the dispatch rail like Gmail does.
|
|
42
|
+
const CHAT_CONFIRM = {
|
|
43
|
+
message: 'Review and confirm this Google Chat message:',
|
|
44
|
+
confirmationLabel: 'Confirm that this Chat message should be posted now.',
|
|
45
|
+
} as const;
|
|
12
46
|
|
|
13
47
|
// Google Chat (gog >= 0.38.0 for the mention/reaction metadata in
|
|
14
48
|
// `messages list`, >= 0.39.0 for `messages search`; the rest is older).
|
|
@@ -54,7 +88,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
54
88
|
account: accountParam,
|
|
55
89
|
}),
|
|
56
90
|
}, async ({ max, pageToken, page, all, account }) => {
|
|
57
|
-
const args = ['chat', 'spaces', 'list'];
|
|
91
|
+
const args: GogArg[] = ['chat', 'spaces', 'list'];
|
|
58
92
|
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
59
93
|
return runOrDiagnose(args, { account });
|
|
60
94
|
});
|
|
@@ -72,7 +106,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
72
106
|
account: accountParam,
|
|
73
107
|
}),
|
|
74
108
|
}, async ({ displayName, exact, max, account }) => {
|
|
75
|
-
const args = ['chat', 'spaces', 'find', displayName];
|
|
109
|
+
const args: GogArg[] = ['chat', 'spaces', 'find', pos(displayName)];
|
|
76
110
|
if (exact) args.push('--exact');
|
|
77
111
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
78
112
|
return runOrDiagnose(args, { account });
|
|
@@ -89,7 +123,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
89
123
|
account: accountParam,
|
|
90
124
|
}),
|
|
91
125
|
}, async ({ displayName, members, account }) => {
|
|
92
|
-
const args = ['chat', 'spaces', 'create', displayName];
|
|
126
|
+
const args: GogArg[] = ['chat', 'spaces', 'create', pos(displayName)];
|
|
93
127
|
if (members) for (const member of members) args.push(`--member=${member}`);
|
|
94
128
|
return runOrDiagnose(args, { account });
|
|
95
129
|
});
|
|
@@ -105,7 +139,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
105
139
|
account: accountParam,
|
|
106
140
|
}),
|
|
107
141
|
}, async ({ space, max, pageToken, page, all, account }) => {
|
|
108
|
-
const args = ['chat', 'threads', 'list', space];
|
|
142
|
+
const args: GogArg[] = ['chat', 'threads', 'list', pos(space)];
|
|
109
143
|
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
110
144
|
return runOrDiagnose(args, { account });
|
|
111
145
|
});
|
|
@@ -128,7 +162,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
128
162
|
account: accountParam,
|
|
129
163
|
}),
|
|
130
164
|
}, async ({ space, thread, unread, order, max, pageToken, page, all, account }) => {
|
|
131
|
-
const args = ['chat', 'messages', 'list', space];
|
|
165
|
+
const args: GogArg[] = ['chat', 'messages', 'list', pos(space)];
|
|
132
166
|
if (thread) args.push(`--thread=${thread}`);
|
|
133
167
|
if (unread) args.push('--unread');
|
|
134
168
|
if (order) args.push(`--order=${order}`);
|
|
@@ -163,7 +197,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
163
197
|
account: accountParam,
|
|
164
198
|
}),
|
|
165
199
|
}, async ({ query, order, view, markup, max, pageToken, page, all, account }) => {
|
|
166
|
-
const args = ['chat', 'messages', 'search', query];
|
|
200
|
+
const args: GogArg[] = ['chat', 'messages', 'search', pos(query)];
|
|
167
201
|
if (order) args.push(`--order=${order}`);
|
|
168
202
|
if (view) args.push(`--view=${view}`);
|
|
169
203
|
if (markup) args.push(`--markup=${markup}`);
|
|
@@ -176,7 +210,9 @@ export function registerChatTools(server: McpServer): void {
|
|
|
176
210
|
'Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through '
|
|
177
211
|
+ 'this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing '
|
|
178
212
|
+ 'conversation (from gog_chat_threads_list or a message\'s thread field); omit it to start a new one. Text supports '
|
|
179
|
-
+ 'Chat\'s markdown-ish formatting (*bold*, _italic_, `code`).
|
|
213
|
+
+ 'Chat\'s markdown-ish formatting (*bold*, _italic_, `code`). Asks the MCP host to show the user a confirmation '
|
|
214
|
+
+ 'prompt with the space, thread, text and attachments first; nothing is posted unless they accept.'
|
|
215
|
+
+ CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
|
|
180
216
|
annotations: { destructiveHint: true },
|
|
181
217
|
inputSchema: z.object({
|
|
182
218
|
space: spaceParam,
|
|
@@ -184,16 +220,18 @@ export function registerChatTools(server: McpServer): void {
|
|
|
184
220
|
thread: threadParam,
|
|
185
221
|
attach: z.array(z.string()).optional().describe(
|
|
186
222
|
'Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine — use '
|
|
187
|
-
+ 'attachInline there instead.',
|
|
223
|
+
+ 'attachInline there instead. Must be inside the server\'s GOG_FILE_ROOTS directories (default ~/gogcli-mcp-files).',
|
|
188
224
|
),
|
|
189
225
|
attachInline: attachInlineParam,
|
|
190
226
|
account: accountParam,
|
|
227
|
+
confirmToken: confirmTokenParam,
|
|
191
228
|
}),
|
|
192
|
-
}, async ({ space, text, thread, attach, attachInline, account }) => {
|
|
229
|
+
}, async ({ space, text, thread, attach, attachInline, account, confirmToken }, ctx) => {
|
|
230
|
+
confinePaths(attach, 'attach');
|
|
193
231
|
if (text === undefined && !attach?.length && !attachInline?.length) {
|
|
194
232
|
throw new Error('A Chat message needs text, an attachment, or both.');
|
|
195
233
|
}
|
|
196
|
-
const args: GogArg[] = ['chat', 'messages', 'send', space];
|
|
234
|
+
const args: GogArg[] = ['chat', 'messages', 'send', pos(space)];
|
|
197
235
|
if (text !== undefined) args.push(`--text=${text}`);
|
|
198
236
|
if (thread) args.push(`--thread=${thread}`);
|
|
199
237
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
@@ -201,24 +239,61 @@ export function registerChatTools(server: McpServer): void {
|
|
|
201
239
|
// a temp file beside gog. `args` is passed so the size check sees the whole
|
|
202
240
|
// request, not just the attachments.
|
|
203
241
|
args.push(...inlineAttachmentArgs('attach', attachInline, args));
|
|
242
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
243
|
+
...CHAT_CONFIRM,
|
|
244
|
+
action: 'chat.message-send',
|
|
245
|
+
details: { space, thread, textPreview: bodyPreview(text), attachments: attachmentNames(attach, attachInline) },
|
|
246
|
+
fallback: {
|
|
247
|
+
tool: 'gog_chat_messages_send',
|
|
248
|
+
account,
|
|
249
|
+
confirmToken,
|
|
250
|
+
subject: () => {
|
|
251
|
+
const attachments = attachmentDetails(attach, attachInline);
|
|
252
|
+
const from = senderPreview(account);
|
|
253
|
+
return {
|
|
254
|
+
target: space,
|
|
255
|
+
payload: { from, space, thread, text, attachments },
|
|
256
|
+
preview: { from, space, thread, text, attachments: attachmentPreview(attachments) },
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
if (confirmation) return confirmation;
|
|
204
262
|
return runOrDiagnose(args, { account });
|
|
205
263
|
});
|
|
206
264
|
|
|
207
265
|
server.registerTool('gog_chat_dm_send', {
|
|
208
266
|
description:
|
|
209
267
|
'Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered '
|
|
210
|
-
+ 'immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send.'
|
|
211
|
-
+
|
|
268
|
+
+ 'immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send. '
|
|
269
|
+
+ 'Asks the MCP host to show the user a confirmation prompt with the recipient and text first; nothing is sent '
|
|
270
|
+
+ 'unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
|
|
212
271
|
annotations: { destructiveHint: true },
|
|
213
272
|
inputSchema: z.object({
|
|
214
273
|
email: z.string().describe('Recipient email address'),
|
|
215
274
|
text: z.string().describe('Message text'),
|
|
216
275
|
thread: threadParam,
|
|
217
276
|
account: accountParam,
|
|
277
|
+
confirmToken: confirmTokenParam,
|
|
218
278
|
}),
|
|
219
|
-
}, async ({ email, text, thread, account }) => {
|
|
220
|
-
const args = ['chat', 'dm', 'send', email, `--text=${text}`];
|
|
279
|
+
}, async ({ email, text, thread, account, confirmToken }, ctx) => {
|
|
280
|
+
const args: GogArg[] = ['chat', 'dm', 'send', pos(email), `--text=${text}`];
|
|
221
281
|
if (thread) args.push(`--thread=${thread}`);
|
|
282
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
283
|
+
...CHAT_CONFIRM,
|
|
284
|
+
action: 'chat.dm-send',
|
|
285
|
+
details: { to: email, thread, textPreview: bodyPreview(text) },
|
|
286
|
+
fallback: {
|
|
287
|
+
tool: 'gog_chat_dm_send',
|
|
288
|
+
account,
|
|
289
|
+
confirmToken,
|
|
290
|
+
subject: () => {
|
|
291
|
+
const from = senderPreview(account);
|
|
292
|
+
return { target: email, payload: { from, to: email, thread, text }, preview: { from, to: email, thread, text } };
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
if (confirmation) return confirmation;
|
|
222
297
|
return runOrDiagnose(args, { account });
|
|
223
298
|
});
|
|
224
299
|
|
|
@@ -232,7 +307,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
232
307
|
account: accountParam,
|
|
233
308
|
}),
|
|
234
309
|
}, async ({ email, account }) => {
|
|
235
|
-
return runOrDiagnose(['chat', 'dm', 'space', email], { account });
|
|
310
|
+
return runOrDiagnose(['chat', 'dm', 'space', pos(email)], { account });
|
|
236
311
|
});
|
|
237
312
|
|
|
238
313
|
server.registerTool('gog_chat_reactions_list', {
|
|
@@ -248,7 +323,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
248
323
|
account: accountParam,
|
|
249
324
|
}),
|
|
250
325
|
}, async ({ message, space, max, pageToken, page, all, account }) => {
|
|
251
|
-
const args = ['chat', 'messages', 'reactions', 'list', message];
|
|
326
|
+
const args: GogArg[] = ['chat', 'messages', 'reactions', 'list', pos(message)];
|
|
252
327
|
if (space) args.push(`--space=${space}`);
|
|
253
328
|
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
254
329
|
return runOrDiagnose(args, { account });
|
|
@@ -266,7 +341,7 @@ export function registerChatTools(server: McpServer): void {
|
|
|
266
341
|
account: accountParam,
|
|
267
342
|
}),
|
|
268
343
|
}, async ({ message, emoji, space, account }) => {
|
|
269
|
-
const args = ['chat', 'messages', 'reactions', 'create', message, emoji];
|
|
344
|
+
const args: GogArg[] = ['chat', 'messages', 'reactions', 'create', pos(message), pos(emoji)];
|
|
270
345
|
if (space) args.push(`--space=${space}`);
|
|
271
346
|
return runOrDiagnose(args, { account });
|
|
272
347
|
});
|
|
@@ -282,12 +357,13 @@ export function registerChatTools(server: McpServer): void {
|
|
|
282
357
|
account: accountParam,
|
|
283
358
|
}),
|
|
284
359
|
}, async ({ reaction, account }) => {
|
|
285
|
-
return runOrDiagnose(['chat', 'messages', 'reactions', 'delete', reaction], { account });
|
|
360
|
+
return runOrDiagnose(['chat', 'messages', 'reactions', 'delete', pos(reaction)], { account });
|
|
286
361
|
});
|
|
287
362
|
|
|
288
363
|
registerRunTool(server, {
|
|
289
364
|
service: 'chat',
|
|
290
365
|
examples: '"spaces", "messages", "dm"',
|
|
366
|
+
vet: vetChatRun,
|
|
291
367
|
note: 'Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes.',
|
|
292
368
|
});
|
|
293
369
|
}
|