dropbox-mcp-server 2.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env ts-node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { Dropbox } from "dropbox";
6
+ import "dotenv/config";
7
+ // ------------------------------------------------------------------
8
+ // 1. Rate Limiter
9
+ // ------------------------------------------------------------------
10
+ const MAX_CONCURRENT = 1;
11
+ const MIN_GAP_MS = 1000;
12
+ const MAX_RETRIES = 5;
13
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
14
+ let active = 0;
15
+ let nextSlot = 0;
16
+ const waiters = [];
17
+ async function acquire() {
18
+ if (active >= MAX_CONCURRENT) {
19
+ await new Promise((res) => waiters.push(res));
20
+ }
21
+ active++;
22
+ const now = Date.now();
23
+ const wait = Math.max(0, nextSlot - now);
24
+ nextSlot = Math.max(now, nextSlot) + MIN_GAP_MS;
25
+ if (wait > 0)
26
+ await sleep(wait);
27
+ }
28
+ function release() {
29
+ active--;
30
+ const next = waiters.shift();
31
+ if (next)
32
+ next();
33
+ }
34
+ function retryAfterMs(err, attempt) {
35
+ const ra = Number(err?.error?.error?.retry_after ?? err?.headers?.["retry-after"] ?? 0);
36
+ if (ra > 0)
37
+ return ra * 1000;
38
+ return Math.min(2000 * (2 ** attempt), 60000);
39
+ }
40
+ async function limited(fn) {
41
+ await acquire();
42
+ try {
43
+ for (let attempt = 0;; attempt++) {
44
+ try {
45
+ return await fn();
46
+ }
47
+ catch (err) {
48
+ if (err?.status === 429 && attempt < MAX_RETRIES) {
49
+ const backoff = retryAfterMs(err, attempt);
50
+ console.error(`[limiter] 429 from Dropbox — retrying in ${backoff}ms`);
51
+ await sleep(backoff);
52
+ continue;
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+ }
58
+ finally {
59
+ release();
60
+ }
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
+ // ------------------------------------------------------------------
74
+ // 2. Error Handling
75
+ // ------------------------------------------------------------------
76
+ function isNotFoundError(err) {
77
+ if (err?.status !== 409)
78
+ return false;
79
+ try {
80
+ const errPayload = typeof err?.error === "string" ? err.error : JSON.stringify(err?.error ?? err ?? "");
81
+ return /not_found/.test(errPayload);
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ // ------------------------------------------------------------------
88
+ // 3. MCP Server Initialization
89
+ // ------------------------------------------------------------------
90
+ const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
91
+ const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
92
+ const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL;
93
+ if (!DROPBOX_APP_KEY || !DROPBOX_REFRESH_TOKEN) {
94
+ console.error("Missing DROPBOX_APP_KEY or DROPBOX_REFRESH_TOKEN environment variables.");
95
+ process.exit(1);
96
+ }
97
+ const rawClient = new Dropbox({
98
+ clientId: DROPBOX_APP_KEY,
99
+ refreshToken: DROPBOX_REFRESH_TOKEN
100
+ });
101
+ const client = throttleClient(rawClient);
102
+ const server = new Server({ name: "dropbox-agent-mcp", version: "2.0.0" }, { capabilities: { tools: {} } });
103
+ // ------------------------------------------------------------------
104
+ // 4. Tool Schemas
105
+ // ------------------------------------------------------------------
106
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
107
+ tools: [
108
+ {
109
+ name: "list_folder",
110
+ description: "List folders and files in a specific Dropbox path.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ path: { type: "string", description: "Path to list, e.g., '' for root or '/Data'" }
115
+ },
116
+ required: ["path"]
117
+ }
118
+ },
119
+ {
120
+ name: "search_files",
121
+ description: "Search for files by name, extension, or content across the Dropbox account.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ query: { type: "string", description: "The search query (e.g., 'sales report' or '.csv')" }
126
+ },
127
+ required: ["query"]
128
+ }
129
+ },
130
+ {
131
+ name: "get_file_metadata",
132
+ description: "Retrieve metadata (size, modified date) for a file to check constraints before downloading.",
133
+ inputSchema: {
134
+ type: "object",
135
+ properties: {
136
+ path: { type: "string", description: "Exact Dropbox file path" }
137
+ },
138
+ required: ["path"]
139
+ }
140
+ },
141
+ {
142
+ name: "describe_table",
143
+ description: "Display the schema (columns/headers) of a tabular file (CSV).",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ identifier: { type: "string", description: "Dropbox path or shared link to the table" }
148
+ },
149
+ required: ["identifier"]
150
+ }
151
+ },
152
+ {
153
+ name: "read_data",
154
+ description: "Download and read data from a file or table. Automatically handles paths and shared links.",
155
+ inputSchema: {
156
+ type: "object",
157
+ properties: {
158
+ identifier: { type: "string", description: "The Dropbox path (e.g., /data.csv) or shared link URL." }
159
+ },
160
+ required: ["identifier"]
161
+ }
162
+ }
163
+ ]
164
+ }));
165
+ // ------------------------------------------------------------------
166
+ // 5. Tool Execution Logic
167
+ // ------------------------------------------------------------------
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
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
187
+ try {
188
+ const name = request.params.name;
189
+ const args = request.params.arguments || {};
190
+ if (name === "list_folder") {
191
+ const path = args.path === "/" ? "" : args.path;
192
+ const requestArgs = { path };
193
+ if (DROPBOX_SHARED_URL) {
194
+ requestArgs.shared_link = { url: DROPBOX_SHARED_URL };
195
+ }
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." }] };
199
+ }
200
+ if (name === "search_files") {
201
+ const response = await client.filesSearchV2({ query: args.query });
202
+ const matches = response.result.matches.map(m => {
203
+ const meta = m.metadata.metadata;
204
+ return `[${meta['.tag'].toUpperCase()}] ${meta.name} (Path: ${meta.path_display})`;
205
+ });
206
+ return { content: [{ type: "text", text: matches.join("\n") || "No matches found." }] };
207
+ }
208
+ if (name === "get_file_metadata") {
209
+ let response;
210
+ if (DROPBOX_SHARED_URL) {
211
+ response = await client.sharingGetSharedLinkMetadata({ url: DROPBOX_SHARED_URL, path: args.path });
212
+ }
213
+ else {
214
+ response = await client.filesGetMetadata({ path: args.path });
215
+ }
216
+ return { content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }] };
217
+ }
218
+ 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
+ }
229
+ }
230
+ 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 }] };
239
+ }
240
+ catch (err) {
241
+ if (isNotFoundError(err)) {
242
+ return { content: [{ type: "text", text: `Error: File not found at ${args.identifier}` }] };
243
+ }
244
+ throw err;
245
+ }
246
+ }
247
+ throw new Error(`Unknown tool: ${name}`);
248
+ }
249
+ catch (err) {
250
+ return {
251
+ content: [{ type: "text", text: `Tool error: ${err.message ?? JSON.stringify(err)}` }],
252
+ isError: true
253
+ };
254
+ }
255
+ });
256
+ // ------------------------------------------------------------------
257
+ // 6. Execution
258
+ // ------------------------------------------------------------------
259
+ async function run() {
260
+ const transport = new StdioServerTransport();
261
+ await server.connect(transport);
262
+ console.error("Dropbox MCP Server running on stdio");
263
+ }
264
+ run().catch(console.error);
package/index.ts ADDED
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env ts-node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { Dropbox } from "dropbox";
6
+ import "dotenv/config";
7
+
8
+ // ------------------------------------------------------------------
9
+ // 1. Rate Limiter
10
+ // ------------------------------------------------------------------
11
+ const MAX_CONCURRENT = 1;
12
+ const MIN_GAP_MS = 1000;
13
+ const MAX_RETRIES = 5;
14
+
15
+ const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
16
+
17
+ let active = 0;
18
+ let nextSlot = 0;
19
+ const waiters: (() => void)[] = [];
20
+
21
+ async function acquire(): Promise<void> {
22
+ if (active >= MAX_CONCURRENT) {
23
+ await new Promise<void>((res) => waiters.push(res));
24
+ }
25
+ active++;
26
+ const now = Date.now();
27
+ const wait = Math.max(0, nextSlot - now);
28
+ nextSlot = Math.max(now, nextSlot) + MIN_GAP_MS;
29
+ if (wait > 0) await sleep(wait);
30
+ }
31
+
32
+ function release(): void {
33
+ active--;
34
+ const next = waiters.shift();
35
+ if (next) next();
36
+ }
37
+
38
+ function retryAfterMs(err: any, attempt: number): number {
39
+ const ra = Number(err?.error?.error?.retry_after ?? err?.headers?.["retry-after"] ?? 0);
40
+ if (ra > 0) return ra * 1000;
41
+ return Math.min(2000 * (2 ** attempt), 60000);
42
+ }
43
+
44
+ async function limited<T>(fn: () => Promise<T>): Promise<T> {
45
+ await acquire();
46
+ try {
47
+ for (let attempt = 0; ; attempt++) {
48
+ try {
49
+ return await fn();
50
+ } catch (err: any) {
51
+ if (err?.status === 429 && attempt < MAX_RETRIES) {
52
+ const backoff = retryAfterMs(err, attempt);
53
+ console.error(`[limiter] 429 from Dropbox — retrying in ${backoff}ms`);
54
+ await sleep(backoff);
55
+ continue;
56
+ }
57
+ throw err;
58
+ }
59
+ }
60
+ } finally {
61
+ release();
62
+ }
63
+ }
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
+ // ------------------------------------------------------------------
78
+ // 2. Error Handling
79
+ // ------------------------------------------------------------------
80
+ function isNotFoundError(err: any): boolean {
81
+ if (err?.status !== 409) return false;
82
+ try {
83
+ const errPayload = typeof err?.error === "string" ? err.error : JSON.stringify(err?.error ?? err ?? "");
84
+ return /not_found/.test(errPayload);
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ // ------------------------------------------------------------------
91
+ // 3. MCP Server Initialization
92
+ // ------------------------------------------------------------------
93
+ const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
94
+ const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
95
+ const DROPBOX_SHARED_URL = process.env.DROPBOX_SHARED_URL;
96
+
97
+ if (!DROPBOX_APP_KEY || !DROPBOX_REFRESH_TOKEN) {
98
+ console.error("Missing DROPBOX_APP_KEY or DROPBOX_REFRESH_TOKEN environment variables.");
99
+ process.exit(1);
100
+ }
101
+
102
+ const rawClient = new Dropbox({
103
+ clientId: DROPBOX_APP_KEY,
104
+ refreshToken: DROPBOX_REFRESH_TOKEN
105
+ });
106
+ const client = throttleClient(rawClient);
107
+
108
+ const server = new Server(
109
+ { name: "dropbox-agent-mcp", version: "2.0.0" },
110
+ { capabilities: { tools: {} } }
111
+ );
112
+
113
+ // ------------------------------------------------------------------
114
+ // 4. Tool Schemas
115
+ // ------------------------------------------------------------------
116
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
117
+ tools: [
118
+ {
119
+ name: "list_folder",
120
+ description: "List folders and files in a specific Dropbox path.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ path: { type: "string", description: "Path to list, e.g., '' for root or '/Data'" }
125
+ },
126
+ required: ["path"]
127
+ }
128
+ },
129
+ {
130
+ name: "search_files",
131
+ description: "Search for files by name, extension, or content across the Dropbox account.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ query: { type: "string", description: "The search query (e.g., 'sales report' or '.csv')" }
136
+ },
137
+ required: ["query"]
138
+ }
139
+ },
140
+ {
141
+ name: "get_file_metadata",
142
+ description: "Retrieve metadata (size, modified date) for a file to check constraints before downloading.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ path: { type: "string", description: "Exact Dropbox file path" }
147
+ },
148
+ required: ["path"]
149
+ }
150
+ },
151
+ {
152
+ name: "describe_table",
153
+ description: "Display the schema (columns/headers) of a tabular file (CSV).",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ identifier: { type: "string", description: "Dropbox path or shared link to the table" }
158
+ },
159
+ required: ["identifier"]
160
+ }
161
+ },
162
+ {
163
+ name: "read_data",
164
+ description: "Download and read data from a file or table. Automatically handles paths and shared links.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ identifier: { type: "string", description: "The Dropbox path (e.g., /data.csv) or shared link URL." }
169
+ },
170
+ required: ["identifier"]
171
+ }
172
+ }
173
+ ]
174
+ }));
175
+
176
+ // ------------------------------------------------------------------
177
+ // 5. Tool Execution Logic
178
+ // ------------------------------------------------------------------
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
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
197
+ try {
198
+ const name = request.params.name;
199
+ const args = request.params.arguments || {};
200
+
201
+ if (name === "list_folder") {
202
+ const path = (args.path as string) === "/" ? "" : (args.path as string);
203
+ const requestArgs: any = { path };
204
+ if (DROPBOX_SHARED_URL) {
205
+ requestArgs.shared_link = { url: DROPBOX_SHARED_URL };
206
+ }
207
+
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." }] };
211
+ }
212
+
213
+ 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;
217
+ return `[${meta['.tag'].toUpperCase()}] ${meta.name} (Path: ${meta.path_display})`;
218
+ });
219
+ return { content: [{ type: "text", text: matches.join("\n") || "No matches found." }] };
220
+ }
221
+
222
+ if (name === "get_file_metadata") {
223
+ let response;
224
+ if (DROPBOX_SHARED_URL) {
225
+ response = await client.sharingGetSharedLinkMetadata({ url: DROPBOX_SHARED_URL, path: args.path as string });
226
+ } else {
227
+ response = await client.filesGetMetadata({ path: args.path as string });
228
+ }
229
+ return { content: [{ type: "text", text: JSON.stringify(response.result, null, 2) }] };
230
+ }
231
+
232
+ 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
+ }
242
+ }
243
+
244
+ 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." }] };
249
+
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;
258
+ }
259
+ }
260
+
261
+ throw new Error(`Unknown tool: ${name}`);
262
+ } catch (err: any) {
263
+ return {
264
+ content: [{ type: "text", text: `Tool error: ${err.message ?? JSON.stringify(err)}` }],
265
+ isError: true
266
+ };
267
+ }
268
+ });
269
+
270
+ // ------------------------------------------------------------------
271
+ // 6. Execution
272
+ // ------------------------------------------------------------------
273
+ async function run() {
274
+ const transport = new StdioServerTransport();
275
+ await server.connect(transport);
276
+ console.error("Dropbox MCP Server running on stdio");
277
+ }
278
+
279
+ run().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "dropbox-mcp-server",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "dropbox-mcp-server": "dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "prepare": "npm run build"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.0.0",
14
+ "dotenv": "^16.4.5",
15
+ "dropbox": "^10.34.0"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.9.3"
19
+ }
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "node16",
5
+ "moduleResolution": "node16",
6
+ "outDir": "dist",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": [
12
+ "index.ts"
13
+ ]
14
+ }