google-tools-mcp 1.0.12 → 1.0.14

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/README.md CHANGED
@@ -235,6 +235,7 @@ Google Forms — create/read forms, manage responses, and publish settings.
235
235
  | `GOOGLE_CLIENT_SECRET` | No* | OAuth 2.0 Client Secret |
236
236
  | `GOOGLE_MCP_PROFILE` | No | Profile name for multi-account support (see above) |
237
237
  | `LOG_LEVEL` | No | `debug`, `info`, `warn`, `error`, or `silent` |
238
+ | `GOOGLE_MCP_LOG_FILE` | No | Set to `1` to log to `~/.config/google-tools-mcp/server.log`, or set to a custom file path |
238
239
  | `SERVICE_ACCOUNT_PATH` | No | Path to service account JSON key (alternative to OAuth) |
239
240
  | `GOOGLE_IMPERSONATE_USER` | No | Email to impersonate with service account |
240
241
 
package/dist/auth.js CHANGED
@@ -271,12 +271,16 @@ export async function authorize() {
271
271
  if (client) {
272
272
  // Proactively refresh to verify the token is still valid
273
273
  try {
274
+ logger.info('Refreshing access token...');
274
275
  const { credentials } = await client.refreshAccessToken();
275
276
  client.setCredentials(credentials);
276
277
  if (credentials.refresh_token) {
277
278
  await saveCredentials(client);
278
279
  }
279
- logger.info('Using saved credentials (token refreshed successfully).');
280
+ const expiryDate = credentials.expiry_date
281
+ ? new Date(credentials.expiry_date).toISOString()
282
+ : 'unknown';
283
+ logger.info(`Token refreshed successfully. Expires: ${expiryDate}`);
280
284
  return client;
281
285
  } catch (err) {
282
286
  const isInvalidGrant = err.message?.includes('invalid_grant') ||
@@ -286,6 +290,7 @@ export async function authorize() {
286
290
  try { await fs.unlink(getTokenPath()); } catch {}
287
291
  return authenticate();
288
292
  }
293
+ logger.error('Token refresh failed:', err.message || err);
289
294
  throw err;
290
295
  }
291
296
  }
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
- message.push('Content-Type: text/plain; charset="UTF-8"');
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
- 'Content-Type: text/plain; charset="UTF-8"',
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/index.js CHANGED
@@ -24,14 +24,26 @@ if (process.argv[2] === 'auth') {
24
24
  }
25
25
  }
26
26
 
27
- // --- Server startup ---
27
+ // --- Process lifecycle logging ---
28
28
  process.on('uncaughtException', (error) => {
29
29
  logger.error('Uncaught Exception:', error);
30
30
  });
31
31
  process.on('unhandledRejection', (reason, _promise) => {
32
32
  logger.error('Unhandled Promise Rejection:', reason);
33
33
  });
34
+ process.on('SIGINT', () => {
35
+ logger.info('Received SIGINT — shutting down.');
36
+ process.exit(0);
37
+ });
38
+ process.on('SIGTERM', () => {
39
+ logger.info('Received SIGTERM — shutting down.');
40
+ process.exit(0);
41
+ });
42
+ process.on('exit', (code) => {
43
+ logger.info(`Process exiting with code ${code}.`);
44
+ });
34
45
 
46
+ // --- Server startup ---
35
47
  const server = new FastMCP({
36
48
  name: 'google-tools-mcp',
37
49
  version: '1.0.0',
package/dist/logger.js CHANGED
@@ -1,5 +1,10 @@
1
1
  // Centralized logger with LOG_LEVEL support.
2
2
  // All log output goes to stderr (stdout reserved for MCP protocol).
3
+ // If GOOGLE_MCP_LOG_FILE is set, logs are also appended to that file.
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as os from 'os';
7
+
3
8
  const LOG_LEVELS = {
4
9
  debug: 0,
5
10
  info: 1,
@@ -21,25 +26,62 @@ export function refreshLogLevel() {
21
26
  function shouldLog(level) {
22
27
  return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
23
28
  }
24
- export const logger = {
25
- debug(...args) {
26
- if (shouldLog('debug')) {
27
- console.error('[DEBUG]', ...args);
28
- }
29
- },
30
- info(...args) {
31
- if (shouldLog('info')) {
32
- console.error('[INFO]', ...args);
33
- }
34
- },
35
- warn(...args) {
36
- if (shouldLog('warn')) {
37
- console.error('[WARN]', ...args);
38
- }
39
- },
40
- error(...args) {
41
- if (shouldLog('error')) {
42
- console.error('[ERROR]', ...args);
29
+
30
+ // --- File logging ---
31
+ let logStream = null;
32
+
33
+ function getDefaultLogPath() {
34
+ const xdg = process.env.XDG_CONFIG_HOME;
35
+ const base = xdg || path.join(os.homedir(), '.config');
36
+ const baseDir = path.join(base, 'google-tools-mcp');
37
+ const profile = process.env.GOOGLE_MCP_PROFILE;
38
+ const dir = profile ? path.join(baseDir, profile) : baseDir;
39
+ return path.join(dir, 'server.log');
40
+ }
41
+
42
+ function initLogFile() {
43
+ if (logStream) return;
44
+ const logPath = process.env.GOOGLE_MCP_LOG_FILE === '1'
45
+ ? getDefaultLogPath()
46
+ : process.env.GOOGLE_MCP_LOG_FILE;
47
+ if (!logPath) return;
48
+ try {
49
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
50
+ logStream = fs.createWriteStream(logPath, { flags: 'a' });
51
+ } catch {
52
+ // If we can't open the log file, continue without file logging
53
+ }
54
+ }
55
+ initLogFile();
56
+
57
+ function timestamp() {
58
+ return new Date().toISOString();
59
+ }
60
+
61
+ function formatArgs(args) {
62
+ return args.map(a => {
63
+ if (a instanceof Error) return a.stack || a.message;
64
+ if (typeof a === 'object') {
65
+ try { return JSON.stringify(a); } catch { return String(a); }
43
66
  }
44
- },
67
+ return String(a);
68
+ }).join(' ');
69
+ }
70
+
71
+ function log(level, args) {
72
+ if (!shouldLog(level)) return;
73
+ const tag = level.toUpperCase();
74
+ const ts = timestamp();
75
+ const msg = formatArgs(args);
76
+ console.error(`${ts} [${tag}] ${msg}`);
77
+ if (logStream) {
78
+ logStream.write(`${ts} [${tag}] ${msg}\n`);
79
+ }
80
+ }
81
+
82
+ export const logger = {
83
+ debug(...args) { log('debug', args); },
84
+ info(...args) { log('info', args); },
85
+ warn(...args) { log('warn', args); },
86
+ error(...args) { log('error', args); },
45
87
  };
@@ -6,9 +6,9 @@ export function register(server) {
6
6
  server.addTool({
7
7
  name: 'manage_event',
8
8
  description:
9
- 'Create, update, or delete a calendar event. Supports attendees, Google Meet, reminders, attachments, visibility, and transparency settings.',
9
+ 'Create, update, or delete a calendar event. Supports attendees, Google Meet, reminders, attachments, visibility, and transparency settings. IMPORTANT: To modify an existing event, always use action "update" with the existing event_id. Never delete and recreate an event to make changes — this destroys the event ID, attendee RSVPs, and sent notifications.',
10
10
  parameters: z.object({
11
- action: z.enum(['create', 'update', 'delete']).describe('The operation to perform.'),
11
+ action: z.enum(['create', 'update', 'delete']).describe('The operation to perform. Use "update" to modify existing events — do not delete and recreate. "delete" should only be used to permanently remove an event, not to redo one.'),
12
12
  calendar_id: z
13
13
  .string()
14
14
  .optional()
@@ -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
  }),
@@ -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"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-tools-mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Combined Google Workspace MCP server (Drive, Docs, Sheets, Gmail) with lazy-loaded tool categories",
5
5
  "type": "module",
6
6
  "bin": {