@triffon/google-docs-mcp 1.11.3-triffon

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 (165) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +567 -0
  3. package/dist/auth.js +327 -0
  4. package/dist/cachedToolsList.js +38 -0
  5. package/dist/clients.js +180 -0
  6. package/dist/config.js +9 -0
  7. package/dist/downloadProxy.js +82 -0
  8. package/dist/driveQueryUtils.js +7 -0
  9. package/dist/firestoreTokenStorage.js +32 -0
  10. package/dist/googleDocsApiHelpers.js +1257 -0
  11. package/dist/googleSheetsApiHelpers.js +849 -0
  12. package/dist/index.js +240 -0
  13. package/dist/landingPage.js +103 -0
  14. package/dist/logger.js +58 -0
  15. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  16. package/dist/markdown-transformer/index.js +128 -0
  17. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  18. package/dist/remoteWrapper.js +72 -0
  19. package/dist/tools/calendar/createEvent.js +89 -0
  20. package/dist/tools/calendar/deleteEvent.js +49 -0
  21. package/dist/tools/calendar/helpers.js +19 -0
  22. package/dist/tools/calendar/index.js +12 -0
  23. package/dist/tools/calendar/listEvents.js +84 -0
  24. package/dist/tools/calendar/quickAddEvent.js +55 -0
  25. package/dist/tools/calendar/updateEvent.js +84 -0
  26. package/dist/tools/docs/addTab.js +84 -0
  27. package/dist/tools/docs/appendTableRows.js +89 -0
  28. package/dist/tools/docs/appendToGoogleDoc.js +85 -0
  29. package/dist/tools/docs/cloneTable.js +159 -0
  30. package/dist/tools/docs/comments/addComment.js +83 -0
  31. package/dist/tools/docs/comments/deleteComment.js +30 -0
  32. package/dist/tools/docs/comments/getComment.js +45 -0
  33. package/dist/tools/docs/comments/index.js +14 -0
  34. package/dist/tools/docs/comments/listComments.js +43 -0
  35. package/dist/tools/docs/comments/replyToComment.js +35 -0
  36. package/dist/tools/docs/comments/resolveComment.js +55 -0
  37. package/dist/tools/docs/deleteRange.js +61 -0
  38. package/dist/tools/docs/deleteTableRows.js +62 -0
  39. package/dist/tools/docs/findAndReplace.js +54 -0
  40. package/dist/tools/docs/findElement.js +43 -0
  41. package/dist/tools/docs/findSectionsByHeading.js +46 -0
  42. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  43. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  44. package/dist/tools/docs/formatting/batchApplyTextStyle.js +86 -0
  45. package/dist/tools/docs/formatting/index.js +16 -0
  46. package/dist/tools/docs/formatting/updateTableBorders.js +81 -0
  47. package/dist/tools/docs/formatting/updateTableCellStyle.js +85 -0
  48. package/dist/tools/docs/formatting/updateTableColumnWidth.js +51 -0
  49. package/dist/tools/docs/formatting/updateTableRowStyle.js +74 -0
  50. package/dist/tools/docs/getTableStructure.js +48 -0
  51. package/dist/tools/docs/index.js +66 -0
  52. package/dist/tools/docs/insertDateChip.js +82 -0
  53. package/dist/tools/docs/insertImage.js +112 -0
  54. package/dist/tools/docs/insertPageBreak.js +47 -0
  55. package/dist/tools/docs/insertPerson.js +53 -0
  56. package/dist/tools/docs/insertRichLink.js +58 -0
  57. package/dist/tools/docs/insertSectionBreak.js +60 -0
  58. package/dist/tools/docs/insertTable.js +42 -0
  59. package/dist/tools/docs/insertTableWithData.js +125 -0
  60. package/dist/tools/docs/insertText.js +49 -0
  61. package/dist/tools/docs/listDocumentTables.js +47 -0
  62. package/dist/tools/docs/listDocumentTabs.js +59 -0
  63. package/dist/tools/docs/listSmartChips.js +41 -0
  64. package/dist/tools/docs/modifyText.js +147 -0
  65. package/dist/tools/docs/readGoogleDoc.js +164 -0
  66. package/dist/tools/docs/renameTab.js +47 -0
  67. package/dist/tools/docs/replaceTableRowData.js +55 -0
  68. package/dist/tools/docs/smartChipHelpers.js +71 -0
  69. package/dist/tools/docs/structureHelpers.js +250 -0
  70. package/dist/tools/docs/tabFieldMasks.js +48 -0
  71. package/dist/tools/docs/tableRowDataHelpers.js +53 -0
  72. package/dist/tools/docs/updateSectionStyle.js +148 -0
  73. package/dist/tools/drive/copyFile.js +63 -0
  74. package/dist/tools/drive/createDocument.js +105 -0
  75. package/dist/tools/drive/createFolder.js +48 -0
  76. package/dist/tools/drive/createFromTemplate.js +82 -0
  77. package/dist/tools/drive/deleteFile.js +72 -0
  78. package/dist/tools/drive/downloadFile.js +266 -0
  79. package/dist/tools/drive/getDocumentInfo.js +48 -0
  80. package/dist/tools/drive/getFolderInfo.js +48 -0
  81. package/dist/tools/drive/index.js +34 -0
  82. package/dist/tools/drive/listDriveFiles.js +129 -0
  83. package/dist/tools/drive/listFolderContents.js +83 -0
  84. package/dist/tools/drive/listGoogleDocs.js +70 -0
  85. package/dist/tools/drive/moveFile.js +54 -0
  86. package/dist/tools/drive/renameFile.js +39 -0
  87. package/dist/tools/drive/savePathGuard.js +86 -0
  88. package/dist/tools/drive/searchDriveFiles.js +148 -0
  89. package/dist/tools/drive/searchGoogleDocs.js +77 -0
  90. package/dist/tools/drive/setFilePermission.js +68 -0
  91. package/dist/tools/gmail/createDraft.js +58 -0
  92. package/dist/tools/gmail/deleteDraft.js +37 -0
  93. package/dist/tools/gmail/getDraft.js +52 -0
  94. package/dist/tools/gmail/getMessage.js +92 -0
  95. package/dist/tools/gmail/helpers.js +113 -0
  96. package/dist/tools/gmail/index.js +28 -0
  97. package/dist/tools/gmail/listDrafts.js +74 -0
  98. package/dist/tools/gmail/listLabels.js +31 -0
  99. package/dist/tools/gmail/listMessages.js +87 -0
  100. package/dist/tools/gmail/modifyMessageLabels.js +54 -0
  101. package/dist/tools/gmail/sendDraft.js +42 -0
  102. package/dist/tools/gmail/sendEmail.js +56 -0
  103. package/dist/tools/gmail/trashMessage.js +37 -0
  104. package/dist/tools/gmail/triageInbox.js +147 -0
  105. package/dist/tools/gmail/updateDraft.js +60 -0
  106. package/dist/tools/index.js +64 -0
  107. package/dist/tools/script/appsScriptShared.js +96 -0
  108. package/dist/tools/script/createAppsScriptProject.js +74 -0
  109. package/dist/tools/script/getAppsScriptContent.js +56 -0
  110. package/dist/tools/script/index.js +14 -0
  111. package/dist/tools/script/updateAppsScriptContent.js +56 -0
  112. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  113. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  114. package/dist/tools/sheets/appendSpreadsheetRows.js +44 -0
  115. package/dist/tools/sheets/appendTableRows.js +51 -0
  116. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  117. package/dist/tools/sheets/autoResizeRows.js +63 -0
  118. package/dist/tools/sheets/batchWrite.js +61 -0
  119. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  120. package/dist/tools/sheets/comments/commentAnchor.js +95 -0
  121. package/dist/tools/sheets/comments/createSheetsCellNote.js +33 -0
  122. package/dist/tools/sheets/comments/createSheetsComment.js +168 -0
  123. package/dist/tools/sheets/comments/deleteSheetsComment.js +32 -0
  124. package/dist/tools/sheets/comments/getSheetsComment.js +51 -0
  125. package/dist/tools/sheets/comments/index.js +16 -0
  126. package/dist/tools/sheets/comments/listSheetsComments.js +177 -0
  127. package/dist/tools/sheets/comments/replyToSheetsComment.js +37 -0
  128. package/dist/tools/sheets/comments/resolveSheetsComment.js +53 -0
  129. package/dist/tools/sheets/copyFormatting.js +59 -0
  130. package/dist/tools/sheets/copySheetTo.js +36 -0
  131. package/dist/tools/sheets/createSpreadsheet.js +72 -0
  132. package/dist/tools/sheets/createTable.js +120 -0
  133. package/dist/tools/sheets/deleteChart.js +41 -0
  134. package/dist/tools/sheets/deleteConditionalFormatting.js +46 -0
  135. package/dist/tools/sheets/deleteSheet.js +43 -0
  136. package/dist/tools/sheets/deleteTable.js +56 -0
  137. package/dist/tools/sheets/duplicateSheet.js +53 -0
  138. package/dist/tools/sheets/formatCells.js +122 -0
  139. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  140. package/dist/tools/sheets/getConditionalFormatting.js +98 -0
  141. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  142. package/dist/tools/sheets/getTable.js +48 -0
  143. package/dist/tools/sheets/groupRows.js +62 -0
  144. package/dist/tools/sheets/index.js +84 -0
  145. package/dist/tools/sheets/insertChart.js +225 -0
  146. package/dist/tools/sheets/listGoogleSheets.js +66 -0
  147. package/dist/tools/sheets/listTables.js +55 -0
  148. package/dist/tools/sheets/protectRange.js +59 -0
  149. package/dist/tools/sheets/readCellFormat.js +143 -0
  150. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  151. package/dist/tools/sheets/renameSheet.js +48 -0
  152. package/dist/tools/sheets/setCellBorders.js +94 -0
  153. package/dist/tools/sheets/setColumnWidths.js +43 -0
  154. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  155. package/dist/tools/sheets/setRowHeights.js +63 -0
  156. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  157. package/dist/tools/sheets/updateTableRange.js +51 -0
  158. package/dist/tools/sheets/writeSpreadsheet.js +45 -0
  159. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +95 -0
  160. package/dist/tools/utils/index.js +8 -0
  161. package/dist/tools/utils/replaceDocumentWithMarkdown.js +161 -0
  162. package/dist/tools/utils/replaceRangeWithMarkdown.js +72 -0
  163. package/dist/types.js +208 -0
  164. package/dist/upstreamAuth.js +62 -0
  165. package/package.json +51 -0
@@ -0,0 +1,72 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { getAuthSession, requireAuth, UserError } from 'fastmcp';
3
+ import { google } from 'googleapis';
4
+ import { OAuth2Client } from 'google-auth-library';
5
+ import { logger } from './logger.js';
6
+ export const requestClients = new AsyncLocalStorage();
7
+ const allowedDomains = (process.env.ALLOWED_DOMAINS || '').split(',').filter(Boolean);
8
+ function checkDomain(idToken) {
9
+ if (allowedDomains.length === 0)
10
+ return true;
11
+ if (!idToken)
12
+ return false;
13
+ const payload = idToken.split('.')[1];
14
+ if (!payload)
15
+ return false;
16
+ try {
17
+ const { hd } = JSON.parse(Buffer.from(payload, 'base64url').toString());
18
+ return hd ? allowedDomains.includes(hd) : false;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ export function createClients(accessToken, refreshToken) {
25
+ const auth = new OAuth2Client(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET);
26
+ auth.setCredentials({
27
+ access_token: accessToken,
28
+ refresh_token: refreshToken,
29
+ });
30
+ return {
31
+ accessToken,
32
+ auth,
33
+ docs: google.docs({ version: 'v1', auth }),
34
+ sheets: google.sheets({ version: 'v4', auth }),
35
+ drive: google.drive({ version: 'v3', auth }),
36
+ script: google.script({ version: 'v1', auth }),
37
+ gmail: google.gmail({ version: 'v1', auth }),
38
+ calendar: google.calendar({ version: 'v3', auth }),
39
+ };
40
+ }
41
+ const wrappedServers = new WeakSet();
42
+ /**
43
+ * Wraps server.addTool() so that in remote (httpStream) mode every tool
44
+ * automatically gets: auth enforcement, domain restriction, and per-request
45
+ * Google API clients via AsyncLocalStorage. Zero changes to tool files.
46
+ */
47
+ export function wrapServerForRemote(server) {
48
+ if (wrappedServers.has(server))
49
+ return;
50
+ wrappedServers.add(server);
51
+ const previousAddTool = server.addTool.bind(server);
52
+ server.addTool = (toolDef) => {
53
+ const originalExecute = toolDef.execute;
54
+ previousAddTool({
55
+ ...toolDef,
56
+ canAccess: toolDef.canAccess
57
+ ? (auth) => requireAuth(auth) && toolDef.canAccess(auth)
58
+ : requireAuth,
59
+ execute: async (args, context) => {
60
+ const { accessToken, refreshToken, idToken } = getAuthSession(context.session);
61
+ if (!checkDomain(idToken)) {
62
+ throw new UserError('Your Google account domain is not allowed on this server.');
63
+ }
64
+ const clients = createClients(accessToken, refreshToken);
65
+ return requestClients.run(clients, () => originalExecute(args, context));
66
+ },
67
+ });
68
+ };
69
+ if (allowedDomains.length > 0) {
70
+ logger.info(`Remote mode: domain restriction active for [${allowedDomains.join(', ')}]`);
71
+ }
72
+ }
@@ -0,0 +1,89 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { getCalendarClient } from '../../clients.js';
5
+ import { createEventDateTimeSchema } from './helpers.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'createEvent',
9
+ description: 'Creates a new event on a Google Calendar. Supports timed events (start/end with dateTime) and all-day events (start/end with date). Set sendUpdates to email invitations to attendees.',
10
+ parameters: z.strictObject({
11
+ calendarId: z
12
+ .string()
13
+ .optional()
14
+ .default('primary')
15
+ .describe('Calendar ID. Defaults to "primary".'),
16
+ summary: z.string().describe('Event title.'),
17
+ description: z.string().optional().describe('Event description / notes.'),
18
+ location: z.string().optional().describe('Physical address or location string.'),
19
+ start: createEventDateTimeSchema().describe('Event start. Provide dateTime or date.'),
20
+ end: createEventDateTimeSchema().describe('Event end. Provide dateTime or date. For all-day events, end.date is exclusive.'),
21
+ attendees: z
22
+ .array(z.strictObject({
23
+ email: z.string().describe('Attendee email address.'),
24
+ optional: z.boolean().optional().describe('Mark attendee as optional.'),
25
+ }))
26
+ .optional()
27
+ .describe('List of attendees to invite.'),
28
+ sendUpdates: z
29
+ .enum(['all', 'externalOnly', 'none'])
30
+ .optional()
31
+ .default('none')
32
+ .describe('Whether to send email invitations: "all" sends to everyone, "externalOnly" only to non-domain attendees, "none" sends nothing (default).'),
33
+ conferenceData: z
34
+ .boolean()
35
+ .optional()
36
+ .default(false)
37
+ .describe('If true, attaches an automatically generated Google Meet link to the event.'),
38
+ }),
39
+ execute: async (args, { log }) => {
40
+ const calendar = await getCalendarClient();
41
+ log.info(`Creating event "${args.summary}" on calendar ${args.calendarId}`);
42
+ try {
43
+ const response = await calendar.events.insert({
44
+ calendarId: args.calendarId,
45
+ sendUpdates: args.sendUpdates,
46
+ conferenceDataVersion: args.conferenceData ? 1 : undefined,
47
+ requestBody: {
48
+ summary: args.summary,
49
+ description: args.description,
50
+ location: args.location,
51
+ start: args.start,
52
+ end: args.end,
53
+ attendees: args.attendees,
54
+ ...(args.conferenceData && {
55
+ conferenceData: {
56
+ createRequest: {
57
+ requestId: `mcp-${randomUUID()}`,
58
+ conferenceSolutionKey: { type: 'hangoutsMeet' },
59
+ },
60
+ },
61
+ }),
62
+ },
63
+ });
64
+ const event = response.data;
65
+ return JSON.stringify({
66
+ success: true,
67
+ id: event.id,
68
+ summary: event.summary,
69
+ start: event.start,
70
+ end: event.end,
71
+ htmlLink: event.htmlLink,
72
+ hangoutLink: event.hangoutLink ?? null,
73
+ attendees: event.attendees?.length ?? 0,
74
+ message: `Event "${event.summary}" created.`,
75
+ }, null, 2);
76
+ }
77
+ catch (error) {
78
+ log.error(`Error creating event: ${error.message || error}`);
79
+ if (error.code === 404)
80
+ throw new UserError(`Calendar not found (ID: ${args.calendarId}).`);
81
+ if (error.code === 403)
82
+ throw new UserError('Permission denied. Confirm the calendar.events scope was granted.');
83
+ if (error.code === 400)
84
+ throw new UserError(`Calendar rejected the event: ${error.message || 'Bad request'}. Check that start/end formats are valid RFC3339.`);
85
+ throw new UserError(`Failed to create event: ${error.message || 'Unknown error'}`);
86
+ }
87
+ },
88
+ });
89
+ }
@@ -0,0 +1,49 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getCalendarClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'deleteEvent',
7
+ description: 'Deletes an event from a Google Calendar. This is permanent — the event is removed, not trashed. Use sendUpdates to email cancellations to attendees.',
8
+ parameters: z.strictObject({
9
+ calendarId: z
10
+ .string()
11
+ .optional()
12
+ .default('primary')
13
+ .describe('Calendar ID. Defaults to "primary".'),
14
+ eventId: z.string().describe('The event ID to delete (from listEvents).'),
15
+ sendUpdates: z
16
+ .enum(['all', 'externalOnly', 'none'])
17
+ .optional()
18
+ .default('none')
19
+ .describe('Whether to email cancellation notices to attendees.'),
20
+ }),
21
+ execute: async (args, { log }) => {
22
+ const calendar = await getCalendarClient();
23
+ log.info(`Deleting event ${args.eventId} from calendar ${args.calendarId}`);
24
+ try {
25
+ await calendar.events.delete({
26
+ calendarId: args.calendarId,
27
+ eventId: args.eventId,
28
+ sendUpdates: args.sendUpdates,
29
+ });
30
+ return JSON.stringify({
31
+ success: true,
32
+ eventId: args.eventId,
33
+ calendarId: args.calendarId,
34
+ message: `Event ${args.eventId} deleted from calendar ${args.calendarId}.`,
35
+ }, null, 2);
36
+ }
37
+ catch (error) {
38
+ log.error(`Error deleting event: ${error.message || error}`);
39
+ if (error.code === 404)
40
+ throw new UserError(`Event not found: ${args.eventId}.`);
41
+ if (error.code === 410)
42
+ throw new UserError(`Event ${args.eventId} was already deleted.`);
43
+ if (error.code === 403)
44
+ throw new UserError('Permission denied. Confirm the calendar.events scope was granted.');
45
+ throw new UserError(`Failed to delete event: ${error.message || 'Unknown error'}`);
46
+ }
47
+ },
48
+ });
49
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export const createEventDateTimeSchema = () => z
3
+ .object({
4
+ dateTime: z
5
+ .string()
6
+ .optional()
7
+ .describe('RFC3339 timestamp with timezone offset, e.g. "2026-04-15T14:00:00-08:00". Use this for timed events.'),
8
+ date: z
9
+ .string()
10
+ .optional()
11
+ .describe('ISO date "YYYY-MM-DD" for all-day events. Use instead of dateTime.'),
12
+ timeZone: z
13
+ .string()
14
+ .optional()
15
+ .describe('IANA timezone like "America/Los_Angeles". Optional when dateTime has an offset.'),
16
+ })
17
+ .refine((v) => v.dateTime || v.date, {
18
+ message: 'Provide either dateTime (timed event) or date (all-day event).',
19
+ });
@@ -0,0 +1,12 @@
1
+ import { register as listEvents } from './listEvents.js';
2
+ import { register as createEvent } from './createEvent.js';
3
+ import { register as updateEvent } from './updateEvent.js';
4
+ import { register as deleteEvent } from './deleteEvent.js';
5
+ import { register as quickAddEvent } from './quickAddEvent.js';
6
+ export function registerCalendarTools(server) {
7
+ listEvents(server);
8
+ createEvent(server);
9
+ updateEvent(server);
10
+ deleteEvent(server);
11
+ quickAddEvent(server);
12
+ }
@@ -0,0 +1,84 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getCalendarClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'listEvents',
7
+ description: "Lists or searches Google Calendar events. Defaults to the user's primary calendar starting now. Use timeMin/timeMax (RFC3339 timestamps) to bound the window, q for free-text search, and maxResults to cap the count. Returns event IDs needed for updateEvent and deleteEvent.",
8
+ parameters: z.strictObject({
9
+ calendarId: z
10
+ .string()
11
+ .optional()
12
+ .default('primary')
13
+ .describe('Calendar ID. Defaults to "primary" (the user\'s main calendar).'),
14
+ q: z
15
+ .string()
16
+ .optional()
17
+ .describe('Free-text search across summary, description, location, and attendees.'),
18
+ timeMin: z
19
+ .string()
20
+ .optional()
21
+ .describe('Lower bound (inclusive) as RFC3339 timestamp, e.g. "2026-04-10T00:00:00-08:00". Defaults to now.'),
22
+ timeMax: z.string().optional().describe('Upper bound (exclusive) as RFC3339 timestamp.'),
23
+ maxResults: z
24
+ .number()
25
+ .int()
26
+ .min(1)
27
+ .max(2500)
28
+ .optional()
29
+ .default(25)
30
+ .describe('Maximum number of events to return (1-2500). Defaults to 25.'),
31
+ singleEvents: z
32
+ .boolean()
33
+ .optional()
34
+ .default(true)
35
+ .describe('If true (default), expands recurring events into individual instances. Set false to receive recurring events as a single record.'),
36
+ }),
37
+ execute: async (args, { log }) => {
38
+ const calendar = await getCalendarClient();
39
+ const timeMin = args.timeMin ?? new Date().toISOString();
40
+ log.info(`Listing calendar events on ${args.calendarId} (timeMin=${timeMin}, q=${args.q ?? 'none'})`);
41
+ try {
42
+ const response = await calendar.events.list({
43
+ calendarId: args.calendarId,
44
+ q: args.q,
45
+ timeMin,
46
+ timeMax: args.timeMax,
47
+ maxResults: args.maxResults,
48
+ singleEvents: args.singleEvents,
49
+ orderBy: args.singleEvents ? 'startTime' : undefined,
50
+ });
51
+ const events = (response.data.items ?? []).map((event) => ({
52
+ id: event.id,
53
+ status: event.status,
54
+ summary: event.summary ?? null,
55
+ description: event.description ?? null,
56
+ location: event.location ?? null,
57
+ start: event.start ?? null,
58
+ end: event.end ?? null,
59
+ attendees: event.attendees?.map((a) => ({
60
+ email: a.email,
61
+ responseStatus: a.responseStatus,
62
+ optional: a.optional ?? false,
63
+ })) ?? [],
64
+ organizer: event.organizer?.email ?? null,
65
+ htmlLink: event.htmlLink ?? null,
66
+ recurringEventId: event.recurringEventId ?? null,
67
+ }));
68
+ return JSON.stringify({
69
+ events,
70
+ count: events.length,
71
+ nextPageToken: response.data.nextPageToken ?? null,
72
+ }, null, 2);
73
+ }
74
+ catch (error) {
75
+ log.error(`Error listing calendar events: ${error.message || error}`);
76
+ if (error.code === 404)
77
+ throw new UserError(`Calendar not found (ID: ${args.calendarId}).`);
78
+ if (error.code === 403)
79
+ throw new UserError('Permission denied. Confirm the calendar.events scope was granted during consent.');
80
+ throw new UserError(`Failed to list events: ${error.message || 'Unknown error'}`);
81
+ }
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,55 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getCalendarClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'quickAddEvent',
7
+ description: 'Creates a calendar event from a natural-language string using Google Calendar\'s quick-add parser. Examples: "Lunch with Sarah tomorrow at 12pm", "Dentist appointment next Tuesday 3-4pm", "Team standup every weekday 9am". Faster than createEvent when you don\'t need attendees, descriptions, or precise control over fields.',
8
+ parameters: z.strictObject({
9
+ calendarId: z
10
+ .string()
11
+ .optional()
12
+ .default('primary')
13
+ .describe('Calendar ID. Defaults to "primary".'),
14
+ text: z
15
+ .string()
16
+ .describe('Natural-language description of the event. Google parses the title and time from this string.'),
17
+ sendUpdates: z
18
+ .enum(['all', 'externalOnly', 'none'])
19
+ .optional()
20
+ .default('none')
21
+ .describe('Whether to email invitations (rarely useful for quick add).'),
22
+ }),
23
+ execute: async (args, { log }) => {
24
+ const calendar = await getCalendarClient();
25
+ log.info(`Quick-adding event on ${args.calendarId}: "${args.text}"`);
26
+ try {
27
+ const response = await calendar.events.quickAdd({
28
+ calendarId: args.calendarId,
29
+ text: args.text,
30
+ sendUpdates: args.sendUpdates,
31
+ });
32
+ const event = response.data;
33
+ return JSON.stringify({
34
+ success: true,
35
+ id: event.id,
36
+ summary: event.summary,
37
+ start: event.start,
38
+ end: event.end,
39
+ htmlLink: event.htmlLink,
40
+ message: `Event "${event.summary}" created from "${args.text}".`,
41
+ }, null, 2);
42
+ }
43
+ catch (error) {
44
+ log.error(`Error quick-adding event: ${error.message || error}`);
45
+ if (error.code === 404)
46
+ throw new UserError(`Calendar not found (ID: ${args.calendarId}).`);
47
+ if (error.code === 403)
48
+ throw new UserError('Permission denied. Confirm the calendar.events scope was granted.');
49
+ if (error.code === 400)
50
+ throw new UserError(`Calendar could not parse "${args.text}" as an event. Try a clearer time format.`);
51
+ throw new UserError(`Failed to quick-add event: ${error.message || 'Unknown error'}`);
52
+ }
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,84 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getCalendarClient } from '../../clients.js';
4
+ import { createEventDateTimeSchema } from './helpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'updateEvent',
8
+ description: 'Updates an existing Google Calendar event with PATCH semantics — only the fields you provide are changed; everything else stays the same. Common uses: reschedule (set start+end), retitle (set summary), add/remove attendees (set attendees array which fully replaces).',
9
+ parameters: z.strictObject({
10
+ calendarId: z
11
+ .string()
12
+ .optional()
13
+ .default('primary')
14
+ .describe('Calendar ID. Defaults to "primary".'),
15
+ eventId: z.string().describe('The event ID to update (from listEvents).'),
16
+ summary: z.string().optional().describe('New event title.'),
17
+ description: z.string().optional().describe('New event description.'),
18
+ location: z.string().optional().describe('New location.'),
19
+ start: createEventDateTimeSchema().optional().describe('New start time.'),
20
+ end: createEventDateTimeSchema().optional().describe('New end time.'),
21
+ attendees: z
22
+ .array(z.strictObject({
23
+ email: z.string(),
24
+ optional: z.boolean().optional(),
25
+ }))
26
+ .optional()
27
+ .describe('Replaces the entire attendee list. To add one, fetch the event first.'),
28
+ sendUpdates: z
29
+ .enum(['all', 'externalOnly', 'none'])
30
+ .optional()
31
+ .default('none')
32
+ .describe('Whether to email attendees about the change.'),
33
+ }),
34
+ execute: async (args, { log }) => {
35
+ const calendar = await getCalendarClient();
36
+ log.info(`Patching event ${args.eventId} on calendar ${args.calendarId}`);
37
+ const requestBody = {};
38
+ if (args.summary !== undefined)
39
+ requestBody.summary = args.summary;
40
+ if (args.description !== undefined)
41
+ requestBody.description = args.description;
42
+ if (args.location !== undefined)
43
+ requestBody.location = args.location;
44
+ if (args.start !== undefined)
45
+ requestBody.start = args.start;
46
+ if (args.end !== undefined)
47
+ requestBody.end = args.end;
48
+ if (args.attendees !== undefined)
49
+ requestBody.attendees = args.attendees;
50
+ if (Object.keys(requestBody).length === 0) {
51
+ throw new UserError('No fields provided to update. Pass at least one of summary, description, location, start, end, or attendees.');
52
+ }
53
+ try {
54
+ const response = await calendar.events.patch({
55
+ calendarId: args.calendarId,
56
+ eventId: args.eventId,
57
+ sendUpdates: args.sendUpdates,
58
+ requestBody,
59
+ });
60
+ const event = response.data;
61
+ return JSON.stringify({
62
+ success: true,
63
+ id: event.id,
64
+ summary: event.summary,
65
+ start: event.start,
66
+ end: event.end,
67
+ htmlLink: event.htmlLink,
68
+ updated: event.updated,
69
+ message: `Event ${event.id} updated.`,
70
+ }, null, 2);
71
+ }
72
+ catch (error) {
73
+ log.error(`Error updating event: ${error.message || error}`);
74
+ if (error.code === 404)
75
+ throw new UserError(`Event not found: ${args.eventId} on calendar ${args.calendarId}.`);
76
+ if (error.code === 403)
77
+ throw new UserError('Permission denied. Confirm the calendar.events scope was granted.');
78
+ if (error.code === 400)
79
+ throw new UserError(`Calendar rejected the update: ${error.message || 'Bad request'}.`);
80
+ throw new UserError(`Failed to update event: ${error.message || 'Unknown error'}`);
81
+ }
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,84 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'addTab',
9
+ description: "Adds a new tab to a Google Docs document. Optionally set the tab title, position, parent tab (for nesting), and icon emoji. Returns the new tab's ID and properties.",
10
+ parameters: DocumentIdParameter.extend({
11
+ title: z
12
+ .string()
13
+ .optional()
14
+ .describe('The title for the new tab. If not specified, Google Docs assigns a default name.'),
15
+ parentTabId: z
16
+ .string()
17
+ .optional()
18
+ .describe('The ID of an existing tab to nest this new tab under as a child. Use listDocumentTabs to get tab IDs. If not specified, the tab is created at the root level.'),
19
+ index: z
20
+ .number()
21
+ .int()
22
+ .min(0)
23
+ .optional()
24
+ .describe('The zero-based position among sibling tabs (within the same parent). If not specified, the tab is added at the end.'),
25
+ iconEmoji: z
26
+ .string()
27
+ .optional()
28
+ .describe('An emoji to display as the tab icon (e.g., "📋").'),
29
+ }),
30
+ execute: async (args, { log }) => {
31
+ const docs = await getDocsClient();
32
+ log.info(`Adding new tab to doc ${args.documentId}`);
33
+ try {
34
+ // If parentTabId is provided, verify it exists
35
+ if (args.parentTabId) {
36
+ await GDocsHelpers.getDocumentTab(docs, args.documentId, args.parentTabId);
37
+ }
38
+ const tabProperties = {};
39
+ if (args.title !== undefined)
40
+ tabProperties.title = args.title;
41
+ if (args.parentTabId !== undefined)
42
+ tabProperties.parentTabId = args.parentTabId;
43
+ if (args.index !== undefined)
44
+ tabProperties.index = args.index;
45
+ if (args.iconEmoji !== undefined)
46
+ tabProperties.iconEmoji = args.iconEmoji;
47
+ const response = await docs.documents.batchUpdate({
48
+ documentId: args.documentId,
49
+ requestBody: {
50
+ requests: [
51
+ {
52
+ addDocumentTab: {
53
+ tabProperties,
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ });
59
+ const newTabProps = response.data.replies?.[0]?.addDocumentTab?.tabProperties;
60
+ if (newTabProps) {
61
+ return JSON.stringify({
62
+ message: `Successfully added new tab "${newTabProps.title || '(untitled)'}"`,
63
+ tabId: newTabProps.tabId,
64
+ title: newTabProps.title,
65
+ index: newTabProps.index,
66
+ parentTabId: newTabProps.parentTabId,
67
+ nestingLevel: newTabProps.nestingLevel,
68
+ }, null, 2);
69
+ }
70
+ return 'Tab created successfully, but could not retrieve the new tab details.';
71
+ }
72
+ catch (error) {
73
+ log.error(`Error adding tab to doc ${args.documentId}: ${error.message || error}`);
74
+ if (error instanceof UserError)
75
+ throw error;
76
+ if (error.code === 404)
77
+ throw new UserError(`Document not found (ID: ${args.documentId}).`);
78
+ if (error.code === 403)
79
+ throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
80
+ throw new UserError(`Failed to add tab: ${error.message || 'Unknown error'}`);
81
+ }
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,89 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
5
+ import { DocumentIdParameter } from '../../types.js';
6
+ import { getTableById } from './structureHelpers.js';
7
+ import { replaceTableRowData as replaceTableRowDataInternal } from './tableRowDataHelpers.js';
8
+ import { TABLE_CONTENT_INDEXED_BODY_FIELDS, buildDocumentGetFields } from './tabFieldMasks.js';
9
+ export function register(server) {
10
+ server.addTool({
11
+ name: 'appendTableRows',
12
+ description: 'Appends one or more plain-text rows to the end of an existing Google Docs table while preserving the table structure.',
13
+ parameters: DocumentIdParameter.extend({
14
+ tableId: z
15
+ .string()
16
+ .min(1)
17
+ .describe('The MCP table ID returned by listDocumentTables, for example "table:body:0".'),
18
+ rows: z
19
+ .array(z.array(z.string()).max(50))
20
+ .min(1)
21
+ .max(200)
22
+ .describe('Rows to append. Each inner array is the plain-text cell values for one row.'),
23
+ tabId: z
24
+ .string()
25
+ .optional()
26
+ .describe('The ID of the specific tab containing the table. If not specified, uses the first tab or legacy document body.'),
27
+ }),
28
+ execute: async (args, { log }) => {
29
+ const docs = await getDocsClient();
30
+ log.info(`Appending ${args.rows.length} row(s) to ${args.tableId} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
31
+ try {
32
+ const tableRowFieldMask = buildDocumentGetFields(TABLE_CONTENT_INDEXED_BODY_FIELDS, args.tabId);
33
+ const res = await docs.documents.get({
34
+ documentId: args.documentId,
35
+ ...(args.tabId && { includeTabsContent: true }),
36
+ fields: tableRowFieldMask,
37
+ });
38
+ const table = getTableById(res.data, args.tableId, args.tabId);
39
+ if (!table) {
40
+ throw new UserError(`Table "${args.tableId}" not found in document.`);
41
+ }
42
+ if (table.startIndex == null) {
43
+ throw new UserError(`Table "${args.tableId}" does not expose a valid table start index.`);
44
+ }
45
+ for (const [offset, rowValues] of args.rows.entries()) {
46
+ if (rowValues.length > table.columnCount) {
47
+ throw new UserError(`Row ${offset} has ${rowValues.length} values, but table ${args.tableId} only has ${table.columnCount} columns.`);
48
+ }
49
+ }
50
+ const insertRequests = args.rows.map(() => GDocsHelpers.buildInsertTableRowRequest(table.startIndex, table.rowCount - 1, true, args.tabId));
51
+ await GDocsHelpers.executeBatchUpdateWithSplitting(docs, args.documentId, insertRequests, log);
52
+ const refreshed = await docs.documents.get({
53
+ documentId: args.documentId,
54
+ ...(args.tabId && { includeTabsContent: true }),
55
+ fields: tableRowFieldMask,
56
+ });
57
+ const updatedTable = getTableById(refreshed.data, args.tableId, args.tabId);
58
+ if (!updatedTable) {
59
+ throw new UserError(`Table "${args.tableId}" could not be found after appending rows.`);
60
+ }
61
+ const firstAppendedRowIndex = table.rowCount;
62
+ for (const [offset, rowValues] of args.rows.entries()) {
63
+ const currentTable = offset === 0
64
+ ? updatedTable
65
+ : getTableById((await docs.documents.get({
66
+ documentId: args.documentId,
67
+ ...(args.tabId && { includeTabsContent: true }),
68
+ fields: tableRowFieldMask,
69
+ })).data, args.tableId, args.tabId);
70
+ if (!currentTable) {
71
+ throw new UserError(`Table "${args.tableId}" could not be re-fetched while populating appended rows.`);
72
+ }
73
+ await replaceTableRowDataInternal(docs, args.documentId, currentTable, firstAppendedRowIndex + offset, rowValues, args.tabId);
74
+ }
75
+ return `Successfully appended ${args.rows.length} row(s) to table ${args.tableId}.`;
76
+ }
77
+ catch (error) {
78
+ log.error(`Error appending rows to ${args.tableId} in doc ${args.documentId}: ${error.message || error}`);
79
+ if (error instanceof UserError)
80
+ throw error;
81
+ if (error.code === 404)
82
+ throw new UserError(`Document not found (ID: ${args.documentId}).`);
83
+ if (error.code === 403)
84
+ throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
85
+ throw new UserError(`Failed to append table rows: ${error.message || 'Unknown error'}`);
86
+ }
87
+ },
88
+ });
89
+ }