google-tools-mcp 1.0.11 → 1.0.13
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/dist/auth.js +19 -2
- package/dist/clients.js +45 -0
- package/dist/helpers.js +7 -3
- package/dist/tools/drafts.js +2 -2
- package/dist/tools/gmail/drafts.js +2 -2
- package/dist/tools/gmail/messages.js +3 -3
- package/dist/tools/index.js +22 -2
- package/dist/tools/messages.js +1 -1
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -269,8 +269,25 @@ export async function authorize() {
|
|
|
269
269
|
logger.info('Attempting OAuth 2.0 authentication...');
|
|
270
270
|
const client = await loadSavedCredentialsIfExist();
|
|
271
271
|
if (client) {
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
// Proactively refresh to verify the token is still valid
|
|
273
|
+
try {
|
|
274
|
+
const { credentials } = await client.refreshAccessToken();
|
|
275
|
+
client.setCredentials(credentials);
|
|
276
|
+
if (credentials.refresh_token) {
|
|
277
|
+
await saveCredentials(client);
|
|
278
|
+
}
|
|
279
|
+
logger.info('Using saved credentials (token refreshed successfully).');
|
|
280
|
+
return client;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
const isInvalidGrant = err.message?.includes('invalid_grant') ||
|
|
283
|
+
err.response?.data?.error === 'invalid_grant';
|
|
284
|
+
if (isInvalidGrant) {
|
|
285
|
+
logger.warn('Saved refresh token is invalid/revoked. Starting re-authentication...');
|
|
286
|
+
try { await fs.unlink(getTokenPath()); } catch {}
|
|
287
|
+
return authenticate();
|
|
288
|
+
}
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
274
291
|
}
|
|
275
292
|
logger.info('No saved token found. Starting interactive authentication flow...');
|
|
276
293
|
return authenticate();
|
package/dist/clients.js
CHANGED
|
@@ -31,6 +31,33 @@ async function ensureAuth() {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Re-authenticate and rebuild all API clients after an invalid_grant error.
|
|
36
|
+
*/
|
|
37
|
+
async function reauthorize() {
|
|
38
|
+
logger.info('Re-authorizing after invalid_grant...');
|
|
39
|
+
authClient = null;
|
|
40
|
+
googleDocs = null;
|
|
41
|
+
googleDrive = null;
|
|
42
|
+
googleSheets = null;
|
|
43
|
+
googleScript = null;
|
|
44
|
+
gmailClient = null;
|
|
45
|
+
calendarClient = null;
|
|
46
|
+
formsClient = null;
|
|
47
|
+
authClient = await authorize();
|
|
48
|
+
logger.info('Re-authorization successful.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if an error is an invalid_grant error (expired/revoked refresh token).
|
|
53
|
+
*/
|
|
54
|
+
function isInvalidGrantError(error) {
|
|
55
|
+
if (!error) return false;
|
|
56
|
+
const msg = error.message || '';
|
|
57
|
+
const code = error.response?.data?.error || error.code || '';
|
|
58
|
+
return msg.includes('invalid_grant') || code === 'invalid_grant';
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
// --- GDrive clients ---
|
|
35
62
|
export async function initializeGoogleClient() {
|
|
36
63
|
if (googleDocs && googleDrive && googleSheets)
|
|
@@ -79,6 +106,24 @@ export function resetClients() {
|
|
|
79
106
|
formsClient = null;
|
|
80
107
|
}
|
|
81
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Execute a function with automatic re-auth on invalid_grant errors.
|
|
111
|
+
* Wraps any API call so that if the refresh token has been revoked mid-session,
|
|
112
|
+
* we re-authenticate transparently and retry once.
|
|
113
|
+
*/
|
|
114
|
+
export async function withAuthRetry(fn) {
|
|
115
|
+
try {
|
|
116
|
+
return await fn();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (isInvalidGrantError(error)) {
|
|
119
|
+
logger.warn('Got invalid_grant during API call. Re-authenticating...');
|
|
120
|
+
await reauthorize();
|
|
121
|
+
return await fn();
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
82
127
|
// --- Individual client getters ---
|
|
83
128
|
export async function getDocsClient() {
|
|
84
129
|
const { googleDocs: docs } = await initializeGoogleClient();
|
package/dist/helpers.js
CHANGED
|
@@ -109,6 +109,8 @@ export const wrapTextBody = (text) => text.split('\n').map(line => {
|
|
|
109
109
|
return chunks.join('=\n');
|
|
110
110
|
}).join('\n');
|
|
111
111
|
|
|
112
|
+
export const isHtmlBody = (text) => /<\/?[a-z][\s\S]*?>/i.test(text);
|
|
113
|
+
|
|
112
114
|
export const constructRawMessage = async (gmail, params) => {
|
|
113
115
|
let thread = null;
|
|
114
116
|
if (params.threadId) {
|
|
@@ -126,11 +128,12 @@ export const constructRawMessage = async (gmail, params) => {
|
|
|
126
128
|
} else {
|
|
127
129
|
message.push('Subject: (No Subject)');
|
|
128
130
|
}
|
|
129
|
-
|
|
131
|
+
const htmlMode = params.body && isHtmlBody(params.body);
|
|
132
|
+
message.push(`Content-Type: ${htmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
|
|
130
133
|
message.push('Content-Transfer-Encoding: quoted-printable');
|
|
131
134
|
message.push('MIME-Version: 1.0');
|
|
132
135
|
message.push('');
|
|
133
|
-
if (params.body) message.push(wrapTextBody(params.body));
|
|
136
|
+
if (params.body) message.push(htmlMode ? params.body : wrapTextBody(params.body));
|
|
134
137
|
if (thread) {
|
|
135
138
|
const quotedContent = getQuotedContent(thread);
|
|
136
139
|
if (quotedContent) {
|
|
@@ -168,9 +171,10 @@ export const constructRawMessageWithAttachments = async (gmail, params) => {
|
|
|
168
171
|
const quotedContent = getQuotedContent(thread);
|
|
169
172
|
if (quotedContent) bodyText += '\n\n' + quotedContent;
|
|
170
173
|
}
|
|
174
|
+
const htmlMode = isHtmlBody(bodyText);
|
|
171
175
|
parts.push([
|
|
172
176
|
`--${boundary}`,
|
|
173
|
-
|
|
177
|
+
`Content-Type: ${htmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`,
|
|
174
178
|
'Content-Transfer-Encoding: base64',
|
|
175
179
|
'',
|
|
176
180
|
Buffer.from(bodyText).toString('base64'),
|
package/dist/tools/drafts.js
CHANGED
|
@@ -15,7 +15,7 @@ export function register(server) {
|
|
|
15
15
|
cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
|
|
16
16
|
bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
|
|
17
17
|
subject: z.string().optional().describe("The subject of the email"),
|
|
18
|
-
body: z.string().optional().describe("The body of the email"),
|
|
18
|
+
body: z.string().optional().describe("The body of the email. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted emails."),
|
|
19
19
|
attachments: z.array(z.object({
|
|
20
20
|
filename: z.string().describe("Attachment file name"),
|
|
21
21
|
mimeType: z.string().describe("MIME type of the attachment"),
|
|
@@ -56,7 +56,7 @@ export function register(server) {
|
|
|
56
56
|
cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
|
|
57
57
|
bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
|
|
58
58
|
subject: z.string().optional().describe("The subject of the email"),
|
|
59
|
-
body: z.string().optional().describe("The body of the email"),
|
|
59
|
+
body: z.string().optional().describe("The body of the email. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted emails."),
|
|
60
60
|
attachments: z.array(z.object({
|
|
61
61
|
filename: z.string().describe("Attachment file name"),
|
|
62
62
|
mimeType: z.string().describe("MIME type of the attachment"),
|
|
@@ -15,7 +15,7 @@ export function register(server) {
|
|
|
15
15
|
cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
|
|
16
16
|
bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
|
|
17
17
|
subject: z.string().optional().describe("The subject of the email"),
|
|
18
|
-
body: z.string().optional().describe("The body of the email"),
|
|
18
|
+
body: z.string().optional().describe("The body of the email. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted emails."),
|
|
19
19
|
attachments: z.array(z.object({
|
|
20
20
|
filename: z.string().describe("Attachment file name"),
|
|
21
21
|
mimeType: z.string().describe("MIME type of the attachment"),
|
|
@@ -56,7 +56,7 @@ export function register(server) {
|
|
|
56
56
|
cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
|
|
57
57
|
bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
|
|
58
58
|
subject: z.string().optional().describe("The subject of the email"),
|
|
59
|
-
body: z.string().optional().describe("The body of the email"),
|
|
59
|
+
body: z.string().optional().describe("The body of the email. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted emails."),
|
|
60
60
|
attachments: z.array(z.object({
|
|
61
61
|
filename: z.string().describe("Attachment file name"),
|
|
62
62
|
mimeType: z.string().describe("MIME type of the attachment"),
|
|
@@ -14,7 +14,7 @@ export function register(server) {
|
|
|
14
14
|
cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
|
|
15
15
|
bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
|
|
16
16
|
subject: z.string().optional().describe("The subject of the email"),
|
|
17
|
-
body: z.string().optional().describe("The body of the email"),
|
|
17
|
+
body: z.string().optional().describe("The body of the email. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted emails."),
|
|
18
18
|
attachments: z.array(z.object({
|
|
19
19
|
filename: z.string().describe("Attachment file name"),
|
|
20
20
|
mimeType: z.string().describe("MIME type of the attachment"),
|
|
@@ -49,7 +49,7 @@ export function register(server) {
|
|
|
49
49
|
description: 'Reply to a message. Automatically handles To/Cc recipients, subject prefix, threading headers, and quoted content. Use replyAll to include all original recipients.',
|
|
50
50
|
parameters: z.object({
|
|
51
51
|
messageId: z.string().describe("The ID of the message to reply to"),
|
|
52
|
-
body: z.string().describe("The reply body text"),
|
|
52
|
+
body: z.string().describe("The reply body text. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted replies."),
|
|
53
53
|
replyAll: z.boolean().optional().describe("If true, reply to all original recipients (To + Cc minus yourself). Default: false"),
|
|
54
54
|
to: z.array(z.string()).optional().describe("Override recipient list (if omitted, replies to sender or Reply-To)"),
|
|
55
55
|
cc: z.array(z.string()).optional().describe("Override CC list (if omitted and replyAll, uses original To + Cc minus yourself)"),
|
|
@@ -168,7 +168,7 @@ export function register(server) {
|
|
|
168
168
|
to: z.array(z.string()).describe("Recipient email addresses to forward to"),
|
|
169
169
|
cc: z.array(z.string()).optional().describe("CC recipient email addresses"),
|
|
170
170
|
bcc: z.array(z.string()).optional().describe("BCC recipient email addresses"),
|
|
171
|
-
body: z.string().optional().describe("Optional commentary to prepend above the forwarded content"),
|
|
171
|
+
body: z.string().optional().describe("Optional commentary to prepend above the forwarded content. Supports plain text or HTML (auto-detected)."),
|
|
172
172
|
includeAttachments: z.boolean().optional().describe("Whether to include original attachments. Default: true"),
|
|
173
173
|
includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return"),
|
|
174
174
|
}),
|
package/dist/tools/index.js
CHANGED
|
@@ -6,9 +6,26 @@ import * as fs from 'fs/promises';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { getTokenPath } from '../auth.js';
|
|
9
|
-
import { resetClients } from '../clients.js';
|
|
9
|
+
import { resetClients, withAuthRetry } from '../clients.js';
|
|
10
10
|
import { logger } from '../logger.js';
|
|
11
11
|
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Wrap server.addTool so every tool's execute() auto-retries on invalid_grant.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function wrapServerWithAuthRetry(server) {
|
|
16
|
+
const originalAddTool = server.addTool.bind(server);
|
|
17
|
+
server.addTool = function (toolDef) {
|
|
18
|
+
const originalExecute = toolDef.execute;
|
|
19
|
+
if (originalExecute) {
|
|
20
|
+
toolDef.execute = function (...args) {
|
|
21
|
+
return withAuthRetry(() => originalExecute.apply(this, args));
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return originalAddTool(toolDef);
|
|
25
|
+
};
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
// --- Category registry ---
|
|
13
30
|
const CATEGORIES = {
|
|
14
31
|
files: {
|
|
@@ -77,9 +94,12 @@ const CATEGORIES = {
|
|
|
77
94
|
// Public: register all tools eagerly, plus the logout utility.
|
|
78
95
|
// ---------------------------------------------------------------------------
|
|
79
96
|
export async function registerAllTools(server) {
|
|
97
|
+
// Wrap server so every tool auto-retries on invalid_grant (expired refresh token)
|
|
98
|
+
const wrappedServer = wrapServerWithAuthRetry(server);
|
|
99
|
+
|
|
80
100
|
// Load every category
|
|
81
101
|
for (const [name, { loader }] of Object.entries(CATEGORIES)) {
|
|
82
|
-
await loader(
|
|
102
|
+
await loader(wrappedServer);
|
|
83
103
|
}
|
|
84
104
|
logger.info(`Loaded all ${Object.keys(CATEGORIES).length} categories at startup.`);
|
|
85
105
|
|
package/dist/tools/messages.js
CHANGED
|
@@ -14,7 +14,7 @@ export function register(server) {
|
|
|
14
14
|
cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
|
|
15
15
|
bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
|
|
16
16
|
subject: z.string().optional().describe("The subject of the email"),
|
|
17
|
-
body: z.string().optional().describe("The body of the email"),
|
|
17
|
+
body: z.string().optional().describe("The body of the email. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted emails."),
|
|
18
18
|
attachments: z.array(z.object({
|
|
19
19
|
filename: z.string().describe("Attachment file name"),
|
|
20
20
|
mimeType: z.string().describe("MIME type of the attachment"),
|