@trops/dash-core 0.1.336 → 0.1.337
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.
|
@@ -195,47 +195,25 @@
|
|
|
195
195
|
{
|
|
196
196
|
"id": "google-drive",
|
|
197
197
|
"name": "Google Drive",
|
|
198
|
-
"description": "
|
|
198
|
+
"description": "Search, read, and write files in Google Drive. Supports folder listing, file creation, and path resolution.",
|
|
199
199
|
"icon": "google-drive",
|
|
200
200
|
"tags": ["google", "files", "cloud-storage"],
|
|
201
201
|
"mcpConfig": {
|
|
202
202
|
"transport": "stdio",
|
|
203
203
|
"command": "node",
|
|
204
204
|
"args": ["{{MCP_DIR}}/servers/google-drive.js"],
|
|
205
|
-
"envMapping": {
|
|
206
|
-
"GDRIVE_OAUTH_PATH": "oauthKeysPath"
|
|
207
|
-
},
|
|
208
205
|
"staticEnv": {
|
|
209
206
|
"GDRIVE_CREDENTIALS_PATH": "~/.gdrive-mcp/credentials.json"
|
|
210
|
-
},
|
|
211
|
-
"tokenRefresh": {
|
|
212
|
-
"credentialsPath": "~/.gdrive-mcp/credentials.json",
|
|
213
|
-
"oauthKeysPath": "~/.gdrive-mcp/gcp-oauth.keys.json"
|
|
214
207
|
}
|
|
215
208
|
},
|
|
216
209
|
"authCommand": {
|
|
217
210
|
"command": "node",
|
|
218
211
|
"args": ["{{MCP_DIR}}/servers/google-drive.js", "auth"],
|
|
219
|
-
"setup": {
|
|
220
|
-
"copyCredential": {
|
|
221
|
-
"from": "oauthKeysPath",
|
|
222
|
-
"to": "~/.gdrive-mcp/gcp-oauth.keys.json"
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
212
|
"staticEnv": {
|
|
226
|
-
"GDRIVE_CREDENTIALS_PATH": "~/.gdrive-mcp/credentials.json"
|
|
227
|
-
"GDRIVE_OAUTH_PATH": "~/.gdrive-mcp/gcp-oauth.keys.json"
|
|
213
|
+
"GDRIVE_CREDENTIALS_PATH": "~/.gdrive-mcp/credentials.json"
|
|
228
214
|
}
|
|
229
215
|
},
|
|
230
|
-
"credentialSchema": {
|
|
231
|
-
"oauthKeysPath": {
|
|
232
|
-
"type": "file",
|
|
233
|
-
"displayName": "OAuth Keys File",
|
|
234
|
-
"required": false,
|
|
235
|
-
"secret": false,
|
|
236
|
-
"instructions": "Path to your Google OAuth keys file (gcp-oauth.keys.json). Create one at console.cloud.google.com > APIs & Services > Credentials > OAuth 2.0 Client IDs."
|
|
237
|
-
}
|
|
238
|
-
}
|
|
216
|
+
"credentialSchema": {}
|
|
239
217
|
},
|
|
240
218
|
{
|
|
241
219
|
"id": "gmail",
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Custom Google Drive MCP server.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* fundamental bug: it creates OAuth2 clients without client_id/client_secret,
|
|
7
|
-
* so it can never refresh tokens.
|
|
5
|
+
* Tools: search, list_folder, create_folder, read_file, write_file, resolve_path
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* OAuth uses PKCE with a bundled client_id — no client_secret, no per-user
|
|
8
|
+
* GCP project setup. Users just run `node google-drive.js auth` to grant
|
|
9
|
+
* Drive access via browser.
|
|
11
10
|
*
|
|
12
11
|
* Usage:
|
|
13
12
|
* MCP server: node google-drive.js (stdio transport)
|
|
@@ -15,7 +14,6 @@
|
|
|
15
14
|
*
|
|
16
15
|
* Environment variables:
|
|
17
16
|
* GDRIVE_CREDENTIALS_PATH — path to stored OAuth credentials (access/refresh tokens)
|
|
18
|
-
* GDRIVE_OAUTH_PATH — path to Google OAuth client keys file
|
|
19
17
|
*/
|
|
20
18
|
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
21
19
|
const {
|
|
@@ -28,26 +26,22 @@ const {
|
|
|
28
26
|
const fs = require("fs");
|
|
29
27
|
const https = require("https");
|
|
30
28
|
const path = require("path");
|
|
29
|
+
const crypto = require("crypto");
|
|
31
30
|
|
|
32
31
|
const credentialsPath = (process.env.GDRIVE_CREDENTIALS_PATH || "").replace(
|
|
33
32
|
/^~/,
|
|
34
33
|
process.env.HOME || "",
|
|
35
34
|
);
|
|
36
|
-
const oauthKeysPath = (process.env.GDRIVE_OAUTH_PATH || "").replace(
|
|
37
|
-
/^~/,
|
|
38
|
-
process.env.HOME || "",
|
|
39
|
-
);
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
36
|
+
// Bundled OAuth client_id for the Dash platform's GCP project.
|
|
37
|
+
// Desktop OAuth client_ids are inherently public — they're identifiers,
|
|
38
|
+
// not secrets. Auth uses PKCE (code_verifier/code_challenge) instead of
|
|
39
|
+
// a client_secret.
|
|
40
|
+
const BUNDLED_CLIENT_ID =
|
|
41
|
+
"785070273499-mr9b0vup4u24he8duh3c6j5gpk7qj54j.apps.googleusercontent.com";
|
|
42
|
+
|
|
43
|
+
function getClientId() {
|
|
44
|
+
return BUNDLED_CLIENT_ID;
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
/**
|
|
@@ -62,17 +56,16 @@ function readCredentials() {
|
|
|
62
56
|
*/
|
|
63
57
|
async function getAccessToken() {
|
|
64
58
|
let creds = readCredentials();
|
|
65
|
-
const
|
|
59
|
+
const clientId = getClientId();
|
|
66
60
|
|
|
67
61
|
// Still valid (>60s remaining)?
|
|
68
62
|
if (creds.expiry_date && creds.expiry_date > Date.now() + 60 * 1000) {
|
|
69
63
|
return creds.access_token;
|
|
70
64
|
}
|
|
71
65
|
|
|
72
|
-
// Refresh
|
|
66
|
+
// Refresh — PKCE-based installed apps don't need client_secret for refresh
|
|
73
67
|
const postData = [
|
|
74
|
-
`client_id=${encodeURIComponent(
|
|
75
|
-
`client_secret=${encodeURIComponent(client_secret)}`,
|
|
68
|
+
`client_id=${encodeURIComponent(clientId)}`,
|
|
76
69
|
`refresh_token=${encodeURIComponent(creds.refresh_token)}`,
|
|
77
70
|
"grant_type=refresh_token",
|
|
78
71
|
].join("&");
|
|
@@ -117,43 +110,194 @@ async function getAccessToken() {
|
|
|
117
110
|
}
|
|
118
111
|
|
|
119
112
|
/**
|
|
120
|
-
* Make a Google Drive API request.
|
|
113
|
+
* Make a Google Drive API request (GET, POST, PATCH, etc.).
|
|
121
114
|
*/
|
|
122
|
-
function driveRequest(
|
|
115
|
+
function driveRequest(apiPath, token, method = "GET", body = null) {
|
|
123
116
|
return new Promise((resolve, reject) => {
|
|
117
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
118
|
+
if (body) {
|
|
119
|
+
headers["Content-Type"] = "application/json";
|
|
120
|
+
headers["Content-Length"] = Buffer.byteLength(body);
|
|
121
|
+
}
|
|
122
|
+
const req = https.request(
|
|
123
|
+
{ hostname: "www.googleapis.com", path: apiPath, method, headers },
|
|
124
|
+
(res) => {
|
|
125
|
+
let data = "";
|
|
126
|
+
res.on("data", (chunk) => (data += chunk));
|
|
127
|
+
res.on("end", () => {
|
|
128
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
129
|
+
try {
|
|
130
|
+
resolve(JSON.parse(data));
|
|
131
|
+
} catch {
|
|
132
|
+
resolve(data);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
reject(
|
|
136
|
+
new Error(`Drive API ${method} (${res.statusCode}): ${data}`),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
req.on("error", reject);
|
|
143
|
+
if (body) req.write(body);
|
|
144
|
+
req.end();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Multipart upload to Google Drive (for creating/updating file content).
|
|
150
|
+
*/
|
|
151
|
+
function driveUploadRequest(
|
|
152
|
+
apiPath,
|
|
153
|
+
token,
|
|
154
|
+
method,
|
|
155
|
+
metadata,
|
|
156
|
+
content,
|
|
157
|
+
mimeType,
|
|
158
|
+
) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const boundary = "dash_boundary_" + Date.now().toString(36);
|
|
161
|
+
const body =
|
|
162
|
+
`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n` +
|
|
163
|
+
`${JSON.stringify(metadata)}\r\n` +
|
|
164
|
+
`--${boundary}\r\nContent-Type: ${mimeType}\r\n\r\n` +
|
|
165
|
+
`${content}\r\n` +
|
|
166
|
+
`--${boundary}--`;
|
|
167
|
+
|
|
124
168
|
const req = https.request(
|
|
125
169
|
{
|
|
126
170
|
hostname: "www.googleapis.com",
|
|
127
|
-
path,
|
|
128
|
-
method
|
|
129
|
-
headers: {
|
|
171
|
+
path: apiPath,
|
|
172
|
+
method,
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${token}`,
|
|
175
|
+
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
176
|
+
"Content-Length": Buffer.byteLength(body),
|
|
177
|
+
},
|
|
130
178
|
},
|
|
131
179
|
(res) => {
|
|
132
180
|
let data = "";
|
|
133
|
-
res.on("data", (
|
|
181
|
+
res.on("data", (c) => (data += c));
|
|
134
182
|
res.on("end", () => {
|
|
135
|
-
if (res.statusCode
|
|
136
|
-
|
|
183
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
184
|
+
try {
|
|
185
|
+
resolve(JSON.parse(data));
|
|
186
|
+
} catch {
|
|
187
|
+
resolve(data);
|
|
188
|
+
}
|
|
137
189
|
} else {
|
|
138
|
-
reject(
|
|
190
|
+
reject(
|
|
191
|
+
new Error(`Drive upload ${method} (${res.statusCode}): ${data}`),
|
|
192
|
+
);
|
|
139
193
|
}
|
|
140
194
|
});
|
|
141
195
|
},
|
|
142
196
|
);
|
|
143
197
|
req.on("error", reject);
|
|
198
|
+
req.write(body);
|
|
144
199
|
req.end();
|
|
145
200
|
});
|
|
146
201
|
}
|
|
147
202
|
|
|
203
|
+
// ── Tool helper functions ────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
async function listFolder(token, folderId) {
|
|
206
|
+
const q = encodeURIComponent(`'${folderId}' in parents and trashed=false`);
|
|
207
|
+
const fields = encodeURIComponent("files(id,name,mimeType)");
|
|
208
|
+
const result = await driveRequest(
|
|
209
|
+
`/drive/v3/files?q=${q}&fields=${fields}&pageSize=200`,
|
|
210
|
+
token,
|
|
211
|
+
);
|
|
212
|
+
return result.files || [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function createFolder(token, parentId, name) {
|
|
216
|
+
const body = JSON.stringify({
|
|
217
|
+
name,
|
|
218
|
+
mimeType: "application/vnd.google-apps.folder",
|
|
219
|
+
parents: [parentId],
|
|
220
|
+
});
|
|
221
|
+
return await driveRequest(
|
|
222
|
+
"/drive/v3/files?fields=id,name",
|
|
223
|
+
token,
|
|
224
|
+
"POST",
|
|
225
|
+
body,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function readFile(token, fileId) {
|
|
230
|
+
return await driveRequest(`/drive/v3/files/${fileId}?alt=media`, token);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function writeFile(token, parentId, name, content, mimeType) {
|
|
234
|
+
mimeType = mimeType || "text/markdown";
|
|
235
|
+
// Upsert: check if file with this name already exists in parent
|
|
236
|
+
const escapedName = name.replace(/'/g, "\\'");
|
|
237
|
+
const q = encodeURIComponent(
|
|
238
|
+
`name='${escapedName}' and '${parentId}' in parents and trashed=false`,
|
|
239
|
+
);
|
|
240
|
+
const existing = await driveRequest(
|
|
241
|
+
`/drive/v3/files?q=${q}&fields=files(id)`,
|
|
242
|
+
token,
|
|
243
|
+
);
|
|
244
|
+
const existingId = existing.files?.[0]?.id;
|
|
245
|
+
|
|
246
|
+
if (existingId) {
|
|
247
|
+
const result = await driveUploadRequest(
|
|
248
|
+
`/upload/drive/v3/files/${existingId}?uploadType=multipart&fields=id,name`,
|
|
249
|
+
token,
|
|
250
|
+
"PATCH",
|
|
251
|
+
{},
|
|
252
|
+
content,
|
|
253
|
+
mimeType,
|
|
254
|
+
);
|
|
255
|
+
return { ...result, _action: "updated" };
|
|
256
|
+
} else {
|
|
257
|
+
const result = await driveUploadRequest(
|
|
258
|
+
`/upload/drive/v3/files?uploadType=multipart&fields=id,name`,
|
|
259
|
+
token,
|
|
260
|
+
"POST",
|
|
261
|
+
{ name, parents: [parentId], mimeType },
|
|
262
|
+
content,
|
|
263
|
+
mimeType,
|
|
264
|
+
);
|
|
265
|
+
return { ...result, _action: "created" };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function resolvePath(token, pathStr) {
|
|
270
|
+
const segments = pathStr
|
|
271
|
+
.split("/")
|
|
272
|
+
.map((s) => s.trim())
|
|
273
|
+
.filter(Boolean);
|
|
274
|
+
let currentId = "root";
|
|
275
|
+
for (const segment of segments) {
|
|
276
|
+
const children = await listFolder(token, currentId);
|
|
277
|
+
const match = children.find((c) => c.name === segment);
|
|
278
|
+
if (!match) return null;
|
|
279
|
+
currentId = match.id;
|
|
280
|
+
}
|
|
281
|
+
return currentId;
|
|
282
|
+
}
|
|
283
|
+
|
|
148
284
|
// ── Auth subcommand ──────────────────────────────────────────────────
|
|
149
285
|
if (process.argv[2] === "auth") {
|
|
150
286
|
(async () => {
|
|
151
287
|
try {
|
|
152
288
|
const http = require("http");
|
|
153
289
|
const { URL } = require("url");
|
|
154
|
-
const
|
|
290
|
+
const clientId = getClientId();
|
|
291
|
+
|
|
292
|
+
const scopes = ["https://www.googleapis.com/auth/drive"];
|
|
293
|
+
|
|
294
|
+
// PKCE: generate code verifier + challenge (no client_secret needed)
|
|
295
|
+
const codeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
296
|
+
const codeChallenge = crypto
|
|
297
|
+
.createHash("sha256")
|
|
298
|
+
.update(codeVerifier)
|
|
299
|
+
.digest("base64url");
|
|
155
300
|
|
|
156
|
-
const scopes = ["https://www.googleapis.com/auth/drive.readonly"];
|
|
157
301
|
let redirectUri;
|
|
158
302
|
|
|
159
303
|
// Start local server to catch the callback
|
|
@@ -166,11 +310,11 @@ if (process.argv[2] === "auth") {
|
|
|
166
310
|
return;
|
|
167
311
|
}
|
|
168
312
|
|
|
169
|
-
// Exchange code for tokens
|
|
313
|
+
// Exchange code for tokens using PKCE code_verifier
|
|
170
314
|
const postData = [
|
|
171
315
|
`code=${encodeURIComponent(code)}`,
|
|
172
|
-
`client_id=${encodeURIComponent(
|
|
173
|
-
`
|
|
316
|
+
`client_id=${encodeURIComponent(clientId)}`,
|
|
317
|
+
`code_verifier=${encodeURIComponent(codeVerifier)}`,
|
|
174
318
|
`redirect_uri=${encodeURIComponent(redirectUri)}`,
|
|
175
319
|
`grant_type=authorization_code`,
|
|
176
320
|
].join("&");
|
|
@@ -246,12 +390,14 @@ if (process.argv[2] === "auth") {
|
|
|
246
390
|
|
|
247
391
|
const authUrl =
|
|
248
392
|
`https://accounts.google.com/o/oauth2/v2/auth?` +
|
|
249
|
-
`client_id=${encodeURIComponent(
|
|
393
|
+
`client_id=${encodeURIComponent(clientId)}` +
|
|
250
394
|
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
251
395
|
`&response_type=code` +
|
|
252
396
|
`&scope=${encodeURIComponent(scopes.join(" "))}` +
|
|
253
397
|
`&access_type=offline` +
|
|
254
|
-
`&prompt=consent
|
|
398
|
+
`&prompt=consent` +
|
|
399
|
+
`&code_challenge=${encodeURIComponent(codeChallenge)}` +
|
|
400
|
+
`&code_challenge_method=S256`;
|
|
255
401
|
|
|
256
402
|
const { exec } = require("child_process");
|
|
257
403
|
exec(`open "${authUrl}"`);
|
|
@@ -280,72 +426,269 @@ if (process.argv[2] === "auth") {
|
|
|
280
426
|
inputSchema: {
|
|
281
427
|
type: "object",
|
|
282
428
|
properties: {
|
|
283
|
-
query: {
|
|
429
|
+
query: { type: "string", description: "Search query" },
|
|
430
|
+
},
|
|
431
|
+
required: ["query"],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: "list_folder",
|
|
436
|
+
description:
|
|
437
|
+
"List children of a Google Drive folder by ID. Use 'root' for My Drive.",
|
|
438
|
+
inputSchema: {
|
|
439
|
+
type: "object",
|
|
440
|
+
properties: {
|
|
441
|
+
folderId: {
|
|
284
442
|
type: "string",
|
|
285
|
-
description: "
|
|
443
|
+
description: "Folder ID, or 'root' for My Drive",
|
|
286
444
|
},
|
|
287
445
|
},
|
|
288
|
-
required: ["
|
|
446
|
+
required: ["folderId"],
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: "create_folder",
|
|
451
|
+
description:
|
|
452
|
+
"Create a new folder inside a parent folder. Returns the new folder's ID.",
|
|
453
|
+
inputSchema: {
|
|
454
|
+
type: "object",
|
|
455
|
+
properties: {
|
|
456
|
+
parentId: { type: "string", description: "Parent folder ID" },
|
|
457
|
+
name: { type: "string", description: "New folder name" },
|
|
458
|
+
},
|
|
459
|
+
required: ["parentId", "name"],
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "read_file",
|
|
464
|
+
description:
|
|
465
|
+
"Read the text content of a Drive file by ID. Plain text files only.",
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: "object",
|
|
468
|
+
properties: {
|
|
469
|
+
fileId: { type: "string", description: "File ID" },
|
|
470
|
+
},
|
|
471
|
+
required: ["fileId"],
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "write_file",
|
|
476
|
+
description:
|
|
477
|
+
"Create or update a text file in a folder (upsert by name).",
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: "object",
|
|
480
|
+
properties: {
|
|
481
|
+
parentId: { type: "string", description: "Parent folder ID" },
|
|
482
|
+
name: { type: "string", description: "File name" },
|
|
483
|
+
content: { type: "string", description: "File content" },
|
|
484
|
+
mimeType: {
|
|
485
|
+
type: "string",
|
|
486
|
+
description: "Optional MIME type (default: text/markdown)",
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
required: ["parentId", "name", "content"],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "resolve_path",
|
|
494
|
+
description:
|
|
495
|
+
"Walk a slash-separated path from My Drive root and return the final file/folder ID, or null if any segment is missing.",
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: "object",
|
|
498
|
+
properties: {
|
|
499
|
+
path: {
|
|
500
|
+
type: "string",
|
|
501
|
+
description:
|
|
502
|
+
"Slash-separated path, e.g. 'Sales Pipeline/AMER/ENT/Acme Corp'",
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
required: ["path"],
|
|
289
506
|
},
|
|
290
507
|
},
|
|
291
508
|
],
|
|
292
509
|
}));
|
|
293
510
|
|
|
294
511
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
content: [
|
|
298
|
-
{
|
|
299
|
-
type: "text",
|
|
300
|
-
text: `Unknown tool: ${request.params.name}`,
|
|
301
|
-
},
|
|
302
|
-
],
|
|
303
|
-
isError: true,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const query = request.params.arguments?.query;
|
|
308
|
-
if (!query) {
|
|
309
|
-
return {
|
|
310
|
-
content: [{ type: "text", text: "Missing required argument: query" }],
|
|
311
|
-
isError: true,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
512
|
+
const toolName = request.params.name;
|
|
513
|
+
const args = request.params.arguments || {};
|
|
314
514
|
|
|
315
515
|
try {
|
|
316
516
|
const token = await getAccessToken();
|
|
317
|
-
const encodedQuery = encodeURIComponent(
|
|
318
|
-
`fullText contains '${query.replace(/'/g, "\\'")}'`,
|
|
319
|
-
);
|
|
320
|
-
const result = await driveRequest(
|
|
321
|
-
`/drive/v3/files?q=${encodedQuery}&fields=files(id,name,mimeType,modifiedTime,webViewLink)&pageSize=20`,
|
|
322
|
-
token,
|
|
323
|
-
);
|
|
324
517
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
{
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
518
|
+
switch (toolName) {
|
|
519
|
+
case "search": {
|
|
520
|
+
const query = args.query;
|
|
521
|
+
if (!query) {
|
|
522
|
+
return {
|
|
523
|
+
content: [
|
|
524
|
+
{ type: "text", text: "Missing required argument: query" },
|
|
525
|
+
],
|
|
526
|
+
isError: true,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const encodedQuery = encodeURIComponent(
|
|
530
|
+
`fullText contains '${query.replace(/'/g, "\\'")}'`,
|
|
531
|
+
);
|
|
532
|
+
const result = await driveRequest(
|
|
533
|
+
`/drive/v3/files?q=${encodedQuery}&fields=files(id,name,mimeType,modifiedTime,webViewLink)&pageSize=20`,
|
|
534
|
+
token,
|
|
535
|
+
);
|
|
536
|
+
const files = result.files || [];
|
|
537
|
+
if (files.length === 0) {
|
|
538
|
+
return {
|
|
539
|
+
content: [
|
|
540
|
+
{ type: "text", text: `No files found for query: ${query}` },
|
|
541
|
+
],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const lines = files.map(
|
|
545
|
+
(f) =>
|
|
546
|
+
`${f.name} (${f.mimeType})${f.webViewLink ? ` - ${f.webViewLink}` : ""}`,
|
|
547
|
+
);
|
|
548
|
+
return {
|
|
549
|
+
content: [
|
|
550
|
+
{
|
|
551
|
+
type: "text",
|
|
552
|
+
text: `Found ${files.length} files:\n${lines.join("\n")}`,
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
336
557
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
558
|
+
case "list_folder": {
|
|
559
|
+
if (!args.folderId) {
|
|
560
|
+
return {
|
|
561
|
+
content: [
|
|
562
|
+
{
|
|
563
|
+
type: "text",
|
|
564
|
+
text: "Missing required argument: folderId",
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
isError: true,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const children = await listFolder(token, args.folderId);
|
|
571
|
+
if (children.length === 0) {
|
|
572
|
+
return {
|
|
573
|
+
content: [{ type: "text", text: "Folder is empty." }],
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const childLines = children.map(
|
|
577
|
+
(f) => `${f.name} (${f.mimeType}) [${f.id}]`,
|
|
578
|
+
);
|
|
579
|
+
return {
|
|
580
|
+
content: [
|
|
581
|
+
{
|
|
582
|
+
type: "text",
|
|
583
|
+
text: `${children.length} children:\n${childLines.join("\n")}`,
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
case "create_folder": {
|
|
590
|
+
if (!args.parentId || !args.name) {
|
|
591
|
+
return {
|
|
592
|
+
content: [
|
|
593
|
+
{
|
|
594
|
+
type: "text",
|
|
595
|
+
text: "Missing required arguments: parentId, name",
|
|
596
|
+
},
|
|
597
|
+
],
|
|
598
|
+
isError: true,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const folder = await createFolder(token, args.parentId, args.name);
|
|
602
|
+
return {
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: `Created folder "${folder.name}" [${folder.id}]`,
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
case "read_file": {
|
|
613
|
+
if (!args.fileId) {
|
|
614
|
+
return {
|
|
615
|
+
content: [
|
|
616
|
+
{ type: "text", text: "Missing required argument: fileId" },
|
|
617
|
+
],
|
|
618
|
+
isError: true,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const content = await readFile(token, args.fileId);
|
|
622
|
+
return {
|
|
623
|
+
content: [
|
|
624
|
+
{
|
|
625
|
+
type: "text",
|
|
626
|
+
text:
|
|
627
|
+
typeof content === "string"
|
|
628
|
+
? content
|
|
629
|
+
: JSON.stringify(content),
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
case "write_file": {
|
|
636
|
+
if (!args.parentId || !args.name || args.content == null) {
|
|
637
|
+
return {
|
|
638
|
+
content: [
|
|
639
|
+
{
|
|
640
|
+
type: "text",
|
|
641
|
+
text: "Missing required arguments: parentId, name, content",
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
isError: true,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const writeResult = await writeFile(
|
|
648
|
+
token,
|
|
649
|
+
args.parentId,
|
|
650
|
+
args.name,
|
|
651
|
+
args.content,
|
|
652
|
+
args.mimeType,
|
|
653
|
+
);
|
|
654
|
+
return {
|
|
655
|
+
content: [
|
|
656
|
+
{
|
|
657
|
+
type: "text",
|
|
658
|
+
text: `${writeResult._action} "${writeResult.name}" [${writeResult.id}]`,
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
case "resolve_path": {
|
|
665
|
+
if (!args.path) {
|
|
666
|
+
return {
|
|
667
|
+
content: [
|
|
668
|
+
{ type: "text", text: "Missing required argument: path" },
|
|
669
|
+
],
|
|
670
|
+
isError: true,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const resolvedId = await resolvePath(token, args.path);
|
|
674
|
+
if (resolvedId) {
|
|
675
|
+
return {
|
|
676
|
+
content: [
|
|
677
|
+
{ type: "text", text: `Resolved to ID: ${resolvedId}` },
|
|
678
|
+
],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
content: [{ type: "text", text: `Path not found: ${args.path}` }],
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
default:
|
|
687
|
+
return {
|
|
688
|
+
content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
|
|
689
|
+
isError: true,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
349
692
|
} catch (err) {
|
|
350
693
|
return {
|
|
351
694
|
content: [{ type: "text", text: `Error: ${err.message}` }],
|