dropbox-mcp-server 2.1.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 +182 -95
- package/index.ts +198 -94
- 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
|
// ------------------------------------------------------------------
|
|
@@ -84,165 +73,263 @@ function isNotFoundError(err) {
|
|
|
84
73
|
return false;
|
|
85
74
|
}
|
|
86
75
|
}
|
|
76
|
+
function isPermissionError(err) {
|
|
77
|
+
return err?.status === 403;
|
|
78
|
+
}
|
|
79
|
+
function isBadRequestError(err) {
|
|
80
|
+
return err?.status === 400;
|
|
81
|
+
}
|
|
87
82
|
// ------------------------------------------------------------------
|
|
88
|
-
// 3.
|
|
83
|
+
// 3. Environment & Client
|
|
89
84
|
// ------------------------------------------------------------------
|
|
90
85
|
const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
|
|
91
86
|
const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
|
|
92
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
|
|
93
89
|
if (!DROPBOX_APP_KEY || !DROPBOX_REFRESH_TOKEN) {
|
|
94
|
-
console.error("Missing
|
|
90
|
+
console.error("Missing required environment variables: DROPBOX_APP_KEY, DROPBOX_REFRESH_TOKEN");
|
|
95
91
|
process.exit(1);
|
|
96
92
|
}
|
|
97
|
-
|
|
93
|
+
// Instantiate direct client (no proxy)
|
|
94
|
+
const client = new Dropbox({
|
|
98
95
|
clientId: DROPBOX_APP_KEY,
|
|
99
|
-
|
|
96
|
+
clientSecret: DROPBOX_APP_SECRET, // Needed if your Dropbox app is configured with a secret
|
|
97
|
+
refreshToken: DROPBOX_REFRESH_TOKEN,
|
|
100
98
|
});
|
|
101
|
-
const
|
|
102
|
-
|
|
99
|
+
const server = new Server({ name: "dropbox-agent-mcp", version: "2.0.1" }, { capabilities: { tools: {} } });
|
|
100
|
+
// ------------------------------------------------------------------
|
|
101
|
+
// 4. Helpers (Fixed Pathing & Node 18+ Blobs)
|
|
103
102
|
// ------------------------------------------------------------------
|
|
104
|
-
|
|
103
|
+
import officeParser from 'officeparser';
|
|
104
|
+
import * as fs from 'fs/promises';
|
|
105
|
+
import * as path from 'path';
|
|
106
|
+
import * as os from 'os';
|
|
107
|
+
// ------------------------------------------------------------------
|
|
108
|
+
// 4. Helpers
|
|
109
|
+
// ------------------------------------------------------------------
|
|
110
|
+
function formatDropboxPath(pathStr) {
|
|
111
|
+
if (!pathStr || pathStr === "/" || pathStr.trim() === "")
|
|
112
|
+
return "";
|
|
113
|
+
return pathStr.startsWith("/") ? pathStr : `/${pathStr}`;
|
|
114
|
+
}
|
|
115
|
+
function isSupportedFile(pathOrName) {
|
|
116
|
+
const fileName = pathOrName.split('/').pop() || '';
|
|
117
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
118
|
+
if (fileName === ext)
|
|
119
|
+
return true;
|
|
120
|
+
const supportedExtensions = [
|
|
121
|
+
'csv', 'tsv', 'txt', 'json', 'md', 'log',
|
|
122
|
+
'xml', 'yaml', 'yml', 'html', 'htm', 'css',
|
|
123
|
+
'js', 'ts', 'py', 'java', 'c', 'cpp', 'h',
|
|
124
|
+
'pdf', 'docx', 'pptx', 'xlsx', 'xls', 'doc', 'ppt', 'odt', 'odp', 'ods'
|
|
125
|
+
];
|
|
126
|
+
return supportedExtensions.includes(ext);
|
|
127
|
+
}
|
|
128
|
+
async function downloadHelper(identifier) {
|
|
129
|
+
let result;
|
|
130
|
+
let fileName;
|
|
131
|
+
if (identifier.startsWith("http://") || identifier.startsWith("https://")) {
|
|
132
|
+
const response = await limited(() => client.sharingGetSharedLinkFile({ url: identifier }));
|
|
133
|
+
result = response.result;
|
|
134
|
+
fileName = result.name || identifier.split('/').pop();
|
|
135
|
+
}
|
|
136
|
+
else if (DROPBOX_SHARED_URL) {
|
|
137
|
+
const reqPath = formatDropboxPath(identifier);
|
|
138
|
+
const response = await limited(() => client.sharingGetSharedLinkFile({
|
|
139
|
+
url: DROPBOX_SHARED_URL,
|
|
140
|
+
path: reqPath,
|
|
141
|
+
}));
|
|
142
|
+
result = response.result;
|
|
143
|
+
fileName = result.name || identifier;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const reqPath = formatDropboxPath(identifier);
|
|
147
|
+
const response = await limited(() => client.filesDownload({ path: reqPath }));
|
|
148
|
+
result = response.result;
|
|
149
|
+
fileName = result.name || identifier;
|
|
150
|
+
}
|
|
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 {
|
|
164
|
+
throw new Error("Download succeeded but no file content was returned.");
|
|
165
|
+
}
|
|
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
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return fileBuffer.toString('utf-8');
|
|
185
|
+
}
|
|
186
|
+
function parseCsvHeader(headerLine) {
|
|
187
|
+
const fields = [];
|
|
188
|
+
let current = '';
|
|
189
|
+
let insideQuotes = false;
|
|
190
|
+
for (let i = 0; i < headerLine.length; i++) {
|
|
191
|
+
const ch = headerLine[i];
|
|
192
|
+
if (ch === '"') {
|
|
193
|
+
insideQuotes = !insideQuotes;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (ch === ',' && !insideQuotes) {
|
|
197
|
+
fields.push(current.trim());
|
|
198
|
+
current = '';
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
current += ch;
|
|
202
|
+
}
|
|
203
|
+
fields.push(current.trim());
|
|
204
|
+
return fields.map(f => f.replace(/^"|"$/g, '').trim());
|
|
205
|
+
}
|
|
206
|
+
// ------------------------------------------------------------------
|
|
207
|
+
// 5. Tool Definitions
|
|
105
208
|
// ------------------------------------------------------------------
|
|
106
209
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
107
210
|
tools: [
|
|
108
211
|
{
|
|
109
212
|
name: "list_folder",
|
|
110
|
-
description: "List folders and files
|
|
213
|
+
description: "List folders and files. Use '' for root, or '/FolderName' for subfolders.",
|
|
111
214
|
inputSchema: {
|
|
112
215
|
type: "object",
|
|
113
|
-
properties: {
|
|
114
|
-
path: { type: "string", description: "Path to list, e.g., '' for root or '/Data'" }
|
|
115
|
-
},
|
|
216
|
+
properties: { path: { type: "string" } },
|
|
116
217
|
required: ["path"]
|
|
117
218
|
}
|
|
118
219
|
},
|
|
119
220
|
{
|
|
120
221
|
name: "search_files",
|
|
121
|
-
description: "Search for files by name, extension, or content
|
|
222
|
+
description: "Search for files by name, extension, or content.",
|
|
122
223
|
inputSchema: {
|
|
123
224
|
type: "object",
|
|
124
|
-
properties: {
|
|
125
|
-
query: { type: "string", description: "The search query (e.g., 'sales report' or '.csv')" }
|
|
126
|
-
},
|
|
225
|
+
properties: { query: { type: "string" } },
|
|
127
226
|
required: ["query"]
|
|
128
227
|
}
|
|
129
228
|
},
|
|
130
229
|
{
|
|
131
230
|
name: "get_file_metadata",
|
|
132
|
-
description: "Retrieve
|
|
231
|
+
description: "Retrieve size/modified date for a file.",
|
|
133
232
|
inputSchema: {
|
|
134
233
|
type: "object",
|
|
135
|
-
properties: {
|
|
136
|
-
path: { type: "string", description: "Exact Dropbox file path" }
|
|
137
|
-
},
|
|
234
|
+
properties: { path: { type: "string" } },
|
|
138
235
|
required: ["path"]
|
|
139
236
|
}
|
|
140
237
|
},
|
|
141
238
|
{
|
|
142
239
|
name: "describe_table",
|
|
143
|
-
description: "Display the schema (columns/headers) of a
|
|
240
|
+
description: "Display the schema (columns/headers) of a CSV or TSV file.",
|
|
144
241
|
inputSchema: {
|
|
145
242
|
type: "object",
|
|
146
|
-
properties: {
|
|
147
|
-
identifier: { type: "string", description: "Dropbox path or shared link to the table" }
|
|
148
|
-
},
|
|
243
|
+
properties: { identifier: { type: "string" } },
|
|
149
244
|
required: ["identifier"]
|
|
150
245
|
}
|
|
151
246
|
},
|
|
152
247
|
{
|
|
153
248
|
name: "read_data",
|
|
154
|
-
description: "Download and read
|
|
249
|
+
description: "Download and read the content of a text file. Truncated if over 100k chars.",
|
|
155
250
|
inputSchema: {
|
|
156
251
|
type: "object",
|
|
157
|
-
properties: {
|
|
158
|
-
identifier: { type: "string", description: "The Dropbox path (e.g., /data.csv) or shared link URL." }
|
|
159
|
-
},
|
|
252
|
+
properties: { identifier: { type: "string" } },
|
|
160
253
|
required: ["identifier"]
|
|
161
254
|
}
|
|
162
255
|
}
|
|
163
256
|
]
|
|
164
257
|
}));
|
|
165
258
|
// ------------------------------------------------------------------
|
|
166
|
-
//
|
|
259
|
+
// 6. Tool Handlers (Using direct limited wrapper)
|
|
167
260
|
// ------------------------------------------------------------------
|
|
168
|
-
async function downloadHelper(identifier) {
|
|
169
|
-
let result;
|
|
170
|
-
if (identifier.startsWith("http://") || identifier.startsWith("https://")) {
|
|
171
|
-
const response = await client.sharingGetSharedLinkFile({ url: identifier });
|
|
172
|
-
result = response.result;
|
|
173
|
-
}
|
|
174
|
-
else if (DROPBOX_SHARED_URL) {
|
|
175
|
-
const response = await client.sharingGetSharedLinkFile({ url: DROPBOX_SHARED_URL, path: identifier });
|
|
176
|
-
result = response.result;
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
const response = await client.filesDownload({ path: identifier });
|
|
180
|
-
result = response.result;
|
|
181
|
-
}
|
|
182
|
-
if (!result.fileBinary)
|
|
183
|
-
throw new Error("Download succeeded but no file content was returned.");
|
|
184
|
-
return Buffer.from(result.fileBinary).toString('utf-8');
|
|
185
|
-
}
|
|
186
261
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
187
262
|
try {
|
|
188
263
|
const name = request.params.name;
|
|
189
264
|
const args = request.params.arguments || {};
|
|
190
265
|
if (name === "list_folder") {
|
|
191
|
-
const
|
|
192
|
-
|
|
266
|
+
const rawPath = args.path || "";
|
|
267
|
+
let requestArgs;
|
|
193
268
|
if (DROPBOX_SHARED_URL) {
|
|
194
|
-
requestArgs
|
|
269
|
+
requestArgs = {
|
|
270
|
+
path: formatDropboxPath(rawPath),
|
|
271
|
+
shared_link: { url: DROPBOX_SHARED_URL }
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
requestArgs = { path: formatDropboxPath(rawPath) };
|
|
195
276
|
}
|
|
196
|
-
const response = await client.filesListFolder(requestArgs);
|
|
197
|
-
const entries = response.result.entries.map(e => `[${e['.tag'].toUpperCase()}] ${e.name} (Path: ${e.path_display})`);
|
|
198
|
-
return {
|
|
277
|
+
const response = await limited(() => client.filesListFolder(requestArgs));
|
|
278
|
+
const entries = response.result.entries.map((e) => `[${e['.tag'].toUpperCase()}] ${e.name} (Path: ${e.path_display})`);
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: entries.join("\n") || "Folder is empty." }]
|
|
281
|
+
};
|
|
199
282
|
}
|
|
200
283
|
if (name === "search_files") {
|
|
201
|
-
const response = await client.filesSearchV2({ query: args.query });
|
|
202
|
-
const matches = response.result.matches.map(m => {
|
|
284
|
+
const response = await limited(() => client.filesSearchV2({ query: args.query }));
|
|
285
|
+
const matches = response.result.matches.map((m) => {
|
|
203
286
|
const meta = m.metadata.metadata;
|
|
204
287
|
return `[${meta['.tag'].toUpperCase()}] ${meta.name} (Path: ${meta.path_display})`;
|
|
205
288
|
});
|
|
206
|
-
return {
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: matches.join("\n") || "No matches found." }]
|
|
291
|
+
};
|
|
207
292
|
}
|
|
208
293
|
if (name === "get_file_metadata") {
|
|
209
294
|
let response;
|
|
295
|
+
const path = formatDropboxPath(args.path || "");
|
|
210
296
|
if (DROPBOX_SHARED_URL) {
|
|
211
|
-
response = await client.sharingGetSharedLinkMetadata({
|
|
297
|
+
response = await limited(() => client.sharingGetSharedLinkMetadata({
|
|
298
|
+
url: DROPBOX_SHARED_URL,
|
|
299
|
+
path: path
|
|
300
|
+
}));
|
|
212
301
|
}
|
|
213
302
|
else {
|
|
214
|
-
response = await client.filesGetMetadata({ path:
|
|
303
|
+
response = await limited(() => client.filesGetMetadata({ path: path }));
|
|
215
304
|
}
|
|
216
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }]
|
|
307
|
+
};
|
|
217
308
|
}
|
|
218
309
|
if (name === "read_data") {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (isNotFoundError(err)) {
|
|
225
|
-
return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
|
|
226
|
-
}
|
|
227
|
-
throw err;
|
|
228
|
-
}
|
|
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 }] };
|
|
229
315
|
}
|
|
230
316
|
if (name === "describe_table") {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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);
|
|
239
327
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
|
|
243
|
-
}
|
|
244
|
-
throw err;
|
|
328
|
+
else {
|
|
329
|
+
headers = lines.map((_, i) => `Column ${i + 1}`);
|
|
245
330
|
}
|
|
331
|
+
const schemaStr = `Columns detected:\n- ${headers.join('\n- ')}\n\nTotal rows: ${lines.length - 1}`;
|
|
332
|
+
return { content: [{ type: "text", text: schemaStr }] };
|
|
246
333
|
}
|
|
247
334
|
throw new Error(`Unknown tool: ${name}`);
|
|
248
335
|
}
|
|
@@ -254,11 +341,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
254
341
|
}
|
|
255
342
|
});
|
|
256
343
|
// ------------------------------------------------------------------
|
|
257
|
-
//
|
|
344
|
+
// 7. Start the Server
|
|
258
345
|
// ------------------------------------------------------------------
|
|
259
346
|
async function run() {
|
|
260
347
|
const transport = new StdioServerTransport();
|
|
261
348
|
await server.connect(transport);
|
|
262
|
-
console.error(
|
|
349
|
+
console.error(`Dropbox MCP Server running on stdio ${DROPBOX_SHARED_URL ? '(shared link mode)' : '(full account mode)'}`);
|
|
263
350
|
}
|
|
264
351
|
run().catch(console.error);
|
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
|
// ------------------------------------------------------------------
|
|
@@ -87,86 +75,202 @@ function isNotFoundError(err: any): boolean {
|
|
|
87
75
|
}
|
|
88
76
|
}
|
|
89
77
|
|
|
78
|
+
function isPermissionError(err: any): boolean {
|
|
79
|
+
return err?.status === 403;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isBadRequestError(err: any): boolean {
|
|
83
|
+
return err?.status === 400;
|
|
84
|
+
}
|
|
85
|
+
|
|
90
86
|
// ------------------------------------------------------------------
|
|
91
|
-
// 3.
|
|
87
|
+
// 3. Environment & Client
|
|
92
88
|
// ------------------------------------------------------------------
|
|
93
89
|
const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
|
|
94
90
|
const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
|
|
95
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
|
|
96
93
|
|
|
97
94
|
if (!DROPBOX_APP_KEY || !DROPBOX_REFRESH_TOKEN) {
|
|
98
|
-
console.error("Missing
|
|
95
|
+
console.error("Missing required environment variables: DROPBOX_APP_KEY, DROPBOX_REFRESH_TOKEN");
|
|
99
96
|
process.exit(1);
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
|
|
99
|
+
// Instantiate direct client (no proxy)
|
|
100
|
+
const client = new Dropbox({
|
|
103
101
|
clientId: DROPBOX_APP_KEY,
|
|
104
|
-
|
|
102
|
+
clientSecret: DROPBOX_APP_SECRET, // Needed if your Dropbox app is configured with a secret
|
|
103
|
+
refreshToken: DROPBOX_REFRESH_TOKEN,
|
|
105
104
|
});
|
|
106
|
-
const client = throttleClient(rawClient);
|
|
107
105
|
|
|
108
106
|
const server = new Server(
|
|
109
|
-
{ name: "dropbox-agent-mcp", version: "2.1
|
|
107
|
+
{ name: "dropbox-agent-mcp", version: "2.0.1" },
|
|
110
108
|
{ capabilities: { tools: {} } }
|
|
111
109
|
);
|
|
112
110
|
|
|
113
111
|
// ------------------------------------------------------------------
|
|
114
|
-
// 4.
|
|
112
|
+
// 4. Helpers (Fixed Pathing & Node 18+ Blobs)
|
|
113
|
+
// ------------------------------------------------------------------
|
|
114
|
+
|
|
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}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isSupportedFile(pathOrName: string): boolean {
|
|
130
|
+
const fileName = pathOrName.split('/').pop() || '';
|
|
131
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
132
|
+
if (fileName === ext) return true;
|
|
133
|
+
|
|
134
|
+
const supportedExtensions = [
|
|
135
|
+
'csv', 'tsv', 'txt', 'json', 'md', 'log',
|
|
136
|
+
'xml', 'yaml', 'yml', 'html', 'htm', 'css',
|
|
137
|
+
'js', 'ts', 'py', 'java', 'c', 'cpp', 'h',
|
|
138
|
+
'pdf', 'docx', 'pptx', 'xlsx', 'xls', 'doc', 'ppt', 'odt', 'odp', 'ods'
|
|
139
|
+
];
|
|
140
|
+
return supportedExtensions.includes(ext);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function downloadHelper(identifier: string): Promise<string> {
|
|
144
|
+
let result: any;
|
|
145
|
+
let fileName: string | undefined;
|
|
146
|
+
|
|
147
|
+
if (identifier.startsWith("http://") || identifier.startsWith("https://")) {
|
|
148
|
+
const response = await limited(() => client.sharingGetSharedLinkFile({ url: identifier }));
|
|
149
|
+
result = response.result;
|
|
150
|
+
fileName = result.name || identifier.split('/').pop();
|
|
151
|
+
} else if (DROPBOX_SHARED_URL) {
|
|
152
|
+
const reqPath = formatDropboxPath(identifier);
|
|
153
|
+
const response = await limited(() => client.sharingGetSharedLinkFile({
|
|
154
|
+
url: DROPBOX_SHARED_URL,
|
|
155
|
+
path: reqPath,
|
|
156
|
+
}));
|
|
157
|
+
result = response.result;
|
|
158
|
+
fileName = result.name || identifier;
|
|
159
|
+
} else {
|
|
160
|
+
const reqPath = formatDropboxPath(identifier);
|
|
161
|
+
const response = await limited(() => client.filesDownload({ path: reqPath }));
|
|
162
|
+
result = response.result;
|
|
163
|
+
fileName = result.name || identifier;
|
|
164
|
+
}
|
|
165
|
+
|
|
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 {
|
|
178
|
+
throw new Error("Download succeeded but no file content was returned.");
|
|
179
|
+
}
|
|
180
|
+
|
|
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
|
+
);
|
|
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
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return fileBuffer.toString('utf-8');
|
|
205
|
+
}
|
|
206
|
+
function parseCsvHeader(headerLine: string): string[] {
|
|
207
|
+
const fields: string[] = [];
|
|
208
|
+
let current = '';
|
|
209
|
+
let insideQuotes = false;
|
|
210
|
+
for (let i = 0; i < headerLine.length; i++) {
|
|
211
|
+
const ch = headerLine[i];
|
|
212
|
+
if (ch === '"') {
|
|
213
|
+
insideQuotes = !insideQuotes;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (ch === ',' && !insideQuotes) {
|
|
217
|
+
fields.push(current.trim());
|
|
218
|
+
current = '';
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
current += ch;
|
|
222
|
+
}
|
|
223
|
+
fields.push(current.trim());
|
|
224
|
+
return fields.map(f => f.replace(/^"|"$/g, '').trim());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ------------------------------------------------------------------
|
|
228
|
+
// 5. Tool Definitions
|
|
115
229
|
// ------------------------------------------------------------------
|
|
116
230
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
117
231
|
tools: [
|
|
118
232
|
{
|
|
119
233
|
name: "list_folder",
|
|
120
|
-
description: "List folders and files
|
|
234
|
+
description: "List folders and files. Use '' for root, or '/FolderName' for subfolders.",
|
|
121
235
|
inputSchema: {
|
|
122
236
|
type: "object",
|
|
123
|
-
properties: {
|
|
124
|
-
path: { type: "string", description: "Path to list, e.g., '' for root or '/Data'" }
|
|
125
|
-
},
|
|
237
|
+
properties: { path: { type: "string" } },
|
|
126
238
|
required: ["path"]
|
|
127
239
|
}
|
|
128
240
|
},
|
|
129
241
|
{
|
|
130
242
|
name: "search_files",
|
|
131
|
-
description: "Search for files by name, extension, or content
|
|
243
|
+
description: "Search for files by name, extension, or content.",
|
|
132
244
|
inputSchema: {
|
|
133
245
|
type: "object",
|
|
134
|
-
properties: {
|
|
135
|
-
query: { type: "string", description: "The search query (e.g., 'sales report' or '.csv')" }
|
|
136
|
-
},
|
|
246
|
+
properties: { query: { type: "string" } },
|
|
137
247
|
required: ["query"]
|
|
138
248
|
}
|
|
139
249
|
},
|
|
140
250
|
{
|
|
141
251
|
name: "get_file_metadata",
|
|
142
|
-
description: "Retrieve
|
|
252
|
+
description: "Retrieve size/modified date for a file.",
|
|
143
253
|
inputSchema: {
|
|
144
254
|
type: "object",
|
|
145
|
-
properties: {
|
|
146
|
-
path: { type: "string", description: "Exact Dropbox file path" }
|
|
147
|
-
},
|
|
255
|
+
properties: { path: { type: "string" } },
|
|
148
256
|
required: ["path"]
|
|
149
257
|
}
|
|
150
258
|
},
|
|
151
259
|
{
|
|
152
260
|
name: "describe_table",
|
|
153
|
-
description: "Display the schema (columns/headers) of a
|
|
261
|
+
description: "Display the schema (columns/headers) of a CSV or TSV file.",
|
|
154
262
|
inputSchema: {
|
|
155
263
|
type: "object",
|
|
156
|
-
properties: {
|
|
157
|
-
identifier: { type: "string", description: "Dropbox path or shared link to the table" }
|
|
158
|
-
},
|
|
264
|
+
properties: { identifier: { type: "string" } },
|
|
159
265
|
required: ["identifier"]
|
|
160
266
|
}
|
|
161
267
|
},
|
|
162
268
|
{
|
|
163
269
|
name: "read_data",
|
|
164
|
-
description: "Download and read
|
|
270
|
+
description: "Download and read the content of a text file. Truncated if over 100k chars.",
|
|
165
271
|
inputSchema: {
|
|
166
272
|
type: "object",
|
|
167
|
-
properties: {
|
|
168
|
-
identifier: { type: "string", description: "The Dropbox path (e.g., /data.csv) or shared link URL." }
|
|
169
|
-
},
|
|
273
|
+
properties: { identifier: { type: "string" } },
|
|
170
274
|
required: ["identifier"]
|
|
171
275
|
}
|
|
172
276
|
}
|
|
@@ -174,88 +278,88 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
174
278
|
}));
|
|
175
279
|
|
|
176
280
|
// ------------------------------------------------------------------
|
|
177
|
-
//
|
|
281
|
+
// 6. Tool Handlers (Using direct limited wrapper)
|
|
178
282
|
// ------------------------------------------------------------------
|
|
179
|
-
async function downloadHelper(identifier: string) {
|
|
180
|
-
let result: any;
|
|
181
|
-
if (identifier.startsWith("http://") || identifier.startsWith("https://")) {
|
|
182
|
-
const response = await client.sharingGetSharedLinkFile({ url: identifier });
|
|
183
|
-
result = response.result;
|
|
184
|
-
} else if (DROPBOX_SHARED_URL) {
|
|
185
|
-
const response = await client.sharingGetSharedLinkFile({ url: DROPBOX_SHARED_URL, path: identifier });
|
|
186
|
-
result = response.result;
|
|
187
|
-
} else {
|
|
188
|
-
const response = await client.filesDownload({ path: identifier });
|
|
189
|
-
result = response.result;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (!result.fileBinary) throw new Error("Download succeeded but no file content was returned.");
|
|
193
|
-
return Buffer.from(result.fileBinary).toString('utf-8');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
283
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
197
284
|
try {
|
|
198
285
|
const name = request.params.name;
|
|
199
286
|
const args = request.params.arguments || {};
|
|
200
287
|
|
|
201
288
|
if (name === "list_folder") {
|
|
202
|
-
const
|
|
203
|
-
|
|
289
|
+
const rawPath = (args.path as string) || "";
|
|
290
|
+
let requestArgs: any;
|
|
291
|
+
|
|
204
292
|
if (DROPBOX_SHARED_URL) {
|
|
205
|
-
requestArgs
|
|
293
|
+
requestArgs = {
|
|
294
|
+
path: formatDropboxPath(rawPath),
|
|
295
|
+
shared_link: { url: DROPBOX_SHARED_URL }
|
|
296
|
+
};
|
|
297
|
+
} else {
|
|
298
|
+
requestArgs = { path: formatDropboxPath(rawPath) };
|
|
206
299
|
}
|
|
207
300
|
|
|
208
|
-
const response = await client.filesListFolder(requestArgs);
|
|
209
|
-
const entries = response.result.entries.map(
|
|
210
|
-
|
|
301
|
+
const response = await limited(() => client.filesListFolder(requestArgs));
|
|
302
|
+
const entries = response.result.entries.map((e: any) =>
|
|
303
|
+
`[${e['.tag'].toUpperCase()}] ${e.name} (Path: ${e.path_display})`
|
|
304
|
+
);
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: entries.join("\n") || "Folder is empty." }]
|
|
307
|
+
};
|
|
211
308
|
}
|
|
212
309
|
|
|
213
310
|
if (name === "search_files") {
|
|
214
|
-
const response = await client.filesSearchV2({ query: args.query as string });
|
|
215
|
-
const matches = response.result.matches.map(m => {
|
|
216
|
-
const meta =
|
|
311
|
+
const response = await limited(() => client.filesSearchV2({ query: args.query as string }));
|
|
312
|
+
const matches = response.result.matches.map((m: any) => {
|
|
313
|
+
const meta = m.metadata.metadata;
|
|
217
314
|
return `[${meta['.tag'].toUpperCase()}] ${meta.name} (Path: ${meta.path_display})`;
|
|
218
315
|
});
|
|
219
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: "text", text: matches.join("\n") || "No matches found." }]
|
|
318
|
+
};
|
|
220
319
|
}
|
|
221
320
|
|
|
222
321
|
if (name === "get_file_metadata") {
|
|
223
322
|
let response;
|
|
323
|
+
const path = formatDropboxPath((args.path as string) || "");
|
|
324
|
+
|
|
224
325
|
if (DROPBOX_SHARED_URL) {
|
|
225
|
-
response = await client.sharingGetSharedLinkMetadata({
|
|
326
|
+
response = await limited(() => client.sharingGetSharedLinkMetadata({
|
|
327
|
+
url: DROPBOX_SHARED_URL,
|
|
328
|
+
path: path
|
|
329
|
+
}));
|
|
226
330
|
} else {
|
|
227
|
-
response = await client.filesGetMetadata({ path:
|
|
331
|
+
response = await limited(() => client.filesGetMetadata({ path: path }));
|
|
228
332
|
}
|
|
229
|
-
return {
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }]
|
|
335
|
+
};
|
|
230
336
|
}
|
|
231
337
|
|
|
232
338
|
if (name === "read_data") {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
|
|
239
|
-
}
|
|
240
|
-
throw err;
|
|
241
|
-
}
|
|
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 }] };
|
|
242
344
|
}
|
|
243
345
|
|
|
244
346
|
if (name === "describe_table") {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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." }] };
|
|
249
352
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
throw err;
|
|
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}`);
|
|
258
359
|
}
|
|
360
|
+
|
|
361
|
+
const schemaStr = `Columns detected:\n- ${headers.join('\n- ')}\n\nTotal rows: ${lines.length - 1}`;
|
|
362
|
+
return { content: [{ type: "text", text: schemaStr }] };
|
|
259
363
|
}
|
|
260
364
|
|
|
261
365
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -268,12 +372,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
268
372
|
});
|
|
269
373
|
|
|
270
374
|
// ------------------------------------------------------------------
|
|
271
|
-
//
|
|
375
|
+
// 7. Start the Server
|
|
272
376
|
// ------------------------------------------------------------------
|
|
273
377
|
async function run() {
|
|
274
378
|
const transport = new StdioServerTransport();
|
|
275
379
|
await server.connect(transport);
|
|
276
|
-
console.error(
|
|
380
|
+
console.error(`Dropbox MCP Server running on stdio ${DROPBOX_SHARED_URL ? '(shared link mode)' : '(full account mode)'}`);
|
|
277
381
|
}
|
|
278
382
|
|
|
279
383
|
run().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dropbox-mcp-server",
|
|
3
|
-
"version": "2.1
|
|
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"
|