@trops/dash-core 0.1.78 → 0.1.80
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,8 +200,8 @@
|
|
|
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
|
},
|
|
@@ -214,8 +214,8 @@
|
|
|
214
214
|
}
|
|
215
215
|
},
|
|
216
216
|
"authCommand": {
|
|
217
|
-
"command": "
|
|
218
|
-
"args": ["
|
|
217
|
+
"command": "node",
|
|
218
|
+
"args": ["{{MCP_DIR}}/servers/google-drive.js", "auth"],
|
|
219
219
|
"setup": {
|
|
220
220
|
"copyCredential": {
|
|
221
221
|
"from": "oauthKeysPath",
|
|
@@ -246,7 +246,11 @@
|
|
|
246
246
|
"mcpConfig": {
|
|
247
247
|
"transport": "stdio",
|
|
248
248
|
"command": "npx",
|
|
249
|
-
"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
|
+
}
|
|
250
254
|
},
|
|
251
255
|
"authCommand": {
|
|
252
256
|
"command": "npx",
|
|
@@ -0,0 +1,359 @@
|
|
|
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 {
|
|
25
|
+
CallToolRequestSchema,
|
|
26
|
+
ListToolsRequestSchema,
|
|
27
|
+
} = require("@modelcontextprotocol/sdk/types.js");
|
|
28
|
+
const fs = require("fs");
|
|
29
|
+
const https = require("https");
|
|
30
|
+
const path = require("path");
|
|
31
|
+
|
|
32
|
+
const credentialsPath = (process.env.GDRIVE_CREDENTIALS_PATH || "").replace(
|
|
33
|
+
/^~/,
|
|
34
|
+
process.env.HOME || "",
|
|
35
|
+
);
|
|
36
|
+
const oauthKeysPath = (process.env.GDRIVE_OAUTH_PATH || "").replace(
|
|
37
|
+
/^~/,
|
|
38
|
+
process.env.HOME || "",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read OAuth client credentials from the keys file.
|
|
43
|
+
*/
|
|
44
|
+
function getClientCredentials() {
|
|
45
|
+
const keysFile = JSON.parse(fs.readFileSync(oauthKeysPath, "utf8"));
|
|
46
|
+
const keyData = keysFile.installed || keysFile.web;
|
|
47
|
+
return {
|
|
48
|
+
client_id: keyData.client_id,
|
|
49
|
+
client_secret: keyData.client_secret,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read stored credentials (access_token, refresh_token, expiry_date).
|
|
55
|
+
*/
|
|
56
|
+
function readCredentials() {
|
|
57
|
+
return JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get a valid access token, refreshing if expired.
|
|
62
|
+
*/
|
|
63
|
+
async function getAccessToken() {
|
|
64
|
+
let creds = readCredentials();
|
|
65
|
+
const { client_id, client_secret } = getClientCredentials();
|
|
66
|
+
|
|
67
|
+
// Still valid (>60s remaining)?
|
|
68
|
+
if (creds.expiry_date && creds.expiry_date > Date.now() + 60 * 1000) {
|
|
69
|
+
return creds.access_token;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Refresh
|
|
73
|
+
const postData = [
|
|
74
|
+
`client_id=${encodeURIComponent(client_id)}`,
|
|
75
|
+
`client_secret=${encodeURIComponent(client_secret)}`,
|
|
76
|
+
`refresh_token=${encodeURIComponent(creds.refresh_token)}`,
|
|
77
|
+
"grant_type=refresh_token",
|
|
78
|
+
].join("&");
|
|
79
|
+
|
|
80
|
+
const body = await new Promise((resolve, reject) => {
|
|
81
|
+
const req = https.request(
|
|
82
|
+
{
|
|
83
|
+
hostname: "oauth2.googleapis.com",
|
|
84
|
+
path: "/token",
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
88
|
+
"Content-Length": Buffer.byteLength(postData),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
(res) => {
|
|
92
|
+
let data = "";
|
|
93
|
+
res.on("data", (chunk) => (data += chunk));
|
|
94
|
+
res.on("end", () => {
|
|
95
|
+
if (res.statusCode === 200) {
|
|
96
|
+
resolve(JSON.parse(data));
|
|
97
|
+
} else {
|
|
98
|
+
reject(
|
|
99
|
+
new Error(`Token refresh failed (${res.statusCode}): ${data}`),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
req.on("error", reject);
|
|
106
|
+
req.write(postData);
|
|
107
|
+
req.end();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
creds.access_token = body.access_token;
|
|
111
|
+
creds.expiry_date = Date.now() + (body.expires_in || 3600) * 1000;
|
|
112
|
+
if (body.refresh_token) {
|
|
113
|
+
creds.refresh_token = body.refresh_token;
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(creds, null, 2));
|
|
116
|
+
return creds.access_token;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Make a Google Drive API request.
|
|
121
|
+
*/
|
|
122
|
+
function driveRequest(path, token) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const req = https.request(
|
|
125
|
+
{
|
|
126
|
+
hostname: "www.googleapis.com",
|
|
127
|
+
path,
|
|
128
|
+
method: "GET",
|
|
129
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
130
|
+
},
|
|
131
|
+
(res) => {
|
|
132
|
+
let data = "";
|
|
133
|
+
res.on("data", (chunk) => (data += chunk));
|
|
134
|
+
res.on("end", () => {
|
|
135
|
+
if (res.statusCode === 200) {
|
|
136
|
+
resolve(JSON.parse(data));
|
|
137
|
+
} else {
|
|
138
|
+
reject(new Error(`Drive API error (${res.statusCode}): ${data}`));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
req.on("error", reject);
|
|
144
|
+
req.end();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Auth subcommand ──────────────────────────────────────────────────
|
|
149
|
+
if (process.argv[2] === "auth") {
|
|
150
|
+
(async () => {
|
|
151
|
+
try {
|
|
152
|
+
const http = require("http");
|
|
153
|
+
const { URL } = require("url");
|
|
154
|
+
const { client_id, client_secret } = getClientCredentials();
|
|
155
|
+
|
|
156
|
+
const keysFile = JSON.parse(fs.readFileSync(oauthKeysPath, "utf8"));
|
|
157
|
+
const keyData = keysFile.installed || keysFile.web;
|
|
158
|
+
const redirectUri =
|
|
159
|
+
keyData.redirect_uris?.[0] || "http://localhost:3000/oauth2callback";
|
|
160
|
+
|
|
161
|
+
// Extract port from redirect URI
|
|
162
|
+
const redirectUrl = new URL(redirectUri);
|
|
163
|
+
const port = parseInt(redirectUrl.port, 10) || 3000;
|
|
164
|
+
|
|
165
|
+
const scopes = ["https://www.googleapis.com/auth/drive.readonly"];
|
|
166
|
+
|
|
167
|
+
const authUrl =
|
|
168
|
+
`https://accounts.google.com/o/oauth2/v2/auth?` +
|
|
169
|
+
`client_id=${encodeURIComponent(client_id)}` +
|
|
170
|
+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
171
|
+
`&response_type=code` +
|
|
172
|
+
`&scope=${encodeURIComponent(scopes.join(" "))}` +
|
|
173
|
+
`&access_type=offline` +
|
|
174
|
+
`&prompt=consent`;
|
|
175
|
+
|
|
176
|
+
console.log(
|
|
177
|
+
`\nOpen this URL in your browser to authorize:\n\n${authUrl}\n`,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Start local server to catch the callback
|
|
181
|
+
const server = http.createServer(async (req, res) => {
|
|
182
|
+
const reqUrl = new URL(req.url, `http://localhost:${port}`);
|
|
183
|
+
const code = reqUrl.searchParams.get("code");
|
|
184
|
+
if (!code) {
|
|
185
|
+
res.writeHead(400);
|
|
186
|
+
res.end("Missing authorization code");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Exchange code for tokens
|
|
191
|
+
const postData = [
|
|
192
|
+
`code=${encodeURIComponent(code)}`,
|
|
193
|
+
`client_id=${encodeURIComponent(client_id)}`,
|
|
194
|
+
`client_secret=${encodeURIComponent(client_secret)}`,
|
|
195
|
+
`redirect_uri=${encodeURIComponent(redirectUri)}`,
|
|
196
|
+
`grant_type=authorization_code`,
|
|
197
|
+
].join("&");
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const body = await new Promise((resolve, reject) => {
|
|
201
|
+
const tokenReq = https.request(
|
|
202
|
+
{
|
|
203
|
+
hostname: "oauth2.googleapis.com",
|
|
204
|
+
path: "/token",
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
208
|
+
"Content-Length": Buffer.byteLength(postData),
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
(tokenRes) => {
|
|
212
|
+
let data = "";
|
|
213
|
+
tokenRes.on("data", (chunk) => (data += chunk));
|
|
214
|
+
tokenRes.on("end", () => {
|
|
215
|
+
if (tokenRes.statusCode === 200) {
|
|
216
|
+
resolve(JSON.parse(data));
|
|
217
|
+
} else {
|
|
218
|
+
reject(
|
|
219
|
+
new Error(
|
|
220
|
+
`Token exchange failed (${tokenRes.statusCode}): ${data}`,
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
tokenReq.on("error", reject);
|
|
228
|
+
tokenReq.write(postData);
|
|
229
|
+
tokenReq.end();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const creds = {
|
|
233
|
+
access_token: body.access_token,
|
|
234
|
+
refresh_token: body.refresh_token,
|
|
235
|
+
expiry_date: Date.now() + (body.expires_in || 3600) * 1000,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const credDir = path.dirname(credentialsPath);
|
|
239
|
+
fs.mkdirSync(credDir, { recursive: true });
|
|
240
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(creds, null, 2));
|
|
241
|
+
|
|
242
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
243
|
+
res.end(
|
|
244
|
+
"<h1>Authorization successful!</h1><p>You can close this tab.</p>",
|
|
245
|
+
);
|
|
246
|
+
console.log(`\nCredentials saved to ${credentialsPath}\n`);
|
|
247
|
+
server.close();
|
|
248
|
+
process.exit(0);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
res.writeHead(500);
|
|
251
|
+
res.end(`Error: ${err.message}`);
|
|
252
|
+
console.error("Token exchange error:", err.message);
|
|
253
|
+
server.close();
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
server.listen(port, () => {
|
|
259
|
+
console.log(`Listening on port ${port} for OAuth callback...`);
|
|
260
|
+
});
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error("Auth error:", err.message);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
})();
|
|
266
|
+
} else {
|
|
267
|
+
// ── MCP Server ──────────────────────────────────────────────────────
|
|
268
|
+
(async () => {
|
|
269
|
+
const server = new Server(
|
|
270
|
+
{ name: "google-drive", version: "1.0.0" },
|
|
271
|
+
{ capabilities: { tools: {} } },
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
275
|
+
tools: [
|
|
276
|
+
{
|
|
277
|
+
name: "search",
|
|
278
|
+
description: "Search for files in Google Drive by name or content",
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: "object",
|
|
281
|
+
properties: {
|
|
282
|
+
query: {
|
|
283
|
+
type: "string",
|
|
284
|
+
description: "Search query",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
required: ["query"],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
294
|
+
if (request.params.name !== "search") {
|
|
295
|
+
return {
|
|
296
|
+
content: [
|
|
297
|
+
{
|
|
298
|
+
type: "text",
|
|
299
|
+
text: `Unknown tool: ${request.params.name}`,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
isError: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const query = request.params.arguments?.query;
|
|
307
|
+
if (!query) {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: "Missing required argument: query" }],
|
|
310
|
+
isError: true,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const token = await getAccessToken();
|
|
316
|
+
const encodedQuery = encodeURIComponent(
|
|
317
|
+
`fullText contains '${query.replace(/'/g, "\\'")}'`,
|
|
318
|
+
);
|
|
319
|
+
const result = await driveRequest(
|
|
320
|
+
`/drive/v3/files?q=${encodedQuery}&fields=files(id,name,mimeType,modifiedTime,webViewLink)&pageSize=20`,
|
|
321
|
+
token,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const files = result.files || [];
|
|
325
|
+
if (files.length === 0) {
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: `No files found for query: ${query}`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const lines = files.map(
|
|
337
|
+
(f) =>
|
|
338
|
+
`${f.name} (${f.mimeType})${f.webViewLink ? ` - ${f.webViewLink}` : ""}`,
|
|
339
|
+
);
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: `Found ${files.length} files:\n${lines.join("\n")}`,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
351
|
+
isError: true,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const transport = new StdioServerTransport();
|
|
357
|
+
await server.connect(transport);
|
|
358
|
+
})();
|
|
359
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trops/dash-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.80",
|
|
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 dist/mcp/",
|
|
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": {
|