proton-drive-mcp 1.0.12

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/cli.js ADDED
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Companion CLI for proton-drive-mcp.
4
+ * Provides a human-friendly interface to the same DriveService used by the MCP server.
5
+ *
6
+ * Usage: proton-drive-cli <command> [args]
7
+ * auth status
8
+ * auth logout
9
+ * version
10
+ * list <remote-path>
11
+ * mkdir <remote-path>
12
+ * upload <local-path> <remote-path> [--conflict skip|overwrite|rename]
13
+ * download <remote-path> <local-path>
14
+ * move <src> <dst>
15
+ * delete <path>
16
+ * share status <path>
17
+ * share invite <path> <email> <role> [--message "..."]
18
+ * share revoke <path> <email>
19
+ * trash <path>
20
+ * trash list
21
+ * trash empty
22
+ * restore <path>
23
+ *
24
+ * Global flags:
25
+ * --json Machine-readable JSON output
26
+ */
27
+ import { DriveService } from "./services/drive.js";
28
+ import { DriveCliError, DriveCliNotFoundError, DriveNotAuthenticatedError, DriveParseError } from "./utils/errors.js";
29
+ import { checkCliAvailable } from "./utils/subprocess.js";
30
+ import { validatePath, validateEmail, validateMessage } from "./utils/validation.js";
31
+ const drive = new DriveService();
32
+ const args = process.argv.slice(2);
33
+ const jsonMode = args.includes("--json");
34
+ function usage() {
35
+ console.log(`
36
+ proton-drive-cli — Proton Drive companion CLI
37
+
38
+ Commands:
39
+ auth status Check authentication status
40
+ auth logout Log out
41
+ version Show CLI/SDK version
42
+ list <path> List files at path
43
+ mkdir <path> Create a new folder
44
+ upload <local> <remote> [--conflict X] Upload file/folder (skip/overwrite/rename)
45
+ download <remote> <local> Download file/folder
46
+ move <src> <dst> Move/rename
47
+ delete <path> Delete file/folder permanently
48
+ share status <path> Show sharing info
49
+ share invite <path> <email> <role> Invite user (viewer/editor/admin)
50
+ share revoke <path> <email> Revoke access
51
+ trash <path> Move to trash
52
+ trash list List trash contents
53
+ trash empty Permanently delete all trash
54
+ restore <path> Restore from trash
55
+
56
+ Flags:
57
+ --json Machine-readable JSON output (one line)
58
+ `);
59
+ }
60
+ function requirePath(value, usage) {
61
+ if (!value) {
62
+ console.error(`Usage: ${usage}`);
63
+ process.exit(1);
64
+ }
65
+ try {
66
+ return validatePath(value);
67
+ }
68
+ catch (e) {
69
+ console.error(e instanceof Error ? e.message : String(e));
70
+ process.exit(1);
71
+ }
72
+ }
73
+ function requireEmail(value) {
74
+ try {
75
+ return validateEmail(value);
76
+ }
77
+ catch (e) {
78
+ console.error(e instanceof Error ? e.message : String(e));
79
+ process.exit(1);
80
+ }
81
+ return ""; // unreachable; process.exit(1) above always terminates
82
+ }
83
+ function getFlag(flag) {
84
+ const idx = args.indexOf(flag);
85
+ if (idx === -1)
86
+ return undefined;
87
+ if (idx + 1 >= args.length || args[idx + 1].startsWith("--")) {
88
+ console.error(`${flag} requires a value`);
89
+ process.exit(1);
90
+ }
91
+ return args[idx + 1];
92
+ }
93
+ function print(data) {
94
+ console.log(jsonMode ? JSON.stringify(data) : JSON.stringify(data, null, 2));
95
+ }
96
+ async function run() {
97
+ if (!(await checkCliAvailable())) {
98
+ console.error("Error: proton-drive CLI not found in PATH.\n" +
99
+ "Download from https://proton.me/download/drive/cli/index.html");
100
+ process.exit(1);
101
+ }
102
+ // Strip --json from positional parsing
103
+ const positional = args.filter((a) => a !== "--json");
104
+ const [cmd, sub, ...rest] = positional;
105
+ if (!cmd || cmd === "--help" || cmd === "-h") {
106
+ usage();
107
+ return;
108
+ }
109
+ switch (cmd) {
110
+ case "auth":
111
+ if (sub === "status") {
112
+ print(await drive.authStatus());
113
+ }
114
+ else if (sub === "logout") {
115
+ await drive.authLogout();
116
+ console.log("Logged out.");
117
+ }
118
+ else {
119
+ console.error(`Unknown auth subcommand: ${sub}`);
120
+ process.exit(1);
121
+ }
122
+ break;
123
+ case "version":
124
+ print(await drive.version());
125
+ break;
126
+ case "list":
127
+ print(await drive.list(requirePath(sub, "list <path>")));
128
+ break;
129
+ case "upload": {
130
+ const local = requirePath(sub, "upload <local> <remote>");
131
+ const remote = requirePath(rest[0], "upload <local> <remote>");
132
+ const conflictRaw = getFlag("--conflict") ?? "skip";
133
+ if (!["skip", "overwrite", "rename"].includes(conflictRaw)) {
134
+ console.error(`Invalid --conflict value: ${conflictRaw}. Must be skip, overwrite, or rename.`);
135
+ process.exit(1);
136
+ }
137
+ const conflict = conflictRaw;
138
+ print(await drive.upload(local, remote, conflict));
139
+ break;
140
+ }
141
+ case "download": {
142
+ const remote = requirePath(sub, "download <remote> <local>");
143
+ const local = requirePath(rest[0], "download <remote> <local>");
144
+ print(await drive.download(remote, local));
145
+ break;
146
+ }
147
+ case "move": {
148
+ const src = requirePath(sub, "move <src> <dst>");
149
+ const dst = requirePath(rest[0], "move <src> <dst>");
150
+ await drive.move(src, dst);
151
+ console.log("Moved.");
152
+ break;
153
+ }
154
+ case "mkdir":
155
+ await drive.mkdir(requirePath(sub, "mkdir <path>"));
156
+ console.log("Folder created.");
157
+ break;
158
+ case "delete":
159
+ await drive.delete(requirePath(sub, "delete <path>"));
160
+ console.log("Deleted.");
161
+ break;
162
+ case "share":
163
+ if (sub === "status") {
164
+ print(await drive.shareStatus(requirePath(rest[0], "share status <path>")));
165
+ }
166
+ else if (sub === "invite") {
167
+ const [rawPath, rawEmail, role] = rest;
168
+ if (!rawPath || !rawEmail || !role) {
169
+ console.error("Usage: share invite <path> <email> <role>");
170
+ process.exit(1);
171
+ }
172
+ if (!["viewer", "editor", "admin"].includes(role)) {
173
+ console.error(`Invalid role: ${role}. Must be viewer, editor, or admin.`);
174
+ process.exit(1);
175
+ }
176
+ const invitePath = requirePath(rawPath, "share invite <path> <email> <role>");
177
+ const inviteEmail = requireEmail(rawEmail);
178
+ const rawMessage = getFlag("--message");
179
+ const message = rawMessage !== undefined ? validateMessage(rawMessage) : undefined;
180
+ await drive.shareInvite(invitePath, inviteEmail, role, message);
181
+ console.log(`Invited ${inviteEmail} as ${role}.`);
182
+ }
183
+ else if (sub === "revoke") {
184
+ const [rawPath, rawEmail] = rest;
185
+ if (!rawPath || !rawEmail) {
186
+ console.error("Usage: share revoke <path> <email>");
187
+ process.exit(1);
188
+ }
189
+ const revokePath = requirePath(rawPath, "share revoke <path> <email>");
190
+ const revokeEmail = requireEmail(rawEmail);
191
+ await drive.shareRevoke(revokePath, revokeEmail);
192
+ console.log(`Revoked access for ${revokeEmail}.`);
193
+ }
194
+ else {
195
+ console.error(`Unknown share subcommand: ${sub}`);
196
+ process.exit(1);
197
+ }
198
+ break;
199
+ case "trash":
200
+ if (sub === "empty") {
201
+ await drive.emptyTrash();
202
+ console.log("Trash emptied.");
203
+ }
204
+ else if (sub === "list" || sub === "ls") {
205
+ print(await drive.listTrash());
206
+ }
207
+ else if (sub) {
208
+ await drive.trash(requirePath(sub, "trash <path>"));
209
+ console.log("Moved to trash.");
210
+ }
211
+ else {
212
+ console.error("Usage: trash <path> | trash list | trash empty");
213
+ process.exit(1);
214
+ }
215
+ break;
216
+ case "restore":
217
+ await drive.restore(requirePath(sub, "restore <path>"));
218
+ console.log("Restored.");
219
+ break;
220
+ default:
221
+ console.error(`Unknown command: ${cmd}`);
222
+ usage();
223
+ process.exit(1);
224
+ }
225
+ }
226
+ run().catch((err) => {
227
+ if (err instanceof DriveCliNotFoundError || err instanceof DriveNotAuthenticatedError) {
228
+ console.error(err.message);
229
+ }
230
+ else if (err instanceof DriveCliError) {
231
+ console.error(`CLI error: ${err.message}`);
232
+ if (err.stderr)
233
+ console.error(err.stderr);
234
+ }
235
+ else if (err instanceof DriveParseError) {
236
+ console.error(`Parse error: ${err.message}`);
237
+ }
238
+ else if (err instanceof Error) {
239
+ console.error(err.message);
240
+ }
241
+ else {
242
+ console.error("Error:", String(err));
243
+ }
244
+ process.exit(1);
245
+ });
package/dist/index.js ADDED
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { createRequire } from "node:module";
6
+ import { DriveService } from "./services/drive.js";
7
+ import { checkCliAvailable } from "./utils/subprocess.js";
8
+ import { DriveCliNotFoundError, DriveCliError, DriveNotAuthenticatedError, DriveParseError, } from "./utils/errors.js";
9
+ import { validatePath, validateEmail, validateMessage } from "./utils/validation.js";
10
+ import { logger } from "./utils/logger.js";
11
+ const require = createRequire(import.meta.url);
12
+ const pkg = require("../package.json");
13
+ const VERSION = pkg.version ?? "1.0.0";
14
+ process.env["PROTON_DRIVE_MCP"] = "1";
15
+ function ok(data) {
16
+ return {
17
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
18
+ };
19
+ }
20
+ function fail(message) {
21
+ return {
22
+ content: [{ type: "text", text: message }],
23
+ isError: true,
24
+ };
25
+ }
26
+ function handleError(err) {
27
+ if (err instanceof DriveCliNotFoundError)
28
+ return fail(err.message);
29
+ if (err instanceof DriveNotAuthenticatedError)
30
+ return fail(err.message);
31
+ if (err instanceof DriveCliError)
32
+ return fail(`CLI error: ${err.message}`);
33
+ if (err instanceof DriveParseError)
34
+ return fail(`Parse error: ${err.message}`);
35
+ if (err instanceof Error)
36
+ return fail(err.message);
37
+ return fail(`Unexpected error: ${String(err)}`);
38
+ }
39
+ const TOOLS = [
40
+ // Auth
41
+ {
42
+ name: "drive_auth_status",
43
+ description: "Check whether the Proton Drive CLI is authenticated.",
44
+ inputSchema: { type: "object", properties: {}, required: [] },
45
+ },
46
+ {
47
+ name: "drive_auth_logout",
48
+ description: "Log out of Proton Drive (clears stored session).",
49
+ inputSchema: { type: "object", properties: {}, required: [] },
50
+ },
51
+ {
52
+ name: "drive_version",
53
+ description: "Get Proton Drive CLI and SDK version information.",
54
+ inputSchema: { type: "object", properties: {}, required: [] },
55
+ },
56
+ // Filesystem
57
+ {
58
+ name: "drive_list",
59
+ description: "List files and folders at a Proton Drive path (e.g. /my-files or /my-files/Reports).",
60
+ inputSchema: {
61
+ type: "object",
62
+ properties: {
63
+ path: {
64
+ type: "string",
65
+ description: "Remote Drive path to list. E.g. /my-files",
66
+ },
67
+ },
68
+ required: ["path"],
69
+ },
70
+ },
71
+ {
72
+ name: "drive_upload",
73
+ description: "Upload a local file or folder to Proton Drive.",
74
+ inputSchema: {
75
+ type: "object",
76
+ properties: {
77
+ localPath: {
78
+ type: "string",
79
+ description: "Absolute local path to upload.",
80
+ },
81
+ remotePath: {
82
+ type: "string",
83
+ description: "Remote Drive destination path. E.g. /my-files/Reports",
84
+ },
85
+ conflictStrategy: {
86
+ type: "string",
87
+ enum: ["skip", "overwrite", "rename"],
88
+ description: "What to do if the file already exists. Default: skip.",
89
+ },
90
+ },
91
+ required: ["localPath", "remotePath"],
92
+ },
93
+ },
94
+ {
95
+ name: "drive_download",
96
+ description: "Download a file or folder from Proton Drive to a local path.",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ remotePath: {
101
+ type: "string",
102
+ description: "Remote Drive path to download. E.g. /my-files/report.pdf",
103
+ },
104
+ localPath: {
105
+ type: "string",
106
+ description: "Absolute local destination path.",
107
+ },
108
+ },
109
+ required: ["remotePath", "localPath"],
110
+ },
111
+ },
112
+ {
113
+ name: "drive_mkdir",
114
+ description: "Create a new empty folder on Proton Drive.",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ path: {
119
+ type: "string",
120
+ description: "Remote Drive path for the new folder. E.g. /my-files/NewFolder",
121
+ },
122
+ },
123
+ required: ["path"],
124
+ },
125
+ },
126
+ {
127
+ name: "drive_move",
128
+ description: "Move or rename a file or folder on Proton Drive.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ sourcePath: {
133
+ type: "string",
134
+ description: "Current remote path of the file/folder.",
135
+ },
136
+ destinationPath: {
137
+ type: "string",
138
+ description: "New remote path.",
139
+ },
140
+ },
141
+ required: ["sourcePath", "destinationPath"],
142
+ },
143
+ },
144
+ {
145
+ name: "drive_delete",
146
+ description: "Permanently delete a file or folder from Proton Drive. " +
147
+ "Requires confirmed=true — always ask the user before calling this.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ path: {
152
+ type: "string",
153
+ description: "Remote Drive path to delete.",
154
+ },
155
+ confirmed: {
156
+ type: "boolean",
157
+ description: "Must be true. Confirms the user has acknowledged this is permanent.",
158
+ },
159
+ },
160
+ required: ["path", "confirmed"],
161
+ },
162
+ },
163
+ // Sharing
164
+ {
165
+ name: "drive_share_status",
166
+ description: "Get sharing status and member list for a Proton Drive path.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ path: {
171
+ type: "string",
172
+ description: "Remote Drive path to check sharing for.",
173
+ },
174
+ },
175
+ required: ["path"],
176
+ },
177
+ },
178
+ {
179
+ name: "drive_list_trash",
180
+ description: "List files and folders currently in the Proton Drive trash.",
181
+ inputSchema: { type: "object", properties: {}, required: [] },
182
+ },
183
+ {
184
+ name: "drive_share_invite",
185
+ description: "Invite someone to access a Proton Drive file or folder.",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: {
189
+ path: {
190
+ type: "string",
191
+ description: "Remote Drive path to share.",
192
+ },
193
+ email: {
194
+ type: "string",
195
+ description: "Email address of the person to invite.",
196
+ },
197
+ role: {
198
+ type: "string",
199
+ enum: ["viewer", "editor", "admin"],
200
+ description: "Access level to grant.",
201
+ },
202
+ message: {
203
+ type: "string",
204
+ description: "Optional message to include in the invitation.",
205
+ },
206
+ },
207
+ required: ["path", "email", "role"],
208
+ },
209
+ },
210
+ {
211
+ name: "drive_share_revoke",
212
+ description: "Remove someone's access to a Proton Drive file or folder.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ path: {
217
+ type: "string",
218
+ description: "Remote Drive path.",
219
+ },
220
+ email: {
221
+ type: "string",
222
+ description: "Email address of the person whose access to revoke.",
223
+ },
224
+ },
225
+ required: ["path", "email"],
226
+ },
227
+ },
228
+ // Trash
229
+ {
230
+ name: "drive_trash",
231
+ description: "Move a file or folder to trash on Proton Drive.",
232
+ inputSchema: {
233
+ type: "object",
234
+ properties: {
235
+ path: {
236
+ type: "string",
237
+ description: "Remote Drive path to trash.",
238
+ },
239
+ },
240
+ required: ["path"],
241
+ },
242
+ },
243
+ {
244
+ name: "drive_restore",
245
+ description: "Restore a file or folder from trash on Proton Drive.",
246
+ inputSchema: {
247
+ type: "object",
248
+ properties: {
249
+ path: {
250
+ type: "string",
251
+ description: "Remote Drive path to restore.",
252
+ },
253
+ },
254
+ required: ["path"],
255
+ },
256
+ },
257
+ {
258
+ name: "drive_empty_trash",
259
+ description: "Permanently delete ALL items in the Proton Drive trash. " +
260
+ "Requires confirmed=true — always show the trash contents (drive_list_trash) and ask the user before calling this.",
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ confirmed: {
265
+ type: "boolean",
266
+ description: "Must be true. Confirms the user has acknowledged this is permanent and irreversible.",
267
+ },
268
+ },
269
+ required: ["confirmed"],
270
+ },
271
+ },
272
+ ];
273
+ async function main() {
274
+ const cliAvailable = await checkCliAvailable();
275
+ if (!cliAvailable) {
276
+ logger.error("proton-drive CLI not found. Download from https://proton.me/download/drive/cli/index.html");
277
+ process.exit(1);
278
+ }
279
+ const drive = new DriveService();
280
+ const server = new Server({ name: "proton-drive-mcp", version: VERSION }, { capabilities: { tools: {} } });
281
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
282
+ tools: TOOLS.map((t) => ({
283
+ name: t.name,
284
+ description: t.description,
285
+ inputSchema: t.inputSchema,
286
+ })),
287
+ }));
288
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
289
+ const { name, arguments: args = {} } = req.params;
290
+ const a = args;
291
+ try {
292
+ switch (name) {
293
+ case "drive_auth_status":
294
+ return ok(await drive.authStatus());
295
+ case "drive_auth_logout":
296
+ await drive.authLogout();
297
+ return ok({ message: "Logged out successfully." });
298
+ case "drive_version":
299
+ return ok(await drive.version());
300
+ case "drive_list":
301
+ return ok(await drive.list(validatePath(a.path)));
302
+ case "drive_mkdir": {
303
+ const mkdirPath = validatePath(a.path);
304
+ await drive.mkdir(mkdirPath);
305
+ return ok({ message: `Folder created: ${mkdirPath}` });
306
+ }
307
+ case "drive_upload": {
308
+ const cs = typeof a.conflictStrategy === "string" ? a.conflictStrategy : "skip";
309
+ if (!["skip", "overwrite", "rename"].includes(cs)) {
310
+ throw new Error(`conflictStrategy must be skip, overwrite, or rename`);
311
+ }
312
+ return ok(await drive.upload(validatePath(a.localPath), validatePath(a.remotePath), cs));
313
+ }
314
+ case "drive_download":
315
+ return ok(await drive.download(validatePath(a.remotePath), validatePath(a.localPath)));
316
+ case "drive_move": {
317
+ const moveSrc = validatePath(a.sourcePath);
318
+ const moveDst = validatePath(a.destinationPath);
319
+ await drive.move(moveSrc, moveDst);
320
+ return ok({ message: `Moved: ${moveSrc} → ${moveDst}` });
321
+ }
322
+ case "drive_delete": {
323
+ if (a.confirmed !== true) {
324
+ return fail("drive_delete requires confirmed=true. Ask the user to confirm before deleting.");
325
+ }
326
+ const deletePath = validatePath(a.path);
327
+ await drive.delete(deletePath);
328
+ return ok({ message: `Deleted: ${deletePath}` });
329
+ }
330
+ case "drive_list_trash":
331
+ return ok(await drive.listTrash());
332
+ case "drive_share_status":
333
+ return ok(await drive.shareStatus(validatePath(a.path)));
334
+ case "drive_share_invite": {
335
+ const email = validateEmail(a.email);
336
+ const role = typeof a.role === "string" ? a.role : "viewer";
337
+ if (!["viewer", "editor", "admin"].includes(role)) {
338
+ throw new Error("role must be viewer, editor, or admin");
339
+ }
340
+ const inviteMsg = typeof a.message === "string" ? validateMessage(a.message) : undefined;
341
+ await drive.shareInvite(validatePath(a.path), email, role, inviteMsg);
342
+ return ok({ message: `Invited ${email} as ${role}.` });
343
+ }
344
+ case "drive_share_revoke":
345
+ {
346
+ const revokeEmail = validateEmail(a.email);
347
+ await drive.shareRevoke(validatePath(a.path), revokeEmail);
348
+ return ok({ message: `Revoked access for ${revokeEmail}.` });
349
+ }
350
+ case "drive_trash": {
351
+ const trashPath = validatePath(a.path);
352
+ await drive.trash(trashPath);
353
+ return ok({ message: `Moved to trash: ${trashPath}` });
354
+ }
355
+ case "drive_restore": {
356
+ const restorePath = validatePath(a.path);
357
+ await drive.restore(restorePath);
358
+ return ok({ message: `Restored from trash: ${restorePath}` });
359
+ }
360
+ case "drive_empty_trash":
361
+ if (a.confirmed !== true) {
362
+ return fail("drive_empty_trash requires confirmed=true. " +
363
+ "Use drive_list_trash first to show the user what will be deleted, then ask for confirmation.");
364
+ }
365
+ await drive.emptyTrash();
366
+ return ok({ message: "Trash emptied." });
367
+ default:
368
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
369
+ }
370
+ }
371
+ catch (err) {
372
+ if (err instanceof McpError)
373
+ throw err;
374
+ return handleError(err);
375
+ }
376
+ });
377
+ const transport = new StdioServerTransport();
378
+ await server.connect(transport);
379
+ logger.info(`proton-drive-mcp v${VERSION} running`);
380
+ }
381
+ main().catch((err) => {
382
+ logger.error("Fatal:", err);
383
+ process.exit(1);
384
+ });