google-tools-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/auth.js +275 -0
  2. package/dist/cachedToolsList.js +52 -0
  3. package/dist/clients.js +97 -0
  4. package/dist/googleDocsApiHelpers.js +883 -0
  5. package/dist/googleSheetsApiHelpers.js +808 -0
  6. package/dist/helpers.js +210 -0
  7. package/dist/index.js +55 -0
  8. package/dist/logger.js +45 -0
  9. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  10. package/dist/markdown-transformer/index.js +126 -0
  11. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  12. package/dist/tools/docs/addTab.js +92 -0
  13. package/dist/tools/docs/appendToGoogleDoc.js +81 -0
  14. package/dist/tools/docs/comments/addComment.js +83 -0
  15. package/dist/tools/docs/comments/deleteComment.js +30 -0
  16. package/dist/tools/docs/comments/getComment.js +45 -0
  17. package/dist/tools/docs/comments/index.js +14 -0
  18. package/dist/tools/docs/comments/listComments.js +43 -0
  19. package/dist/tools/docs/comments/replyToComment.js +35 -0
  20. package/dist/tools/docs/comments/resolveComment.js +55 -0
  21. package/dist/tools/docs/deleteRange.js +72 -0
  22. package/dist/tools/docs/findAndReplace.js +54 -0
  23. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  24. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  25. package/dist/tools/docs/formatting/index.js +6 -0
  26. package/dist/tools/docs/index.js +38 -0
  27. package/dist/tools/docs/insertImage.js +122 -0
  28. package/dist/tools/docs/insertPageBreak.js +58 -0
  29. package/dist/tools/docs/insertTable.js +53 -0
  30. package/dist/tools/docs/insertTableWithData.js +135 -0
  31. package/dist/tools/docs/insertText.js +61 -0
  32. package/dist/tools/docs/listDocumentTabs.js +60 -0
  33. package/dist/tools/docs/modifyText.js +158 -0
  34. package/dist/tools/docs/readGoogleDoc.js +165 -0
  35. package/dist/tools/docs/renameTab.js +61 -0
  36. package/dist/tools/drive/copyFile.js +63 -0
  37. package/dist/tools/drive/createDocument.js +89 -0
  38. package/dist/tools/drive/createFolder.js +48 -0
  39. package/dist/tools/drive/createFromTemplate.js +82 -0
  40. package/dist/tools/drive/deleteFile.js +72 -0
  41. package/dist/tools/drive/getDocumentInfo.js +48 -0
  42. package/dist/tools/drive/getFolderInfo.js +48 -0
  43. package/dist/tools/drive/index.js +30 -0
  44. package/dist/tools/drive/listFolderContents.js +82 -0
  45. package/dist/tools/drive/listGoogleDocs.js +67 -0
  46. package/dist/tools/drive/listSharedDrives.js +52 -0
  47. package/dist/tools/drive/listSharedWithMe.js +97 -0
  48. package/dist/tools/drive/moveFile.js +54 -0
  49. package/dist/tools/drive/renameFile.js +39 -0
  50. package/dist/tools/drive/searchGoogleDocs.js +98 -0
  51. package/dist/tools/extras/index.js +7 -0
  52. package/dist/tools/extras/readFile.js +82 -0
  53. package/dist/tools/extras/searchFileContents.js +81 -0
  54. package/dist/tools/gmail/drafts.js +165 -0
  55. package/dist/tools/gmail/labels.js +103 -0
  56. package/dist/tools/gmail/messages.js +448 -0
  57. package/dist/tools/gmail/settings.js +528 -0
  58. package/dist/tools/gmail/threads.js +145 -0
  59. package/dist/tools/index.js +163 -0
  60. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  61. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  62. package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
  63. package/dist/tools/sheets/appendTableRows.js +50 -0
  64. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  65. package/dist/tools/sheets/batchWrite.js +59 -0
  66. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  67. package/dist/tools/sheets/copyFormatting.js +59 -0
  68. package/dist/tools/sheets/createSpreadsheet.js +71 -0
  69. package/dist/tools/sheets/createTable.js +120 -0
  70. package/dist/tools/sheets/deleteChart.js +41 -0
  71. package/dist/tools/sheets/deleteSheet.js +43 -0
  72. package/dist/tools/sheets/deleteTable.js +56 -0
  73. package/dist/tools/sheets/duplicateSheet.js +53 -0
  74. package/dist/tools/sheets/formatCells.js +106 -0
  75. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  76. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  77. package/dist/tools/sheets/getTable.js +48 -0
  78. package/dist/tools/sheets/groupRows.js +62 -0
  79. package/dist/tools/sheets/index.js +66 -0
  80. package/dist/tools/sheets/insertChart.js +225 -0
  81. package/dist/tools/sheets/listGoogleSheets.js +62 -0
  82. package/dist/tools/sheets/listTables.js +55 -0
  83. package/dist/tools/sheets/readCellFormat.js +143 -0
  84. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  85. package/dist/tools/sheets/renameSheet.js +48 -0
  86. package/dist/tools/sheets/setColumnWidths.js +43 -0
  87. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  88. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  89. package/dist/tools/sheets/updateTableRange.js +51 -0
  90. package/dist/tools/sheets/writeSpreadsheet.js +43 -0
  91. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
  92. package/dist/tools/utils/index.js +6 -0
  93. package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
  94. package/dist/types.js +186 -0
  95. package/package.json +30 -0
package/dist/auth.js ADDED
@@ -0,0 +1,275 @@
1
+ // Combined auth for google-tools-mcp.
2
+ // Merges GDrive + Gmail scopes into a single OAuth flow.
3
+ // Config dir: ~/.config/google-tools-mcp/ (with GOOGLE_MCP_PROFILE subdirs).
4
+ import { google } from 'googleapis';
5
+ import { JWT } from 'google-auth-library';
6
+ import * as fs from 'fs/promises';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import * as http from 'http';
10
+ import { exec } from 'child_process';
11
+ import { fileURLToPath } from 'url';
12
+ import { logger } from './logger.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ const projectRootDir = path.resolve(__dirname, '..');
17
+ const CREDENTIALS_PATH = path.join(projectRootDir, 'credentials.json');
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Paths
21
+ // ---------------------------------------------------------------------------
22
+ function getConfigDir() {
23
+ const xdg = process.env.XDG_CONFIG_HOME;
24
+ const base = xdg || path.join(os.homedir(), '.config');
25
+ const baseDir = path.join(base, 'google-tools-mcp');
26
+ const profile = process.env.GOOGLE_MCP_PROFILE;
27
+ return profile ? path.join(baseDir, profile) : baseDir;
28
+ }
29
+
30
+ function getTokenPath() {
31
+ return path.join(getConfigDir(), 'token.json');
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Scopes (GDrive + Gmail combined)
36
+ // ---------------------------------------------------------------------------
37
+ const SCOPES = [
38
+ // GDrive / Docs / Sheets
39
+ 'https://www.googleapis.com/auth/documents',
40
+ 'https://www.googleapis.com/auth/drive',
41
+ 'https://www.googleapis.com/auth/spreadsheets',
42
+ 'https://www.googleapis.com/auth/script.external_request',
43
+ // Gmail
44
+ 'https://www.googleapis.com/auth/gmail.modify',
45
+ 'https://www.googleapis.com/auth/gmail.compose',
46
+ 'https://www.googleapis.com/auth/gmail.send',
47
+ 'https://www.googleapis.com/auth/gmail.settings.basic',
48
+ 'https://www.googleapis.com/auth/gmail.settings.sharing',
49
+ ];
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // .env file loader
53
+ // ---------------------------------------------------------------------------
54
+ async function loadEnvFile(filePath) {
55
+ try {
56
+ const content = await fs.readFile(filePath, 'utf8');
57
+ for (const line of content.split('\n')) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed || trimmed.startsWith('#')) continue;
60
+ const eqIdx = trimmed.indexOf('=');
61
+ if (eqIdx === -1) continue;
62
+ const key = trimmed.slice(0, eqIdx).trim();
63
+ let value = trimmed.slice(eqIdx + 1).trim();
64
+ if ((value.startsWith('"') && value.endsWith('"')) ||
65
+ (value.startsWith("'") && value.endsWith("'"))) {
66
+ value = value.slice(1, -1);
67
+ }
68
+ if (!process.env[key]) {
69
+ process.env[key] = value;
70
+ }
71
+ }
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Client secrets resolution
80
+ // ---------------------------------------------------------------------------
81
+ async function loadClientSecrets() {
82
+ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
83
+ return { client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET };
84
+ }
85
+ const configDir = getConfigDir();
86
+ const cwd = process.cwd();
87
+ await loadEnvFile(path.join(configDir, '.env'));
88
+ await loadEnvFile(path.join(cwd, '.env'));
89
+ await loadEnvFile(path.join(projectRootDir, '.env'));
90
+ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
91
+ logger.info('Loaded client credentials from .env file.');
92
+ return { client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET };
93
+ }
94
+ const credentialsPaths = [
95
+ path.join(configDir, 'credentials.json'),
96
+ path.join(cwd, 'credentials.json'),
97
+ CREDENTIALS_PATH,
98
+ ];
99
+ for (const credPath of credentialsPaths) {
100
+ try {
101
+ const content = await fs.readFile(credPath, 'utf8');
102
+ const keys = JSON.parse(content);
103
+ const key = keys.installed || keys.web;
104
+ if (key) {
105
+ logger.info('Loaded client credentials from', credPath);
106
+ return { client_id: key.client_id, client_secret: key.client_secret };
107
+ }
108
+ } catch (err) {
109
+ if (err.code !== 'ENOENT') throw err;
110
+ }
111
+ }
112
+ const configDirDisplay = configDir.replace(os.homedir(), '~');
113
+ throw new Error(
114
+ 'No OAuth credentials found. Provide them in any of these ways:\n' +
115
+ ` 1. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars in your MCP config\n` +
116
+ ` 2. Create a .env file with GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in ${configDirDisplay}/ or your project directory\n` +
117
+ ` 3. Place your credentials.json (from Google Cloud Console) in ${configDirDisplay}/ or your project directory`
118
+ );
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Service account auth
123
+ // ---------------------------------------------------------------------------
124
+ async function authorizeWithServiceAccount() {
125
+ const serviceAccountPath = process.env.SERVICE_ACCOUNT_PATH;
126
+ const impersonateUser = process.env.GOOGLE_IMPERSONATE_USER;
127
+ try {
128
+ const keyFileContent = await fs.readFile(serviceAccountPath, 'utf8');
129
+ const serviceAccountKey = JSON.parse(keyFileContent);
130
+ const auth = new JWT({
131
+ email: serviceAccountKey.client_email,
132
+ key: serviceAccountKey.private_key,
133
+ scopes: SCOPES,
134
+ subject: impersonateUser,
135
+ });
136
+ await auth.authorize();
137
+ if (impersonateUser) {
138
+ logger.info(`Service Account authentication successful, impersonating: ${impersonateUser}`);
139
+ } else {
140
+ logger.info('Service Account authentication successful!');
141
+ }
142
+ return auth;
143
+ } catch (error) {
144
+ if (error.code === 'ENOENT') {
145
+ logger.error(`FATAL: Service account key file not found at path: ${serviceAccountPath}`);
146
+ throw new Error('Service account key file not found. Please check the path in SERVICE_ACCOUNT_PATH.');
147
+ }
148
+ logger.error('FATAL: Error loading or authorizing the service account key:', error.message);
149
+ throw new Error('Failed to authorize using the service account.');
150
+ }
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Token persistence
155
+ // ---------------------------------------------------------------------------
156
+ async function loadSavedCredentialsIfExist() {
157
+ try {
158
+ const tokenPath = getTokenPath();
159
+ const content = await fs.readFile(tokenPath, 'utf8');
160
+ const credentials = JSON.parse(content);
161
+ const { client_secret, client_id } = await loadClientSecrets();
162
+ const client = new google.auth.OAuth2(client_id, client_secret);
163
+ client.setCredentials(credentials);
164
+ return client;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ async function saveCredentials(client) {
171
+ const { client_secret, client_id } = await loadClientSecrets();
172
+ const configDir = getConfigDir();
173
+ await fs.mkdir(configDir, { recursive: true });
174
+ const tokenPath = getTokenPath();
175
+ const payload = JSON.stringify({
176
+ type: 'authorized_user',
177
+ client_id,
178
+ client_secret,
179
+ refresh_token: client.credentials.refresh_token,
180
+ }, null, 2);
181
+ await fs.writeFile(tokenPath, payload);
182
+ logger.info('Token stored to', tokenPath);
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Browser opener
187
+ // ---------------------------------------------------------------------------
188
+ function openBrowser(url) {
189
+ const platform = process.platform;
190
+ let cmd;
191
+ if (platform === 'win32') {
192
+ cmd = `start "" "${url}"`;
193
+ } else if (platform === 'darwin') {
194
+ cmd = `open "${url}"`;
195
+ } else {
196
+ cmd = `xdg-open "${url}"`;
197
+ }
198
+ exec(cmd, (err) => {
199
+ if (err) {
200
+ logger.warn('Could not auto-open browser. Please open this URL manually.');
201
+ }
202
+ });
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Interactive OAuth browser flow
207
+ // ---------------------------------------------------------------------------
208
+ async function authenticate() {
209
+ const { client_secret, client_id } = await loadClientSecrets();
210
+ const server = http.createServer();
211
+ await new Promise((resolve) => server.listen(0, 'localhost', resolve));
212
+ const port = server.address().port;
213
+ const redirectUri = `http://localhost:${port}`;
214
+ const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
215
+ const authorizeUrl = oAuth2Client.generateAuthUrl({
216
+ access_type: 'offline',
217
+ scope: SCOPES.join(' '),
218
+ });
219
+ logger.info('Opening browser for Google authorization...');
220
+ logger.info('If the browser does not open, visit this URL:', authorizeUrl);
221
+ openBrowser(authorizeUrl);
222
+ const code = await new Promise((resolve, reject) => {
223
+ server.on('request', (req, res) => {
224
+ const url = new URL(req.url, `http://localhost:${port}`);
225
+ const authCode = url.searchParams.get('code');
226
+ const error = url.searchParams.get('error');
227
+ if (error) {
228
+ res.writeHead(200, { 'Content-Type': 'text/html' });
229
+ res.end('<h1>Authorization failed</h1><p>You can close this tab.</p>');
230
+ reject(new Error(`Authorization error: ${error}`));
231
+ server.close();
232
+ return;
233
+ }
234
+ if (authCode) {
235
+ res.writeHead(200, { 'Content-Type': 'text/html' });
236
+ res.end('<h1>Google authorization successful!</h1><p>You can close this tab.</p>');
237
+ resolve(authCode);
238
+ server.close();
239
+ }
240
+ });
241
+ });
242
+ const { tokens } = await oAuth2Client.getToken(code);
243
+ oAuth2Client.setCredentials(tokens);
244
+ if (tokens.refresh_token) {
245
+ await saveCredentials(oAuth2Client);
246
+ } else {
247
+ logger.warn('Did not receive refresh token. Token might expire.');
248
+ }
249
+ logger.info('Authentication successful!');
250
+ return oAuth2Client;
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Public API
255
+ // ---------------------------------------------------------------------------
256
+ export { getTokenPath };
257
+
258
+ export async function authorize() {
259
+ if (process.env.SERVICE_ACCOUNT_PATH) {
260
+ logger.info('Service account path detected. Attempting service account authentication...');
261
+ return authorizeWithServiceAccount();
262
+ }
263
+ logger.info('Attempting OAuth 2.0 authentication...');
264
+ const client = await loadSavedCredentialsIfExist();
265
+ if (client) {
266
+ logger.info('Using saved credentials.');
267
+ return client;
268
+ }
269
+ logger.info('No saved token found. Starting interactive authentication flow...');
270
+ return authenticate();
271
+ }
272
+
273
+ export async function runAuthFlow() {
274
+ await authenticate();
275
+ }
@@ -0,0 +1,52 @@
1
+ // Precompute tools/list payload to avoid repeated toJsonSchema() calls.
2
+ // Supports rebuilding the cache when new tools are dynamically loaded.
3
+ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { toJsonSchema } from 'xsschema';
5
+ import { logger } from './logger.js';
6
+
7
+ export function collectToolsWhileRegistering(server, out) {
8
+ const add = server.addTool.bind(server);
9
+ server.addTool = (tool) => {
10
+ out.push(tool);
11
+ add(tool);
12
+ };
13
+ }
14
+
15
+ export async function buildCachedToolsListPayload(tools) {
16
+ return {
17
+ tools: await Promise.all(tools.map(async (tool) => ({
18
+ annotations: tool.annotations,
19
+ description: tool.description,
20
+ inputSchema: tool.parameters
21
+ ? await toJsonSchema(tool.parameters)
22
+ : {
23
+ additionalProperties: false,
24
+ properties: {},
25
+ type: 'object',
26
+ },
27
+ name: tool.name,
28
+ }))),
29
+ };
30
+ }
31
+
32
+ export function installCachedToolsListHandler(server, registeredTools) {
33
+ const session = server.sessions[0];
34
+ if (!session) {
35
+ logger.warn('No MCP session; skipping tools/list cache install.');
36
+ return;
37
+ }
38
+
39
+ // Build the initial cache from whatever tools are registered at startup
40
+ let cachedPayload = null;
41
+
42
+ session.server.setRequestHandler(ListToolsRequestSchema, async () => {
43
+ // Rebuild cache when tool count changes (new tools dynamically loaded)
44
+ if (!cachedPayload || cachedPayload.tools.length !== registeredTools.length) {
45
+ logger.debug(`Rebuilding tools/list cache (${registeredTools.length} tools)...`);
46
+ cachedPayload = await buildCachedToolsListPayload(registeredTools);
47
+ }
48
+ return cachedPayload;
49
+ });
50
+
51
+ logger.debug(`Installed dynamic tools/list cache handler (${registeredTools.length} tools initially).`);
52
+ }
@@ -0,0 +1,97 @@
1
+ // Combined clients for google-tools-mcp.
2
+ // Lazy-loads all Google API clients (Docs, Drive, Sheets, Script, Gmail) on first use.
3
+ import { google } from 'googleapis';
4
+ import { UserError } from 'fastmcp';
5
+ import { authorize } from './auth.js';
6
+ import { logger } from './logger.js';
7
+
8
+ let authClient = null;
9
+ let googleDocs = null;
10
+ let googleDrive = null;
11
+ let googleSheets = null;
12
+ let googleScript = null;
13
+ let gmailClient = null;
14
+
15
+ async function ensureAuth() {
16
+ if (authClient) return;
17
+ try {
18
+ logger.info('Attempting to authorize Google API client...');
19
+ authClient = await authorize();
20
+ logger.info('Google API client authorized successfully.');
21
+ } catch (error) {
22
+ logger.error('Failed to initialize Google API client:', error);
23
+ authClient = null;
24
+ throw new UserError(
25
+ 'Google authentication required. A browser window should have opened automatically. ' +
26
+ 'If not, run: npx google-tools-mcp auth\n\n' +
27
+ 'Details: ' + (error.message || error)
28
+ );
29
+ }
30
+ }
31
+
32
+ // --- GDrive clients ---
33
+ export async function initializeGoogleClient() {
34
+ if (googleDocs && googleDrive && googleSheets)
35
+ return { authClient, googleDocs, googleDrive, googleSheets, googleScript };
36
+ await ensureAuth();
37
+ if (!googleDocs) googleDocs = google.docs({ version: 'v1', auth: authClient });
38
+ if (!googleDrive) googleDrive = google.drive({ version: 'v3', auth: authClient });
39
+ if (!googleSheets) googleSheets = google.sheets({ version: 'v4', auth: authClient });
40
+ if (!googleScript) googleScript = google.script({ version: 'v1', auth: authClient });
41
+ return { authClient, googleDocs, googleDrive, googleSheets, googleScript };
42
+ }
43
+
44
+ // --- Gmail client ---
45
+ export async function initializeGmailClient() {
46
+ if (gmailClient) return { authClient, gmailClient };
47
+ await ensureAuth();
48
+ if (!gmailClient) gmailClient = google.gmail({ version: 'v1', auth: authClient });
49
+ return { authClient, gmailClient };
50
+ }
51
+
52
+ // --- Reset all clients (used by logout) ---
53
+ export function resetClients() {
54
+ authClient = null;
55
+ googleDocs = null;
56
+ googleDrive = null;
57
+ googleSheets = null;
58
+ googleScript = null;
59
+ gmailClient = null;
60
+ }
61
+
62
+ // --- Individual client getters ---
63
+ export async function getDocsClient() {
64
+ const { googleDocs: docs } = await initializeGoogleClient();
65
+ if (!docs) throw new UserError('Google Docs client is not initialized.');
66
+ return docs;
67
+ }
68
+
69
+ export async function getDriveClient() {
70
+ const { googleDrive: drive } = await initializeGoogleClient();
71
+ if (!drive) throw new UserError('Google Drive client is not initialized.');
72
+ return drive;
73
+ }
74
+
75
+ export async function getSheetsClient() {
76
+ const { googleSheets: sheets } = await initializeGoogleClient();
77
+ if (!sheets) throw new UserError('Google Sheets client is not initialized.');
78
+ return sheets;
79
+ }
80
+
81
+ export async function getAuthClient() {
82
+ const { authClient: client } = await initializeGoogleClient();
83
+ if (!client) throw new UserError('Auth client is not initialized.');
84
+ return client;
85
+ }
86
+
87
+ export async function getScriptClient() {
88
+ const { googleScript: script } = await initializeGoogleClient();
89
+ if (!script) throw new UserError('Google Script client is not initialized.');
90
+ return script;
91
+ }
92
+
93
+ export async function getGmailClient() {
94
+ const { gmailClient: gmail } = await initializeGmailClient();
95
+ if (!gmail) throw new UserError('Gmail client is not initialized.');
96
+ return gmail;
97
+ }