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.
Files changed (3) hide show
  1. package/dist/index.js +182 -95
  2. package/index.ts +198 -94
  3. 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. MCP Server Initialization
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 DROPBOX_APP_KEY or DROPBOX_REFRESH_TOKEN environment variables.");
90
+ console.error("Missing required environment variables: DROPBOX_APP_KEY, DROPBOX_REFRESH_TOKEN");
95
91
  process.exit(1);
96
92
  }
97
- const rawClient = new Dropbox({
93
+ // Instantiate direct client (no proxy)
94
+ const client = new Dropbox({
98
95
  clientId: DROPBOX_APP_KEY,
99
- refreshToken: DROPBOX_REFRESH_TOKEN
96
+ clientSecret: DROPBOX_APP_SECRET, // Needed if your Dropbox app is configured with a secret
97
+ refreshToken: DROPBOX_REFRESH_TOKEN,
100
98
  });
101
- const client = throttleClient(rawClient);
102
- const server = new Server({ name: "dropbox-agent-mcp", version: "2.1.0" }, { capabilities: { tools: {} } });
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
- // 4. Tool Schemas
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 in a specific Dropbox path.",
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 across the Dropbox account.",
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 metadata (size, modified date) for a file to check constraints before downloading.",
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 tabular file (CSV).",
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 data from a file or table. Automatically handles paths and shared links.",
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
- // 5. Tool Execution Logic
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 path = args.path === "/" ? "" : args.path;
192
- const requestArgs = { path };
266
+ const rawPath = args.path || "";
267
+ let requestArgs;
193
268
  if (DROPBOX_SHARED_URL) {
194
- requestArgs.shared_link = { url: DROPBOX_SHARED_URL };
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 { content: [{ type: "text", text: entries.join("\n") || "Folder is empty." }] };
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 { content: [{ type: "text", text: matches.join("\n") || "No matches found." }] };
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({ url: DROPBOX_SHARED_URL, path: args.path });
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: args.path });
303
+ response = await limited(() => client.filesGetMetadata({ path: path }));
215
304
  }
216
- return { content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }] };
305
+ return {
306
+ content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }]
307
+ };
217
308
  }
218
309
  if (name === "read_data") {
219
- try {
220
- const text = await downloadHelper(args.identifier);
221
- return { content: [{ type: "text", text: text.substring(0, 100000) }] };
222
- }
223
- catch (err) {
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
- try {
232
- const text = await downloadHelper(args.identifier);
233
- const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
234
- if (lines.length === 0)
235
- return { content: [{ type: "text", text: "File is empty." }] };
236
- const headers = lines[0].split(',').map(h => h.trim());
237
- const schemaStr = `Columns detected:\n- ${headers.join('\n- ')}\n\nTotal rows: ${lines.length - 1}`;
238
- 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);
239
327
  }
240
- catch (err) {
241
- if (isNotFoundError(err)) {
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
- // 6. Execution
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("Dropbox MCP Server running on stdio");
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. MCP Server Initialization
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 DROPBOX_APP_KEY or DROPBOX_REFRESH_TOKEN environment variables.");
95
+ console.error("Missing required environment variables: DROPBOX_APP_KEY, DROPBOX_REFRESH_TOKEN");
99
96
  process.exit(1);
100
97
  }
101
98
 
102
- const rawClient = new Dropbox({
99
+ // Instantiate direct client (no proxy)
100
+ const client = new Dropbox({
103
101
  clientId: DROPBOX_APP_KEY,
104
- refreshToken: DROPBOX_REFRESH_TOKEN
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.0" },
107
+ { name: "dropbox-agent-mcp", version: "2.0.1" },
110
108
  { capabilities: { tools: {} } }
111
109
  );
112
110
 
113
111
  // ------------------------------------------------------------------
114
- // 4. Tool Schemas
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 in a specific Dropbox path.",
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 across the Dropbox account.",
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 metadata (size, modified date) for a file to check constraints before downloading.",
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 tabular file (CSV).",
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 data from a file or table. Automatically handles paths and shared links.",
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
- // 5. Tool Execution Logic
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 path = (args.path as string) === "/" ? "" : (args.path as string);
203
- const requestArgs: any = { path };
289
+ const rawPath = (args.path as string) || "";
290
+ let requestArgs: any;
291
+
204
292
  if (DROPBOX_SHARED_URL) {
205
- requestArgs.shared_link = { url: DROPBOX_SHARED_URL };
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(e => `[${e['.tag'].toUpperCase()}] ${e.name} (Path: ${e.path_display})`);
210
- return { content: [{ type: "text", text: entries.join("\n") || "Folder is empty." }] };
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 = (m.metadata as any).metadata;
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 { content: [{ type: "text", text: matches.join("\n") || "No matches found." }] };
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({ url: DROPBOX_SHARED_URL, path: args.path as string });
326
+ response = await limited(() => client.sharingGetSharedLinkMetadata({
327
+ url: DROPBOX_SHARED_URL,
328
+ path: path
329
+ }));
226
330
  } else {
227
- response = await client.filesGetMetadata({ path: args.path as string });
331
+ response = await limited(() => client.filesGetMetadata({ path: path }));
228
332
  }
229
- return { content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }] };
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
- try {
234
- const text = await downloadHelper(args.identifier as string);
235
- return { content: [{ type: "text", text: text.substring(0, 100000) }] };
236
- } catch (err: any) {
237
- if (isNotFoundError(err)) {
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
- try {
246
- const text = await downloadHelper(args.identifier as string);
247
- const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
248
- if (lines.length === 0) return { content: [{ type: "text", text: "File is empty." }] };
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
- const headers = lines[0].split(',').map(h => h.trim());
251
- const schemaStr = `Columns detected:\n- ${headers.join('\n- ')}\n\nTotal rows: ${lines.length - 1}`;
252
- return { content: [{ type: "text", text: schemaStr }] };
253
- } catch (err: any) {
254
- if (isNotFoundError(err)) {
255
- return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
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
- // 6. Execution
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("Dropbox MCP Server running on stdio");
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.0",
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"