mcp-google-multi 5.2.0 → 5.2.1-alpha.1

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/executor.js CHANGED
@@ -30,6 +30,17 @@ export async function executeApiMethod(method, args, deps = {}) {
30
30
  account: args.account,
31
31
  }, true);
32
32
  }
33
+ // drive.files.export streams binary and, without alt=media, Google returns a confusing
34
+ // "Export requires alt=media" — either way the escape hatch can't surface the content.
35
+ if (method.id === 'drive.files.export') {
36
+ return jsonResult({
37
+ error: 'binary_unsupported',
38
+ message: 'drive.files.export returns binary content, not JSON, through this tool.',
39
+ hint: 'Use drive_export to export a Google Workspace file to disk.',
40
+ retriable: false,
41
+ account: args.account,
42
+ }, true);
43
+ }
33
44
  let url;
34
45
  try {
35
46
  url = method.baseUrl + expandPath(method.path, args.pathParams ?? {});
@@ -1,4 +1,11 @@
1
1
  import type { ToolRegistry } from '../registry.js';
2
+ export declare function prepareLocalDest(savePath: string, filename: string): string;
3
+ export declare function resolveShareNotification(opts: {
4
+ type: string;
5
+ role: string;
6
+ transferOwnership?: boolean;
7
+ sendNotification?: boolean;
8
+ }): boolean | undefined;
2
9
  export declare function registerDriveTools(server: ToolRegistry): void;
3
10
  export type TransferPlan = {
4
11
  kind: 'binary';
@@ -28,6 +28,23 @@ const COMMENT_FIELDS = `${COMMENT_BASE_FIELDS},replies(${REPLY_SUBFIELDS})`;
28
28
  const COMMENT_LIST_FIELDS = `nextPageToken,comments(${COMMENT_BASE_FIELDS},replies(${REPLY_SUBFIELDS}))`;
29
29
  const REPLY_FIELDS = `kind,htmlContent,${REPLY_SUBFIELDS}`;
30
30
  const REPLY_LIST_FIELDS = `nextPageToken,replies(${REPLY_FIELDS})`;
31
+ // basename-sanitize the caller filename (never escapes savePath), create the destination
32
+ // directory if missing (callers otherwise hit ENOENT on a non-existent savePath), and
33
+ // return the absolute path to write.
34
+ export function prepareLocalDest(savePath, filename) {
35
+ const dest = path.join(savePath, path.basename(filename));
36
+ fs.mkdirSync(savePath, { recursive: true });
37
+ return dest;
38
+ }
39
+ // sendNotificationEmail is only valid for user/group permissions, and Google forbids
40
+ // disabling it on an ownership transfer. Returns undefined to omit the param entirely.
41
+ export function resolveShareNotification(opts) {
42
+ if (opts.type !== 'user' && opts.type !== 'group')
43
+ return undefined;
44
+ if (opts.transferOwnership || opts.role === 'owner')
45
+ return true;
46
+ return opts.sendNotification ?? true;
47
+ }
31
48
  export function registerDriveTools(server) {
32
49
  // ─── Read / search / list ──────────────────────────────────────────────
33
50
  server.registerTool('drive_search', {
@@ -243,7 +260,7 @@ export function registerDriveTools(server) {
243
260
  try {
244
261
  const auth = await getClient(account);
245
262
  const drive = google.drive({ version: 'v3', auth });
246
- const dest = path.join(savePath, path.basename(filename));
263
+ const dest = prepareLocalDest(savePath, filename);
247
264
  const res = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'stream' });
248
265
  // pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
249
266
  await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600 }));
@@ -269,7 +286,7 @@ export function registerDriveTools(server) {
269
286
  try {
270
287
  const auth = await getClient(account);
271
288
  const drive = google.drive({ version: 'v3', auth });
272
- const dest = path.join(savePath, path.basename(filename));
289
+ const dest = prepareLocalDest(savePath, filename);
273
290
  const res = await drive.files.export({ fileId, mimeType: exportMime }, { responseType: 'stream' });
274
291
  // pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
275
292
  await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600 }));
@@ -525,9 +542,10 @@ export function registerDriveTools(server) {
525
542
  const requestBody = { type, role, emailAddress, domain };
526
543
  if (expirationTime)
527
544
  requestBody.expirationTime = expirationTime;
545
+ const notify = resolveShareNotification({ type, role, transferOwnership, sendNotification });
528
546
  const res = await drive.permissions.create({
529
547
  fileId,
530
- sendNotificationEmail: sendNotification ?? true,
548
+ ...(notify === undefined ? {} : { sendNotificationEmail: notify }),
531
549
  emailMessage,
532
550
  transferOwnership: transferOwnership ?? false,
533
551
  supportsAllDrives: true,
@@ -92,7 +92,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
92
92
  registry.registerMeta('google_api_call', {
93
93
  description: 'Invoke any Google Workspace REST method by Discovery id (escape hatch for operations without a ' +
94
94
  'dedicated tool). Find methods with google_api_search first. Subject to the same write-control ' +
95
- 'policy as named tools.',
95
+ 'policy as named tools. Returns JSON only — for binary/file content (media downloads, ' +
96
+ 'drive.files.export) use drive_download / drive_export instead.',
96
97
  inputSchema: {
97
98
  account: accountEnum.describe('Google account alias'),
98
99
  api: z.string().describe(`API alias: ${apiList}`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-google-multi",
3
- "version": "5.2.0",
3
+ "version": "5.2.1-alpha.1",
4
4
  "description": "Local MCP server for Google Workspace (Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Search Console, +Forms/Chat/Admin) across multiple accounts — OAuth-only, encrypted token storage, deny-by-default writes.",
5
5
  "type": "module",
6
6
  "license": "MIT",