mcp-google-gdrive 2.0.0 → 2.2.0
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 +36 -3
- package/SPEC.md +8 -6
- package/package.json +2 -1
- package/src/index.js +170 -37
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ An [MCP](https://modelcontextprotocol.io/) server for Google Drive, Docs, Sheets
|
|
|
19
19
|
| `gdrive_list_files` | List files with optional search query and folder filtering |
|
|
20
20
|
| `gdrive_get_metadata` | Get detailed metadata for a file by ID |
|
|
21
21
|
| `gdrive_read_file` | Read file content with automatic Workspace format conversion |
|
|
22
|
+
| `gdrive_download_file` | Download a file to local disk (supports Workspace export) |
|
|
22
23
|
| `gdrive_create_file` | Create a new file with optional OCR on image/PDF uploads |
|
|
23
24
|
| `gdrive_update_file` | Update a file's content, name, or description |
|
|
24
25
|
| `gdrive_copy_file` | Copy a file, optionally to a different folder |
|
|
@@ -64,7 +65,7 @@ An [MCP](https://modelcontextprotocol.io/) server for Google Drive, Docs, Sheets
|
|
|
64
65
|
|
|
65
66
|
| Tool | Description |
|
|
66
67
|
|------|-------------|
|
|
67
|
-
| `gdrive_export_doc` | Export a Google Doc (markdown, html, text, docx, pdf) |
|
|
68
|
+
| `gdrive_export_doc` | Export a Google Doc (markdown, html, text, docx, pdf). Use `savePath` to save binary formats to disk |
|
|
68
69
|
| `gdrive_create_doc` | Create a Google Doc from Markdown content |
|
|
69
70
|
| `gdrive_update_doc` | Replace a Google Doc's content with Markdown |
|
|
70
71
|
|
|
@@ -73,7 +74,7 @@ An [MCP](https://modelcontextprotocol.io/) server for Google Drive, Docs, Sheets
|
|
|
73
74
|
| Tool | Description |
|
|
74
75
|
|------|-------------|
|
|
75
76
|
| `gdrive_create_sheet` | Create a new spreadsheet with named tabs |
|
|
76
|
-
| `gdrive_export_sheet` | Export a sheet to CSV, JSON, or XLSX |
|
|
77
|
+
| `gdrive_export_sheet` | Export a sheet to CSV, JSON, or XLSX. Use `savePath` to save XLSX to disk |
|
|
77
78
|
| `gdrive_list_sheets` | List all sheets/tabs in a spreadsheet |
|
|
78
79
|
| `gdrive_read_sheet_range` | Read a specific range from any tab (A1 notation) |
|
|
79
80
|
| `gdrive_write_sheet_range` | Write data to a specific range |
|
|
@@ -83,9 +84,41 @@ An [MCP](https://modelcontextprotocol.io/) server for Google Drive, Docs, Sheets
|
|
|
83
84
|
| Tool | Description |
|
|
84
85
|
|------|-------------|
|
|
85
86
|
| `gdrive_create_slides` | Create a new presentation |
|
|
86
|
-
| `gdrive_export_slides` | Export slides to text, PDF, or PPTX |
|
|
87
|
+
| `gdrive_export_slides` | Export slides to text, PDF, or PPTX. Use `savePath` to save binary formats to disk |
|
|
87
88
|
| `gdrive_get_slide_thumbnail` | Get a thumbnail image URL for a specific slide |
|
|
88
89
|
|
|
90
|
+
## What's New in v2.2
|
|
91
|
+
|
|
92
|
+
- **`outputFormat` parameter** — 12 read/list tools now accept `outputFormat: "json" | "yaml" | "text"`. Default is `json` (backwards compatible). Use `yaml` for structured output or `text` for compact human-readable tables. Supported on: `list_accounts`, `list_files`, `list_folder`, `list_trash`, `list_permissions`, `list_labels`, `list_file_labels`, `list_sheets`, `get_metadata`, `read_sheet_range`, `get_slide_thumbnail`, `search_files`.
|
|
93
|
+
|
|
94
|
+
### v2.1
|
|
95
|
+
|
|
96
|
+
- **`gdrive_download_file`** — Download any file from Drive to a local disk path. Workspace files (Docs, Sheets, Slides) are automatically exported to the specified format (pdf, docx, xlsx, pptx, csv, txt, html, or markdown).
|
|
97
|
+
- **`savePath` on export tools** — `gdrive_export_doc`, `gdrive_export_sheet`, and `gdrive_export_slides` now accept an optional `savePath` parameter. When provided, binary exports (docx, pdf, xlsx, pptx) are saved to disk instead of returned as base64. Omitting `savePath` preserves the v2.0 behavior.
|
|
98
|
+
- **`gdrive_list_accounts` email resolution** — Accounts migrated from v0.x that showed "migrated" instead of an email address now auto-resolve via the Google userinfo API on first call.
|
|
99
|
+
- **Documentation fixes** — `gdrive_empty_trash` notes eventual consistency; `gdrive_transfer_ownership` notes cross-org limitation.
|
|
100
|
+
|
|
101
|
+
## Output Formats
|
|
102
|
+
|
|
103
|
+
All read and list tools support an `outputFormat` parameter:
|
|
104
|
+
|
|
105
|
+
| Format | Description | Use Case |
|
|
106
|
+
|--------|-------------|----------|
|
|
107
|
+
| `json` | Pretty-printed JSON (default) | Machine consumption, API pipelines |
|
|
108
|
+
| `yaml` | YAML serialization | Human-readable structured data, config files |
|
|
109
|
+
| `text` | Compact aligned tables and key-value pairs | Quick scanning, terminal output, token-efficient LLM context |
|
|
110
|
+
|
|
111
|
+
Example: `gdrive_list_files({ query: "name contains 'report'", outputFormat: "text" })` returns:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
files:
|
|
115
|
+
id name mimeType modifiedTime size
|
|
116
|
+
------------- ---------------- --------------- ------------ ----
|
|
117
|
+
abc123def456 Q1 Report.pdf application/pdf 2026-04-01 1024
|
|
118
|
+
ghi789jkl012 Q2 Report.docx application/pdf 2026-03-15 2048
|
|
119
|
+
resultCount: 2
|
|
120
|
+
```
|
|
121
|
+
|
|
89
122
|
## Prerequisites
|
|
90
123
|
|
|
91
124
|
- Node.js 18+
|
package/SPEC.md
CHANGED
|
@@ -9,17 +9,19 @@ MCP server providing Claude Code with full read/write access to a personal Googl
|
|
|
9
9
|
| Item | Value |
|
|
10
10
|
|------|-------|
|
|
11
11
|
| Package name | mcp-google-gdrive |
|
|
12
|
-
| Version |
|
|
13
|
-
| Runtime | Node.js 18+ (ESM)
|
|
12
|
+
| Version | 2.2.0 |
|
|
13
|
+
| Runtime | Node.js 18+ (ESM) |
|
|
14
14
|
| MCP SDK | @modelcontextprotocol/sdk ^1.26.0 |
|
|
15
15
|
| Validation | zod ^3.25.0 |
|
|
16
|
-
| Auth | Google OAuth2 (desktop app flow) |
|
|
17
|
-
| API | Google Drive API v3 |
|
|
18
|
-
|
|
|
19
|
-
|
|
|
16
|
+
| Auth | Google OAuth2 (desktop app flow), multi-account |
|
|
17
|
+
| API | Google Drive API v3, Docs v1, Sheets v4, Slides v1, Labels v2 |
|
|
18
|
+
| Deps | googleapis ^144.0.0, turndown ^7.2.0, js-yaml ^4.1.1 |
|
|
19
|
+
| Source | github.com/sleepytimeshon/mcp-google-gdrive |
|
|
20
|
+
| npm | npmjs.com/package/mcp-google-gdrive |
|
|
20
21
|
| Entry point | src/index.js |
|
|
21
22
|
| Transport | stdio (JSON-RPC) |
|
|
22
23
|
| License | MIT |
|
|
24
|
+
| Tools | 38 across 9 categories |
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-gdrive",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "MCP server for Google Drive, Docs, Sheets, and Slides with multi-account support, permissions, labels, and full CRUD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
35
35
|
"googleapis": "^144.0.0",
|
|
36
|
+
"js-yaml": "^4.1.1",
|
|
36
37
|
"turndown": "^7.2.0",
|
|
37
38
|
"zod": "^3.25.0"
|
|
38
39
|
}
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { google } from "googleapis";
|
|
7
7
|
import TurndownService from "turndown";
|
|
8
|
+
import yaml from "js-yaml";
|
|
8
9
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
+
import { join, resolve, dirname } from "node:path";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { existsSync } from "node:fs";
|
|
12
13
|
import { createInterface } from "node:readline";
|
|
@@ -228,6 +229,30 @@ function sanitizeDriveId(id) {
|
|
|
228
229
|
return id;
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
function validateSavePath(savePath) {
|
|
233
|
+
const resolved = resolve(savePath);
|
|
234
|
+
if (resolved.includes("\0")) {
|
|
235
|
+
throw new Error("Invalid save path: null bytes not allowed");
|
|
236
|
+
}
|
|
237
|
+
return resolved;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function saveToDisk(data, savePath) {
|
|
241
|
+
const resolved = validateSavePath(savePath);
|
|
242
|
+
await mkdir(dirname(resolved), { recursive: true });
|
|
243
|
+
const buf = Buffer.from(data);
|
|
244
|
+
await writeFile(resolved, buf);
|
|
245
|
+
return { path: resolved, size: buf.length };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function handleBinaryExport(data, format, savePath) {
|
|
249
|
+
if (savePath) {
|
|
250
|
+
const result = await saveToDisk(data, savePath);
|
|
251
|
+
return jsonResponse({ exported: format.toUpperCase(), path: result.path, size: result.size });
|
|
252
|
+
}
|
|
253
|
+
return binaryResponse(data, format.toUpperCase());
|
|
254
|
+
}
|
|
255
|
+
|
|
231
256
|
function markdownToHtml(md) {
|
|
232
257
|
return md
|
|
233
258
|
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
|
|
@@ -239,6 +264,41 @@ function markdownToHtml(md) {
|
|
|
239
264
|
.replace(/^/, "<p>").replace(/$/, "</p>");
|
|
240
265
|
}
|
|
241
266
|
|
|
267
|
+
// ─── Output Format ──────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
const OUTPUT_FORMAT_SCHEMA = z.enum(["json", "yaml", "text"]).optional().default("json").describe("Output format: json (default), yaml, or text (compact human-readable)");
|
|
270
|
+
|
|
271
|
+
function dataToText(data) {
|
|
272
|
+
if (Array.isArray(data) && data.length > 0 && typeof data[0] === "object") {
|
|
273
|
+
const keys = Object.keys(data[0]);
|
|
274
|
+
const widths = keys.map(k => k.length);
|
|
275
|
+
for (const row of data) { keys.forEach((k, i) => { const len = String(row[k] ?? "").length; if (len > widths[i]) widths[i] = len; }); }
|
|
276
|
+
const pad = (val, i) => String(val ?? "").padEnd(widths[i]);
|
|
277
|
+
const header = keys.map((k, i) => pad(k, i)).join(" ");
|
|
278
|
+
const sep = widths.map(w => "-".repeat(w)).join(" ");
|
|
279
|
+
const rows = data.map(row => keys.map((k, i) => pad(row[k], i)).join(" "));
|
|
280
|
+
return [header, sep, ...rows].join("\n");
|
|
281
|
+
}
|
|
282
|
+
if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
|
|
283
|
+
return data.map(row => (row || []).map(c => String(c ?? "")).join("\t")).join("\n");
|
|
284
|
+
}
|
|
285
|
+
if (typeof data === "object" && data !== null) {
|
|
286
|
+
return Object.entries(data).map(([k, v]) => {
|
|
287
|
+
if (Array.isArray(v) && v.length > 0 && typeof v[0] === "object") return `${k}:\n${dataToText(v).split("\n").map(l => " " + l).join("\n")}`;
|
|
288
|
+
if (Array.isArray(v)) return `${k}: ${v.join(", ")}`;
|
|
289
|
+
if (typeof v === "object" && v !== null) return `${k}: ${JSON.stringify(v)}`;
|
|
290
|
+
return `${k}: ${v}`;
|
|
291
|
+
}).join("\n");
|
|
292
|
+
}
|
|
293
|
+
return String(data);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatOutput(data, format) {
|
|
297
|
+
if (format === "yaml") return textResponse(yaml.dump(data, { lineWidth: 120, noRefs: true }));
|
|
298
|
+
if (format === "text") return textResponse(dataToText(data));
|
|
299
|
+
return jsonResponse(data);
|
|
300
|
+
}
|
|
301
|
+
|
|
242
302
|
const FILE_FIELDS = "id,name,mimeType,size,createdTime,modifiedTime,parents,webViewLink,owners,shared,description,trashed,starred";
|
|
243
303
|
const LIST_FIELDS = "id,name,mimeType,modifiedTime,size,parents";
|
|
244
304
|
|
|
@@ -253,15 +313,34 @@ const server = new McpServer({
|
|
|
253
313
|
|
|
254
314
|
server.registerTool("gdrive_list_accounts", {
|
|
255
315
|
description: "List all configured Google accounts with their labels and email addresses.",
|
|
256
|
-
inputSchema: {
|
|
316
|
+
inputSchema: {
|
|
317
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
318
|
+
},
|
|
257
319
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
258
|
-
}, async () => {
|
|
320
|
+
}, async ({ outputFormat }) => {
|
|
259
321
|
try {
|
|
260
322
|
const config = await loadConfig();
|
|
323
|
+
const unresolved = Object.entries(config.accounts)
|
|
324
|
+
.filter(([, info]) => info.email === "migrated" || !info.email);
|
|
325
|
+
if (unresolved.length > 0) {
|
|
326
|
+
const results = await Promise.all(unresolved.map(async ([label]) => {
|
|
327
|
+
try {
|
|
328
|
+
const auth = await authorizeAccount(label);
|
|
329
|
+
const oauth2 = google.oauth2({ version: "v2", auth });
|
|
330
|
+
const userInfo = await oauth2.userinfo.get();
|
|
331
|
+
return { label, email: userInfo.data.email };
|
|
332
|
+
} catch { return null; }
|
|
333
|
+
}));
|
|
334
|
+
let configDirty = false;
|
|
335
|
+
for (const r of results) {
|
|
336
|
+
if (r?.email) { config.accounts[r.label].email = r.email; configDirty = true; }
|
|
337
|
+
}
|
|
338
|
+
if (configDirty) await saveConfig(config);
|
|
339
|
+
}
|
|
261
340
|
const accounts = Object.entries(config.accounts).map(([label, info]) => ({
|
|
262
341
|
label, email: info.email, active: label === config.active,
|
|
263
342
|
}));
|
|
264
|
-
return
|
|
343
|
+
return formatOutput({ accounts, activeAccount: config.active }, outputFormat);
|
|
265
344
|
} catch (err) { return errorResponse(err.message); }
|
|
266
345
|
});
|
|
267
346
|
|
|
@@ -311,10 +390,11 @@ server.registerTool("gdrive_list_files", {
|
|
|
311
390
|
pageSize: z.number().optional().default(20).describe("Results per page (max 100, default: 20)"),
|
|
312
391
|
pageToken: z.string().optional().describe("Token for next page of results"),
|
|
313
392
|
orderBy: z.string().optional().default("modifiedTime desc").describe("Sort order"),
|
|
393
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
314
394
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
315
395
|
},
|
|
316
396
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
317
|
-
}, async ({ query, folderId, pageSize, pageToken, orderBy, account }) => {
|
|
397
|
+
}, async ({ query, folderId, pageSize, pageToken, orderBy, outputFormat, account }) => {
|
|
318
398
|
try {
|
|
319
399
|
const { drive } = await getClients(account);
|
|
320
400
|
let q = "trashed = false";
|
|
@@ -325,7 +405,7 @@ server.registerTool("gdrive_list_files", {
|
|
|
325
405
|
orderBy: orderBy || "modifiedTime desc", fields: `nextPageToken,files(${LIST_FIELDS})`,
|
|
326
406
|
supportsAllDrives: true, includeItemsFromAllDrives: true,
|
|
327
407
|
});
|
|
328
|
-
return
|
|
408
|
+
return formatOutput({ files: res.data.files || [], nextPageToken: res.data.nextPageToken || null, resultCount: (res.data.files || []).length }, outputFormat);
|
|
329
409
|
} catch (err) { return errorResponse(err.message); }
|
|
330
410
|
});
|
|
331
411
|
|
|
@@ -333,14 +413,15 @@ server.registerTool("gdrive_get_metadata", {
|
|
|
333
413
|
description: "Get detailed metadata for a specific file in Google Drive.",
|
|
334
414
|
inputSchema: {
|
|
335
415
|
fileId: z.string().describe("Google Drive file ID"),
|
|
416
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
336
417
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
337
418
|
},
|
|
338
419
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
339
|
-
}, async ({ fileId, account }) => {
|
|
420
|
+
}, async ({ fileId, outputFormat, account }) => {
|
|
340
421
|
try {
|
|
341
422
|
const { drive } = await getClients(account);
|
|
342
423
|
const res = await drive.files.get({ fileId, fields: FILE_FIELDS, supportsAllDrives: true });
|
|
343
|
-
return
|
|
424
|
+
return formatOutput(res.data, outputFormat);
|
|
344
425
|
} catch (err) { return errorResponse(err.code === 404 ? `File not found: ${fileId}` : err.message); }
|
|
345
426
|
});
|
|
346
427
|
|
|
@@ -384,6 +465,46 @@ server.registerTool("gdrive_read_file", {
|
|
|
384
465
|
} catch (err) { return errorResponse(err.code === 404 ? `File not found: ${fileId}` : err.message); }
|
|
385
466
|
});
|
|
386
467
|
|
|
468
|
+
server.registerTool("gdrive_download_file", {
|
|
469
|
+
description: "Download a file from Google Drive to a local disk path. For Workspace files (Docs/Sheets/Slides), exports to the specified format first.",
|
|
470
|
+
inputSchema: {
|
|
471
|
+
fileId: z.string().describe("Google Drive file ID"),
|
|
472
|
+
savePath: z.string().describe("Local file path to save to (absolute or relative)"),
|
|
473
|
+
exportFormat: z.enum(["pdf", "docx", "xlsx", "pptx", "csv", "txt", "html", "markdown"]).optional().describe("Export format for Workspace files (default: pdf for Docs/Slides, csv for Sheets)"),
|
|
474
|
+
account: z.string().optional().describe("Account label (default: active account)"),
|
|
475
|
+
},
|
|
476
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
477
|
+
}, async ({ fileId, savePath, exportFormat, account }) => {
|
|
478
|
+
try {
|
|
479
|
+
const { drive } = await getClients(account);
|
|
480
|
+
const meta = await drive.files.get({ fileId, fields: "id,name,mimeType,size", supportsAllDrives: true });
|
|
481
|
+
const { name, mimeType, size } = meta.data;
|
|
482
|
+
|
|
483
|
+
const workspaceExportMap = {
|
|
484
|
+
"application/vnd.google-apps.document": { default: "pdf", mimeTypes: { pdf: "application/pdf", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", txt: "text/plain", html: "text/html", markdown: "text/html" } },
|
|
485
|
+
"application/vnd.google-apps.spreadsheet": { default: "csv", mimeTypes: { csv: "text/csv", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", pdf: "application/pdf" } },
|
|
486
|
+
"application/vnd.google-apps.presentation": { default: "pdf", mimeTypes: { pdf: "application/pdf", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", txt: "text/plain" } },
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const wsConfig = workspaceExportMap[mimeType];
|
|
490
|
+
let data;
|
|
491
|
+
|
|
492
|
+
if (wsConfig) {
|
|
493
|
+
const fmt = exportFormat || wsConfig.default;
|
|
494
|
+
const exportMime = wsConfig.mimeTypes[fmt];
|
|
495
|
+
if (!exportMime) return errorResponse(`Unsupported export format "${fmt}" for ${mimeType}. Available: ${Object.keys(wsConfig.mimeTypes).join(", ")}`);
|
|
496
|
+
const res = await drive.files.export({ fileId, mimeType: exportMime }, { responseType: "arraybuffer" });
|
|
497
|
+
data = res.data;
|
|
498
|
+
} else {
|
|
499
|
+
const res = await drive.files.get({ fileId, alt: "media", supportsAllDrives: true }, { responseType: "arraybuffer" });
|
|
500
|
+
data = res.data;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const result = await saveToDisk(data, savePath);
|
|
504
|
+
return jsonResponse({ downloaded: name, format: exportFormat || (wsConfig ? wsConfig.default : mimeType), path: result.path, size: result.size });
|
|
505
|
+
} catch (err) { return errorResponse(err.message); }
|
|
506
|
+
});
|
|
507
|
+
|
|
387
508
|
server.registerTool("gdrive_create_file", {
|
|
388
509
|
description: "Create a new file in Google Drive. Set ocrLanguage to trigger OCR on image/PDF uploads.",
|
|
389
510
|
inputSchema: {
|
|
@@ -480,10 +601,11 @@ server.registerTool("gdrive_search_files", {
|
|
|
480
601
|
modifiedAfter: z.string().optional().describe("ISO date — only files modified after this date"),
|
|
481
602
|
trashed: z.boolean().optional().default(false).describe("Include trashed files (default: false)"),
|
|
482
603
|
maxResults: z.number().optional().default(20).describe("Maximum results (default: 20)"),
|
|
604
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
483
605
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
484
606
|
},
|
|
485
607
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
486
|
-
}, async ({ text, mimeType, modifiedAfter, trashed, maxResults, account }) => {
|
|
608
|
+
}, async ({ text, mimeType, modifiedAfter, trashed, maxResults, outputFormat, account }) => {
|
|
487
609
|
try {
|
|
488
610
|
const { drive } = await getClients(account);
|
|
489
611
|
const parts = [`fullText contains '${text.replace(/'/g, "\\'")}'`];
|
|
@@ -495,7 +617,7 @@ server.registerTool("gdrive_search_files", {
|
|
|
495
617
|
fields: `files(${LIST_FIELDS},description)`,
|
|
496
618
|
supportsAllDrives: true, includeItemsFromAllDrives: true,
|
|
497
619
|
});
|
|
498
|
-
return
|
|
620
|
+
return formatOutput({ files: res.data.files || [], resultCount: (res.data.files || []).length }, outputFormat);
|
|
499
621
|
} catch (err) { return errorResponse(err.message); }
|
|
500
622
|
});
|
|
501
623
|
|
|
@@ -526,10 +648,11 @@ server.registerTool("gdrive_list_folder", {
|
|
|
526
648
|
pageSize: z.number().optional().default(50).describe("Results per page (max 100, default: 50)"),
|
|
527
649
|
pageToken: z.string().optional().describe("Token for next page"),
|
|
528
650
|
orderBy: z.string().optional().default("folder,name").describe("Sort order (default: folders first, then by name)"),
|
|
651
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
529
652
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
530
653
|
},
|
|
531
654
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
532
|
-
}, async ({ folderId, pageSize, pageToken, orderBy, account }) => {
|
|
655
|
+
}, async ({ folderId, pageSize, pageToken, orderBy, outputFormat, account }) => {
|
|
533
656
|
try {
|
|
534
657
|
const { drive } = await getClients(account);
|
|
535
658
|
const res = await drive.files.list({
|
|
@@ -538,7 +661,7 @@ server.registerTool("gdrive_list_folder", {
|
|
|
538
661
|
orderBy: orderBy || "folder,name", fields: `nextPageToken,files(${LIST_FIELDS})`,
|
|
539
662
|
supportsAllDrives: true, includeItemsFromAllDrives: true,
|
|
540
663
|
});
|
|
541
|
-
return
|
|
664
|
+
return formatOutput({ files: res.data.files || [], nextPageToken: res.data.nextPageToken || null }, outputFormat);
|
|
542
665
|
} catch (err) { return errorResponse(err.message); }
|
|
543
666
|
});
|
|
544
667
|
|
|
@@ -595,7 +718,7 @@ server.registerTool("gdrive_untrash_file", {
|
|
|
595
718
|
});
|
|
596
719
|
|
|
597
720
|
server.registerTool("gdrive_empty_trash", {
|
|
598
|
-
description: "Permanently delete ALL files in trash. Irreversible.",
|
|
721
|
+
description: "Permanently delete ALL files in trash. Irreversible. Note: Google returns success before files are fully purged (eventual consistency — files may still appear briefly in trash listings).",
|
|
599
722
|
inputSchema: {
|
|
600
723
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
601
724
|
},
|
|
@@ -613,10 +736,11 @@ server.registerTool("gdrive_list_trash", {
|
|
|
613
736
|
inputSchema: {
|
|
614
737
|
pageSize: z.number().optional().default(20).describe("Results per page (max 100, default: 20)"),
|
|
615
738
|
pageToken: z.string().optional().describe("Token for next page"),
|
|
739
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
616
740
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
617
741
|
},
|
|
618
742
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
619
|
-
}, async ({ pageSize, pageToken, account }) => {
|
|
743
|
+
}, async ({ pageSize, pageToken, outputFormat, account }) => {
|
|
620
744
|
try {
|
|
621
745
|
const { drive } = await getClients(account);
|
|
622
746
|
const res = await drive.files.list({
|
|
@@ -624,7 +748,7 @@ server.registerTool("gdrive_list_trash", {
|
|
|
624
748
|
pageToken: pageToken || undefined, fields: `nextPageToken,files(${LIST_FIELDS},trashedTime)`, orderBy: "modifiedTime desc",
|
|
625
749
|
supportsAllDrives: true, includeItemsFromAllDrives: true,
|
|
626
750
|
});
|
|
627
|
-
return
|
|
751
|
+
return formatOutput({ files: res.data.files || [], nextPageToken: res.data.nextPageToken || null }, outputFormat);
|
|
628
752
|
} catch (err) { return errorResponse(err.message); }
|
|
629
753
|
});
|
|
630
754
|
|
|
@@ -661,16 +785,17 @@ server.registerTool("gdrive_list_permissions", {
|
|
|
661
785
|
description: "List all permissions (sharing) on a file or folder.",
|
|
662
786
|
inputSchema: {
|
|
663
787
|
fileId: z.string().describe("File or folder ID"),
|
|
788
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
664
789
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
665
790
|
},
|
|
666
791
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
667
|
-
}, async ({ fileId, account }) => {
|
|
792
|
+
}, async ({ fileId, outputFormat, account }) => {
|
|
668
793
|
try {
|
|
669
794
|
const { drive } = await getClients(account);
|
|
670
795
|
const res = await drive.permissions.list({
|
|
671
796
|
fileId, fields: "permissions(id,role,type,emailAddress,domain,displayName)", supportsAllDrives: true,
|
|
672
797
|
});
|
|
673
|
-
return
|
|
798
|
+
return formatOutput({ permissions: res.data.permissions || [] }, outputFormat);
|
|
674
799
|
} catch (err) { return errorResponse(err.message); }
|
|
675
800
|
});
|
|
676
801
|
|
|
@@ -710,7 +835,7 @@ server.registerTool("gdrive_remove_permission", {
|
|
|
710
835
|
});
|
|
711
836
|
|
|
712
837
|
server.registerTool("gdrive_transfer_ownership", {
|
|
713
|
-
description: "Transfer ownership of a file to another user.",
|
|
838
|
+
description: "Transfer ownership of a file to another user. Note: cross-organization transfers (e.g., personal to Workspace) are blocked by Google.",
|
|
714
839
|
inputSchema: {
|
|
715
840
|
fileId: z.string().describe("File ID to transfer"),
|
|
716
841
|
email: z.string().describe("New owner's email address"),
|
|
@@ -733,17 +858,18 @@ server.registerTool("gdrive_transfer_ownership", {
|
|
|
733
858
|
server.registerTool("gdrive_list_labels", {
|
|
734
859
|
description: "List available Drive labels that can be applied to files.",
|
|
735
860
|
inputSchema: {
|
|
861
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
736
862
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
737
863
|
},
|
|
738
864
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
739
|
-
}, async ({ account }) => {
|
|
865
|
+
}, async ({ outputFormat, account }) => {
|
|
740
866
|
try {
|
|
741
867
|
const config = await loadConfig();
|
|
742
868
|
const label = account || config.active || "primary";
|
|
743
869
|
const auth = await authorizeAccount(label);
|
|
744
870
|
const labels = google.drivelabels({ version: "v2", auth });
|
|
745
871
|
const res = await labels.labels.list({ publishedOnly: true, view: "LABEL_VIEW_FULL" });
|
|
746
|
-
return
|
|
872
|
+
return formatOutput({ labels: res.data.labels || [] }, outputFormat);
|
|
747
873
|
} catch (err) { return errorResponse(err.message); }
|
|
748
874
|
});
|
|
749
875
|
|
|
@@ -751,14 +877,15 @@ server.registerTool("gdrive_list_file_labels", {
|
|
|
751
877
|
description: "List labels currently applied to a file.",
|
|
752
878
|
inputSchema: {
|
|
753
879
|
fileId: z.string().describe("File ID"),
|
|
880
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
754
881
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
755
882
|
},
|
|
756
883
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
757
|
-
}, async ({ fileId, account }) => {
|
|
884
|
+
}, async ({ fileId, outputFormat, account }) => {
|
|
758
885
|
try {
|
|
759
886
|
const { drive } = await getClients(account);
|
|
760
887
|
const res = await drive.files.listLabels({ fileId });
|
|
761
|
-
return
|
|
888
|
+
return formatOutput({ labels: res.data.labels || [] }, outputFormat);
|
|
762
889
|
} catch (err) { return errorResponse(err.message); }
|
|
763
890
|
});
|
|
764
891
|
|
|
@@ -791,21 +918,22 @@ server.registerTool("gdrive_set_file_labels", {
|
|
|
791
918
|
// ═══ DOCS TOOLS ══════════════════════════════════════════════════════════════
|
|
792
919
|
|
|
793
920
|
server.registerTool("gdrive_export_doc", {
|
|
794
|
-
description: "Export a Google Doc. Default: markdown. Also: html, text, docx, pdf.",
|
|
921
|
+
description: "Export a Google Doc. Default: markdown. Also: html, text, docx, pdf. Use savePath to save binary formats to disk.",
|
|
795
922
|
inputSchema: {
|
|
796
923
|
fileId: z.string().describe("Google Doc file ID"),
|
|
797
924
|
format: z.enum(["markdown", "html", "text", "docx", "pdf"]).optional().default("markdown").describe("Export format (default: markdown)"),
|
|
925
|
+
savePath: z.string().optional().describe("Local file path to save binary exports (docx/pdf). If omitted, returns base64."),
|
|
798
926
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
799
927
|
},
|
|
800
928
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
801
|
-
}, async ({ fileId, format, account }) => {
|
|
929
|
+
}, async ({ fileId, format, savePath, account }) => {
|
|
802
930
|
try {
|
|
803
931
|
const { drive } = await getClients(account);
|
|
804
932
|
const mimeMap = { markdown: "text/html", html: "text/html", text: "text/plain", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", pdf: "application/pdf" };
|
|
805
933
|
const isBinary = ["docx", "pdf"].includes(format);
|
|
806
934
|
const res = await drive.files.export({ fileId, mimeType: mimeMap[format] }, { responseType: isBinary ? "arraybuffer" : "text" });
|
|
807
935
|
if (format === "markdown") return textResponse(turndown.turndown(res.data));
|
|
808
|
-
if (isBinary) return
|
|
936
|
+
if (isBinary) return handleBinaryExport(res.data, format, savePath);
|
|
809
937
|
return textResponse(res.data);
|
|
810
938
|
} catch (err) { return errorResponse(err.message); }
|
|
811
939
|
});
|
|
@@ -875,20 +1003,21 @@ server.registerTool("gdrive_create_sheet", {
|
|
|
875
1003
|
});
|
|
876
1004
|
|
|
877
1005
|
server.registerTool("gdrive_export_sheet", {
|
|
878
|
-
description: "Export a Google Sheet to CSV, JSON, or XLSX.",
|
|
1006
|
+
description: "Export a Google Sheet to CSV, JSON, or XLSX. Use savePath to save XLSX to disk.",
|
|
879
1007
|
inputSchema: {
|
|
880
1008
|
fileId: z.string().describe("Google Sheet file ID"),
|
|
881
1009
|
format: z.enum(["csv", "json", "xlsx"]).optional().default("csv").describe("Export format (default: csv)"),
|
|
882
1010
|
sheetName: z.string().optional().describe("Specific sheet/tab name (default: first sheet). Only for csv/json."),
|
|
1011
|
+
savePath: z.string().optional().describe("Local file path to save XLSX export. If omitted, returns base64."),
|
|
883
1012
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
884
1013
|
},
|
|
885
1014
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
886
|
-
}, async ({ fileId, format, sheetName, account }) => {
|
|
1015
|
+
}, async ({ fileId, format, sheetName, savePath, account }) => {
|
|
887
1016
|
try {
|
|
888
1017
|
const { sheets, drive } = await getClients(account);
|
|
889
1018
|
if (format === "xlsx") {
|
|
890
1019
|
const res = await drive.files.export({ fileId, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, { responseType: "arraybuffer" });
|
|
891
|
-
return
|
|
1020
|
+
return handleBinaryExport(res.data, "xlsx", savePath);
|
|
892
1021
|
}
|
|
893
1022
|
let rangeParam = sheetName;
|
|
894
1023
|
if (!rangeParam) {
|
|
@@ -916,14 +1045,15 @@ server.registerTool("gdrive_list_sheets", {
|
|
|
916
1045
|
description: "List all sheets/tabs in a Google Spreadsheet.",
|
|
917
1046
|
inputSchema: {
|
|
918
1047
|
fileId: z.string().describe("Google Sheet file ID"),
|
|
1048
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
919
1049
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
920
1050
|
},
|
|
921
1051
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
922
|
-
}, async ({ fileId, account }) => {
|
|
1052
|
+
}, async ({ fileId, outputFormat, account }) => {
|
|
923
1053
|
try {
|
|
924
1054
|
const { sheets } = await getClients(account);
|
|
925
1055
|
const res = await sheets.spreadsheets.get({ spreadsheetId: fileId, fields: "sheets.properties" });
|
|
926
|
-
return
|
|
1056
|
+
return formatOutput({ sheets: (res.data.sheets || []).map(s => ({ title: s.properties.title, sheetId: s.properties.sheetId, index: s.properties.index, rowCount: s.properties.gridProperties?.rowCount, columnCount: s.properties.gridProperties?.columnCount })) }, outputFormat);
|
|
927
1057
|
} catch (err) { return errorResponse(err.message); }
|
|
928
1058
|
});
|
|
929
1059
|
|
|
@@ -933,15 +1063,16 @@ server.registerTool("gdrive_read_sheet_range", {
|
|
|
933
1063
|
fileId: z.string().describe("Google Sheet file ID"),
|
|
934
1064
|
range: z.string().describe("A1 notation range (e.g., 'Sheet1!A1:D10', 'Sales!A:A')"),
|
|
935
1065
|
valueRender: z.enum(["formatted", "unformatted", "formula"]).optional().default("formatted").describe("How to render values (default: formatted)"),
|
|
1066
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
936
1067
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
937
1068
|
},
|
|
938
1069
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
939
|
-
}, async ({ fileId, range, valueRender, account }) => {
|
|
1070
|
+
}, async ({ fileId, range, valueRender, outputFormat, account }) => {
|
|
940
1071
|
try {
|
|
941
1072
|
const { sheets } = await getClients(account);
|
|
942
1073
|
const renderMap = { formatted: "FORMATTED_VALUE", unformatted: "UNFORMATTED_VALUE", formula: "FORMULA" };
|
|
943
1074
|
const res = await sheets.spreadsheets.values.get({ spreadsheetId: fileId, range, valueRenderOption: renderMap[valueRender] || "FORMATTED_VALUE" });
|
|
944
|
-
return
|
|
1075
|
+
return formatOutput({ range: res.data.range, values: res.data.values || [] }, outputFormat);
|
|
945
1076
|
} catch (err) { return errorResponse(err.message); }
|
|
946
1077
|
});
|
|
947
1078
|
|
|
@@ -987,20 +1118,21 @@ server.registerTool("gdrive_create_slides", {
|
|
|
987
1118
|
});
|
|
988
1119
|
|
|
989
1120
|
server.registerTool("gdrive_export_slides", {
|
|
990
|
-
description: "Export Google Slides to text, PDF, or PPTX.",
|
|
1121
|
+
description: "Export Google Slides to text, PDF, or PPTX. Use savePath to save binary formats to disk.",
|
|
991
1122
|
inputSchema: {
|
|
992
1123
|
fileId: z.string().describe("Google Slides file ID"),
|
|
993
1124
|
format: z.enum(["text", "pdf", "pptx"]).optional().default("text").describe("Export format (default: text)"),
|
|
1125
|
+
savePath: z.string().optional().describe("Local file path to save binary exports (pdf/pptx). If omitted, returns base64."),
|
|
994
1126
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
995
1127
|
},
|
|
996
1128
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
997
|
-
}, async ({ fileId, format, account }) => {
|
|
1129
|
+
}, async ({ fileId, format, savePath, account }) => {
|
|
998
1130
|
try {
|
|
999
1131
|
const { drive } = await getClients(account);
|
|
1000
1132
|
const mimeMap = { text: "text/plain", pdf: "application/pdf", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation" };
|
|
1001
1133
|
const isBinary = ["pdf", "pptx"].includes(format);
|
|
1002
1134
|
const res = await drive.files.export({ fileId, mimeType: mimeMap[format] }, { responseType: isBinary ? "arraybuffer" : "text" });
|
|
1003
|
-
if (isBinary) return
|
|
1135
|
+
if (isBinary) return handleBinaryExport(res.data, format, savePath);
|
|
1004
1136
|
return textResponse(res.data);
|
|
1005
1137
|
} catch (err) { return errorResponse(err.message); }
|
|
1006
1138
|
});
|
|
@@ -1010,17 +1142,18 @@ server.registerTool("gdrive_get_slide_thumbnail", {
|
|
|
1010
1142
|
inputSchema: {
|
|
1011
1143
|
fileId: z.string().describe("Google Slides file ID"),
|
|
1012
1144
|
slideIndex: z.number().optional().default(0).describe("Slide index, 0-based (default: first slide)"),
|
|
1145
|
+
outputFormat: OUTPUT_FORMAT_SCHEMA,
|
|
1013
1146
|
account: z.string().optional().describe("Account label (default: active account)"),
|
|
1014
1147
|
},
|
|
1015
1148
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1016
|
-
}, async ({ fileId, slideIndex, account }) => {
|
|
1149
|
+
}, async ({ fileId, slideIndex, outputFormat, account }) => {
|
|
1017
1150
|
try {
|
|
1018
1151
|
const { slides } = await getClients(account);
|
|
1019
1152
|
const pres = await slides.presentations.get({ presentationId: fileId, fields: "slides.objectId" });
|
|
1020
1153
|
const pages = pres.data.slides || [];
|
|
1021
1154
|
if (slideIndex >= pages.length) return errorResponse(`Slide index ${slideIndex} out of range (${pages.length} slides)`);
|
|
1022
1155
|
const thumb = await slides.presentations.pages.getThumbnail({ presentationId: fileId, pageObjectId: pages[slideIndex].objectId, "thumbnailProperties.mimeType": "PNG" });
|
|
1023
|
-
return
|
|
1156
|
+
return formatOutput({ slideIndex, pageId: pages[slideIndex].objectId, thumbnailUrl: thumb.data.contentUrl, width: thumb.data.width, height: thumb.data.height }, outputFormat);
|
|
1024
1157
|
} catch (err) { return errorResponse(err.message); }
|
|
1025
1158
|
});
|
|
1026
1159
|
|