mcp-google-multi 5.1.0-alpha.4 → 5.1.0-alpha.5
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 +2 -0
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +5 -4
- package/dist/tools/drive.d.ts +10 -0
- package/dist/tools/drive.js +282 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,8 @@ Every read tool's `account` argument also accepts `"*"` (all configured accounts
|
|
|
96
96
|
|
|
97
97
|
Fan-out is **read-only by design** (write tools take exactly one account), and the three read tools that save files to disk (`drive_download`, `drive_export`, `gmail_download_attachment`) are excluded so parallel accounts can't clobber the same path. `account_list` shows what's configured: alias, email, token health (`ok` / `expired_refreshable` / `needs_reauth` / `missing` / `decrypt_error`), and granted-vs-configured scopes — without ever touching token values.
|
|
98
98
|
|
|
99
|
+
`drive_transfer` copies or moves a file between two of your accounts: server-side share+copy when possible (the temporary read grant on the source is revoked right after; the copy gets a clean name, never "Copy of"), download+upload as the fallback. `move: true` trashes the source after a successful copy — that part is **delete-gated by write-control**, so `safe-writes` can copy but never move. Comments, revision history, and permissions don't transfer; if the fallback has to change the format (Drawings export as PNG), the result is flagged `lossy` and a requested move keeps the source intact. Native files over Google's 10MB export cap and binaries over 1GB can't take the fallback path.
|
|
100
|
+
|
|
99
101
|
## Escape hatch: any Workspace REST method
|
|
100
102
|
|
|
101
103
|
Two eager tools cover everything the curated set doesn't: `google_api_search` finds any method in Google's API Discovery index (including APIs with no dedicated tools here, like Slides), and `google_api_call` invokes a method by its Discovery id (`drive.revisions.list`, `slides.presentations.create`, …) with path/query params and a JSON body. Calls run through your account's OAuth client and the **same write-control policy** as named tools: the read/create/update/delete class is derived from the method's HTTP verb and name (POST deletes like `batchDelete`/`clear` count as deletes), and policy globs/`GOOGLE_TOOLSETS` match the same service names as named tools (`people` counts as `contacts`, `admin_*` as `admin`; `slides`/`driveactivity`/`drivelabels`/`groupssettings` have no named service and are always available).
|
package/dist/registry.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare function inferCud(name: string): Cud;
|
|
|
21
21
|
export declare class ToolRegistry {
|
|
22
22
|
private readonly server;
|
|
23
23
|
readonly tools: ToolEntry[];
|
|
24
|
+
readonly policy: Policy;
|
|
24
25
|
readonly registerTool: McpServer['registerTool'];
|
|
25
26
|
private readonly revealed;
|
|
26
27
|
private readonly jsonSchemaCache;
|
package/dist/registry.js
CHANGED
|
@@ -5,12 +5,12 @@ import { compactResult, trimEnabled } from './trim.js';
|
|
|
5
5
|
import { fanoutAccountField, invalidAccountsResult, parseAccountSelector, runFanout } from './fanout.js';
|
|
6
6
|
const CUD_OVERRIDES = {
|
|
7
7
|
drive_untrash: 'update',
|
|
8
|
+
drive_transfer: 'create',
|
|
8
9
|
};
|
|
9
10
|
const SERVICE_OVERRIDES = {
|
|
10
11
|
reports_activities_list: 'admin',
|
|
11
12
|
};
|
|
12
|
-
//
|
|
13
|
-
// across accounts would clobber (last write wins).
|
|
13
|
+
// read tools that write local files — same savePath fanned across accounts would clobber
|
|
14
14
|
const FANOUT_EXCLUDE = new Set(['gmail_download_attachment', 'drive_download', 'drive_export']);
|
|
15
15
|
function isAccountEnum(field) {
|
|
16
16
|
return field?._zod?.def?.type === 'enum';
|
|
@@ -33,6 +33,7 @@ export function inferCud(name) {
|
|
|
33
33
|
export class ToolRegistry {
|
|
34
34
|
server;
|
|
35
35
|
tools = [];
|
|
36
|
+
policy;
|
|
36
37
|
registerTool;
|
|
37
38
|
revealed = new Set();
|
|
38
39
|
jsonSchemaCache = new Map();
|
|
@@ -40,6 +41,7 @@ export class ToolRegistry {
|
|
|
40
41
|
registeringMeta = false;
|
|
41
42
|
constructor(server, policy) {
|
|
42
43
|
this.server = server;
|
|
44
|
+
this.policy = policy;
|
|
43
45
|
this.registerTool = ((name, config, handler) => {
|
|
44
46
|
const service = SERVICE_OVERRIDES[name] ?? (name.includes('_') ? name.slice(0, name.indexOf('_')) : name);
|
|
45
47
|
const cud = inferCud(name);
|
|
@@ -49,8 +51,7 @@ export class ToolRegistry {
|
|
|
49
51
|
destructiveHint: cud === 'delete' || cud === 'update',
|
|
50
52
|
...config.annotations,
|
|
51
53
|
};
|
|
52
|
-
//
|
|
53
|
-
// registry cud is "read" but it executes arbitrary writes.
|
|
54
|
+
// never fan out meta tools: google_api_call infers cud=read but executes writes
|
|
54
55
|
let inputShape = config.inputSchema ?? {};
|
|
55
56
|
let baseHandler = handler;
|
|
56
57
|
if (cud === 'read' && !this.registeringMeta && !FANOUT_EXCLUDE.has(name) && isAccountEnum(inputShape.account)) {
|
package/dist/tools/drive.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
2
|
export declare function registerDriveTools(server: ToolRegistry): void;
|
|
3
|
+
export type TransferPlan = {
|
|
4
|
+
kind: 'binary';
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'native';
|
|
7
|
+
exportMime: string;
|
|
8
|
+
convertTo?: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'unsupported';
|
|
11
|
+
};
|
|
12
|
+
export declare function transferExportPlan(mimeType: string | null | undefined): TransferPlan;
|
package/dist/tools/drive.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { coerceArray, coerceBoolean } from './_coerce.js';
|
|
3
3
|
import { google } from 'googleapis';
|
|
4
|
-
import { ACCOUNTS } from '../accounts.js';
|
|
4
|
+
import { ACCOUNTS, ACCOUNT_CONFIG } from '../accounts.js';
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError } from './_errors.js';
|
|
7
|
+
import { isAllowed, writeDisabledResult } from '../write-control.js';
|
|
7
8
|
import { capText } from '../trim.js';
|
|
8
9
|
import * as fs from 'fs';
|
|
9
10
|
import * as path from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import * as crypto from 'crypto';
|
|
10
13
|
import { pipeline } from 'node:stream/promises';
|
|
11
14
|
import mime from 'mime-types';
|
|
12
15
|
const accountEnum = z.enum(ACCOUNTS);
|
|
13
16
|
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
|
17
|
+
const FALLBACK_MAX_BYTES = 1024 * 1024 * 1024; // 1GB
|
|
14
18
|
const GOOGLE_WORKSPACE_TYPES = new Set([
|
|
15
19
|
'application/vnd.google-apps.document',
|
|
16
20
|
'application/vnd.google-apps.spreadsheet',
|
|
@@ -1067,6 +1071,134 @@ export function registerDriveTools(server) {
|
|
|
1067
1071
|
return handleDriveError(error, account);
|
|
1068
1072
|
}
|
|
1069
1073
|
});
|
|
1074
|
+
// ─── Cross-account transfer ────────────────────────────────────────────
|
|
1075
|
+
server.registerTool('drive_transfer', {
|
|
1076
|
+
description: 'Copy or move a file from one configured account to another (server-side share+copy, with a ' +
|
|
1077
|
+
'download+upload fallback). The copy is owned by the target account with a clean name ' +
|
|
1078
|
+
'(no "Copy of"); the temporary share on the source is revoked afterwards. Comments, ' +
|
|
1079
|
+
'revision history, and permissions do not transfer. move=true trashes the source after a ' +
|
|
1080
|
+
'successful copy and requires deletes to be allowed by write-control.',
|
|
1081
|
+
inputSchema: {
|
|
1082
|
+
fromAccount: accountEnum.describe('Source account alias'),
|
|
1083
|
+
toAccount: accountEnum.describe('Target account alias'),
|
|
1084
|
+
fileId: z.string().describe('File ID in the source account (folders are not supported)'),
|
|
1085
|
+
parentFolderId: z.string().optional().describe('Target folder ID (default: target My Drive root)'),
|
|
1086
|
+
newName: z.string().optional().describe('Rename the copy (default: keep the source name)'),
|
|
1087
|
+
move: coerceBoolean.optional().describe('Trash the source after a successful copy (delete-gated)'),
|
|
1088
|
+
},
|
|
1089
|
+
}, async ({ fromAccount, toAccount, fileId, parentFolderId, newName, move }) => {
|
|
1090
|
+
// op candidate is transfer_move, not transfer: allowing "drive:transfer" must not grant the delete
|
|
1091
|
+
const moveRef = { name: 'drive_transfer_move', service: 'drive', cud: 'delete' };
|
|
1092
|
+
if (move && !isAllowed(moveRef, server.policy)) {
|
|
1093
|
+
return writeDisabledResult(moveRef, server.policy);
|
|
1094
|
+
}
|
|
1095
|
+
if (fromAccount === toAccount) {
|
|
1096
|
+
return {
|
|
1097
|
+
content: [{
|
|
1098
|
+
type: 'text',
|
|
1099
|
+
text: JSON.stringify({
|
|
1100
|
+
error: 'validation_error',
|
|
1101
|
+
message: 'fromAccount and toAccount are the same.',
|
|
1102
|
+
hint: 'Use drive_copy to duplicate a file within one account.',
|
|
1103
|
+
retriable: false,
|
|
1104
|
+
}),
|
|
1105
|
+
}],
|
|
1106
|
+
isError: true,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
let activeAccount = fromAccount;
|
|
1110
|
+
try {
|
|
1111
|
+
const sourceAuth = await getClient(fromAccount);
|
|
1112
|
+
const sourceDrive = google.drive({ version: 'v3', auth: sourceAuth });
|
|
1113
|
+
activeAccount = toAccount;
|
|
1114
|
+
const targetAuth = await getClient(toAccount);
|
|
1115
|
+
const targetDrive = google.drive({ version: 'v3', auth: targetAuth });
|
|
1116
|
+
activeAccount = fromAccount;
|
|
1117
|
+
const meta = await sourceDrive.files.get({
|
|
1118
|
+
fileId,
|
|
1119
|
+
fields: 'id,name,mimeType,size',
|
|
1120
|
+
supportsAllDrives: true,
|
|
1121
|
+
});
|
|
1122
|
+
if (meta.data.mimeType === 'application/vnd.google-apps.folder') {
|
|
1123
|
+
return {
|
|
1124
|
+
content: [{
|
|
1125
|
+
type: 'text',
|
|
1126
|
+
text: JSON.stringify({
|
|
1127
|
+
error: 'validation_error',
|
|
1128
|
+
message: 'Folders cannot be transferred.',
|
|
1129
|
+
hint: 'Transfer files individually, or create the folder on the target with drive_create_folder.',
|
|
1130
|
+
retriable: false,
|
|
1131
|
+
}),
|
|
1132
|
+
}],
|
|
1133
|
+
isError: true,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
const intendedName = newName ?? meta.data.name ?? 'transferred-file';
|
|
1137
|
+
const targetEmail = ACCOUNT_CONFIG[toAccount].email;
|
|
1138
|
+
const finish = async (data, strategy, flags) => {
|
|
1139
|
+
let moveFailed = false;
|
|
1140
|
+
let moveSkipped = false;
|
|
1141
|
+
if (move && flags.lossy) {
|
|
1142
|
+
moveSkipped = true;
|
|
1143
|
+
}
|
|
1144
|
+
else if (move) {
|
|
1145
|
+
activeAccount = fromAccount;
|
|
1146
|
+
try {
|
|
1147
|
+
await sourceDrive.files.update({ fileId, requestBody: { trashed: true }, supportsAllDrives: true });
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
moveFailed = true;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return transferResult(data, strategy, fromAccount, toAccount, {
|
|
1154
|
+
...flags,
|
|
1155
|
+
moved: move ? !moveFailed && !moveSkipped : undefined,
|
|
1156
|
+
moveFailed,
|
|
1157
|
+
moveSkipped,
|
|
1158
|
+
});
|
|
1159
|
+
};
|
|
1160
|
+
try {
|
|
1161
|
+
const { data, cleanupFailed, renameFailed } = await shareAndCopy(sourceDrive, targetDrive, fileId, targetEmail, intendedName, parentFolderId);
|
|
1162
|
+
return await finish(data, 'share_copy', { cleanupFailed, renameFailed });
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
// fall through to download+upload
|
|
1166
|
+
}
|
|
1167
|
+
const plan = transferExportPlan(meta.data.mimeType);
|
|
1168
|
+
if (plan.kind === 'unsupported') {
|
|
1169
|
+
return {
|
|
1170
|
+
content: [{
|
|
1171
|
+
type: 'text',
|
|
1172
|
+
text: JSON.stringify({
|
|
1173
|
+
error: 'unsupported_type',
|
|
1174
|
+
message: `Cannot transfer "${meta.data.mimeType}": share+copy was blocked and this native type has no export fallback.`,
|
|
1175
|
+
retriable: false,
|
|
1176
|
+
}),
|
|
1177
|
+
}],
|
|
1178
|
+
isError: true,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
if (plan.kind === 'binary' && Number(meta.data.size ?? 0) > FALLBACK_MAX_BYTES) {
|
|
1182
|
+
return {
|
|
1183
|
+
content: [{
|
|
1184
|
+
type: 'text',
|
|
1185
|
+
text: JSON.stringify({
|
|
1186
|
+
error: 'too_large',
|
|
1187
|
+
message: `Share+copy was blocked and the file (${meta.data.size} bytes) exceeds the ${FALLBACK_MAX_BYTES} byte download+upload fallback cap (uploads are not resumable).`,
|
|
1188
|
+
hint: 'Use drive_download + drive_upload manually, or fix sharing between the accounts.',
|
|
1189
|
+
retriable: false,
|
|
1190
|
+
}),
|
|
1191
|
+
}],
|
|
1192
|
+
isError: true,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const data = await downloadAndUpload(sourceDrive, targetDrive, fileId, meta.data.mimeType ?? null, plan, intendedName, parentFolderId, (side) => { activeAccount = (side === 'source' ? fromAccount : toAccount); });
|
|
1196
|
+
return await finish(data, 'download_upload', { lossy: plan.kind === 'native' && !plan.convertTo });
|
|
1197
|
+
}
|
|
1198
|
+
catch (error) {
|
|
1199
|
+
return handleDriveError(error, activeAccount);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1070
1202
|
// ─── About ─────────────────────────────────────────────────────────────
|
|
1071
1203
|
server.registerTool('drive_get_about', {
|
|
1072
1204
|
description: 'Get Drive storage quota, user display name, and email for an account',
|
|
@@ -1092,3 +1224,152 @@ export function registerDriveTools(server) {
|
|
|
1092
1224
|
function handleDriveError(error, account) {
|
|
1093
1225
|
return handleGoogleApiError(error, account);
|
|
1094
1226
|
}
|
|
1227
|
+
export function transferExportPlan(mimeType) {
|
|
1228
|
+
if (!mimeType || !mimeType.startsWith('application/vnd.google-apps.'))
|
|
1229
|
+
return { kind: 'binary' };
|
|
1230
|
+
switch (mimeType) {
|
|
1231
|
+
case 'application/vnd.google-apps.document':
|
|
1232
|
+
return {
|
|
1233
|
+
kind: 'native',
|
|
1234
|
+
exportMime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
1235
|
+
convertTo: mimeType,
|
|
1236
|
+
};
|
|
1237
|
+
case 'application/vnd.google-apps.spreadsheet':
|
|
1238
|
+
return {
|
|
1239
|
+
kind: 'native',
|
|
1240
|
+
exportMime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1241
|
+
convertTo: mimeType,
|
|
1242
|
+
};
|
|
1243
|
+
case 'application/vnd.google-apps.presentation':
|
|
1244
|
+
return {
|
|
1245
|
+
kind: 'native',
|
|
1246
|
+
exportMime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
1247
|
+
convertTo: mimeType,
|
|
1248
|
+
};
|
|
1249
|
+
case 'application/vnd.google-apps.drawing':
|
|
1250
|
+
return { kind: 'native', exportMime: 'image/png' };
|
|
1251
|
+
default:
|
|
1252
|
+
return { kind: 'unsupported' };
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function transferResult(data, strategy, from, to, flags) {
|
|
1256
|
+
return {
|
|
1257
|
+
content: [{
|
|
1258
|
+
type: 'text',
|
|
1259
|
+
text: JSON.stringify({
|
|
1260
|
+
id: data.id,
|
|
1261
|
+
name: data.name,
|
|
1262
|
+
mimeType: data.mimeType,
|
|
1263
|
+
webViewLink: data.webViewLink,
|
|
1264
|
+
strategy,
|
|
1265
|
+
from,
|
|
1266
|
+
to,
|
|
1267
|
+
...(flags.lossy ? { lossy: true } : {}),
|
|
1268
|
+
...(flags.moved !== undefined ? { moved: flags.moved } : {}),
|
|
1269
|
+
...(flags.moveSkipped ? { hint: 'Move skipped: the fallback changed the file format (e.g. Drawing → PNG), so the editable source was kept. Trash it manually if intended.' } : {}),
|
|
1270
|
+
...(flags.moveFailed ? { moveFailed: true, hint: 'Copy succeeded but trashing the source failed — the source file is still present.' } : {}),
|
|
1271
|
+
...(flags.renameFailed ? { renameFailed: true } : {}),
|
|
1272
|
+
...(flags.cleanupFailed ? { cleanupFailed: true } : {}),
|
|
1273
|
+
}, null, 2),
|
|
1274
|
+
}],
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
async function shareAndCopy(sourceDrive, targetDrive, fileId, targetEmail, intendedName, parentFolderId) {
|
|
1278
|
+
const perm = await sourceDrive.permissions.create({
|
|
1279
|
+
fileId,
|
|
1280
|
+
requestBody: {
|
|
1281
|
+
type: 'user',
|
|
1282
|
+
role: 'reader',
|
|
1283
|
+
emailAddress: targetEmail,
|
|
1284
|
+
expirationTime: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
1285
|
+
},
|
|
1286
|
+
sendNotificationEmail: false,
|
|
1287
|
+
supportsAllDrives: true,
|
|
1288
|
+
fields: 'id',
|
|
1289
|
+
});
|
|
1290
|
+
let cleanupFailed = false;
|
|
1291
|
+
let renameFailed = false;
|
|
1292
|
+
let data;
|
|
1293
|
+
try {
|
|
1294
|
+
const copy = await targetDrive.files.copy({
|
|
1295
|
+
fileId,
|
|
1296
|
+
requestBody: {
|
|
1297
|
+
name: intendedName,
|
|
1298
|
+
// cross-account copy with omitted parents lands in an indeterminate place
|
|
1299
|
+
parents: [parentFolderId ?? 'root'],
|
|
1300
|
+
},
|
|
1301
|
+
supportsAllDrives: true,
|
|
1302
|
+
fields: 'id,name,mimeType,webViewLink',
|
|
1303
|
+
});
|
|
1304
|
+
data = copy.data;
|
|
1305
|
+
if (data.name !== intendedName && data.id) {
|
|
1306
|
+
// copy already succeeded — a failed cosmetic rename must not trigger the fallback
|
|
1307
|
+
try {
|
|
1308
|
+
const renamed = await targetDrive.files.update({
|
|
1309
|
+
fileId: data.id,
|
|
1310
|
+
requestBody: { name: intendedName },
|
|
1311
|
+
supportsAllDrives: true,
|
|
1312
|
+
fields: 'id,name,mimeType,webViewLink',
|
|
1313
|
+
});
|
|
1314
|
+
data = renamed.data;
|
|
1315
|
+
}
|
|
1316
|
+
catch {
|
|
1317
|
+
renameFailed = true;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
finally {
|
|
1322
|
+
if (perm.data.id) {
|
|
1323
|
+
try {
|
|
1324
|
+
await sourceDrive.permissions.delete({ fileId, permissionId: perm.data.id, supportsAllDrives: true });
|
|
1325
|
+
}
|
|
1326
|
+
catch {
|
|
1327
|
+
cleanupFailed = true;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return { data, cleanupFailed, renameFailed };
|
|
1332
|
+
}
|
|
1333
|
+
async function downloadAndUpload(sourceDrive, targetDrive, fileId, sourceMime, plan, intendedName, parentFolderId, onSide) {
|
|
1334
|
+
const tmp = path.join(os.tmpdir(), `gmulti-transfer-${crypto.randomBytes(8).toString('hex')}`);
|
|
1335
|
+
try {
|
|
1336
|
+
onSide('source');
|
|
1337
|
+
let download;
|
|
1338
|
+
try {
|
|
1339
|
+
download =
|
|
1340
|
+
plan.kind === 'native'
|
|
1341
|
+
? await sourceDrive.files.export({ fileId, mimeType: plan.exportMime }, { responseType: 'stream' })
|
|
1342
|
+
: await sourceDrive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'stream' });
|
|
1343
|
+
}
|
|
1344
|
+
catch (err) {
|
|
1345
|
+
if (plan.kind === 'native' && /too large/i.test(err.message ?? '')) {
|
|
1346
|
+
throw new Error("Google caps native-file exports at 10MB and share+copy was blocked — use drive_export + drive_upload manually, or google_api_call (drive.files.download).", { cause: err });
|
|
1347
|
+
}
|
|
1348
|
+
throw err;
|
|
1349
|
+
}
|
|
1350
|
+
try {
|
|
1351
|
+
await pipeline(download.data, fs.createWriteStream(tmp, { mode: 0o600 }));
|
|
1352
|
+
}
|
|
1353
|
+
catch (err) {
|
|
1354
|
+
throw new Error(`Local temp-file write failed (not a Google error): ${err.message}`, { cause: err });
|
|
1355
|
+
}
|
|
1356
|
+
onSide('target');
|
|
1357
|
+
const created = await targetDrive.files.create({
|
|
1358
|
+
requestBody: {
|
|
1359
|
+
name: intendedName,
|
|
1360
|
+
...(plan.kind === 'native' && plan.convertTo ? { mimeType: plan.convertTo } : {}),
|
|
1361
|
+
...(parentFolderId ? { parents: [parentFolderId] } : {}),
|
|
1362
|
+
},
|
|
1363
|
+
media: {
|
|
1364
|
+
mimeType: plan.kind === 'native' ? plan.exportMime : (sourceMime ?? 'application/octet-stream'),
|
|
1365
|
+
body: fs.createReadStream(tmp),
|
|
1366
|
+
},
|
|
1367
|
+
supportsAllDrives: true,
|
|
1368
|
+
fields: 'id,name,mimeType,webViewLink',
|
|
1369
|
+
});
|
|
1370
|
+
return created.data;
|
|
1371
|
+
}
|
|
1372
|
+
finally {
|
|
1373
|
+
await fs.promises.unlink(tmp).catch(() => { });
|
|
1374
|
+
}
|
|
1375
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "5.1.0-alpha.
|
|
3
|
+
"version": "5.1.0-alpha.5",
|
|
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",
|