mcp-google-multi 4.2.0 → 4.3.0-alpha.2

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  A local [MCP](https://modelcontextprotocol.io) server that gives Claude Code (and any MCP client) access to **Gmail**, **Google Drive**, **Google Calendar**, **Google Sheets**, **Google Docs**, **Google Contacts**, **Google Search Console**, **Google Tasks**, **Google Meet**, and optionally **Google Forms**, **Google Chat**, and **Google Workspace Admin** APIs across multiple Google accounts simultaneously.
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/mcp-google-multi?label=npm&color=cb3837)](https://www.npmjs.com/package/mcp-google-multi)
6
+
5
7
  ## Features
6
8
 
7
9
  - **Multi-account** — manage any number of Google accounts from a single server
@@ -249,14 +251,25 @@ Enable with `GOOGLE_OPTIONAL_SCOPES=alertcenter` in `.env`.
249
251
 
250
252
  ### 2. Install & Configure
251
253
 
254
+ **Option A — from npm (no clone):**
255
+
256
+ ```bash
257
+ npm install -g mcp-google-multi # installs the `mcp-google-multi` CLI
258
+ # …or run on demand without installing: npx mcp-google-multi
259
+ ```
260
+
261
+ Published with channel dist-tags — `@latest` (stable), `@dev` (alpha), `@staging` (beta); e.g. `npm i -g mcp-google-multi@dev` for the latest alpha.
262
+
263
+ **Option B — from source:**
264
+
252
265
  ```bash
253
266
  git clone https://github.com/bakissation/mcp-google-multi.git
254
267
  cd mcp-google-multi
255
- npm install
268
+ npm install && npm run build
256
269
  cp .env.example .env
257
270
  ```
258
271
 
259
- Edit `.env`:
272
+ Create a `.env` (from source, `.env.example` is the template; from npm, make one in the directory you'll run the server from) with:
260
273
 
261
274
  ```env
262
275
  GOOGLE_CLIENT_ID=your_client_id_here
@@ -310,6 +323,24 @@ Or add it manually to your Claude Code MCP config:
310
323
  }
311
324
  ```
312
325
 
326
+ If you installed from npm, point at the package instead and pass config via `env` (authenticate first with `mcp-google-multi auth --account <alias>`):
327
+
328
+ ```json
329
+ {
330
+ "mcpServers": {
331
+ "google-multi": {
332
+ "command": "npx",
333
+ "args": ["-y", "mcp-google-multi"],
334
+ "env": {
335
+ "GOOGLE_CLIENT_ID": "your_client_id_here",
336
+ "GOOGLE_CLIENT_SECRET": "your_client_secret_here",
337
+ "GOOGLE_ACCOUNTS": "work:you@company.com,personal:you@gmail.com"
338
+ }
339
+ }
340
+ }
341
+ }
342
+ ```
343
+
313
344
  ## Migrating from v3
314
345
 
315
346
  v4.0.0 grows the base OAuth scope set (Tasks and Meet are now included by default). Existing v3 tokens lack those scopes and the corresponding tools will 403.
@@ -311,8 +311,14 @@ export function registerDriveTools(server) {
311
311
  newParentFolderId: z.string().optional().describe('Move to this folder'),
312
312
  localPath: z.string().optional().describe('Replace file content with this local file'),
313
313
  mimeType: z.string().optional().describe('MIME type of the replacement file (required if localPath is provided)'),
314
+ convertTo: z.enum([
315
+ 'application/vnd.google-apps.document',
316
+ 'application/vnd.google-apps.spreadsheet',
317
+ 'application/vnd.google-apps.presentation',
318
+ 'application/vnd.google-apps.drawing',
319
+ ]).optional().describe('When replacing content via localPath, convert the new content into this native Google Workspace type on import (e.g. replace a Google Doc body from a local .docx). Source must be an importable format.'),
314
320
  },
315
- }, async ({ account, fileId, newName, newParentFolderId, localPath: localPathArg, mimeType: mimeTypeArg }) => {
321
+ }, async ({ account, fileId, newName, newParentFolderId, localPath: localPathArg, mimeType: mimeTypeArg, convertTo }) => {
316
322
  try {
317
323
  const auth = await getClient(account);
318
324
  const drive = google.drive({ version: 'v3', auth });
@@ -335,6 +341,8 @@ export function registerDriveTools(server) {
335
341
  mimeType: mimeTypeArg ?? (mime.lookup(localPathArg) || 'application/octet-stream'),
336
342
  body: fs.createReadStream(localPathArg),
337
343
  };
344
+ if (convertTo)
345
+ requestBody.mimeType = convertTo;
338
346
  }
339
347
  const res = await drive.files.update(params);
340
348
  return {
@@ -14,3 +14,12 @@ export declare function encodeAddressHeader(value: string): string;
14
14
  * Normalize bare `\n` or `\r` to CRLF.
15
15
  */
16
16
  export declare function normalizeBodyLineEndings(body: string): string;
17
+ /**
18
+ * Build a multipart/alternative body so HTML-capable clients render the rich
19
+ * version and plain clients fall back. Returns the header value AND the body.
20
+ * Caller composes the full message: headers (including this Content-Type) + CRLF + body.
21
+ */
22
+ export declare function buildMultipartAlternative(plainBody: string, htmlBody: string): {
23
+ contentType: string;
24
+ body: string;
25
+ };
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from 'node:crypto';
1
2
  /**
2
3
  * RFC 2047 encoded-word (`=?utf-8?B?...?=`) for non-ASCII header text.
3
4
  * Long values are split into multiple <=75-char encoded-words separated
@@ -62,3 +63,37 @@ export function encodeAddressHeader(value) {
62
63
  export function normalizeBodyLineEndings(body) {
63
64
  return body.replace(/\r\n|\r|\n/g, '\r\n');
64
65
  }
66
+ /**
67
+ * RFC 2046 §5.1.1 boundary token: 1-70 chars from a restricted set, no trailing space.
68
+ * randomBytes hex output is only [0-9a-f], all of which are bcharsnospace.
69
+ */
70
+ function generateMimeBoundary() {
71
+ // 5-char prefix + 32 hex chars = 37 chars, well under the 70-char limit.
72
+ return `=_gm_${randomBytes(16).toString('hex')}`;
73
+ }
74
+ /**
75
+ * Build a multipart/alternative body so HTML-capable clients render the rich
76
+ * version and plain clients fall back. Returns the header value AND the body.
77
+ * Caller composes the full message: headers (including this Content-Type) + CRLF + body.
78
+ */
79
+ export function buildMultipartAlternative(plainBody, htmlBody) {
80
+ const boundary = generateMimeBoundary();
81
+ const parts = [
82
+ `--${boundary}`,
83
+ 'Content-Type: text/plain; charset="UTF-8"',
84
+ 'Content-Transfer-Encoding: 8bit',
85
+ '',
86
+ normalizeBodyLineEndings(plainBody),
87
+ `--${boundary}`,
88
+ 'Content-Type: text/html; charset="UTF-8"',
89
+ 'Content-Transfer-Encoding: 8bit',
90
+ '',
91
+ normalizeBodyLineEndings(htmlBody),
92
+ `--${boundary}--`,
93
+ '',
94
+ ];
95
+ return {
96
+ contentType: `multipart/alternative; boundary="${boundary}"`,
97
+ body: parts.join('\r\n'),
98
+ };
99
+ }
@@ -3,7 +3,7 @@ import { google } from 'googleapis';
3
3
  import { ACCOUNTS } from '../accounts.js';
4
4
  import { getClient } from '../client.js';
5
5
  import { handleGoogleApiError } from './_errors.js';
6
- import { encodeAddressHeader, encodeHeaderValue, normalizeBodyLineEndings } from './gmail-mime.js';
6
+ import { buildMultipartAlternative, encodeAddressHeader, encodeHeaderValue, normalizeBodyLineEndings } from './gmail-mime.js';
7
7
  import * as path from 'path';
8
8
  import * as fs from 'fs';
9
9
  const accountEnum = z.enum(ACCOUNTS);
@@ -163,14 +163,16 @@ export function registerGmailTools(server) {
163
163
  account: accountEnum.describe('Google account alias'),
164
164
  to: z.string().describe('Recipient(s), comma-separated'),
165
165
  subject: z.string().describe('Email subject'),
166
- body: z.string().describe('Plain text body'),
166
+ body: z.string().describe('Plain text body (always required; also used as fallback when htmlBody is set)'),
167
+ htmlBody: z.string().optional()
168
+ .describe('Optional HTML body. When set, sends multipart/alternative so HTML-capable clients render the rich version. Use bare tags only: <p>, <a>, <br>, <strong>, <em>, <ul><li>.'),
167
169
  cc: z.string().optional().describe('CC recipients, comma-separated'),
168
170
  replyToMessageId: z.string().optional()
169
171
  .describe('Message ID to reply to (sets In-Reply-To and References headers)'),
170
172
  replyToThreadId: z.string().optional()
171
173
  .describe('Thread ID to send the message in'),
172
174
  },
173
- }, async ({ account, to, subject, body, cc, replyToMessageId, replyToThreadId }) => {
175
+ }, async ({ account, to, subject, body, htmlBody, cc, replyToMessageId, replyToThreadId }) => {
174
176
  try {
175
177
  const auth = await getClient(account);
176
178
  const gmail = google.gmail({ version: 'v1', auth });
@@ -180,16 +182,25 @@ export function registerGmailTools(server) {
180
182
  `To: ${encodeAddressHeader(to)}`,
181
183
  `Subject: ${encodeHeaderValue(subject)}`,
182
184
  'MIME-Version: 1.0',
183
- 'Content-Type: text/plain; charset="UTF-8"',
184
- 'Content-Transfer-Encoding: 8bit',
185
185
  ];
186
+ let bodyText;
187
+ if (htmlBody) {
188
+ const { contentType, body: mp } = buildMultipartAlternative(body, htmlBody);
189
+ headers.push(`Content-Type: ${contentType}`);
190
+ bodyText = mp;
191
+ }
192
+ else {
193
+ headers.push('Content-Type: text/plain; charset="UTF-8"');
194
+ headers.push('Content-Transfer-Encoding: 8bit');
195
+ bodyText = normalizeBodyLineEndings(body);
196
+ }
186
197
  if (cc)
187
198
  headers.push(`Cc: ${encodeAddressHeader(cc)}`);
188
199
  if (replyToMessageId) {
189
200
  headers.push(`In-Reply-To: ${replyToMessageId}`);
190
201
  headers.push(`References: ${replyToMessageId}`);
191
202
  }
192
- const rawMessage = [...headers, '', normalizeBodyLineEndings(body)].join('\r\n');
203
+ const rawMessage = [...headers, '', bodyText].join('\r\n');
193
204
  const encoded = Buffer.from(rawMessage, 'utf-8').toString('base64url');
194
205
  const sendParams = {
195
206
  userId: 'me',
@@ -249,12 +260,14 @@ export function registerGmailTools(server) {
249
260
  account: accountEnum.describe('Google account alias'),
250
261
  to: z.string().describe('Recipient(s), comma-separated'),
251
262
  subject: z.string().describe('Email subject'),
252
- body: z.string().describe('Plain text body'),
263
+ body: z.string().describe('Plain text body (always required; also used as fallback when htmlBody is set)'),
264
+ htmlBody: z.string().optional()
265
+ .describe('Optional HTML body. When set, drafts as multipart/alternative so HTML-capable clients render the rich version. Use bare tags only: <p>, <a>, <br>, <strong>, <em>, <ul><li>.'),
253
266
  cc: z.string().optional().describe('CC recipients, comma-separated'),
254
267
  replyToThreadId: z.string().optional()
255
268
  .describe('Thread ID to associate the draft with'),
256
269
  },
257
- }, async ({ account, to, subject, body, cc, replyToThreadId }) => {
270
+ }, async ({ account, to, subject, body, htmlBody, cc, replyToThreadId }) => {
258
271
  try {
259
272
  const auth = await getClient(account);
260
273
  const gmail = google.gmail({ version: 'v1', auth });
@@ -264,12 +277,21 @@ export function registerGmailTools(server) {
264
277
  `To: ${encodeAddressHeader(to)}`,
265
278
  `Subject: ${encodeHeaderValue(subject)}`,
266
279
  'MIME-Version: 1.0',
267
- 'Content-Type: text/plain; charset="UTF-8"',
268
- 'Content-Transfer-Encoding: 8bit',
269
280
  ];
281
+ let bodyText;
282
+ if (htmlBody) {
283
+ const { contentType, body: mp } = buildMultipartAlternative(body, htmlBody);
284
+ headers.push(`Content-Type: ${contentType}`);
285
+ bodyText = mp;
286
+ }
287
+ else {
288
+ headers.push('Content-Type: text/plain; charset="UTF-8"');
289
+ headers.push('Content-Transfer-Encoding: 8bit');
290
+ bodyText = normalizeBodyLineEndings(body);
291
+ }
270
292
  if (cc)
271
293
  headers.push(`Cc: ${encodeAddressHeader(cc)}`);
272
- const rawMessage = [...headers, '', normalizeBodyLineEndings(body)].join('\r\n');
294
+ const rawMessage = [...headers, '', bodyText].join('\r\n');
273
295
  const encoded = Buffer.from(rawMessage, 'utf-8').toString('base64url');
274
296
  const draftParams = {
275
297
  userId: 'me',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-google-multi",
3
- "version": "4.2.0",
3
+ "version": "4.3.0-alpha.2",
4
4
  "description": "MCP server for Gmail, Google Drive, Calendar, Sheets, Docs, and Contacts with multi-account support",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,6 +67,6 @@
67
67
  "semantic-release": "^25.0.3",
68
68
  "typescript": "^6.0.3",
69
69
  "typescript-eslint": "^8.59.1",
70
- "vitest": "^4.1.5"
70
+ "vitest": "^3.2.4"
71
71
  }
72
72
  }