dropbox-mcp-server 2.1.0 → 2.2.0

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 +222 -52
  2. package/index.ts +240 -51
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -84,41 +84,143 @@ function isNotFoundError(err) {
84
84
  return false;
85
85
  }
86
86
  }
87
+ function isPermissionError(err) {
88
+ return err?.status === 403;
89
+ }
90
+ function isBadRequestError(err) {
91
+ return err?.status === 400;
92
+ }
87
93
  // ------------------------------------------------------------------
88
- // 3. MCP Server Initialization
94
+ // 3. Environment & Client
89
95
  // ------------------------------------------------------------------
90
96
  const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
91
97
  const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
92
- const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL;
98
+ const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL; // optional
93
99
  if (!DROPBOX_APP_KEY || !DROPBOX_REFRESH_TOKEN) {
94
- console.error("Missing DROPBOX_APP_KEY or DROPBOX_REFRESH_TOKEN environment variables.");
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.");
95
104
  process.exit(1);
96
105
  }
97
106
  const rawClient = new Dropbox({
98
107
  clientId: DROPBOX_APP_KEY,
99
- refreshToken: DROPBOX_REFRESH_TOKEN
108
+ refreshToken: DROPBOX_REFRESH_TOKEN,
100
109
  });
101
110
  const client = throttleClient(rawClient);
102
- const server = new Server({ name: "dropbox-agent-mcp", version: "2.1.0" }, { capabilities: { tools: {} } });
111
+ const server = new Server({ name: "dropbox-agent-mcp", version: "2.0.0" }, { capabilities: { tools: {} } });
112
+ // ------------------------------------------------------------------
113
+ // 4. Helpers
114
+ // ------------------------------------------------------------------
115
+ /** Normalise a path for use with a shared link: strip leading "/" and handle root. */
116
+ function normalizeSharedPath(path) {
117
+ if (!path || path === "/")
118
+ return "";
119
+ return path.replace(/^\/+/, "");
120
+ }
121
+ /** Check if a filename likely belongs to a text file. */
122
+ function isTextFile(pathOrName) {
123
+ const fileName = pathOrName.split('/').pop() || '';
124
+ const ext = fileName.split('.').pop()?.toLowerCase() || '';
125
+ const textExtensions = [
126
+ 'csv', 'tsv', 'txt', 'json', 'md', 'log',
127
+ 'xml', 'yaml', 'yml', 'html', 'htm', 'css',
128
+ 'js', 'ts', 'py', 'java', 'c', 'cpp', 'h'
129
+ ];
130
+ return textExtensions.includes(ext);
131
+ }
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
+ async function downloadHelper(identifier) {
139
+ let result;
140
+ let fileName;
141
+ // If the identifier is a full URL, we always treat it as a shared link.
142
+ if (identifier.startsWith("http://") || identifier.startsWith("https://")) {
143
+ const response = await client.sharingGetSharedLinkFile({ url: identifier });
144
+ result = response.result;
145
+ fileName = result.name || identifier.split('/').pop();
146
+ }
147
+ // Otherwise, decide based on DROPBOX_SHARED_URL.
148
+ else if (DROPBOX_SHARED_URL) {
149
+ // Shared link mode – identifier is a relative path.
150
+ const path = normalizeSharedPath(identifier);
151
+ const response = await client.sharingGetSharedLinkFile({
152
+ url: DROPBOX_SHARED_URL,
153
+ path: path,
154
+ });
155
+ result = response.result;
156
+ fileName = result.name || identifier;
157
+ }
158
+ else {
159
+ // Regular mode – identifier is an absolute path.
160
+ const response = await client.filesDownload({ path: identifier });
161
+ result = response.result;
162
+ fileName = result.name || identifier;
163
+ }
164
+ if (!result.fileBinary) {
165
+ throw new Error("Download succeeded but no file content was returned.");
166
+ }
167
+ const fileRef = fileName || identifier;
168
+ if (!isTextFile(fileRef)) {
169
+ throw new Error(`File "${fileRef}" appears to be a binary file. ` +
170
+ `This tool only supports text‑based files (CSV, JSON, TXT, etc.).`);
171
+ }
172
+ return Buffer.from(result.fileBinary).toString('utf-8');
173
+ }
174
+ /**
175
+ * Parse a CSV header line, handling quoted fields and trimming.
176
+ * Returns an array of clean column names.
177
+ */
178
+ function parseCsvHeader(headerLine) {
179
+ const fields = [];
180
+ let current = '';
181
+ let insideQuotes = false;
182
+ for (let i = 0; i < headerLine.length; i++) {
183
+ const ch = headerLine[i];
184
+ if (ch === '"') {
185
+ insideQuotes = !insideQuotes;
186
+ continue;
187
+ }
188
+ if (ch === ',' && !insideQuotes) {
189
+ fields.push(current.trim());
190
+ current = '';
191
+ continue;
192
+ }
193
+ current += ch;
194
+ }
195
+ fields.push(current.trim());
196
+ return fields.map(f => f.replace(/^"|"$/g, '').trim());
197
+ }
103
198
  // ------------------------------------------------------------------
104
- // 4. Tool Schemas
199
+ // 5. Tool Definitions
105
200
  // ------------------------------------------------------------------
106
201
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
107
202
  tools: [
108
203
  {
109
204
  name: "list_folder",
110
- description: "List folders and files in a specific Dropbox path.",
205
+ description: DROPBOX_SHARED_URL
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).",
111
208
  inputSchema: {
112
209
  type: "object",
113
210
  properties: {
114
- path: { type: "string", description: "Path to list, e.g., '' for root or '/Data'" }
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
+ }
115
217
  },
116
218
  required: ["path"]
117
219
  }
118
220
  },
119
221
  {
120
222
  name: "search_files",
121
- description: "Search for files by name, extension, or content across the Dropbox account.",
223
+ description: "Search for files by name, extension, or content across the entire Dropbox account.",
122
224
  inputSchema: {
123
225
  type: "object",
124
226
  properties: {
@@ -129,33 +231,47 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
129
231
  },
130
232
  {
131
233
  name: "get_file_metadata",
132
- description: "Retrieve metadata (size, modified date) for a file to check constraints before downloading.",
234
+ description: DROPBOX_SHARED_URL
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.",
133
237
  inputSchema: {
134
238
  type: "object",
135
239
  properties: {
136
- path: { type: "string", description: "Exact Dropbox file path" }
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
+ }
137
245
  },
138
246
  required: ["path"]
139
247
  }
140
248
  },
141
249
  {
142
250
  name: "describe_table",
143
- description: "Display the schema (columns/headers) of a tabular file (CSV).",
251
+ description: "Display the schema (columns/headers) of a CSV or TSV file.",
144
252
  inputSchema: {
145
253
  type: "object",
146
254
  properties: {
147
- identifier: { type: "string", description: "Dropbox path or shared link to the table" }
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
+ }
148
260
  },
149
261
  required: ["identifier"]
150
262
  }
151
263
  },
152
264
  {
153
265
  name: "read_data",
154
- description: "Download and read data from a file or table. Automatically handles paths and shared links.",
266
+ description: "Download and read the content of a text file (CSV, JSON, TXT, etc.).",
155
267
  inputSchema: {
156
268
  type: "object",
157
269
  properties: {
158
- identifier: { type: "string", description: "The Dropbox path (e.g., /data.csv) or shared link URL." }
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
+ }
159
275
  },
160
276
  required: ["identifier"]
161
277
  }
@@ -163,83 +279,137 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
163
279
  ]
164
280
  }));
165
281
  // ------------------------------------------------------------------
166
- // 5. Tool Execution Logic
282
+ // 6. Tool Handlers
167
283
  // ------------------------------------------------------------------
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
284
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
187
285
  try {
188
286
  const name = request.params.name;
189
287
  const args = request.params.arguments || {};
288
+ // --- list_folder ---
190
289
  if (name === "list_folder") {
191
- const path = args.path === "/" ? "" : args.path;
192
- const requestArgs = { path };
290
+ let path = args.path || "";
291
+ let requestArgs;
193
292
  if (DROPBOX_SHARED_URL) {
194
- requestArgs.shared_link = { url: DROPBOX_SHARED_URL };
293
+ path = normalizeSharedPath(path);
294
+ requestArgs = {
295
+ path: path,
296
+ shared_link: { url: DROPBOX_SHARED_URL }
297
+ };
298
+ }
299
+ else {
300
+ // Absolute path: if "/" then empty string for root
301
+ requestArgs = { path: path === "/" ? "" : path };
195
302
  }
196
303
  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." }] };
304
+ const entries = response.result.entries.map((e) => `[${e['.tag'].toUpperCase()}] ${e.name} (Path: ${e.path_display})`);
305
+ return {
306
+ content: [{ type: "text", text: entries.join("\n") || "Folder is empty." }]
307
+ };
199
308
  }
309
+ // --- search_files ---
200
310
  if (name === "search_files") {
201
311
  const response = await client.filesSearchV2({ query: args.query });
202
- const matches = response.result.matches.map(m => {
312
+ const matches = response.result.matches.map((m) => {
203
313
  const meta = m.metadata.metadata;
204
314
  return `[${meta['.tag'].toUpperCase()}] ${meta.name} (Path: ${meta.path_display})`;
205
315
  });
206
- 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
+ };
207
319
  }
320
+ // --- get_file_metadata ---
208
321
  if (name === "get_file_metadata") {
209
322
  let response;
323
+ const rawPath = args.path || "";
210
324
  if (DROPBOX_SHARED_URL) {
211
- response = await client.sharingGetSharedLinkMetadata({ url: DROPBOX_SHARED_URL, path: args.path });
325
+ const path = normalizeSharedPath(rawPath);
326
+ response = await client.sharingGetSharedLinkMetadata({
327
+ url: DROPBOX_SHARED_URL,
328
+ path: path
329
+ });
212
330
  }
213
331
  else {
214
- response = await client.filesGetMetadata({ path: args.path });
332
+ response = await client.filesGetMetadata({ path: rawPath });
215
333
  }
216
- return { content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }] };
334
+ return {
335
+ content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }]
336
+ };
217
337
  }
338
+ // --- read_data ---
218
339
  if (name === "read_data") {
219
340
  try {
220
341
  const text = await downloadHelper(args.identifier);
221
- return { content: [{ type: "text", text: text.substring(0, 100000) }] };
342
+ const truncated = text.length > 100000
343
+ ? text.substring(0, 100000) + "\n... (truncated)"
344
+ : text;
345
+ return { content: [{ type: "text", text: truncated }] };
222
346
  }
223
347
  catch (err) {
224
348
  if (isNotFoundError(err)) {
225
- return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
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 };
226
368
  }
227
369
  throw err;
228
370
  }
229
371
  }
372
+ // --- describe_table ---
230
373
  if (name === "describe_table") {
231
374
  try {
232
375
  const text = await downloadHelper(args.identifier);
233
376
  const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
234
- if (lines.length === 0)
377
+ if (lines.length === 0) {
235
378
  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}`;
379
+ }
380
+ let headers;
381
+ let totalRows = lines.length - 1;
382
+ const firstLine = lines[0];
383
+ const commaCount = (firstLine.match(/,/g) || []).length;
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}`;
238
396
  return { content: [{ type: "text", text: schemaStr }] };
239
397
  }
240
398
  catch (err) {
241
399
  if (isNotFoundError(err)) {
242
- return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
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 };
243
413
  }
244
414
  throw err;
245
415
  }
@@ -254,11 +424,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
254
424
  }
255
425
  });
256
426
  // ------------------------------------------------------------------
257
- // 6. Execution
427
+ // 7. Start the Server
258
428
  // ------------------------------------------------------------------
259
429
  async function run() {
260
430
  const transport = new StdioServerTransport();
261
431
  await server.connect(transport);
262
- console.error("Dropbox MCP Server running on stdio");
432
+ console.error(`Dropbox MCP Server running on stdio ${DROPBOX_SHARED_URL ? '(shared link mode)' : '(full account mode)'}`);
263
433
  }
264
434
  run().catch(console.error);
package/index.ts CHANGED
@@ -87,48 +87,161 @@ function isNotFoundError(err: any): boolean {
87
87
  }
88
88
  }
89
89
 
90
+ function isPermissionError(err: any): boolean {
91
+ return err?.status === 403;
92
+ }
93
+
94
+ function isBadRequestError(err: any): boolean {
95
+ return err?.status === 400;
96
+ }
97
+
90
98
  // ------------------------------------------------------------------
91
- // 3. MCP Server Initialization
99
+ // 3. Environment & Client
92
100
  // ------------------------------------------------------------------
93
101
  const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
94
102
  const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
95
- const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL;
103
+ const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL; // optional
96
104
 
97
105
  if (!DROPBOX_APP_KEY || !DROPBOX_REFRESH_TOKEN) {
98
- console.error("Missing DROPBOX_APP_KEY or DROPBOX_REFRESH_TOKEN environment variables.");
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.");
99
110
  process.exit(1);
100
111
  }
101
112
 
102
113
  const rawClient = new Dropbox({
103
114
  clientId: DROPBOX_APP_KEY,
104
- refreshToken: DROPBOX_REFRESH_TOKEN
115
+ refreshToken: DROPBOX_REFRESH_TOKEN,
105
116
  });
106
117
  const client = throttleClient(rawClient);
107
118
 
108
119
  const server = new Server(
109
- { name: "dropbox-agent-mcp", version: "2.1.0" },
120
+ { name: "dropbox-agent-mcp", version: "2.0.0" },
110
121
  { capabilities: { tools: {} } }
111
122
  );
112
123
 
113
124
  // ------------------------------------------------------------------
114
- // 4. Tool Schemas
125
+ // 4. Helpers
126
+ // ------------------------------------------------------------------
127
+
128
+ /** Normalise a path for use with a shared link: strip leading "/" and handle root. */
129
+ function normalizeSharedPath(path: string): string {
130
+ if (!path || path === "/") return "";
131
+ return path.replace(/^\/+/, "");
132
+ }
133
+
134
+ /** Check if a filename likely belongs to a text file. */
135
+ function isTextFile(pathOrName: string): boolean {
136
+ const fileName = pathOrName.split('/').pop() || '';
137
+ const ext = fileName.split('.').pop()?.toLowerCase() || '';
138
+ const textExtensions = [
139
+ 'csv', 'tsv', 'txt', 'json', 'md', 'log',
140
+ 'xml', 'yaml', 'yml', 'html', 'htm', 'css',
141
+ 'js', 'ts', 'py', 'java', 'c', 'cpp', 'h'
142
+ ];
143
+ return textExtensions.includes(ext);
144
+ }
145
+
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
+ async function downloadHelper(identifier: string): Promise<string> {
153
+ let result: any;
154
+ let fileName: string | undefined;
155
+
156
+ // If the identifier is a full URL, we always treat it as a shared link.
157
+ if (identifier.startsWith("http://") || identifier.startsWith("https://")) {
158
+ const response = await client.sharingGetSharedLinkFile({ url: identifier });
159
+ result = response.result;
160
+ fileName = result.name || identifier.split('/').pop();
161
+ }
162
+ // Otherwise, decide based on DROPBOX_SHARED_URL.
163
+ else if (DROPBOX_SHARED_URL) {
164
+ // Shared link mode – identifier is a relative path.
165
+ const path = normalizeSharedPath(identifier);
166
+ const response = await client.sharingGetSharedLinkFile({
167
+ url: DROPBOX_SHARED_URL,
168
+ path: path,
169
+ });
170
+ result = response.result;
171
+ fileName = result.name || identifier;
172
+ } else {
173
+ // Regular mode – identifier is an absolute path.
174
+ const response = await client.filesDownload({ path: identifier });
175
+ result = response.result;
176
+ fileName = result.name || identifier;
177
+ }
178
+
179
+ if (!result.fileBinary) {
180
+ throw new Error("Download succeeded but no file content was returned.");
181
+ }
182
+
183
+ const fileRef = fileName || identifier;
184
+ if (!isTextFile(fileRef)) {
185
+ throw new Error(
186
+ `File "${fileRef}" appears to be a binary file. ` +
187
+ `This tool only supports text‑based files (CSV, JSON, TXT, etc.).`
188
+ );
189
+ }
190
+
191
+ return Buffer.from(result.fileBinary).toString('utf-8');
192
+ }
193
+
194
+ /**
195
+ * Parse a CSV header line, handling quoted fields and trimming.
196
+ * Returns an array of clean column names.
197
+ */
198
+ function parseCsvHeader(headerLine: string): string[] {
199
+ const fields: string[] = [];
200
+ let current = '';
201
+ let insideQuotes = false;
202
+ for (let i = 0; i < headerLine.length; i++) {
203
+ const ch = headerLine[i];
204
+ if (ch === '"') {
205
+ insideQuotes = !insideQuotes;
206
+ continue;
207
+ }
208
+ if (ch === ',' && !insideQuotes) {
209
+ fields.push(current.trim());
210
+ current = '';
211
+ continue;
212
+ }
213
+ current += ch;
214
+ }
215
+ fields.push(current.trim());
216
+ return fields.map(f => f.replace(/^"|"$/g, '').trim());
217
+ }
218
+
219
+ // ------------------------------------------------------------------
220
+ // 5. Tool Definitions
115
221
  // ------------------------------------------------------------------
116
222
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
117
223
  tools: [
118
224
  {
119
225
  name: "list_folder",
120
- description: "List folders and files in a specific Dropbox path.",
226
+ description: DROPBOX_SHARED_URL
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).",
121
229
  inputSchema: {
122
230
  type: "object",
123
231
  properties: {
124
- path: { type: "string", description: "Path to list, e.g., '' for root or '/Data'" }
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
+ }
125
238
  },
126
239
  required: ["path"]
127
240
  }
128
241
  },
129
242
  {
130
243
  name: "search_files",
131
- description: "Search for files by name, extension, or content across the Dropbox account.",
244
+ description: "Search for files by name, extension, or content across the entire Dropbox account.",
132
245
  inputSchema: {
133
246
  type: "object",
134
247
  properties: {
@@ -139,33 +252,47 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
139
252
  },
140
253
  {
141
254
  name: "get_file_metadata",
142
- description: "Retrieve metadata (size, modified date) for a file to check constraints before downloading.",
255
+ description: DROPBOX_SHARED_URL
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.",
143
258
  inputSchema: {
144
259
  type: "object",
145
260
  properties: {
146
- path: { type: "string", description: "Exact Dropbox file path" }
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
+ }
147
266
  },
148
267
  required: ["path"]
149
268
  }
150
269
  },
151
270
  {
152
271
  name: "describe_table",
153
- description: "Display the schema (columns/headers) of a tabular file (CSV).",
272
+ description: "Display the schema (columns/headers) of a CSV or TSV file.",
154
273
  inputSchema: {
155
274
  type: "object",
156
275
  properties: {
157
- identifier: { type: "string", description: "Dropbox path or shared link to the table" }
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
+ }
158
281
  },
159
282
  required: ["identifier"]
160
283
  }
161
284
  },
162
285
  {
163
286
  name: "read_data",
164
- description: "Download and read data from a file or table. Automatically handles paths and shared links.",
287
+ description: "Download and read the content of a text file (CSV, JSON, TXT, etc.).",
165
288
  inputSchema: {
166
289
  type: "object",
167
290
  properties: {
168
- identifier: { type: "string", description: "The Dropbox path (e.g., /data.csv) or shared link URL." }
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
+ }
169
296
  },
170
297
  required: ["identifier"]
171
298
  }
@@ -174,85 +301,147 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
174
301
  }));
175
302
 
176
303
  // ------------------------------------------------------------------
177
- // 5. Tool Execution Logic
304
+ // 6. Tool Handlers
178
305
  // ------------------------------------------------------------------
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
306
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
197
307
  try {
198
308
  const name = request.params.name;
199
309
  const args = request.params.arguments || {};
200
310
 
311
+ // --- list_folder ---
201
312
  if (name === "list_folder") {
202
- const path = (args.path as string) === "/" ? "" : (args.path as string);
203
- const requestArgs: any = { path };
313
+ let path = (args.path as string) || "";
314
+ let requestArgs: any;
315
+
204
316
  if (DROPBOX_SHARED_URL) {
205
- requestArgs.shared_link = { url: DROPBOX_SHARED_URL };
317
+ path = normalizeSharedPath(path);
318
+ requestArgs = {
319
+ path: path,
320
+ shared_link: { url: DROPBOX_SHARED_URL }
321
+ };
322
+ } else {
323
+ // Absolute path: if "/" then empty string for root
324
+ requestArgs = { path: path === "/" ? "" : path };
206
325
  }
207
326
 
208
327
  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." }] };
328
+ const entries = response.result.entries.map((e: any) =>
329
+ `[${e['.tag'].toUpperCase()}] ${e.name} (Path: ${e.path_display})`
330
+ );
331
+ return {
332
+ content: [{ type: "text", text: entries.join("\n") || "Folder is empty." }]
333
+ };
211
334
  }
212
335
 
336
+ // --- search_files ---
213
337
  if (name === "search_files") {
214
338
  const response = await client.filesSearchV2({ query: args.query as string });
215
- const matches = response.result.matches.map(m => {
339
+ const matches = response.result.matches.map((m: any) => {
216
340
  const meta = (m.metadata as any).metadata;
217
341
  return `[${meta['.tag'].toUpperCase()}] ${meta.name} (Path: ${meta.path_display})`;
218
342
  });
219
- return { content: [{ type: "text", text: matches.join("\n") || "No matches found." }] };
343
+ return {
344
+ content: [{ type: "text", text: matches.join("\n") || "No matches found." }]
345
+ };
220
346
  }
221
347
 
348
+ // --- get_file_metadata ---
222
349
  if (name === "get_file_metadata") {
223
350
  let response;
351
+ const rawPath = (args.path as string) || "";
352
+
224
353
  if (DROPBOX_SHARED_URL) {
225
- response = await client.sharingGetSharedLinkMetadata({ url: DROPBOX_SHARED_URL, path: args.path as string });
354
+ const path = normalizeSharedPath(rawPath);
355
+ response = await client.sharingGetSharedLinkMetadata({
356
+ url: DROPBOX_SHARED_URL,
357
+ path: path
358
+ });
226
359
  } else {
227
- response = await client.filesGetMetadata({ path: args.path as string });
360
+ response = await client.filesGetMetadata({ path: rawPath });
228
361
  }
229
- return { content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }] };
362
+
363
+ return {
364
+ content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }]
365
+ };
230
366
  }
231
367
 
368
+ // --- read_data ---
232
369
  if (name === "read_data") {
233
370
  try {
234
371
  const text = await downloadHelper(args.identifier as string);
235
- return { content: [{ type: "text", text: text.substring(0, 100000) }] };
372
+ const truncated = text.length > 100000
373
+ ? text.substring(0, 100000) + "\n... (truncated)"
374
+ : text;
375
+ return { content: [{ type: "text", text: truncated }] };
236
376
  } catch (err: any) {
237
377
  if (isNotFoundError(err)) {
238
- return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
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 };
239
397
  }
240
398
  throw err;
241
399
  }
242
400
  }
243
401
 
402
+ // --- describe_table ---
244
403
  if (name === "describe_table") {
245
404
  try {
246
405
  const text = await downloadHelper(args.identifier as string);
247
406
  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." }] };
407
+ if (lines.length === 0) {
408
+ return { content: [{ type: "text", text: "File is empty." }] };
409
+ }
249
410
 
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}`;
411
+ let headers: string[];
412
+ let totalRows = lines.length - 1;
413
+ const firstLine = lines[0];
414
+ const commaCount = (firstLine.match(/,/g) || []).length;
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}`;
252
429
  return { content: [{ type: "text", text: schemaStr }] };
253
430
  } catch (err: any) {
254
431
  if (isNotFoundError(err)) {
255
- return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
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 };
256
445
  }
257
446
  throw err;
258
447
  }
@@ -268,12 +457,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
268
457
  });
269
458
 
270
459
  // ------------------------------------------------------------------
271
- // 6. Execution
460
+ // 7. Start the Server
272
461
  // ------------------------------------------------------------------
273
462
  async function run() {
274
463
  const transport = new StdioServerTransport();
275
464
  await server.connect(transport);
276
- console.error("Dropbox MCP Server running on stdio");
465
+ console.error(`Dropbox MCP Server running on stdio ${DROPBOX_SHARED_URL ? '(shared link mode)' : '(full account mode)'}`);
277
466
  }
278
467
 
279
468
  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.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dropbox-mcp-server": "dist/index.js"