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.
@@ -0,0 +1,137 @@
1
+ import { runDrive as defaultRunDrive } from "../utils/subprocess.js";
2
+ import { DriveParseError } from "../utils/errors.js";
3
+ export class DriveService {
4
+ run;
5
+ constructor(runner) {
6
+ this.run = runner ?? defaultRunDrive;
7
+ }
8
+ // Auth
9
+ async authStatus() {
10
+ const result = await this.run(["auth", "status"]);
11
+ if (result === null)
12
+ return { authenticated: false };
13
+ const r = result;
14
+ return {
15
+ authenticated: Boolean(r?.authenticated ?? r?.loggedIn ?? false),
16
+ email: typeof r?.email === "string" ? r.email : undefined,
17
+ };
18
+ }
19
+ async authLogout() {
20
+ await this.run(["auth", "logout"]);
21
+ }
22
+ async version() {
23
+ const result = await this.run(["version"]);
24
+ const r = result;
25
+ return {
26
+ cli: typeof r?.cli === "string" ? r.cli : String(r?.version ?? "unknown"),
27
+ sdk: typeof r?.sdk === "string" ? r.sdk : "unknown",
28
+ };
29
+ }
30
+ // Filesystem
31
+ async list(remotePath) {
32
+ const result = await this.run(["filesystem", "list", remotePath]);
33
+ if (result === null)
34
+ return [];
35
+ if (!Array.isArray(result))
36
+ throw new DriveParseError(`Expected array from list, got: ${JSON.stringify(result).slice(0, 100)}`);
37
+ return result.map((item) => ({
38
+ name: String(item.name ?? ""),
39
+ path: String(item.path ?? remotePath),
40
+ type: item.type === "folder" ? "folder" : "file",
41
+ size: typeof item.size === "number" ? item.size : undefined,
42
+ modifiedAt: typeof item.modifiedAt === "string" ? item.modifiedAt : undefined,
43
+ mimeType: typeof item.mimeType === "string" ? item.mimeType : undefined,
44
+ }));
45
+ }
46
+ async upload(localPath, remotePath, conflictStrategy = "skip") {
47
+ const result = await this.run([
48
+ "filesystem",
49
+ "upload",
50
+ localPath,
51
+ remotePath,
52
+ "--conflict-strategy",
53
+ conflictStrategy,
54
+ "--skip-thumbnails",
55
+ ]);
56
+ const r = result;
57
+ return {
58
+ path: remotePath,
59
+ uploaded: typeof r?.uploaded === "number" ? r.uploaded : 0,
60
+ skipped: typeof r?.skipped === "number" ? r.skipped : 0,
61
+ failed: typeof r?.failed === "number" ? r.failed : 0,
62
+ };
63
+ }
64
+ async download(remotePath, localPath) {
65
+ const result = await this.run(["filesystem", "download", remotePath, localPath]);
66
+ const r = result;
67
+ return {
68
+ path: remotePath,
69
+ localPath,
70
+ downloaded: typeof r?.downloaded === "number" ? r.downloaded : 0,
71
+ };
72
+ }
73
+ async mkdir(remotePath) {
74
+ await this.run(["filesystem", "mkdir", remotePath]);
75
+ }
76
+ async move(remoteSrc, remoteDst) {
77
+ await this.run(["filesystem", "move", remoteSrc, remoteDst]);
78
+ }
79
+ async delete(remotePath) {
80
+ await this.run(["filesystem", "delete", remotePath]);
81
+ }
82
+ // Sharing
83
+ async shareStatus(remotePath) {
84
+ const result = await this.run(["sharing", "status", remotePath]);
85
+ const r = result;
86
+ const members = Array.isArray(r?.members)
87
+ ? r.members.map((m) => ({
88
+ email: String(m.email ?? ""),
89
+ role: (m.role ?? "viewer"),
90
+ addedAt: typeof m.addedAt === "string" ? m.addedAt : undefined,
91
+ }))
92
+ : [];
93
+ return {
94
+ path: remotePath,
95
+ isShared: Boolean(r?.isShared ?? members.length > 0),
96
+ members,
97
+ shareUrl: typeof r?.shareUrl === "string" ? r.shareUrl : undefined,
98
+ };
99
+ }
100
+ async shareInvite(remotePath, email, role, message) {
101
+ await this.run([
102
+ "sharing", "invite",
103
+ ...(message ? ["--message", message] : []),
104
+ "--user", email,
105
+ "--role", role,
106
+ remotePath,
107
+ ]);
108
+ }
109
+ async shareRevoke(remotePath, email) {
110
+ await this.run(["sharing", "revoke", "--user", email, remotePath]);
111
+ }
112
+ // Trash
113
+ async listTrash() {
114
+ const result = await this.run(["trash", "list"]);
115
+ if (result === null)
116
+ return [];
117
+ if (!Array.isArray(result))
118
+ throw new DriveParseError(`Expected array from trash list, got: ${JSON.stringify(result).slice(0, 100)}`);
119
+ return result.map((item) => ({
120
+ name: String(item.name ?? ""),
121
+ path: String(item.path ?? ""),
122
+ type: item.type === "folder" ? "folder" : "file",
123
+ size: typeof item.size === "number" ? item.size : undefined,
124
+ modifiedAt: typeof item.modifiedAt === "string" ? item.modifiedAt : undefined,
125
+ mimeType: typeof item.mimeType === "string" ? item.mimeType : undefined,
126
+ }));
127
+ }
128
+ async trash(remotePath) {
129
+ await this.run(["trash", remotePath]);
130
+ }
131
+ async restore(remotePath) {
132
+ await this.run(["restore", remotePath]);
133
+ }
134
+ async emptyTrash() {
135
+ await this.run(["trash", "empty"]);
136
+ }
137
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ export class DriveCliNotFoundError extends Error {
2
+ constructor() {
3
+ super("proton-drive CLI not found in PATH. " +
4
+ "Download it from https://proton.me/download/drive/cli/index.html and ensure it is in your PATH.");
5
+ this.name = "DriveCliNotFoundError";
6
+ }
7
+ }
8
+ export class DriveCliError extends Error {
9
+ stderr;
10
+ constructor(message, stderr = "") {
11
+ super(message);
12
+ this.stderr = stderr;
13
+ this.name = "DriveCliError";
14
+ }
15
+ }
16
+ export class DriveNotAuthenticatedError extends Error {
17
+ constructor() {
18
+ super("Not authenticated. Run `proton-drive auth login` in your terminal first.");
19
+ this.name = "DriveNotAuthenticatedError";
20
+ }
21
+ }
22
+ export class DriveParseError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "DriveParseError";
26
+ }
27
+ }
@@ -0,0 +1,15 @@
1
+ const isMcp = () => process.env["PROTON_DRIVE_MCP"] === "1";
2
+ export const logger = {
3
+ info: (...args) => {
4
+ if (!isMcp())
5
+ console.log("[INFO]", ...args);
6
+ else
7
+ process.stderr.write(`[INFO] ${args.join(" ")}\n`);
8
+ },
9
+ warn: (...args) => {
10
+ process.stderr.write(`[WARN] ${args.join(" ")}\n`);
11
+ },
12
+ error: (...args) => {
13
+ process.stderr.write(`[ERROR] ${args.join(" ")}\n`);
14
+ },
15
+ };
@@ -0,0 +1,74 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { DriveCliError, DriveCliNotFoundError, DriveNotAuthenticatedError, DriveParseError } from "./errors.js";
4
+ const execFileAsync = promisify(execFile);
5
+ const CLI_BINARY = process.env["PROTON_DRIVE_BIN"] ?? "proton-drive";
6
+ const DEFAULT_TIMEOUT_MS = 60_000;
7
+ // Upload and download transfer actual file bytes — use a much longer timeout.
8
+ const TRANSFER_TIMEOUT_MS = 30 * 60_000; // 30 minutes
9
+ const TRANSFER_COMMANDS = new Set(["upload", "download"]);
10
+ function timeoutFor(args) {
11
+ // args[0] is the group (e.g. "filesystem"), args[1] is the subcommand
12
+ if (args[0] === "filesystem" && args[1] && TRANSFER_COMMANDS.has(args[1])) {
13
+ return TRANSFER_TIMEOUT_MS;
14
+ }
15
+ return DEFAULT_TIMEOUT_MS;
16
+ }
17
+ export async function runDrive(args) {
18
+ try {
19
+ const { stdout, stderr } = await execFileAsync(CLI_BINARY, [...args, "--json"], { timeout: timeoutFor(args), maxBuffer: 50 * 1024 * 1024, killSignal: "SIGKILL" });
20
+ const raw = stdout.trim();
21
+ // Only check stderr for auth errors when stdout is empty. If the CLI wrote
22
+ // valid JSON, we honour it even when warnings appear on stderr.
23
+ if (!raw) {
24
+ if (stderr && stderr.trim()) {
25
+ const lower = stderr.toLowerCase();
26
+ if (lower.includes("not authenticated") || lower.includes("not logged in")) {
27
+ throw new DriveNotAuthenticatedError();
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ try {
33
+ return JSON.parse(raw);
34
+ }
35
+ catch {
36
+ process.stderr.write(`[proton-drive-mcp] parse error: ${raw.slice(0, 500)}\n`);
37
+ throw new DriveParseError("Failed to parse CLI output as JSON");
38
+ }
39
+ }
40
+ catch (err) {
41
+ if (err instanceof DriveCliError ||
42
+ err instanceof DriveNotAuthenticatedError ||
43
+ err instanceof DriveParseError) {
44
+ throw err;
45
+ }
46
+ const e = err;
47
+ if (e.code === "ENOENT") {
48
+ throw new DriveCliNotFoundError();
49
+ }
50
+ if (e.killed === true || e.code === "ETIMEDOUT" || e.signal != null) {
51
+ const secs = timeoutFor(args) / 1000;
52
+ throw new DriveCliError(`CLI process timed out after ${secs}s`, "");
53
+ }
54
+ const stderr = e.stderr ?? "";
55
+ const lower = stderr.toLowerCase();
56
+ if (lower.includes("not authenticated") || lower.includes("not logged in")) {
57
+ throw new DriveNotAuthenticatedError();
58
+ }
59
+ throw new DriveCliError(e.message ?? String(e), stderr);
60
+ }
61
+ }
62
+ // Returns false only when the binary is not installed (ENOENT). Any other
63
+ // error (non-zero exit, auth required, etc.) still returns true — the binary
64
+ // is present and real errors will surface on the first actual tool call.
65
+ export async function checkCliAvailable() {
66
+ try {
67
+ await execFileAsync(CLI_BINARY, ["version", "--json"], { timeout: 5_000 });
68
+ return true;
69
+ }
70
+ catch (e) {
71
+ const err = e;
72
+ return err.code !== "ENOENT";
73
+ }
74
+ }
@@ -0,0 +1,35 @@
1
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2
+ // Control chars: 0x00-0x1F, DEL (0x7F), Unicode separators U+2028 and U+2029.
3
+ const CONTROL_RE = new RegExp("[\\x00-\\x1f\\x7f\\u2028\\u2029]");
4
+ export function validatePath(path) {
5
+ const p = String(path ?? "").trim();
6
+ if (!p)
7
+ throw new Error("path must not be empty");
8
+ if (p.startsWith("-"))
9
+ throw new Error(`path must not start with '-': ${p}`);
10
+ if (CONTROL_RE.test(p))
11
+ throw new Error("path contains control characters");
12
+ if (p.includes(".."))
13
+ throw new Error(`path must not contain '..' segments: ${p}`);
14
+ return p;
15
+ }
16
+ export function validateMessage(message) {
17
+ const m = String(message ?? "").trim();
18
+ if (m.startsWith("-"))
19
+ throw new Error(`message must not start with '-': ${m}`);
20
+ if (CONTROL_RE.test(m))
21
+ throw new Error("message contains control characters");
22
+ if (m.length > 2000)
23
+ throw new Error("message must be 2000 characters or fewer");
24
+ return m;
25
+ }
26
+ export function validateEmail(email) {
27
+ const e = String(email ?? "").trim();
28
+ if (e.startsWith("-"))
29
+ throw new Error(`email must not start with '-': ${e}`);
30
+ if (CONTROL_RE.test(e))
31
+ throw new Error("email contains control characters");
32
+ if (!EMAIL_RE.test(e))
33
+ throw new Error(`invalid email address: ${e}`);
34
+ return e;
35
+ }
package/glama.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/glama.json",
3
+ "name": "proton-drive-mcp",
4
+ "description": "Claude Desktop MCP and CLI for Proton Drive. List, upload, download, share, and manage end-to-end encrypted files.",
5
+ "icon": "https://proton.me/favicon.ico",
6
+ "categories": ["storage", "productivity", "privacy"],
7
+ "license": "MIT",
8
+ "repository": "https://github.com/googlarz/proton-drive-mcp"
9
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "proton-drive-mcp",
3
+ "version": "1.0.12",
4
+ "description": "Proton Drive MCP — Claude Desktop MCP and CLI for Proton Drive. List, upload, download, share, and manage files with end-to-end encryption.",
5
+ "type": "module",
6
+ "packageManager": "npm@10.9.3",
7
+ "main": "dist/index.js",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "bin": {
13
+ "proton-drive-mcp": "dist/index.js",
14
+ "proton-drive-cli": "dist/cli.js"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsc --watch",
19
+ "start": "node dist/index.js",
20
+ "cli": "npm run build && node dist/cli.js",
21
+ "test": "npm run build && node --test test/*.test.mjs",
22
+ "lint": "tsc --noEmit",
23
+ "clean": "rm -rf dist",
24
+ "ci": "npm run lint && npm test",
25
+ "pack:check": "npm pack --dry-run",
26
+ "release:check": "npm run lint && npm test && npm run pack:check"
27
+ },
28
+ "keywords": [
29
+ "proton",
30
+ "proton-drive",
31
+ "mcp",
32
+ "cloud-storage",
33
+ "encryption",
34
+ "claude",
35
+ "ai",
36
+ "automation",
37
+ "typescript"
38
+ ],
39
+ "author": {
40
+ "name": "googlarz",
41
+ "url": "https://github.com/googlarz"
42
+ },
43
+ "license": "MIT",
44
+ "homepage": "https://github.com/googlarz/proton-drive-mcp#readme",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/googlarz/proton-drive-mcp.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/googlarz/proton-drive-mcp/issues"
51
+ },
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "dependencies": {
56
+ "@modelcontextprotocol/sdk": "^1.11.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^22.10.2",
60
+ "typescript": "^5.7.2"
61
+ },
62
+ "files": [
63
+ "dist/**/*",
64
+ "README.md",
65
+ "LICENSE",
66
+ "CHANGELOG.md",
67
+ "glama.json",
68
+ "smithery.yaml"
69
+ ],
70
+ "publishConfig": {
71
+ "access": "public"
72
+ }
73
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,37 @@
1
+ name: proton-drive-mcp
2
+ displayName: Proton Drive MCP
3
+ description: >
4
+ Claude Desktop MCP and companion CLI for Proton Drive.
5
+ List, upload, download, share, and manage end-to-end encrypted files
6
+ without credential exposure — auth handled by the official Proton Drive CLI.
7
+ license: MIT
8
+ homepage: https://github.com/googlarz/proton-drive-mcp
9
+ repository: https://github.com/googlarz/proton-drive-mcp
10
+ categories:
11
+ - storage
12
+ - productivity
13
+ - privacy
14
+ tools:
15
+ - drive_auth_status
16
+ - drive_auth_logout
17
+ - drive_version
18
+ - drive_list
19
+ - drive_mkdir
20
+ - drive_upload
21
+ - drive_download
22
+ - drive_move
23
+ - drive_delete
24
+ - drive_list_trash
25
+ - drive_share_status
26
+ - drive_share_invite
27
+ - drive_share_revoke
28
+ - drive_trash
29
+ - drive_restore
30
+ - drive_empty_trash
31
+ startCommand:
32
+ type: stdio
33
+ configSchema:
34
+ type: object
35
+ properties: {}
36
+ commandFunction: |-
37
+ (_config) => ({ command: "npx", args: ["-y", "proton-drive-mcp"] })