@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": "npx",
204
- "args": ["-y", "-p", "@modelcontextprotocol/server-gdrive", "node", "{{MCP_DIR}}/gdrive-server.mjs"],
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
- "GDRIVE_OAUTH_KEYS_PATH": "~/.gdrive-mcp/gcp-oauth.keys.json"
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": "npx",
215
- "args": ["-y", "@modelcontextprotocol/server-gdrive", "auth"],
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.77",
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 electron/mcp/gdrive-server.mjs 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": {
@@ -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);