@trops/dash-core 0.1.77 → 0.1.79
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.
|
@@ -200,19 +200,22 @@
|
|
|
200
200
|
"tags": ["google", "files", "cloud-storage"],
|
|
201
201
|
"mcpConfig": {
|
|
202
202
|
"transport": "stdio",
|
|
203
|
-
"command": "
|
|
204
|
-
"args": ["
|
|
203
|
+
"command": "node",
|
|
204
|
+
"args": ["{{MCP_DIR}}/servers/google-drive.js"],
|
|
205
205
|
"envMapping": {
|
|
206
206
|
"GDRIVE_OAUTH_PATH": "oauthKeysPath"
|
|
207
207
|
},
|
|
208
208
|
"staticEnv": {
|
|
209
|
-
"GDRIVE_CREDENTIALS_PATH": "~/.gdrive-mcp/credentials.json"
|
|
210
|
-
|
|
209
|
+
"GDRIVE_CREDENTIALS_PATH": "~/.gdrive-mcp/credentials.json"
|
|
210
|
+
},
|
|
211
|
+
"tokenRefresh": {
|
|
212
|
+
"credentialsPath": "~/.gdrive-mcp/credentials.json",
|
|
213
|
+
"oauthKeysPath": "~/.gdrive-mcp/gcp-oauth.keys.json"
|
|
211
214
|
}
|
|
212
215
|
},
|
|
213
216
|
"authCommand": {
|
|
214
|
-
"command": "
|
|
215
|
-
"args": ["
|
|
217
|
+
"command": "node",
|
|
218
|
+
"args": ["{{MCP_DIR}}/servers/google-drive.js", "auth"],
|
|
216
219
|
"setup": {
|
|
217
220
|
"copyCredential": {
|
|
218
221
|
"from": "oauthKeysPath",
|
|
@@ -243,7 +246,11 @@
|
|
|
243
246
|
"mcpConfig": {
|
|
244
247
|
"transport": "stdio",
|
|
245
248
|
"command": "npx",
|
|
246
|
-
"args": ["-y", "@gongrzhe/server-gmail-autoauth-mcp"]
|
|
249
|
+
"args": ["-y", "@gongrzhe/server-gmail-autoauth-mcp"],
|
|
250
|
+
"tokenRefresh": {
|
|
251
|
+
"credentialsPath": "~/.gmail-mcp/credentials.json",
|
|
252
|
+
"oauthKeysPath": "~/.gmail-mcp/gcp-oauth.keys.json"
|
|
253
|
+
}
|
|
247
254
|
},
|
|
248
255
|
"authCommand": {
|
|
249
256
|
"command": "npx",
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Custom Google Drive MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the archived @modelcontextprotocol/server-gdrive which has a
|
|
6
|
+
* fundamental bug: it creates OAuth2 clients without client_id/client_secret,
|
|
7
|
+
* so it can never refresh tokens.
|
|
8
|
+
*
|
|
9
|
+
* Exposes a single "search" tool with { query: string } input — identical
|
|
10
|
+
* interface to the original, so no widget changes are needed.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* MCP server: node google-drive.js (stdio transport)
|
|
14
|
+
* OAuth auth: node google-drive.js auth (browser-based OAuth flow)
|
|
15
|
+
*
|
|
16
|
+
* Environment variables:
|
|
17
|
+
* GDRIVE_CREDENTIALS_PATH — path to stored OAuth credentials (access/refresh tokens)
|
|
18
|
+
* GDRIVE_OAUTH_PATH — path to Google OAuth client keys file
|
|
19
|
+
*/
|
|
20
|
+
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
21
|
+
const {
|
|
22
|
+
StdioServerTransport,
|
|
23
|
+
} = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const https = require("https");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
|
|
28
|
+
const credentialsPath = (process.env.GDRIVE_CREDENTIALS_PATH || "").replace(
|
|
29
|
+
/^~/,
|
|
30
|
+
process.env.HOME || "",
|
|
31
|
+
);
|
|
32
|
+
const oauthKeysPath = (process.env.GDRIVE_OAUTH_PATH || "").replace(
|
|
33
|
+
/^~/,
|
|
34
|
+
process.env.HOME || "",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read OAuth client credentials from the keys file.
|
|
39
|
+
*/
|
|
40
|
+
function getClientCredentials() {
|
|
41
|
+
const keysFile = JSON.parse(fs.readFileSync(oauthKeysPath, "utf8"));
|
|
42
|
+
const keyData = keysFile.installed || keysFile.web;
|
|
43
|
+
return {
|
|
44
|
+
client_id: keyData.client_id,
|
|
45
|
+
client_secret: keyData.client_secret,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read stored credentials (access_token, refresh_token, expiry_date).
|
|
51
|
+
*/
|
|
52
|
+
function readCredentials() {
|
|
53
|
+
return JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get a valid access token, refreshing if expired.
|
|
58
|
+
*/
|
|
59
|
+
async function getAccessToken() {
|
|
60
|
+
let creds = readCredentials();
|
|
61
|
+
const { client_id, client_secret } = getClientCredentials();
|
|
62
|
+
|
|
63
|
+
// Still valid (>60s remaining)?
|
|
64
|
+
if (creds.expiry_date && creds.expiry_date > Date.now() + 60 * 1000) {
|
|
65
|
+
return creds.access_token;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Refresh
|
|
69
|
+
const postData = [
|
|
70
|
+
`client_id=${encodeURIComponent(client_id)}`,
|
|
71
|
+
`client_secret=${encodeURIComponent(client_secret)}`,
|
|
72
|
+
`refresh_token=${encodeURIComponent(creds.refresh_token)}`,
|
|
73
|
+
"grant_type=refresh_token",
|
|
74
|
+
].join("&");
|
|
75
|
+
|
|
76
|
+
const body = await new Promise((resolve, reject) => {
|
|
77
|
+
const req = https.request(
|
|
78
|
+
{
|
|
79
|
+
hostname: "oauth2.googleapis.com",
|
|
80
|
+
path: "/token",
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
84
|
+
"Content-Length": Buffer.byteLength(postData),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
(res) => {
|
|
88
|
+
let data = "";
|
|
89
|
+
res.on("data", (chunk) => (data += chunk));
|
|
90
|
+
res.on("end", () => {
|
|
91
|
+
if (res.statusCode === 200) {
|
|
92
|
+
resolve(JSON.parse(data));
|
|
93
|
+
} else {
|
|
94
|
+
reject(
|
|
95
|
+
new Error(`Token refresh failed (${res.statusCode}): ${data}`),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
req.on("error", reject);
|
|
102
|
+
req.write(postData);
|
|
103
|
+
req.end();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
creds.access_token = body.access_token;
|
|
107
|
+
creds.expiry_date = Date.now() + (body.expires_in || 3600) * 1000;
|
|
108
|
+
if (body.refresh_token) {
|
|
109
|
+
creds.refresh_token = body.refresh_token;
|
|
110
|
+
}
|
|
111
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(creds, null, 2));
|
|
112
|
+
return creds.access_token;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Make a Google Drive API request.
|
|
117
|
+
*/
|
|
118
|
+
function driveRequest(path, token) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const req = https.request(
|
|
121
|
+
{
|
|
122
|
+
hostname: "www.googleapis.com",
|
|
123
|
+
path,
|
|
124
|
+
method: "GET",
|
|
125
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
126
|
+
},
|
|
127
|
+
(res) => {
|
|
128
|
+
let data = "";
|
|
129
|
+
res.on("data", (chunk) => (data += chunk));
|
|
130
|
+
res.on("end", () => {
|
|
131
|
+
if (res.statusCode === 200) {
|
|
132
|
+
resolve(JSON.parse(data));
|
|
133
|
+
} else {
|
|
134
|
+
reject(new Error(`Drive API error (${res.statusCode}): ${data}`));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
req.on("error", reject);
|
|
140
|
+
req.end();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Auth subcommand ──────────────────────────────────────────────────
|
|
145
|
+
if (process.argv[2] === "auth") {
|
|
146
|
+
(async () => {
|
|
147
|
+
try {
|
|
148
|
+
const http = require("http");
|
|
149
|
+
const { URL } = require("url");
|
|
150
|
+
const { client_id, client_secret } = getClientCredentials();
|
|
151
|
+
|
|
152
|
+
const keysFile = JSON.parse(fs.readFileSync(oauthKeysPath, "utf8"));
|
|
153
|
+
const keyData = keysFile.installed || keysFile.web;
|
|
154
|
+
const redirectUri =
|
|
155
|
+
keyData.redirect_uris?.[0] || "http://localhost:3000/oauth2callback";
|
|
156
|
+
|
|
157
|
+
// Extract port from redirect URI
|
|
158
|
+
const redirectUrl = new URL(redirectUri);
|
|
159
|
+
const port = parseInt(redirectUrl.port, 10) || 3000;
|
|
160
|
+
|
|
161
|
+
const scopes = ["https://www.googleapis.com/auth/drive.readonly"];
|
|
162
|
+
|
|
163
|
+
const authUrl =
|
|
164
|
+
`https://accounts.google.com/o/oauth2/v2/auth?` +
|
|
165
|
+
`client_id=${encodeURIComponent(client_id)}` +
|
|
166
|
+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
167
|
+
`&response_type=code` +
|
|
168
|
+
`&scope=${encodeURIComponent(scopes.join(" "))}` +
|
|
169
|
+
`&access_type=offline` +
|
|
170
|
+
`&prompt=consent`;
|
|
171
|
+
|
|
172
|
+
console.log(
|
|
173
|
+
`\nOpen this URL in your browser to authorize:\n\n${authUrl}\n`,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Start local server to catch the callback
|
|
177
|
+
const server = http.createServer(async (req, res) => {
|
|
178
|
+
const reqUrl = new URL(req.url, `http://localhost:${port}`);
|
|
179
|
+
const code = reqUrl.searchParams.get("code");
|
|
180
|
+
if (!code) {
|
|
181
|
+
res.writeHead(400);
|
|
182
|
+
res.end("Missing authorization code");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Exchange code for tokens
|
|
187
|
+
const postData = [
|
|
188
|
+
`code=${encodeURIComponent(code)}`,
|
|
189
|
+
`client_id=${encodeURIComponent(client_id)}`,
|
|
190
|
+
`client_secret=${encodeURIComponent(client_secret)}`,
|
|
191
|
+
`redirect_uri=${encodeURIComponent(redirectUri)}`,
|
|
192
|
+
`grant_type=authorization_code`,
|
|
193
|
+
].join("&");
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const body = await new Promise((resolve, reject) => {
|
|
197
|
+
const tokenReq = https.request(
|
|
198
|
+
{
|
|
199
|
+
hostname: "oauth2.googleapis.com",
|
|
200
|
+
path: "/token",
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
204
|
+
"Content-Length": Buffer.byteLength(postData),
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
(tokenRes) => {
|
|
208
|
+
let data = "";
|
|
209
|
+
tokenRes.on("data", (chunk) => (data += chunk));
|
|
210
|
+
tokenRes.on("end", () => {
|
|
211
|
+
if (tokenRes.statusCode === 200) {
|
|
212
|
+
resolve(JSON.parse(data));
|
|
213
|
+
} else {
|
|
214
|
+
reject(
|
|
215
|
+
new Error(
|
|
216
|
+
`Token exchange failed (${tokenRes.statusCode}): ${data}`,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
tokenReq.on("error", reject);
|
|
224
|
+
tokenReq.write(postData);
|
|
225
|
+
tokenReq.end();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const creds = {
|
|
229
|
+
access_token: body.access_token,
|
|
230
|
+
refresh_token: body.refresh_token,
|
|
231
|
+
expiry_date: Date.now() + (body.expires_in || 3600) * 1000,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const credDir = path.dirname(credentialsPath);
|
|
235
|
+
fs.mkdirSync(credDir, { recursive: true });
|
|
236
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(creds, null, 2));
|
|
237
|
+
|
|
238
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
239
|
+
res.end(
|
|
240
|
+
"<h1>Authorization successful!</h1><p>You can close this tab.</p>",
|
|
241
|
+
);
|
|
242
|
+
console.log(`\nCredentials saved to ${credentialsPath}\n`);
|
|
243
|
+
server.close();
|
|
244
|
+
process.exit(0);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
res.writeHead(500);
|
|
247
|
+
res.end(`Error: ${err.message}`);
|
|
248
|
+
console.error("Token exchange error:", err.message);
|
|
249
|
+
server.close();
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
server.listen(port, () => {
|
|
255
|
+
console.log(`Listening on port ${port} for OAuth callback...`);
|
|
256
|
+
});
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error("Auth error:", err.message);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
} else {
|
|
263
|
+
// ── MCP Server ──────────────────────────────────────────────────────
|
|
264
|
+
(async () => {
|
|
265
|
+
const server = new Server(
|
|
266
|
+
{ name: "google-drive", version: "1.0.0" },
|
|
267
|
+
{ capabilities: { tools: {} } },
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
server.setRequestHandler({ method: "tools/list" }, async () => ({
|
|
271
|
+
tools: [
|
|
272
|
+
{
|
|
273
|
+
name: "search",
|
|
274
|
+
description: "Search for files in Google Drive by name or content",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
query: {
|
|
279
|
+
type: "string",
|
|
280
|
+
description: "Search query",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
required: ["query"],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
server.setRequestHandler({ method: "tools/call" }, async (request) => {
|
|
290
|
+
if (request.params.name !== "search") {
|
|
291
|
+
return {
|
|
292
|
+
content: [
|
|
293
|
+
{
|
|
294
|
+
type: "text",
|
|
295
|
+
text: `Unknown tool: ${request.params.name}`,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
isError: true,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const query = request.params.arguments?.query;
|
|
303
|
+
if (!query) {
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: "text", text: "Missing required argument: query" }],
|
|
306
|
+
isError: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const token = await getAccessToken();
|
|
312
|
+
const encodedQuery = encodeURIComponent(
|
|
313
|
+
`fullText contains '${query.replace(/'/g, "\\'")}'`,
|
|
314
|
+
);
|
|
315
|
+
const result = await driveRequest(
|
|
316
|
+
`/drive/v3/files?q=${encodedQuery}&fields=files(id,name,mimeType,modifiedTime,webViewLink)&pageSize=20`,
|
|
317
|
+
token,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const files = result.files || [];
|
|
321
|
+
if (files.length === 0) {
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: "text",
|
|
326
|
+
text: `No files found for query: ${query}`,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const lines = files.map(
|
|
333
|
+
(f) =>
|
|
334
|
+
`${f.name} (${f.mimeType})${f.webViewLink ? ` - ${f.webViewLink}` : ""}`,
|
|
335
|
+
);
|
|
336
|
+
return {
|
|
337
|
+
content: [
|
|
338
|
+
{
|
|
339
|
+
type: "text",
|
|
340
|
+
text: `Found ${files.length} files:\n${lines.join("\n")}`,
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
} catch (err) {
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
347
|
+
isError: true,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const transport = new StdioServerTransport();
|
|
353
|
+
await server.connect(transport);
|
|
354
|
+
})();
|
|
355
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trops/dash-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.79",
|
|
4
4
|
"description": "Core framework for Dash dashboard applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "npm run build:renderer && npm run build:electron",
|
|
21
21
|
"build:renderer": "rollup -c rollup.config.renderer.mjs",
|
|
22
|
-
"build:electron": "rollup -c rollup.config.electron.mjs && mkdir -p dist/mcp && cp electron/mcp/mcpServerCatalog.json
|
|
22
|
+
"build:electron": "rollup -c rollup.config.electron.mjs && mkdir -p dist/mcp/servers && cp electron/mcp/mcpServerCatalog.json dist/mcp/ && cp -r electron/mcp/servers/ dist/mcp/servers/",
|
|
23
23
|
"clean": "rm -rf dist",
|
|
24
24
|
"prepublishOnly": "npm run clean && npm run build",
|
|
25
|
+
"test:mcp": "node --test electron/controller/mcpController.test.js electron/mcp/mcpServerCatalog.test.js",
|
|
25
26
|
"prettify": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\" \"electron/**/*.js\""
|
|
26
27
|
},
|
|
27
28
|
"peerDependencies": {
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* gdrive-server.mjs
|
|
4
|
-
*
|
|
5
|
-
* Local Google Drive MCP server wrapper that fixes the upstream OAuth2 bug.
|
|
6
|
-
*
|
|
7
|
-
* The upstream @modelcontextprotocol/server-gdrive creates OAuth2 without
|
|
8
|
-
* client_id/client_secret, which prevents token refresh after ~1 hour.
|
|
9
|
-
* This wrapper reads both the credentials file AND the OAuth keys file
|
|
10
|
-
* to properly initialize OAuth2 with client credentials.
|
|
11
|
-
*
|
|
12
|
-
* Env vars:
|
|
13
|
-
* GDRIVE_CREDENTIALS_PATH - path to saved credentials (access_token, refresh_token)
|
|
14
|
-
* GDRIVE_OAUTH_KEYS_PATH - path to gcp-oauth.keys.json (client_id, client_secret)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
-
import {
|
|
20
|
-
CallToolRequestSchema,
|
|
21
|
-
ListResourcesRequestSchema,
|
|
22
|
-
ListToolsRequestSchema,
|
|
23
|
-
ReadResourceRequestSchema,
|
|
24
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
-
import fs from "fs";
|
|
26
|
-
import { google } from "googleapis";
|
|
27
|
-
|
|
28
|
-
const drive = google.drive("v3");
|
|
29
|
-
|
|
30
|
-
const server = new Server(
|
|
31
|
-
{
|
|
32
|
-
name: "dash/gdrive",
|
|
33
|
-
version: "1.0.0",
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
capabilities: {
|
|
37
|
-
resources: {},
|
|
38
|
-
tools: {},
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
// --- List Resources ---
|
|
44
|
-
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
45
|
-
const pageSize = 10;
|
|
46
|
-
const params = {
|
|
47
|
-
pageSize,
|
|
48
|
-
fields: "nextPageToken, files(id, name, mimeType)",
|
|
49
|
-
};
|
|
50
|
-
if (request.params?.cursor) {
|
|
51
|
-
params.pageToken = request.params.cursor;
|
|
52
|
-
}
|
|
53
|
-
const res = await drive.files.list(params);
|
|
54
|
-
const files = res.data.files;
|
|
55
|
-
return {
|
|
56
|
-
resources: files.map((file) => ({
|
|
57
|
-
uri: `gdrive:///${file.id}`,
|
|
58
|
-
mimeType: file.mimeType,
|
|
59
|
-
name: file.name,
|
|
60
|
-
})),
|
|
61
|
-
nextCursor: res.data.nextPageToken,
|
|
62
|
-
};
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// --- Read Resource ---
|
|
66
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
67
|
-
const fileId = request.params.uri.replace("gdrive:///", "");
|
|
68
|
-
const file = await drive.files.get({
|
|
69
|
-
fileId,
|
|
70
|
-
fields: "mimeType",
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Google Docs/Sheets/etc need export
|
|
74
|
-
if (file.data.mimeType?.startsWith("application/vnd.google-apps")) {
|
|
75
|
-
let exportMimeType;
|
|
76
|
-
switch (file.data.mimeType) {
|
|
77
|
-
case "application/vnd.google-apps.document":
|
|
78
|
-
exportMimeType = "text/markdown";
|
|
79
|
-
break;
|
|
80
|
-
case "application/vnd.google-apps.spreadsheet":
|
|
81
|
-
exportMimeType = "text/csv";
|
|
82
|
-
break;
|
|
83
|
-
case "application/vnd.google-apps.presentation":
|
|
84
|
-
exportMimeType = "text/plain";
|
|
85
|
-
break;
|
|
86
|
-
case "application/vnd.google-apps.drawing":
|
|
87
|
-
exportMimeType = "image/png";
|
|
88
|
-
break;
|
|
89
|
-
default:
|
|
90
|
-
exportMimeType = "text/plain";
|
|
91
|
-
}
|
|
92
|
-
const res = await drive.files.export(
|
|
93
|
-
{ fileId, mimeType: exportMimeType },
|
|
94
|
-
{ responseType: "text" }
|
|
95
|
-
);
|
|
96
|
-
return {
|
|
97
|
-
contents: [
|
|
98
|
-
{
|
|
99
|
-
uri: request.params.uri,
|
|
100
|
-
mimeType: exportMimeType,
|
|
101
|
-
text: res.data,
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Regular files — download content
|
|
108
|
-
const res = await drive.files.get(
|
|
109
|
-
{ fileId, alt: "media" },
|
|
110
|
-
{ responseType: "arraybuffer" }
|
|
111
|
-
);
|
|
112
|
-
const mimeType = file.data.mimeType || "application/octet-stream";
|
|
113
|
-
if (mimeType.startsWith("text/") || mimeType === "application/json") {
|
|
114
|
-
return {
|
|
115
|
-
contents: [
|
|
116
|
-
{
|
|
117
|
-
uri: request.params.uri,
|
|
118
|
-
mimeType,
|
|
119
|
-
text: Buffer.from(res.data).toString("utf-8"),
|
|
120
|
-
},
|
|
121
|
-
],
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
contents: [
|
|
126
|
-
{
|
|
127
|
-
uri: request.params.uri,
|
|
128
|
-
mimeType,
|
|
129
|
-
blob: Buffer.from(res.data).toString("base64"),
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
};
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// --- List Tools ---
|
|
136
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
137
|
-
return {
|
|
138
|
-
tools: [
|
|
139
|
-
{
|
|
140
|
-
name: "search",
|
|
141
|
-
description: "Search for files in Google Drive",
|
|
142
|
-
inputSchema: {
|
|
143
|
-
type: "object",
|
|
144
|
-
properties: {
|
|
145
|
-
query: {
|
|
146
|
-
type: "string",
|
|
147
|
-
description: "Search query",
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
required: ["query"],
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
};
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// --- Call Tool ---
|
|
158
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
159
|
-
if (request.params.name === "search") {
|
|
160
|
-
const userQuery = request.params.arguments?.query;
|
|
161
|
-
const escapedQuery = userQuery
|
|
162
|
-
.replace(/\\/g, "\\\\")
|
|
163
|
-
.replace(/'/g, "\\'");
|
|
164
|
-
const formattedQuery = `fullText contains '${escapedQuery}'`;
|
|
165
|
-
const res = await drive.files.list({
|
|
166
|
-
q: formattedQuery,
|
|
167
|
-
pageSize: 10,
|
|
168
|
-
fields: "files(id, name, mimeType, modifiedTime, size)",
|
|
169
|
-
});
|
|
170
|
-
const fileList = res.data.files
|
|
171
|
-
?.map((file) => `${file.name} (${file.mimeType})`)
|
|
172
|
-
.join("\n");
|
|
173
|
-
return {
|
|
174
|
-
content: [
|
|
175
|
-
{
|
|
176
|
-
type: "text",
|
|
177
|
-
text: `Found ${res.data.files?.length ?? 0} files:\n${fileList}`,
|
|
178
|
-
},
|
|
179
|
-
],
|
|
180
|
-
isError: false,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
throw new Error("Tool not found");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// --- Load credentials and start server ---
|
|
187
|
-
const credentialsPath = process.env.GDRIVE_CREDENTIALS_PATH;
|
|
188
|
-
const oauthKeysPath = process.env.GDRIVE_OAUTH_KEYS_PATH;
|
|
189
|
-
|
|
190
|
-
if (!credentialsPath || !fs.existsSync(credentialsPath)) {
|
|
191
|
-
console.error(
|
|
192
|
-
"Credentials not found. Please run OAuth auth flow first."
|
|
193
|
-
);
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const credentials = JSON.parse(fs.readFileSync(credentialsPath, "utf-8"));
|
|
198
|
-
|
|
199
|
-
// THE FIX: Read client_id and client_secret from the OAuth keys file
|
|
200
|
-
// so that googleapis can refresh the access_token when it expires.
|
|
201
|
-
let clientId, clientSecret;
|
|
202
|
-
if (oauthKeysPath && fs.existsSync(oauthKeysPath)) {
|
|
203
|
-
const keysFile = JSON.parse(fs.readFileSync(oauthKeysPath, "utf-8"));
|
|
204
|
-
const keyData = keysFile.installed || keysFile.web;
|
|
205
|
-
if (keyData) {
|
|
206
|
-
clientId = keyData.client_id;
|
|
207
|
-
clientSecret = keyData.client_secret;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const auth = new google.auth.OAuth2(clientId, clientSecret);
|
|
212
|
-
auth.setCredentials(credentials);
|
|
213
|
-
google.options({ auth });
|
|
214
|
-
|
|
215
|
-
console.error(
|
|
216
|
-
`Credentials loaded (refresh_token: ${credentials.refresh_token ? "present" : "missing"}). Starting server.`
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
const transport = new StdioServerTransport();
|
|
220
|
-
await server.connect(transport);
|