dropbox-mcp-server 2.2.0 → 2.2.1
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/dist/index.js +102 -185
- package/index.ts +109 -194
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
5
5
|
import { Dropbox } from "dropbox";
|
|
6
6
|
import "dotenv/config";
|
|
7
7
|
// ------------------------------------------------------------------
|
|
8
|
-
// 1. Rate Limiter
|
|
8
|
+
// 1. Rate Limiter (Fixed: Removed dangerous Proxy)
|
|
9
9
|
// ------------------------------------------------------------------
|
|
10
10
|
const MAX_CONCURRENT = 1;
|
|
11
11
|
const MIN_GAP_MS = 1000;
|
|
@@ -59,17 +59,6 @@ async function limited(fn) {
|
|
|
59
59
|
release();
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
function throttleClient(client) {
|
|
63
|
-
return new Proxy(client, {
|
|
64
|
-
get(target, prop, receiver) {
|
|
65
|
-
const val = Reflect.get(target, prop, receiver);
|
|
66
|
-
if (typeof val === "function") {
|
|
67
|
-
return (...args) => limited(() => val.apply(target, args));
|
|
68
|
-
}
|
|
69
|
-
return val;
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
62
|
// ------------------------------------------------------------------
|
|
74
63
|
// 2. Error Handling
|
|
75
64
|
// ------------------------------------------------------------------
|
|
@@ -95,86 +84,105 @@ function isBadRequestError(err) {
|
|
|
95
84
|
// ------------------------------------------------------------------
|
|
96
85
|
const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
|
|
97
86
|
const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
|
|
98
|
-
const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL;
|
|
87
|
+
const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL;
|
|
88
|
+
const DROPBOX_APP_SECRET = process.env.DROPBOX_APP_SECRET; // Highly recommended for token refresh
|
|
99
89
|
if (!DROPBOX_APP_KEY || !DROPBOX_REFRESH_TOKEN) {
|
|
100
|
-
console.error("Missing required environment variables:");
|
|
101
|
-
console.error(" - DROPBOX_APP_KEY");
|
|
102
|
-
console.error(" - DROPBOX_REFRESH_TOKEN");
|
|
103
|
-
console.error("Note: DROPBOX_SHARED_URL is optional.");
|
|
90
|
+
console.error("Missing required environment variables: DROPBOX_APP_KEY, DROPBOX_REFRESH_TOKEN");
|
|
104
91
|
process.exit(1);
|
|
105
92
|
}
|
|
106
|
-
|
|
93
|
+
// Instantiate direct client (no proxy)
|
|
94
|
+
const client = new Dropbox({
|
|
107
95
|
clientId: DROPBOX_APP_KEY,
|
|
96
|
+
clientSecret: DROPBOX_APP_SECRET, // Needed if your Dropbox app is configured with a secret
|
|
108
97
|
refreshToken: DROPBOX_REFRESH_TOKEN,
|
|
109
98
|
});
|
|
110
|
-
const
|
|
111
|
-
|
|
99
|
+
const server = new Server({ name: "dropbox-agent-mcp", version: "2.0.1" }, { capabilities: { tools: {} } });
|
|
100
|
+
// ------------------------------------------------------------------
|
|
101
|
+
// 4. Helpers (Fixed Pathing & Node 18+ Blobs)
|
|
102
|
+
// ------------------------------------------------------------------
|
|
103
|
+
import officeParser from 'officeparser';
|
|
104
|
+
import * as fs from 'fs/promises';
|
|
105
|
+
import * as path from 'path';
|
|
106
|
+
import * as os from 'os';
|
|
112
107
|
// ------------------------------------------------------------------
|
|
113
|
-
// 4. Helpers
|
|
108
|
+
// 4. Helpers
|
|
114
109
|
// ------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (!path || path === "/")
|
|
110
|
+
function formatDropboxPath(pathStr) {
|
|
111
|
+
if (!pathStr || pathStr === "/" || pathStr.trim() === "")
|
|
118
112
|
return "";
|
|
119
|
-
return
|
|
113
|
+
return pathStr.startsWith("/") ? pathStr : `/${pathStr}`;
|
|
120
114
|
}
|
|
121
|
-
|
|
122
|
-
function isTextFile(pathOrName) {
|
|
115
|
+
function isSupportedFile(pathOrName) {
|
|
123
116
|
const fileName = pathOrName.split('/').pop() || '';
|
|
124
117
|
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
125
|
-
|
|
118
|
+
if (fileName === ext)
|
|
119
|
+
return true;
|
|
120
|
+
const supportedExtensions = [
|
|
126
121
|
'csv', 'tsv', 'txt', 'json', 'md', 'log',
|
|
127
122
|
'xml', 'yaml', 'yml', 'html', 'htm', 'css',
|
|
128
|
-
'js', 'ts', 'py', 'java', 'c', 'cpp', 'h'
|
|
123
|
+
'js', 'ts', 'py', 'java', 'c', 'cpp', 'h',
|
|
124
|
+
'pdf', 'docx', 'pptx', 'xlsx', 'xls', 'doc', 'ppt', 'odt', 'odp', 'ods'
|
|
129
125
|
];
|
|
130
|
-
return
|
|
126
|
+
return supportedExtensions.includes(ext);
|
|
131
127
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Download a file from Dropbox.
|
|
134
|
-
* - If DROPBOX_SHARED_URL is set, uses shared link APIs with relative paths.
|
|
135
|
-
* - Otherwise, uses the regular filesDownload with absolute paths.
|
|
136
|
-
* Returns UTF‑8 text, or throws if binary.
|
|
137
|
-
*/
|
|
138
128
|
async function downloadHelper(identifier) {
|
|
139
129
|
let result;
|
|
140
130
|
let fileName;
|
|
141
|
-
// If the identifier is a full URL, we always treat it as a shared link.
|
|
142
131
|
if (identifier.startsWith("http://") || identifier.startsWith("https://")) {
|
|
143
|
-
const response = await client.sharingGetSharedLinkFile({ url: identifier });
|
|
132
|
+
const response = await limited(() => client.sharingGetSharedLinkFile({ url: identifier }));
|
|
144
133
|
result = response.result;
|
|
145
134
|
fileName = result.name || identifier.split('/').pop();
|
|
146
135
|
}
|
|
147
|
-
// Otherwise, decide based on DROPBOX_SHARED_URL.
|
|
148
136
|
else if (DROPBOX_SHARED_URL) {
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const response = await client.sharingGetSharedLinkFile({
|
|
137
|
+
const reqPath = formatDropboxPath(identifier);
|
|
138
|
+
const response = await limited(() => client.sharingGetSharedLinkFile({
|
|
152
139
|
url: DROPBOX_SHARED_URL,
|
|
153
|
-
path:
|
|
154
|
-
});
|
|
140
|
+
path: reqPath,
|
|
141
|
+
}));
|
|
155
142
|
result = response.result;
|
|
156
143
|
fileName = result.name || identifier;
|
|
157
144
|
}
|
|
158
145
|
else {
|
|
159
|
-
|
|
160
|
-
const response = await client.filesDownload({ path:
|
|
146
|
+
const reqPath = formatDropboxPath(identifier);
|
|
147
|
+
const response = await limited(() => client.filesDownload({ path: reqPath }));
|
|
161
148
|
result = response.result;
|
|
162
149
|
fileName = result.name || identifier;
|
|
163
150
|
}
|
|
164
|
-
|
|
151
|
+
const fileRef = fileName || identifier;
|
|
152
|
+
if (!isSupportedFile(fileRef)) {
|
|
153
|
+
throw new Error(`File "${fileRef}" appears to be an unsupported file format.`);
|
|
154
|
+
}
|
|
155
|
+
let fileBuffer;
|
|
156
|
+
if (result.fileBinary) {
|
|
157
|
+
fileBuffer = Buffer.from(result.fileBinary);
|
|
158
|
+
}
|
|
159
|
+
else if (result.fileBlob) {
|
|
160
|
+
const arrayBuffer = await result.fileBlob.arrayBuffer();
|
|
161
|
+
fileBuffer = Buffer.from(arrayBuffer);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
165
164
|
throw new Error("Download succeeded but no file content was returned.");
|
|
166
165
|
}
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
const ext = fileRef.split('.').pop()?.toLowerCase() || '';
|
|
167
|
+
const binaryExtensions = ['pdf', 'docx', 'pptx', 'xlsx', 'xls', 'doc', 'ppt', 'odt', 'odp', 'ods'];
|
|
168
|
+
if (binaryExtensions.includes(ext)) {
|
|
169
|
+
const tempFilePath = path.join(os.tmpdir(), `temp_${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`);
|
|
170
|
+
try {
|
|
171
|
+
await fs.writeFile(tempFilePath, fileBuffer);
|
|
172
|
+
// officeParser is called from the default import
|
|
173
|
+
const ast = await officeParser.parseOffice(fileBuffer);
|
|
174
|
+
const result = await ast.to('md');
|
|
175
|
+
return result.value || "Document parsed, but it appears to be empty.";
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw new Error(`Failed to parse binary document: ${error.message}`);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await fs.unlink(tempFilePath).catch(() => { });
|
|
182
|
+
}
|
|
171
183
|
}
|
|
172
|
-
return
|
|
184
|
+
return fileBuffer.toString('utf-8');
|
|
173
185
|
}
|
|
174
|
-
/**
|
|
175
|
-
* Parse a CSV header line, handling quoted fields and trimming.
|
|
176
|
-
* Returns an array of clean column names.
|
|
177
|
-
*/
|
|
178
186
|
function parseCsvHeader(headerLine) {
|
|
179
187
|
const fields = [];
|
|
180
188
|
let current = '';
|
|
@@ -202,47 +210,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
202
210
|
tools: [
|
|
203
211
|
{
|
|
204
212
|
name: "list_folder",
|
|
205
|
-
description:
|
|
206
|
-
? "List folders and files inside the shared Dropbox folder. Path must be relative to the shared root (e.g., '' for root, 'Data' for /Data)."
|
|
207
|
-
: "List folders and files in a specific Dropbox path (absolute, e.g., '/Data' or '' for root).",
|
|
213
|
+
description: "List folders and files. Use '' for root, or '/FolderName' for subfolders.",
|
|
208
214
|
inputSchema: {
|
|
209
215
|
type: "object",
|
|
210
|
-
properties: {
|
|
211
|
-
path: {
|
|
212
|
-
type: "string",
|
|
213
|
-
description: DROPBOX_SHARED_URL
|
|
214
|
-
? "Relative path inside the shared folder (e.g., '' or 'Data')."
|
|
215
|
-
: "Absolute path to list (e.g., '' for root or '/Data')."
|
|
216
|
-
}
|
|
217
|
-
},
|
|
216
|
+
properties: { path: { type: "string" } },
|
|
218
217
|
required: ["path"]
|
|
219
218
|
}
|
|
220
219
|
},
|
|
221
220
|
{
|
|
222
221
|
name: "search_files",
|
|
223
|
-
description: "Search for files by name, extension, or content
|
|
222
|
+
description: "Search for files by name, extension, or content.",
|
|
224
223
|
inputSchema: {
|
|
225
224
|
type: "object",
|
|
226
|
-
properties: {
|
|
227
|
-
query: { type: "string", description: "The search query (e.g., 'sales report' or '.csv')" }
|
|
228
|
-
},
|
|
225
|
+
properties: { query: { type: "string" } },
|
|
229
226
|
required: ["query"]
|
|
230
227
|
}
|
|
231
228
|
},
|
|
232
229
|
{
|
|
233
230
|
name: "get_file_metadata",
|
|
234
|
-
description:
|
|
235
|
-
? "Retrieve metadata (size, modified date) for a file inside the shared folder. Path is relative to the shared root."
|
|
236
|
-
: "Retrieve metadata (size, modified date) for a file at an absolute Dropbox path.",
|
|
231
|
+
description: "Retrieve size/modified date for a file.",
|
|
237
232
|
inputSchema: {
|
|
238
233
|
type: "object",
|
|
239
|
-
properties: {
|
|
240
|
-
path: {
|
|
241
|
-
type: "string", description: DROPBOX_SHARED_URL
|
|
242
|
-
? "Relative path to the file inside the shared folder."
|
|
243
|
-
: "Exact Dropbox file path (e.g., '/Data/file.csv')."
|
|
244
|
-
}
|
|
245
|
-
},
|
|
234
|
+
properties: { path: { type: "string" } },
|
|
246
235
|
required: ["path"]
|
|
247
236
|
}
|
|
248
237
|
},
|
|
@@ -251,64 +240,48 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
251
240
|
description: "Display the schema (columns/headers) of a CSV or TSV file.",
|
|
252
241
|
inputSchema: {
|
|
253
242
|
type: "object",
|
|
254
|
-
properties: {
|
|
255
|
-
identifier: {
|
|
256
|
-
type: "string", description: DROPBOX_SHARED_URL
|
|
257
|
-
? "Relative path to the table file (e.g., 'data.csv') or a shared link URL."
|
|
258
|
-
: "Dropbox path (e.g., '/Data/file.csv') or shared link URL."
|
|
259
|
-
}
|
|
260
|
-
},
|
|
243
|
+
properties: { identifier: { type: "string" } },
|
|
261
244
|
required: ["identifier"]
|
|
262
245
|
}
|
|
263
246
|
},
|
|
264
247
|
{
|
|
265
248
|
name: "read_data",
|
|
266
|
-
description: "Download and read the content of a text file
|
|
249
|
+
description: "Download and read the content of a text file. Truncated if over 100k chars.",
|
|
267
250
|
inputSchema: {
|
|
268
251
|
type: "object",
|
|
269
|
-
properties: {
|
|
270
|
-
identifier: {
|
|
271
|
-
type: "string", description: DROPBOX_SHARED_URL
|
|
272
|
-
? "Relative path (e.g., 'data.csv') or a shared link URL."
|
|
273
|
-
: "Dropbox path (e.g., '/Data/file.csv') or shared link URL."
|
|
274
|
-
}
|
|
275
|
-
},
|
|
252
|
+
properties: { identifier: { type: "string" } },
|
|
276
253
|
required: ["identifier"]
|
|
277
254
|
}
|
|
278
255
|
}
|
|
279
256
|
]
|
|
280
257
|
}));
|
|
281
258
|
// ------------------------------------------------------------------
|
|
282
|
-
// 6. Tool Handlers
|
|
259
|
+
// 6. Tool Handlers (Using direct limited wrapper)
|
|
283
260
|
// ------------------------------------------------------------------
|
|
284
261
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
285
262
|
try {
|
|
286
263
|
const name = request.params.name;
|
|
287
264
|
const args = request.params.arguments || {};
|
|
288
|
-
// --- list_folder ---
|
|
289
265
|
if (name === "list_folder") {
|
|
290
|
-
|
|
266
|
+
const rawPath = args.path || "";
|
|
291
267
|
let requestArgs;
|
|
292
268
|
if (DROPBOX_SHARED_URL) {
|
|
293
|
-
path = normalizeSharedPath(path);
|
|
294
269
|
requestArgs = {
|
|
295
|
-
path:
|
|
270
|
+
path: formatDropboxPath(rawPath),
|
|
296
271
|
shared_link: { url: DROPBOX_SHARED_URL }
|
|
297
272
|
};
|
|
298
273
|
}
|
|
299
274
|
else {
|
|
300
|
-
|
|
301
|
-
requestArgs = { path: path === "/" ? "" : path };
|
|
275
|
+
requestArgs = { path: formatDropboxPath(rawPath) };
|
|
302
276
|
}
|
|
303
|
-
const response = await client.filesListFolder(requestArgs);
|
|
277
|
+
const response = await limited(() => client.filesListFolder(requestArgs));
|
|
304
278
|
const entries = response.result.entries.map((e) => `[${e['.tag'].toUpperCase()}] ${e.name} (Path: ${e.path_display})`);
|
|
305
279
|
return {
|
|
306
280
|
content: [{ type: "text", text: entries.join("\n") || "Folder is empty." }]
|
|
307
281
|
};
|
|
308
282
|
}
|
|
309
|
-
// --- search_files ---
|
|
310
283
|
if (name === "search_files") {
|
|
311
|
-
const response = await client.filesSearchV2({ query: args.query });
|
|
284
|
+
const response = await limited(() => client.filesSearchV2({ query: args.query }));
|
|
312
285
|
const matches = response.result.matches.map((m) => {
|
|
313
286
|
const meta = m.metadata.metadata;
|
|
314
287
|
return `[${meta['.tag'].toUpperCase()}] ${meta.name} (Path: ${meta.path_display})`;
|
|
@@ -317,102 +290,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
317
290
|
content: [{ type: "text", text: matches.join("\n") || "No matches found." }]
|
|
318
291
|
};
|
|
319
292
|
}
|
|
320
|
-
// --- get_file_metadata ---
|
|
321
293
|
if (name === "get_file_metadata") {
|
|
322
294
|
let response;
|
|
323
|
-
const
|
|
295
|
+
const path = formatDropboxPath(args.path || "");
|
|
324
296
|
if (DROPBOX_SHARED_URL) {
|
|
325
|
-
|
|
326
|
-
response = await client.sharingGetSharedLinkMetadata({
|
|
297
|
+
response = await limited(() => client.sharingGetSharedLinkMetadata({
|
|
327
298
|
url: DROPBOX_SHARED_URL,
|
|
328
299
|
path: path
|
|
329
|
-
});
|
|
300
|
+
}));
|
|
330
301
|
}
|
|
331
302
|
else {
|
|
332
|
-
response = await client.filesGetMetadata({ path:
|
|
303
|
+
response = await limited(() => client.filesGetMetadata({ path: path }));
|
|
333
304
|
}
|
|
334
305
|
return {
|
|
335
306
|
content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }]
|
|
336
307
|
};
|
|
337
308
|
}
|
|
338
|
-
// --- read_data ---
|
|
339
309
|
if (name === "read_data") {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return { content: [{ type: "text", text: truncated }] };
|
|
346
|
-
}
|
|
347
|
-
catch (err) {
|
|
348
|
-
if (isNotFoundError(err)) {
|
|
349
|
-
return {
|
|
350
|
-
content: [{ type: "text", text: `File not found at "${args.identifier}".` }],
|
|
351
|
-
isError: true
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
if (isPermissionError(err)) {
|
|
355
|
-
return {
|
|
356
|
-
content: [{ type: "text", text: `Permission denied for "${args.identifier}".` }],
|
|
357
|
-
isError: true
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
if (isBadRequestError(err)) {
|
|
361
|
-
return {
|
|
362
|
-
content: [{ type: "text", text: `Invalid request. Check the path/identifier: "${args.identifier}".` }],
|
|
363
|
-
isError: true
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
if (err.message && err.message.includes("binary file")) {
|
|
367
|
-
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
368
|
-
}
|
|
369
|
-
throw err;
|
|
370
|
-
}
|
|
310
|
+
const text = await downloadHelper(args.identifier);
|
|
311
|
+
const truncated = text.length > 100000
|
|
312
|
+
? text.substring(0, 100000) + "\n... [WARNING: FILE TRUNCATED TO 100K CHARACTERS]"
|
|
313
|
+
: text;
|
|
314
|
+
return { content: [{ type: "text", text: truncated }] };
|
|
371
315
|
}
|
|
372
|
-
// --- describe_table ---
|
|
373
316
|
if (name === "describe_table") {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (commaCount > 0) {
|
|
385
|
-
headers = parseCsvHeader(firstLine);
|
|
386
|
-
if (headers.length < 2) {
|
|
387
|
-
headers = lines.map((_, i) => `Column ${i + 1}`);
|
|
388
|
-
totalRows = lines.length;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
headers = lines.map((_, i) => `Column ${i + 1}`);
|
|
393
|
-
totalRows = lines.length;
|
|
394
|
-
}
|
|
395
|
-
const schemaStr = `Columns detected:\n- ${headers.join('\n- ')}\n\nTotal rows: ${totalRows}`;
|
|
396
|
-
return { content: [{ type: "text", text: schemaStr }] };
|
|
317
|
+
// Note: This still downloads the full file to read the first line.
|
|
318
|
+
// Be careful running this on files >50MB as it may cause timeouts.
|
|
319
|
+
const text = await downloadHelper(args.identifier);
|
|
320
|
+
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
|
|
321
|
+
if (lines.length === 0)
|
|
322
|
+
return { content: [{ type: "text", text: "File is empty." }] };
|
|
323
|
+
let headers;
|
|
324
|
+
const firstLine = lines[0];
|
|
325
|
+
if ((firstLine.match(/,/g) || []).length > 0) {
|
|
326
|
+
headers = parseCsvHeader(firstLine);
|
|
397
327
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
content: [{ type: "text", text: `File not found at "${args.identifier}".` }],
|
|
402
|
-
isError: true
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
if (isPermissionError(err)) {
|
|
406
|
-
return {
|
|
407
|
-
content: [{ type: "text", text: `Permission denied for "${args.identifier}".` }],
|
|
408
|
-
isError: true
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
if (err.message && err.message.includes("binary file")) {
|
|
412
|
-
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
413
|
-
}
|
|
414
|
-
throw err;
|
|
328
|
+
else {
|
|
329
|
+
headers = lines.map((_, i) => `Column ${i + 1}`);
|
|
415
330
|
}
|
|
331
|
+
const schemaStr = `Columns detected:\n- ${headers.join('\n- ')}\n\nTotal rows: ${lines.length - 1}`;
|
|
332
|
+
return { content: [{ type: "text", text: schemaStr }] };
|
|
416
333
|
}
|
|
417
334
|
throw new Error(`Unknown tool: ${name}`);
|
|
418
335
|
}
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Dropbox } from "dropbox";
|
|
|
6
6
|
import "dotenv/config";
|
|
7
7
|
|
|
8
8
|
// ------------------------------------------------------------------
|
|
9
|
-
// 1. Rate Limiter
|
|
9
|
+
// 1. Rate Limiter (Fixed: Removed dangerous Proxy)
|
|
10
10
|
// ------------------------------------------------------------------
|
|
11
11
|
const MAX_CONCURRENT = 1;
|
|
12
12
|
const MIN_GAP_MS = 1000;
|
|
@@ -62,18 +62,6 @@ async function limited<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function throttleClient<T extends object>(client: T): T {
|
|
66
|
-
return new Proxy(client, {
|
|
67
|
-
get(target, prop, receiver) {
|
|
68
|
-
const val = Reflect.get(target, prop, receiver);
|
|
69
|
-
if (typeof val === "function") {
|
|
70
|
-
return (...args: any[]) => limited(() => val.apply(target, args));
|
|
71
|
-
}
|
|
72
|
-
return val;
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
65
|
// ------------------------------------------------------------------
|
|
78
66
|
// 2. Error Handling
|
|
79
67
|
// ------------------------------------------------------------------
|
|
@@ -100,101 +88,121 @@ function isBadRequestError(err: any): boolean {
|
|
|
100
88
|
// ------------------------------------------------------------------
|
|
101
89
|
const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
|
|
102
90
|
const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
|
|
103
|
-
const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL;
|
|
91
|
+
const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL;
|
|
92
|
+
const DROPBOX_APP_SECRET = process.env.DROPBOX_APP_SECRET; // Highly recommended for token refresh
|
|
104
93
|
|
|
105
94
|
if (!DROPBOX_APP_KEY || !DROPBOX_REFRESH_TOKEN) {
|
|
106
|
-
console.error("Missing required environment variables:");
|
|
107
|
-
console.error(" - DROPBOX_APP_KEY");
|
|
108
|
-
console.error(" - DROPBOX_REFRESH_TOKEN");
|
|
109
|
-
console.error("Note: DROPBOX_SHARED_URL is optional.");
|
|
95
|
+
console.error("Missing required environment variables: DROPBOX_APP_KEY, DROPBOX_REFRESH_TOKEN");
|
|
110
96
|
process.exit(1);
|
|
111
97
|
}
|
|
112
98
|
|
|
113
|
-
|
|
99
|
+
// Instantiate direct client (no proxy)
|
|
100
|
+
const client = new Dropbox({
|
|
114
101
|
clientId: DROPBOX_APP_KEY,
|
|
102
|
+
clientSecret: DROPBOX_APP_SECRET, // Needed if your Dropbox app is configured with a secret
|
|
115
103
|
refreshToken: DROPBOX_REFRESH_TOKEN,
|
|
116
104
|
});
|
|
117
|
-
const client = throttleClient(rawClient);
|
|
118
105
|
|
|
119
106
|
const server = new Server(
|
|
120
|
-
{ name: "dropbox-agent-mcp", version: "2.0.
|
|
107
|
+
{ name: "dropbox-agent-mcp", version: "2.0.1" },
|
|
121
108
|
{ capabilities: { tools: {} } }
|
|
122
109
|
);
|
|
123
110
|
|
|
124
111
|
// ------------------------------------------------------------------
|
|
125
|
-
// 4. Helpers
|
|
112
|
+
// 4. Helpers (Fixed Pathing & Node 18+ Blobs)
|
|
126
113
|
// ------------------------------------------------------------------
|
|
127
114
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
115
|
+
import officeParser from 'officeparser';
|
|
116
|
+
import * as fs from 'fs/promises';
|
|
117
|
+
import * as path from 'path';
|
|
118
|
+
import * as os from 'os';
|
|
119
|
+
|
|
120
|
+
// ------------------------------------------------------------------
|
|
121
|
+
// 4. Helpers
|
|
122
|
+
// ------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function formatDropboxPath(pathStr: string): string {
|
|
125
|
+
if (!pathStr || pathStr === "/" || pathStr.trim() === "") return "";
|
|
126
|
+
return pathStr.startsWith("/") ? pathStr : `/${pathStr}`;
|
|
132
127
|
}
|
|
133
128
|
|
|
134
|
-
|
|
135
|
-
function isTextFile(pathOrName: string): boolean {
|
|
129
|
+
function isSupportedFile(pathOrName: string): boolean {
|
|
136
130
|
const fileName = pathOrName.split('/').pop() || '';
|
|
137
131
|
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
138
|
-
|
|
132
|
+
if (fileName === ext) return true;
|
|
133
|
+
|
|
134
|
+
const supportedExtensions = [
|
|
139
135
|
'csv', 'tsv', 'txt', 'json', 'md', 'log',
|
|
140
136
|
'xml', 'yaml', 'yml', 'html', 'htm', 'css',
|
|
141
|
-
'js', 'ts', 'py', 'java', 'c', 'cpp', 'h'
|
|
137
|
+
'js', 'ts', 'py', 'java', 'c', 'cpp', 'h',
|
|
138
|
+
'pdf', 'docx', 'pptx', 'xlsx', 'xls', 'doc', 'ppt', 'odt', 'odp', 'ods'
|
|
142
139
|
];
|
|
143
|
-
return
|
|
140
|
+
return supportedExtensions.includes(ext);
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
/**
|
|
147
|
-
* Download a file from Dropbox.
|
|
148
|
-
* - If DROPBOX_SHARED_URL is set, uses shared link APIs with relative paths.
|
|
149
|
-
* - Otherwise, uses the regular filesDownload with absolute paths.
|
|
150
|
-
* Returns UTF‑8 text, or throws if binary.
|
|
151
|
-
*/
|
|
152
143
|
async function downloadHelper(identifier: string): Promise<string> {
|
|
153
144
|
let result: any;
|
|
154
145
|
let fileName: string | undefined;
|
|
155
146
|
|
|
156
|
-
// If the identifier is a full URL, we always treat it as a shared link.
|
|
157
147
|
if (identifier.startsWith("http://") || identifier.startsWith("https://")) {
|
|
158
|
-
const response = await client.sharingGetSharedLinkFile({ url: identifier });
|
|
148
|
+
const response = await limited(() => client.sharingGetSharedLinkFile({ url: identifier }));
|
|
159
149
|
result = response.result;
|
|
160
150
|
fileName = result.name || identifier.split('/').pop();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// Shared link mode – identifier is a relative path.
|
|
165
|
-
const path = normalizeSharedPath(identifier);
|
|
166
|
-
const response = await client.sharingGetSharedLinkFile({
|
|
151
|
+
} else if (DROPBOX_SHARED_URL) {
|
|
152
|
+
const reqPath = formatDropboxPath(identifier);
|
|
153
|
+
const response = await limited(() => client.sharingGetSharedLinkFile({
|
|
167
154
|
url: DROPBOX_SHARED_URL,
|
|
168
|
-
path:
|
|
169
|
-
});
|
|
155
|
+
path: reqPath,
|
|
156
|
+
}));
|
|
170
157
|
result = response.result;
|
|
171
158
|
fileName = result.name || identifier;
|
|
172
159
|
} else {
|
|
173
|
-
|
|
174
|
-
const response = await client.filesDownload({ path:
|
|
160
|
+
const reqPath = formatDropboxPath(identifier);
|
|
161
|
+
const response = await limited(() => client.filesDownload({ path: reqPath }));
|
|
175
162
|
result = response.result;
|
|
176
163
|
fileName = result.name || identifier;
|
|
177
164
|
}
|
|
178
165
|
|
|
179
|
-
|
|
166
|
+
const fileRef = fileName || identifier;
|
|
167
|
+
if (!isSupportedFile(fileRef)) {
|
|
168
|
+
throw new Error(`File "${fileRef}" appears to be an unsupported file format.`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let fileBuffer: Buffer;
|
|
172
|
+
if (result.fileBinary) {
|
|
173
|
+
fileBuffer = Buffer.from(result.fileBinary);
|
|
174
|
+
} else if (result.fileBlob) {
|
|
175
|
+
const arrayBuffer = await result.fileBlob.arrayBuffer();
|
|
176
|
+
fileBuffer = Buffer.from(arrayBuffer);
|
|
177
|
+
} else {
|
|
180
178
|
throw new Error("Download succeeded but no file content was returned.");
|
|
181
179
|
}
|
|
182
180
|
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
const ext = fileRef.split('.').pop()?.toLowerCase() || '';
|
|
182
|
+
const binaryExtensions = ['pdf', 'docx', 'pptx', 'xlsx', 'xls', 'doc', 'ppt', 'odt', 'odp', 'ods'];
|
|
183
|
+
|
|
184
|
+
if (binaryExtensions.includes(ext)) {
|
|
185
|
+
const tempFilePath = path.join(
|
|
186
|
+
os.tmpdir(),
|
|
187
|
+
`temp_${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`
|
|
188
188
|
);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await fs.writeFile(tempFilePath, fileBuffer);
|
|
192
|
+
// officeParser is called from the default import
|
|
193
|
+
const ast = await officeParser.parseOffice(fileBuffer);
|
|
194
|
+
|
|
195
|
+
const result = await ast.to('md');
|
|
196
|
+
return (result.value as string) || "Document parsed, but it appears to be empty.";
|
|
197
|
+
} catch (error: any) {
|
|
198
|
+
throw new Error(`Failed to parse binary document: ${error.message}`);
|
|
199
|
+
} finally {
|
|
200
|
+
await fs.unlink(tempFilePath).catch(() => { });
|
|
201
|
+
}
|
|
189
202
|
}
|
|
190
203
|
|
|
191
|
-
return
|
|
204
|
+
return fileBuffer.toString('utf-8');
|
|
192
205
|
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Parse a CSV header line, handling quoted fields and trimming.
|
|
196
|
-
* Returns an array of clean column names.
|
|
197
|
-
*/
|
|
198
206
|
function parseCsvHeader(headerLine: string): string[] {
|
|
199
207
|
const fields: string[] = [];
|
|
200
208
|
let current = '';
|
|
@@ -223,47 +231,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
223
231
|
tools: [
|
|
224
232
|
{
|
|
225
233
|
name: "list_folder",
|
|
226
|
-
description:
|
|
227
|
-
? "List folders and files inside the shared Dropbox folder. Path must be relative to the shared root (e.g., '' for root, 'Data' for /Data)."
|
|
228
|
-
: "List folders and files in a specific Dropbox path (absolute, e.g., '/Data' or '' for root).",
|
|
234
|
+
description: "List folders and files. Use '' for root, or '/FolderName' for subfolders.",
|
|
229
235
|
inputSchema: {
|
|
230
236
|
type: "object",
|
|
231
|
-
properties: {
|
|
232
|
-
path: {
|
|
233
|
-
type: "string",
|
|
234
|
-
description: DROPBOX_SHARED_URL
|
|
235
|
-
? "Relative path inside the shared folder (e.g., '' or 'Data')."
|
|
236
|
-
: "Absolute path to list (e.g., '' for root or '/Data')."
|
|
237
|
-
}
|
|
238
|
-
},
|
|
237
|
+
properties: { path: { type: "string" } },
|
|
239
238
|
required: ["path"]
|
|
240
239
|
}
|
|
241
240
|
},
|
|
242
241
|
{
|
|
243
242
|
name: "search_files",
|
|
244
|
-
description: "Search for files by name, extension, or content
|
|
243
|
+
description: "Search for files by name, extension, or content.",
|
|
245
244
|
inputSchema: {
|
|
246
245
|
type: "object",
|
|
247
|
-
properties: {
|
|
248
|
-
query: { type: "string", description: "The search query (e.g., 'sales report' or '.csv')" }
|
|
249
|
-
},
|
|
246
|
+
properties: { query: { type: "string" } },
|
|
250
247
|
required: ["query"]
|
|
251
248
|
}
|
|
252
249
|
},
|
|
253
250
|
{
|
|
254
251
|
name: "get_file_metadata",
|
|
255
|
-
description:
|
|
256
|
-
? "Retrieve metadata (size, modified date) for a file inside the shared folder. Path is relative to the shared root."
|
|
257
|
-
: "Retrieve metadata (size, modified date) for a file at an absolute Dropbox path.",
|
|
252
|
+
description: "Retrieve size/modified date for a file.",
|
|
258
253
|
inputSchema: {
|
|
259
254
|
type: "object",
|
|
260
|
-
properties: {
|
|
261
|
-
path: {
|
|
262
|
-
type: "string", description: DROPBOX_SHARED_URL
|
|
263
|
-
? "Relative path to the file inside the shared folder."
|
|
264
|
-
: "Exact Dropbox file path (e.g., '/Data/file.csv')."
|
|
265
|
-
}
|
|
266
|
-
},
|
|
255
|
+
properties: { path: { type: "string" } },
|
|
267
256
|
required: ["path"]
|
|
268
257
|
}
|
|
269
258
|
},
|
|
@@ -272,28 +261,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
272
261
|
description: "Display the schema (columns/headers) of a CSV or TSV file.",
|
|
273
262
|
inputSchema: {
|
|
274
263
|
type: "object",
|
|
275
|
-
properties: {
|
|
276
|
-
identifier: {
|
|
277
|
-
type: "string", description: DROPBOX_SHARED_URL
|
|
278
|
-
? "Relative path to the table file (e.g., 'data.csv') or a shared link URL."
|
|
279
|
-
: "Dropbox path (e.g., '/Data/file.csv') or shared link URL."
|
|
280
|
-
}
|
|
281
|
-
},
|
|
264
|
+
properties: { identifier: { type: "string" } },
|
|
282
265
|
required: ["identifier"]
|
|
283
266
|
}
|
|
284
267
|
},
|
|
285
268
|
{
|
|
286
269
|
name: "read_data",
|
|
287
|
-
description: "Download and read the content of a text file
|
|
270
|
+
description: "Download and read the content of a text file. Truncated if over 100k chars.",
|
|
288
271
|
inputSchema: {
|
|
289
272
|
type: "object",
|
|
290
|
-
properties: {
|
|
291
|
-
identifier: {
|
|
292
|
-
type: "string", description: DROPBOX_SHARED_URL
|
|
293
|
-
? "Relative path (e.g., 'data.csv') or a shared link URL."
|
|
294
|
-
: "Dropbox path (e.g., '/Data/file.csv') or shared link URL."
|
|
295
|
-
}
|
|
296
|
-
},
|
|
273
|
+
properties: { identifier: { type: "string" } },
|
|
297
274
|
required: ["identifier"]
|
|
298
275
|
}
|
|
299
276
|
}
|
|
@@ -301,30 +278,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
301
278
|
}));
|
|
302
279
|
|
|
303
280
|
// ------------------------------------------------------------------
|
|
304
|
-
// 6. Tool Handlers
|
|
281
|
+
// 6. Tool Handlers (Using direct limited wrapper)
|
|
305
282
|
// ------------------------------------------------------------------
|
|
306
283
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
307
284
|
try {
|
|
308
285
|
const name = request.params.name;
|
|
309
286
|
const args = request.params.arguments || {};
|
|
310
287
|
|
|
311
|
-
// --- list_folder ---
|
|
312
288
|
if (name === "list_folder") {
|
|
313
|
-
|
|
289
|
+
const rawPath = (args.path as string) || "";
|
|
314
290
|
let requestArgs: any;
|
|
315
291
|
|
|
316
292
|
if (DROPBOX_SHARED_URL) {
|
|
317
|
-
path = normalizeSharedPath(path);
|
|
318
293
|
requestArgs = {
|
|
319
|
-
path:
|
|
294
|
+
path: formatDropboxPath(rawPath),
|
|
320
295
|
shared_link: { url: DROPBOX_SHARED_URL }
|
|
321
296
|
};
|
|
322
297
|
} else {
|
|
323
|
-
|
|
324
|
-
requestArgs = { path: path === "/" ? "" : path };
|
|
298
|
+
requestArgs = { path: formatDropboxPath(rawPath) };
|
|
325
299
|
}
|
|
326
300
|
|
|
327
|
-
const response = await client.filesListFolder(requestArgs);
|
|
301
|
+
const response = await limited(() => client.filesListFolder(requestArgs));
|
|
328
302
|
const entries = response.result.entries.map((e: any) =>
|
|
329
303
|
`[${e['.tag'].toUpperCase()}] ${e.name} (Path: ${e.path_display})`
|
|
330
304
|
);
|
|
@@ -333,11 +307,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
333
307
|
};
|
|
334
308
|
}
|
|
335
309
|
|
|
336
|
-
// --- search_files ---
|
|
337
310
|
if (name === "search_files") {
|
|
338
|
-
const response = await client.filesSearchV2({ query: args.query as string });
|
|
311
|
+
const response = await limited(() => client.filesSearchV2({ query: args.query as string }));
|
|
339
312
|
const matches = response.result.matches.map((m: any) => {
|
|
340
|
-
const meta =
|
|
313
|
+
const meta = m.metadata.metadata;
|
|
341
314
|
return `[${meta['.tag'].toUpperCase()}] ${meta.name} (Path: ${meta.path_display})`;
|
|
342
315
|
});
|
|
343
316
|
return {
|
|
@@ -345,106 +318,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
345
318
|
};
|
|
346
319
|
}
|
|
347
320
|
|
|
348
|
-
// --- get_file_metadata ---
|
|
349
321
|
if (name === "get_file_metadata") {
|
|
350
322
|
let response;
|
|
351
|
-
const
|
|
323
|
+
const path = formatDropboxPath((args.path as string) || "");
|
|
352
324
|
|
|
353
325
|
if (DROPBOX_SHARED_URL) {
|
|
354
|
-
|
|
355
|
-
response = await client.sharingGetSharedLinkMetadata({
|
|
326
|
+
response = await limited(() => client.sharingGetSharedLinkMetadata({
|
|
356
327
|
url: DROPBOX_SHARED_URL,
|
|
357
328
|
path: path
|
|
358
|
-
});
|
|
329
|
+
}));
|
|
359
330
|
} else {
|
|
360
|
-
response = await client.filesGetMetadata({ path:
|
|
331
|
+
response = await limited(() => client.filesGetMetadata({ path: path }));
|
|
361
332
|
}
|
|
362
|
-
|
|
363
333
|
return {
|
|
364
334
|
content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }]
|
|
365
335
|
};
|
|
366
336
|
}
|
|
367
337
|
|
|
368
|
-
// --- read_data ---
|
|
369
338
|
if (name === "read_data") {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return { content: [{ type: "text", text: truncated }] };
|
|
376
|
-
} catch (err: any) {
|
|
377
|
-
if (isNotFoundError(err)) {
|
|
378
|
-
return {
|
|
379
|
-
content: [{ type: "text", text: `File not found at "${args.identifier}".` }],
|
|
380
|
-
isError: true
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
if (isPermissionError(err)) {
|
|
384
|
-
return {
|
|
385
|
-
content: [{ type: "text", text: `Permission denied for "${args.identifier}".` }],
|
|
386
|
-
isError: true
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
if (isBadRequestError(err)) {
|
|
390
|
-
return {
|
|
391
|
-
content: [{ type: "text", text: `Invalid request. Check the path/identifier: "${args.identifier}".` }],
|
|
392
|
-
isError: true
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
if (err.message && err.message.includes("binary file")) {
|
|
396
|
-
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
397
|
-
}
|
|
398
|
-
throw err;
|
|
399
|
-
}
|
|
339
|
+
const text = await downloadHelper(args.identifier as string);
|
|
340
|
+
const truncated = text.length > 100000
|
|
341
|
+
? text.substring(0, 100000) + "\n... [WARNING: FILE TRUNCATED TO 100K CHARACTERS]"
|
|
342
|
+
: text;
|
|
343
|
+
return { content: [{ type: "text", text: truncated }] };
|
|
400
344
|
}
|
|
401
345
|
|
|
402
|
-
// --- describe_table ---
|
|
403
346
|
if (name === "describe_table") {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (commaCount > 0) {
|
|
417
|
-
headers = parseCsvHeader(firstLine);
|
|
418
|
-
if (headers.length < 2) {
|
|
419
|
-
headers = lines.map((_, i) => `Column ${i + 1}`);
|
|
420
|
-
totalRows = lines.length;
|
|
421
|
-
}
|
|
422
|
-
} else {
|
|
423
|
-
headers = lines.map((_, i) => `Column ${i + 1}`);
|
|
424
|
-
totalRows = lines.length;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const schemaStr =
|
|
428
|
-
`Columns detected:\n- ${headers.join('\n- ')}\n\nTotal rows: ${totalRows}`;
|
|
429
|
-
return { content: [{ type: "text", text: schemaStr }] };
|
|
430
|
-
} catch (err: any) {
|
|
431
|
-
if (isNotFoundError(err)) {
|
|
432
|
-
return {
|
|
433
|
-
content: [{ type: "text", text: `File not found at "${args.identifier}".` }],
|
|
434
|
-
isError: true
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
if (isPermissionError(err)) {
|
|
438
|
-
return {
|
|
439
|
-
content: [{ type: "text", text: `Permission denied for "${args.identifier}".` }],
|
|
440
|
-
isError: true
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
if (err.message && err.message.includes("binary file")) {
|
|
444
|
-
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
445
|
-
}
|
|
446
|
-
throw err;
|
|
347
|
+
// Note: This still downloads the full file to read the first line.
|
|
348
|
+
// Be careful running this on files >50MB as it may cause timeouts.
|
|
349
|
+
const text = await downloadHelper(args.identifier as string);
|
|
350
|
+
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
|
|
351
|
+
if (lines.length === 0) return { content: [{ type: "text", text: "File is empty." }] };
|
|
352
|
+
|
|
353
|
+
let headers: string[];
|
|
354
|
+
const firstLine = lines[0];
|
|
355
|
+
if ((firstLine.match(/,/g) || []).length > 0) {
|
|
356
|
+
headers = parseCsvHeader(firstLine);
|
|
357
|
+
} else {
|
|
358
|
+
headers = lines.map((_, i) => `Column ${i + 1}`);
|
|
447
359
|
}
|
|
360
|
+
|
|
361
|
+
const schemaStr = `Columns detected:\n- ${headers.join('\n- ')}\n\nTotal rows: ${lines.length - 1}`;
|
|
362
|
+
return { content: [{ type: "text", text: schemaStr }] };
|
|
448
363
|
}
|
|
449
364
|
|
|
450
365
|
throw new Error(`Unknown tool: ${name}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dropbox-mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dropbox-mcp-server": "dist/index.js"
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
14
14
|
"dotenv": "^16.4.5",
|
|
15
15
|
"dropbox": "^10.34.0",
|
|
16
|
-
"dropbox-mcp-server": "^2.0.0"
|
|
16
|
+
"dropbox-mcp-server": "^2.0.0",
|
|
17
|
+
"fs": "^0.0.1-security",
|
|
18
|
+
"officeparser": "^7.3.0",
|
|
19
|
+
"os": "^0.1.2",
|
|
20
|
+
"path": "^0.12.7"
|
|
17
21
|
},
|
|
18
22
|
"devDependencies": {
|
|
19
23
|
"typescript": "^5.9.3"
|