proton-drive-mcp 1.1.0 → 1.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 — 2026-09-30
4
+
5
+ Proton Drive has no search, and its end-to-end encryption rules out server-side indexing. This release adds the client-side layer: find things, understand what uses your space, and clean up safely. 4 builders worked in parallel, then 3 verifiers (live on a real account, an independent security review, packaging/CI) and a re-check of every fix. Review findings fixed before release include two ReDoS vectors in search and a local-path guard gap.
6
+
7
+ ### Added
8
+ - **`drive_tree`** and **`drive_search`** (also in the `core` tier): a bounded, parallel walk of the drive (8 concurrent calls, `.git` and `node_modules` skipped, call cap, cancellable) with a 5-minute in-memory cache that the server's own writes invalidate. Search by name, glob (relative to the search folder when it contains `/`), extension, media type, size and date, with stable pagination. The first walk of a large drive takes minutes (about 25 s per 1,200 files); later calls are instant.
9
+ - **`drive_usage`**: largest files and folders, breakdown by type, old files and trash stats (sums of file sizes, not the account quota).
10
+ - **`drive_find_duplicates`**: groups by the uploader-claimed sha1 or by size; `verify` downloads and hashes candidates within a size and total budget. It never deletes.
11
+ - **`drive_sharing_audit`**: public links (role, expiry, password; the URL is never printed), invitees, pending invitations and risk flags.
12
+ - **`drive_sync_plan`**: compares a local folder with a Drive folder (plan only).
13
+ - **`drive_bulk_move`** and **`drive_bulk_trash`**: up to 200 items in one batched call. Always two steps: without `confirmed` you get a plan and any problems; `confirmed:true` applies only if there are none. Trash stays reversible.
14
+ - **MCP prompts** (`organise-folder`, `find-files`, `storage-audit`, `sharing-audit`), listed only when the tools they need are in the active tier; also declared in the MCPB manifest.
15
+ - **Human confirmation through elicitation** for clients that support it (Claude Code CLI; Claude Desktop is unverified): gated calls and bulk applies ask the person instead of only failing. Clients without it behave as before. It does not protect against a model that passes `confirmed:true` itself.
16
+ - `drive_list` and `drive_info` now return `mtime`, `uploadedAt` and `sha1` (claimed, often absent); `drive_list` takes a `type` filter; `drive_share_status` adds `shareUrlRole`.
17
+ - Tool titles on every tool, corrected annotations (for example `drive_trash` is not destructive), `maxResultSizeChars` hints on the large-result tools.
18
+
19
+ ### Changed
20
+ - The full tool list is now 46 tools (about 39 KB, was 29.5 KB); `core` is 18 tools (16.8 KB). Use `PROTON_DRIVE_TOOL_TIER=core` to keep the token cost down.
21
+ - Folder uploads and `drive_sync_plan` refuse a local folder that contains protected locations (home directory, `/`, `~/.ssh`, ...); scans skip secret-named files and report `protectedSkipped`. Downloads into your home folder still work. Note that for uploads the CLI does the recursion, so a `.env` inside an uploaded subfolder is not filtered.
22
+ - `.gitignore` covers `node_modules` as a symlink too.
23
+
24
+ ### Known limitations
25
+ - Windows is not supported. The CLI has no search, version history, quota or change notifications, so none of those are offered.
26
+ - The walk cache does not see changes made by other clients until it expires or `refresh:true`.
27
+ - `drive_share_leave`, `drive_invitation_accept` and `drive_invitation_reject` have never been tested live (no second Proton account). Elicitation and prompts are verified with the official SDK client and Claude Code semantics, not with Claude Desktop.
28
+
29
+ ## 1.2.0 — 2026-09-28
30
+
31
+ Setup, packaging and reliability round. Five features were built in parallel on separate branches, merged, then verified live (real account, 3 concurrent servers), on Node 22/24 and on the new Ubuntu + macOS CI matrix, plus an independent code review. Review and CI findings were fixed before release.
32
+
33
+ **Breaking:** Node 20 is no longer supported (`engines` is `>=22`; Node 20 is past end-of-life). It may still start, but it is not tested and `doctor` fails on it.
34
+
35
+ ### Added
36
+ - **`proton-drive-cli doctor`**: checks Node, the `proton-drive` CLI, login state, sync folder and the Claude Desktop config; `--json` output; exits 1 on failure. It only reports on our own config entry, never other servers' entries.
37
+ - **`proton-drive-cli setup-claude-desktop`**: prints (default) or writes (`--write`) the Claude Desktop entry with absolute paths. Touches only `mcpServers["proton-drive-mcp"]`, keeps a timestamped `.bak`, writes atomically, skips the write if nothing changed, and refuses to run from a temporary `npx` cache or on invalid JSON.
38
+ - **MCPB bundle** (`npm run package:mcpb`): one-click Claude Desktop extension attached to each GitHub release (macOS and Linux only; installing it into Claude Desktop is untested).
39
+ - **`PROTON_DRIVE_TOOL_TIER=core`**: exposes 16 everyday tools (~13 KB of tool definitions instead of ~29 KB). Hidden tools are refused at call time. Tool descriptions were also shortened; every safety note was kept.
40
+ - **Retry of read-only calls** on rate limiting, timeouts and network errors (max 3 attempts, bounded by the call timeout). Mutating and transfer commands are never retried.
41
+ - **CI**: Ubuntu and macOS × Node 22/24, non-blocking Windows probe; publish workflow is ready for npm Trusted Publishing (the `NPM_TOKEN` fallback stays until one OIDC publish has succeeded).
42
+
43
+ ### Fixed
44
+ - An empty `PROTON_DRIVE_BIN` (some clients pass `""` for an optional setting) is now treated as unset.
45
+ - The README Claude Desktop snippets use the same server key as `setup-claude-desktop` (`proton-drive-mcp`), so `doctor` finds it.
46
+ - Local-path guard: an unreadable ancestor directory (such as `/var/root` for a non-root user) no longer aborts the denylist check with a raw `EACCES`.
47
+
48
+ ### Known limitations
49
+ - Windows is not supported (tests never passed there; only probed in CI).
50
+ - `drive_share_leave`, `drive_invitation_accept` and `drive_invitation_reject` have never been tested live (no second Proton account).
51
+ - The MCPB bundle and the npm OIDC publish path have not been exercised against the real Claude Desktop / npm yet.
52
+
3
53
  ## 1.1.0 — 2026-09-28
4
54
 
5
55
  A "test it like Mail Bridge" round: 6 agents live-tested every tool group against a real account for the first time at scale — full-byte round trips up to 200 MB, the real Proton Drive sync folder, real photo uploads/albums, three MCP servers sharing one CLI session concurrently, official MCP clients (SDK, Inspector, Node 20/22/24, a packed-and-installed tarball), and adversarial edge cases. 3 high-severity and 7 medium-severity bugs were found, fixed by 5 agents working in parallel on separate branches, then the merged result was re-verified live by another 5 agents (one an independent code review) before anything shipped. Every fix below was reproduced from the original bug, not assumed from a commit message.
package/README.md CHANGED
@@ -12,12 +12,12 @@
12
12
  [![npm version](https://img.shields.io/npm/v/proton-drive-mcp?color=%236d4aff&label=npm)](https://www.npmjs.com/package/proton-drive-mcp)
13
13
  [![CI](https://github.com/googlarz/proton-drive-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/googlarz/proton-drive-mcp/actions/workflows/ci.yml)
14
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
15
- [![Node.js 20+](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
15
+ [![Node.js 22+](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
16
16
  [![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
17
17
  [![MCP](https://img.shields.io/badge/MCP-compatible-blueviolet)](https://modelcontextprotocol.io)
18
18
  [![GitHub stars](https://img.shields.io/github/stars/googlarz/proton-drive-mcp?style=social)](https://github.com/googlarz/proton-drive-mcp)
19
19
  [![Last commit](https://img.shields.io/github/last-commit/googlarz/proton-drive-mcp?color=brightgreen&label=last%20commit)](https://github.com/googlarz/proton-drive-mcp/commits/main)
20
- [![Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)](https://github.com/googlarz/proton-drive-mcp)
20
+ [![Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey)](https://github.com/googlarz/proton-drive-mcp)
21
21
  [![proton-drive-mcp MCP server](https://glama.ai/mcp/servers/googlarz/proton-drive-mcp/badges/score.svg)](https://glama.ai/mcp/servers/googlarz/proton-drive-mcp)
22
22
 
23
23
  </div>
@@ -30,8 +30,8 @@ Give Claude Desktop (or any MCP client) full access to your Proton Drive and Pro
30
30
 
31
31
  - **Claude manages your Proton Drive** — list, upload, download, move, share, trash, restore
32
32
  - **Proton Photos album management** — list albums, create/delete albums, add and remove photos
33
- - **Full CLI** — same 38 operations, scriptable and pipeable, works in cron and shell scripts
34
- - **100% CLI coverage** — every scriptable Proton Drive CLI command has a matching tool (verified against the CLI's own source; `auth login` is the one command excluded, since it's an interactive browser flow)
33
+ - **Companion CLI** — 44 of the 46 operations, scriptable and pipeable, works in cron and shell scripts (the two sync-folder tools, `drive_read_file` and `drive_write_file`, are MCP-only)
34
+ - **Full Proton Drive CLI coverage** — every scriptable Proton Drive CLI command has a matching tool (verified against the CLI's own source; `auth login` is the one command excluded, since it's an interactive browser flow)
35
35
  - **Zero credential exposure** — auth is handled entirely by the official Proton Drive CLI; this MCP never touches your password or session token
36
36
  - **Shell injection safe** — all CLI calls use `execFile` with discrete argument arrays, never string interpolation
37
37
  - **Privacy-native** — end-to-end encryption is handled by Proton's own CLI; this server is just a thin MCP wrapper
@@ -60,7 +60,7 @@ proton-drive auth login
60
60
 
61
61
  This opens a browser for Proton's standard sign-in flow. Credentials are stored in your OS keychain — not on disk, not in config files.
62
62
 
63
- **3. Node.js 20 or later** — `node --version` to check.
63
+ **3. Node.js 22 or later** — `node --version` to check.
64
64
 
65
65
  ---
66
66
 
@@ -91,7 +91,7 @@ Add to your `claude_desktop_config.json`:
91
91
  ```json
92
92
  {
93
93
  "mcpServers": {
94
- "proton-drive": {
94
+ "proton-drive-mcp": {
95
95
  "command": "npx",
96
96
  "args": ["-y", "proton-drive-mcp"],
97
97
  "env": { "PROTON_DRIVE_BIN": "/absolute/path/to/proton-drive" }
@@ -102,16 +102,20 @@ Add to your `claude_desktop_config.json`:
102
102
 
103
103
  Claude Desktop starts servers with a minimal `PATH`, so a `proton-drive` in `~/.local/bin` is usually not found. Set `PROTON_DRIVE_BIN` to the output of `which proton-drive` (the default install location is `~/.local/bin/proton-drive`, written out in full, e.g. `/Users/you/.local/bin/proton-drive`). If `npx` itself is not found, use its absolute path as `command` (`which npx`).
104
104
 
105
- Restart Claude Desktop. Check **`+` → Connectors → proton-drive** to confirm the server is connected.
105
+ Restart Claude Desktop. Check **`+` → Connectors → proton-drive-mcp** to confirm the server is connected.
106
106
 
107
107
  > **Tip:** Make sure `proton-drive auth login` has been run at least once before starting Claude Desktop.
108
108
 
109
+ ### One-click install (MCPB bundle)
110
+
111
+ Instead of editing JSON, download `proton-drive-mcp-<version>.mcpb` from the [latest GitHub release](https://github.com/googlarz/proton-drive-mcp/releases/latest) and open it (or drag it into **Settings → Extensions** in Claude Desktop). In the extension settings, set **proton-drive CLI path** to the output of `which proton-drive` (usually `~/.local/bin/proton-drive`); optionally set the **Proton Drive sync folder** to enable `drive_read_file` / `drive_write_file`. You still need the official `proton-drive` CLI installed and `proton-drive auth login` done once.
112
+
109
113
  ### If installed globally
110
114
 
111
115
  ```json
112
116
  {
113
117
  "mcpServers": {
114
- "proton-drive": {
118
+ "proton-drive-mcp": {
115
119
  "command": "proton-drive-mcp"
116
120
  }
117
121
  }
@@ -120,6 +124,29 @@ Restart Claude Desktop. Check **`+` → Connectors → proton-drive** to confirm
120
124
 
121
125
  ---
122
126
 
127
+ ## Setup and diagnostics
128
+
129
+ Two commands of the companion CLI help when Claude Desktop reports "proton-drive CLI not found" (it starts servers with a minimal `PATH`):
130
+
131
+ ```bash
132
+ # Read-only checks: Node >= 22, proton-drive CLI + version, auth, PROTON_DRIVE_SYNC_PATH, your Claude Desktop entry
133
+ proton-drive-cli doctor [--json] [--config <path>]
134
+
135
+ # Dry run: prints the entry it would add and the target file, changes nothing
136
+ proton-drive-cli setup-claude-desktop [--config <path>] [--sync-path <dir>]
137
+
138
+ # Apply it (timestamped backup first; only mcpServers["proton-drive-mcp"] is touched)
139
+ proton-drive-cli setup-claude-desktop --sync-path "$HOME/Proton Drive" --write
140
+ ```
141
+
142
+ `doctor` exits 1 if any check fails (warnings do not fail). It only ever reports on the `proton-drive-mcp` entry of the config, never on other servers. `setup-claude-desktop` writes the absolute path of the running `node`, of this package's `dist/index.js`, and of the resolved `proton-drive` binary (`PROTON_DRIVE_BIN`). It refuses to run if the CLI cannot be found, the config file is not valid JSON, or the package is running from a temporary `npx` cache (install it first with `npm install -g proton-drive-mcp`). The entry pins the current `node` binary, so re-run it after switching Node versions (e.g. with nvm). Re-running with unchanged settings leaves the file alone; otherwise the file is replaced atomically and a timestamped `.bak-…` copy is kept.
143
+
144
+ The default config location is per OS (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%\Claude\`, Linux `$XDG_CONFIG_HOME` or `~/.config/Claude/`); use `--config` or the `CLAUDE_DESKTOP_CONFIG` environment variable to point elsewhere. On Windows the CLI lookup honours `PATHEXT`.
145
+
146
+ **Claude Desktop only reads its config at startup: fully quit and restart it afterwards** (closing the window is not enough).
147
+
148
+ ---
149
+
123
150
  ## Try it: example Claude prompts
124
151
 
125
152
  **Backup a build artifact**
@@ -202,6 +229,28 @@ proton-drive-cli restore /my-files/old-draft.pdf # restore from trash
202
229
  proton-drive-cli trash empty --confirm # permanently delete all trashed items
203
230
  ```
204
231
 
232
+ ### Search, analytics and cleanup
233
+ `drive_search` · `drive_tree` · `drive_usage` · `drive_find_duplicates` · `drive_sharing_audit` · `drive_sync_plan` · `drive_bulk_move` · `drive_bulk_trash`
234
+
235
+ ```bash
236
+ proton-drive-cli tree /my-files --depth 2
237
+ proton-drive-cli search /my-files --ext pdf --min-size 1000000
238
+ proton-drive-cli usage /my-files --top 20 --older-than 365
239
+ proton-drive-cli duplicates /my-files --min-size 1000000 [--verify]
240
+ proton-drive-cli share audit /my-files
241
+ proton-drive-cli sync-plan ./local /my-files/backup --direction up
242
+ proton-drive-cli bulk-move /my-files/Archive /my-files/a.pdf /my-files/b.pdf # plan only
243
+ proton-drive-cli bulk-trash /my-files/old1.txt /my-files/old2.txt --confirm # apply
244
+ ```
245
+
246
+ - The first walk of a large drive can take minutes (measured ~25 s per 1,200 files at concurrency 8). Later calls reuse a 5-minute cache; `refresh` bypasses it.
247
+ - Walks skip `.git` and `node_modules` by default.
248
+ - Read-only tools (`drive_search`, `drive_tree`, `drive_usage`, `drive_find_duplicates`, `drive_sharing_audit`, `drive_sync_plan`) change nothing. `drive_bulk_move` and `drive_bulk_trash` are two-step: call without `confirmed` for the plan, then with `confirmed: true` to apply. Bulk trash is reversible with `drive_restore`.
249
+ - Duplicate detection uses the sha1 the uploader claimed, which is unverified. `verify: true` downloads the candidates and hashes them.
250
+ - The sharing audit flags an invitee as external when the address is outside Proton domains, so Proton users on a custom domain are over-flagged.
251
+ - Sizes are sums of file sizes, not your account quota.
252
+ - `drive_usage` trash stats always make one uncached `/trash` listing (~2.5 s), even when the walk is cached.
253
+
205
254
  ### Photos
206
255
 
207
256
  ```bash
@@ -233,13 +282,19 @@ proton-drive-cli share status /my-files/Projects
233
282
 
234
283
  ---
235
284
 
285
+ ## Prompts
286
+
287
+ The server offers four MCP prompts (also declared in the MCPB manifest): `organise-folder` (`path`), `find-files` (`description`), `storage-audit` and `sharing-audit`. In Claude Code they are listed as `/servername:promptname (MCP)`, and `/mcp__servername__promptname` also runs them. Other clients show them as prompts. A prompt is hidden when a tool it needs is outside the active tool tier: with `PROTON_DRIVE_TOOL_TIER=core` only `find-files` is offered.
288
+
289
+ ---
290
+
236
291
  ## Tool surface
237
292
 
238
293
  ### Auth
239
294
  `drive_auth_status` · `drive_auth_logout` · `drive_version`
240
295
 
241
296
  ### Filesystem
242
- `drive_list` · `drive_info` · `drive_mkdir` · `drive_upload` · `drive_download` · `drive_rename` · `drive_move` · `drive_delete`
297
+ `drive_list` · `drive_info` · `drive_tree` · `drive_search` · `drive_mkdir` · `drive_upload` · `drive_download` · `drive_rename` · `drive_move` · `drive_delete`
243
298
 
244
299
  ### Sharing
245
300
  `drive_share_status` · `drive_share_invite` · `drive_share_revoke` · `drive_share_remove_all` · `drive_share_set_url` · `drive_share_remove_url`
@@ -253,6 +308,9 @@ proton-drive-cli share status /my-files/Projects
253
308
  ### Copy
254
309
  `drive_copy`
255
310
 
311
+ ### Plan & bulk
312
+ `drive_sync_plan` · `drive_bulk_move` · `drive_bulk_trash`
313
+
256
314
  ### Invitations
257
315
  `drive_list_invitations` · `drive_invitation_accept` · `drive_invitation_reject` · `drive_share_leave`
258
316
 
@@ -270,6 +328,8 @@ proton-drive-cli share status /my-files/Projects
270
328
  | `drive_version` | CLI and SDK version info | — |
271
329
  | `drive_list` | List files and folders at a path (paginated, default 200; `/` lists the roots) | `path`, `limit?`, `offset?` |
272
330
  | `drive_info` | Get metadata for one file/folder, including revision details (noise trimmed) | `path`, `verbose?` (raw CLI node) |
331
+ | `drive_tree` | Folder overview: per-folder file counts and size totals (largest first), depth-limited, cached 5 min | `path?`, `depth?` (default 2, max 10), `limit?`, `foldersOnly?`, `refresh?` |
332
+ | `drive_search` | Find files/folders under a path in one cached walk: name (substring/glob; a glob containing `/` matches the path relative to `path`), type, extension, MIME prefix, size, modified date; sortable, paginated. Reports `walk.complete` — false means the walk was cut short and matches may be missing | `query?`, `glob?`, `path?`, `type?`, `mediaType?`, `extensions?`, `minSize?`, `maxSize?`, `modifiedAfter?`, `modifiedBefore?`, `sort?`, `limit?` (default 50, max 500), `offset?`, `refresh?` |
273
333
  | `drive_mkdir` | Create a new empty folder | `path` |
274
334
  | `drive_upload` | Upload local file or folder | `localPath`, `remotePath`, `fileConflictStrategy?` (skip/create-new-revision/rename/replace), `folderConflictStrategy?` (skip/merge/rename/replace), `confirmed?` (**required for `replace`** — it trashes the existing remote item) |
275
335
  | `drive_download` | Download to local path | `remotePath`, `localPath`, `fileConflictStrategy?` (skip/rename/remove), `folderConflictStrategy?` (skip/merge/rename/remove), `confirmed?` (**required for `remove`** — it deletes the existing local item) |
@@ -279,12 +339,18 @@ proton-drive-cli share status /my-files/Projects
279
339
  | `drive_delete` | Permanently delete an item already in trash ⚠️ | `path`, `confirmed: true` |
280
340
  | `drive_list_trash` | List items currently in trash (paginated, default 100; includes `uid` — names are not unique in trash) | `limit?`, `offset?` |
281
341
  | `drive_share_status` | Get sharing members and URL | `path` |
342
+ | `drive_usage` | Storage analytics for a subtree: totals, largest files/folders, extension and media-type breakdown, old files, trash stats (sum of file sizes, not the account quota) | `path?`, `top?`, `olderThanDays?`, `refresh?` |
343
+ | `drive_find_duplicates` | Likely duplicate groups (claimed sha1 / same size; `verify` downloads and sha256s candidates), wasted bytes, suggested keeper — never deletes | `path?`, `minSize?`, `verify?`, `maxVerifyBytes?`, `maxVerifyTotalBytes?` (default 500 MB, max 2 GB), `limit?`, `refresh?` |
344
+ | `drive_sharing_audit` | Public links (no URL), invitees, pending invitations and risk flags for shared items (max 100) | `path?`, `refresh?` |
282
345
  | `drive_share_invite` | Invite a user (sends an email) ⚠️ | `path`, `email`, `role` (viewer/editor/admin), `message?`, `confirmed: true` |
283
346
  | `drive_share_revoke` | Revoke one person's access (fails if not a member) ⚠️ | `path`, `email`, `confirmed: true` |
284
347
  | `drive_share_remove_all` | Remove every member + pending invitation at once ⚠️ | `path`, `confirmed: true` |
285
348
  | `drive_share_set_url` | Create/replace a public share link ⚠️ (re-running without `password`/`expiration` removes them; expiry max ~90 days) | `path`, `role?` (viewer/editor), `password?`, `expiration?`, `confirmed: true` |
286
349
  | `drive_share_remove_url` | Remove the public share link ⚠️ | `path`, `confirmed: true` |
287
350
  | `drive_trash` | Move to trash | `path` |
351
+ | `drive_sync_plan` | Read-only diff of a local folder vs a Drive folder (nothing transferred) | `localPath`, `drivePath`, `direction?` (up/down/both), `ignore?`, `compare?` (size-mtime/sha1), `limit?` |
352
+ | `drive_bulk_move` | Move up to 200 items into an existing folder; without `confirmed` returns the plan and problems only | `sources`, `destinationFolder`, `confirmed?` (**required to apply**) |
353
+ | `drive_bulk_trash` | Trash up to 200 items (recoverable); without `confirmed` returns the plan and problems only | `paths`, `confirmed?` (**required to apply**) |
288
354
  | `drive_restore` | Restore from trash | `path` |
289
355
  | `drive_empty_trash` | Permanently delete all trash ⚠️ | `confirmed: true` |
290
356
  | `drive_read_file` | Read text file from local sync folder | `path` |
@@ -329,10 +395,20 @@ proton-drive-cli share status /my-files/Projects
329
395
  - Error messages come from the CLI's own output; the command line (and therefore any `--password`) is never echoed back.
330
396
  - `drive_move` accepts a full destination path (parent + new name) for a familiar interface, but the underlying CLI only has separate `move` (change parent) and `rename` (change name) commands — this MCP translates automatically, issuing one or both as needed.
331
397
  - `drive_delete` only works on items already in `/trash` or `/photos-trash` — the CLI rejects live paths. Trash an item first with `drive_trash`, or use `drive_empty_trash` to clear everything at once.
398
+ - If the client declares MCP elicitation (form mode), a refused `confirmed` gate is put to the human instead, and `drive_bulk_move` / `drive_bulk_trash` ask before applying a `confirmed: true` call. Only an explicit approval runs the call; decline, cancel, timeout or error keep the refusal, and after 5 declined prompts in a minute the server stops prompting. This only upgrades refusals and bulk applies: a compromised model that supplies `confirmed: true` on any other tool is **not** stopped by it. Clients without elicitation behave as before. Confirmation prompts need a client that supports elicitation (Claude Code CLI today; Claude Desktop is unverified).
399
+ - Walk-based tools (`drive_tree`, `drive_search`, `drive_usage`, `drive_find_duplicates`) read from a cache up to 5 minutes old; changes made by other clients or processes are not seen until it expires or you pass `refresh: true`. The first walk of a large drive can take minutes (measured about 25 s per 1200 files at concurrency 8).
332
400
  - `drive_auth_status` has no native CLI equivalent — it probes by resolving `/my-files` and reports authenticated based on whether that succeeds.
333
401
  - Paths are always Drive-absolute: `/my-files/folder/file.pdf`. Relative paths are not supported.
334
402
  - All calls include `--json` automatically, except `drive_version`, whose underlying CLI command ignores `--json` and always prints plain text — this MCP parses it directly.
335
403
 
404
+ ### Token cost
405
+
406
+ `tools/list` is sent to the model in every session. Measured payload (JSON bytes): `full` 39.0 KB (46 tools), `core` 16.8 KB (18 tools). Set `PROTON_DRIVE_TOOL_TIER=core` to load only:
407
+
408
+ `drive_auth_status`, `drive_version`, `drive_list`, `drive_info`, `drive_list_trash`, `drive_search`, `drive_tree`, `drive_mkdir`, `drive_upload`, `drive_download`, `drive_rename`, `drive_move`, `drive_copy`, `drive_trash`, `drive_restore`, `drive_share_status`, `photos_list_timeline`, `photos_download`.
409
+
410
+ Left out of `core` (use `full`): permanent deletion (`drive_delete`, `drive_empty_trash`), `drive_auth_logout`, public links, invitations and invites, album management, `photos_upload` and the sync-file tools. A call to a hidden tool returns an error asking for `PROTON_DRIVE_TOOL_TIER=full`; no CLI command runs.
411
+
336
412
  ## Known limitations
337
413
 
338
414
  These come from the upstream `proton-drive` CLI (v0.8.0), not from this server:
@@ -347,7 +423,11 @@ These come from the upstream `proton-drive` CLI (v0.8.0), not from this server:
347
423
  - `/albums/...` paths cannot be downloaded — download a photo via `/photos/<name>`.
348
424
  - A name ending in a backslash (e.g. `tail\`, created by another client) cannot be used as a parent in a path: the CLI reads `tail\/child` as an escaped `/`, and has no escape for a literal backslash (`\\` does not work either). The folder itself is reachable; its children are not reachable by path.
349
425
  - Trashed items are addressed by name only. When two items in `/trash` or `/photos-trash` share a name, `drive_restore` and `drive_delete` refuse (listing the uids) instead of acting on an arbitrary one — restore or delete that item in the Proton Drive web or desktop app.
350
- - When several programs use the CLI at the same moment (e.g. Claude Desktop and Claude Code), its local cache can briefly report `database is locked`. Read-only calls are retried automatically; writes are not (a retry could repeat the change), so just run the write again.
426
+ - When several programs use the CLI at the same moment (e.g. Claude Desktop and Claude Code), its local cache can briefly report `database is locked`. Read-only calls are retried automatically; writes are not (a retry could repeat the change), so just run the write again. The same read-only retry (up to two more attempts, short jittered backoff, honoring a `Retry-After` of at most 5 s and the call's overall timeout) also covers rate limiting (HTTP 429), single-request timeouts (`Request timed out`) and transient network resets; auth and not-found errors are never retried.
427
+
428
+ ## Platform support
429
+
430
+ Developed and live-tested on **macOS**. CI runs the test suite on Ubuntu and macOS (Node 22 and 24). **Windows is not supported yet**: the test suite has never passed there and CI does not run it, and the Windows paths in `doctor` / `setup-claude-desktop` (`%APPDATA%`, `PATHEXT` lookup) are untested. The system-PATH warning in `doctor` only knows POSIX directories. Linux is covered by CI with the fake CLI but has not been live-tested against a real Proton account.
351
431
 
352
432
  ## Testing status
353
433
 
@@ -360,9 +440,13 @@ Every tool group was live-tested on 2026-09-28 against a real Proton account, **
360
440
  | Variable | Required | Description |
361
441
  |---|---|---|
362
442
  | `PROTON_DRIVE_SYNC_PATH` | Optional | Absolute path to your local Proton Drive sync folder root (e.g. `/Users/you/Proton Drive`). Required only for `drive_read_file` and `drive_write_file`. The Proton Drive desktop app must be running to sync written files to the cloud. |
363
- | `PROTON_DRIVE_BIN` | Optional | Override the `proton-drive` binary name or path (default: `proton-drive`). Useful for non-standard installations. |
443
+ | `PROTON_DRIVE_BIN` | Optional | Override the `proton-drive` binary name or path (default: `proton-drive`; an empty value counts as unset). Useful for non-standard installations. |
364
444
  | `PROTON_DRIVE_LOCAL_ROOT` | Optional | Path-delimiter-separated list of local directories that upload/download/photos tools may touch. Unset = any path except the built-in credential denylist. |
365
445
  | `PROTON_DRIVE_ALLOW_SENSITIVE_PATHS` | Optional | Set to `1` to disable the built-in credential-location denylist (not recommended). |
446
+ | `CLAUDE_DESKTOP_CONFIG` | Optional | Path of the Claude Desktop config that `doctor` and `setup-claude-desktop` read/write instead of the per-OS default. |
447
+ | `PROTON_DRIVE_RETRY_BASE_MS` | Optional | Test hook: base backoff in ms for retrying read-only calls (default 250). |
448
+ | `PROTON_DRIVE_WALK_TTL_MS` | Optional | How long `drive_tree`/`drive_search` reuse a cached folder walk, in ms (default 300000; 0 disables the cache). Results can be up to this old, and changes made by other clients or processes are not seen until it expires or `refresh: true`. The first walk of a large drive can take minutes. The cache is dropped for any path this server writes to. |
449
+ | `PROTON_DRIVE_TOOL_TIER` | Optional | `full` (default, all 46 tools) or `core` (18 everyday tools; see [Token cost](#token-cost)). Tools outside the active tier are hidden from `tools/list` and refused at call time. Read once at startup; an unknown value falls back to `full` with a warning on stderr. |
366
450
 
367
451
  ---
368
452
 
@@ -378,7 +462,7 @@ Download from [proton.me/download/drive/cli](https://proton.me/download/drive/cl
378
462
  Run `proton-drive auth login` in your terminal. Auth state is stored in your OS keychain and persists across sessions.
379
463
 
380
464
  **Claude can't see the connector**
381
- Restart Claude Desktop fully after changing the MCP config. Check **`+` → Connectors → proton-drive**. The Proton Drive CLI must be in the `PATH` that Claude Desktop inherits (on macOS this may differ from your shell PATH — use the full binary path in config if needed).
465
+ Restart Claude Desktop fully after changing the MCP config. Check **`+` → Connectors → proton-drive-mcp**. The Proton Drive CLI must be in the `PATH` that Claude Desktop inherits (on macOS this may differ from your shell PATH — use the full binary path in config if needed).
382
466
 
383
467
  **Upload fails on image files**
384
468
  The CLI generates WebP thumbnails by default using Bun's image API. If Bun isn't installed or doesn't support thumbnails on your platform, the MCP passes `--skip-thumbnails` to bypass this. No action needed.
@@ -392,7 +476,7 @@ Or in Claude Desktop config:
392
476
  ```json
393
477
  {
394
478
  "mcpServers": {
395
- "proton-drive": {
479
+ "proton-drive-mcp": {
396
480
  "command": "npx",
397
481
  "args": ["-y", "proton-drive-mcp"],
398
482
  "env": { "PROTON_DRIVE_BIN": "/usr/local/bin/proton-drive" }
@@ -406,7 +490,7 @@ Use the full path to the `proton-drive.exe` binary in your Claude Desktop config
406
490
  ```json
407
491
  {
408
492
  "mcpServers": {
409
- "proton-drive": {
493
+ "proton-drive-mcp": {
410
494
  "command": "C:\\path\\to\\proton-drive-mcp.cmd"
411
495
  }
412
496
  }
package/dist/cli.js CHANGED
@@ -9,7 +9,14 @@
9
9
  import { DriveService, } from "./services/drive.js";
10
10
  import { DriveCliError, DriveCliNotFoundError, DriveNotAuthenticatedError, DriveParseError } from "./utils/errors.js";
11
11
  import { checkCliAvailable } from "./utils/subprocess.js";
12
- import { validateRemotePath, validateLocalPath, validateEmail, validateMessage, validateName, validateFlagValue } from "./utils/validation.js";
12
+ import { runDoctor, formatDoctor } from "./utils/doctor.js";
13
+ import { buildEntry, defaultConfigPath, isDirectory, resolveDriveCli, writeEntry, SERVER_KEY } from "./utils/claudeConfig.js";
14
+ import { syncPlan, planBulkMove, planBulkTrash, loadListing, foldersToList } from "./services/plan.js";
15
+ import { invalidatePath } from "./services/walk.js";
16
+ import { resolve } from "node:path";
17
+ import { driveSearch, driveTree } from "./services/find.js";
18
+ import { driveUsage, driveFindDuplicates, driveSharingAudit } from "./services/analytics.js";
19
+ import { validateRemotePath, validateRemotePathList, validateLocalPath, validateEmail, validateMessage, validateName, validateFlagValue } from "./utils/validation.js";
13
20
  const drive = new DriveService();
14
21
  const args = process.argv.slice(2);
15
22
  const jsonMode = args.includes("--json");
@@ -23,6 +30,12 @@ Commands:
23
30
  version Show CLI/SDK version
24
31
  list <path> List files at path
25
32
  info <path> [--verbose] Show node metadata (--verbose = raw CLI node)
33
+ tree [path] [--depth N] [--limit N] [--folders-only] [--refresh]
34
+ Folder overview with per-folder counts and sizes (default /my-files, depth 2)
35
+ search [path] [--query S] [--glob G] [--type file|folder] [--media-type P] [--ext pdf,docx]
36
+ [--min-size B] [--max-size B] [--after DATE] [--before DATE] [--sort name|size|mtime]
37
+ [--limit N] [--offset N] [--refresh]
38
+ Find files/folders under path (default /my-files) in one walk
26
39
  mkdir <path> Create a new folder
27
40
  upload <local> <remote> [--file-conflict X] [--folder-conflict X] [--confirm]
28
41
  Upload file/folder (files: skip/create-new-revision/rename/replace; folders: skip/merge/rename/replace;
@@ -34,6 +47,7 @@ Commands:
34
47
  move <src> <dst> Move and/or rename
35
48
  delete <path>|--uid <uid> --confirm Delete a file/folder already in trash, permanently
36
49
  share status <path> Show sharing info
50
+ share audit [path] [--refresh] Audit public links, invitees and risk flags under a path (default /my-files)
37
51
  share invite <path> <email> <role> Invite user (viewer/editor/admin)
38
52
  share revoke <path> <email> Revoke one user's access
39
53
  share remove-all <path> --confirm Remove everyone's access + pending invitations
@@ -42,7 +56,15 @@ Commands:
42
56
  share leave <path> Leave a folder shared with you
43
57
  copy <src> <dst-parent-folder> [--name N] Copy file/folder into a folder (optionally under a new name)
44
58
  trash <path> Move to trash
59
+ sync-plan <local> <remote> [--direction up|down|both] [--compare size-mtime|sha1] [--ignore g1,g2] [--limit N]
60
+ Read-only diff of a local folder vs a Drive folder (nothing transferred)
61
+ bulk-move <dst-folder> <src>... [--confirm] Move many items into an existing folder; without --confirm prints the plan only
62
+ bulk-trash <path>... [--confirm] Trash many items; without --confirm prints the plan only
45
63
  trash list List trash contents
64
+ usage [path] [--top N] [--older-than DAYS] [--refresh]
65
+ Storage analytics: totals, largest files/folders, breakdowns, trash (sum of file sizes, not the quota)
66
+ duplicates [path] [--min-size B] [--verify] [--max-verify-bytes B] [--max-verify-total-bytes B] [--limit N] [--refresh]
67
+ Likely duplicate files; --verify downloads and hashes candidates. Never deletes.
46
68
  trash empty --confirm Permanently delete all trash
47
69
  restore <path>|--uid <uid> Restore from trash (uid from 'trash list')
48
70
  invitation list List pending invitations
@@ -59,6 +81,12 @@ Commands:
59
81
  photo download <photo>... <local> [--conflict X] [--confirm] Download photos (skip/rename/remove; 'remove' needs --confirm)
60
82
  photo upload <local>... [--conflict X] Upload photos to your library (skip/rename)
61
83
 
84
+ doctor [--config <path>] Read-only diagnostics: Node, proton-drive CLI, auth, sync path, Claude Desktop config
85
+ (exit 1 if any check fails)
86
+ setup-claude-desktop [--config <path>] [--sync-path <dir>] [--write]
87
+ Print the Claude Desktop entry for this server (dry run); --write adds it to the
88
+ config (timestamped backup first). Restart Claude Desktop afterwards.
89
+
62
90
  Flags:
63
91
  --json Machine-readable JSON output (one line)
64
92
  `);
@@ -77,6 +105,19 @@ function requirePath(value, usage) {
77
105
  }
78
106
  return ""; // unreachable; process.exit(1) above always terminates
79
107
  }
108
+ function pathOrRoot(value) {
109
+ return value && !value.startsWith("--") ? requirePath(value, "<command> [path]") : "/my-files";
110
+ }
111
+ function intFlag(flag) {
112
+ const v = getFlag(flag);
113
+ if (v === undefined)
114
+ return undefined;
115
+ if (!/^\d+$/.test(v)) {
116
+ console.error(`${flag} must be a non-negative integer`);
117
+ process.exit(1);
118
+ }
119
+ return Number(v);
120
+ }
80
121
  function requireEmail(value) {
81
122
  try {
82
123
  return validateEmail(value);
@@ -97,9 +138,44 @@ function getFlag(flag) {
97
138
  }
98
139
  return args[idx + 1];
99
140
  }
141
+ function numFlag(flag) {
142
+ const v = getFlag(flag);
143
+ if (v === undefined)
144
+ return undefined;
145
+ if (!/^\d+$/.test(v)) {
146
+ console.error(`${flag} must be a non-negative integer`);
147
+ process.exit(1);
148
+ }
149
+ return Number(v);
150
+ }
151
+ function numFlags(...names) {
152
+ return Object.fromEntries(names.map((n) => [n, numFlag(`--${n}`)]));
153
+ }
100
154
  function print(data) {
101
155
  console.log(jsonMode ? JSON.stringify(data) : JSON.stringify(data, null, 2));
102
156
  }
157
+ function setupClaudeDesktop() {
158
+ const configPath = resolve(getFlag("--config") ?? defaultConfigPath());
159
+ const rawSync = getFlag("--sync-path");
160
+ const cliPath = resolveDriveCli();
161
+ if (!cliPath) {
162
+ console.error("Error: proton-drive CLI not found. Install it from https://proton.me/download/drive/cli/index.html, or set PROTON_DRIVE_BIN to its absolute path (`which proton-drive`).");
163
+ process.exit(1);
164
+ }
165
+ const syncPath = rawSync !== undefined ? resolve(rawSync) : undefined;
166
+ if (syncPath && !isDirectory(syncPath)) {
167
+ console.error(`Error: --sync-path ${syncPath} is not an existing directory.`);
168
+ process.exit(1);
169
+ }
170
+ const entry = buildEntry(cliPath, syncPath);
171
+ const shown = JSON.stringify({ mcpServers: { [SERVER_KEY]: entry } }, null, 2);
172
+ if (!args.includes("--write")) {
173
+ console.log(`Dry run: nothing changed. Would set this entry in ${configPath}:\n${shown}\nRe-run with --write to apply.`);
174
+ return;
175
+ }
176
+ const backup = writeEntry(configPath, entry);
177
+ console.log(`Wrote ${SERVER_KEY} to ${configPath}${backup ? ` (backup: ${backup})` : ""}.\n${shown}\nFully quit and restart Claude Desktop for the change to take effect.`);
178
+ }
103
179
  async function run() {
104
180
  // Strip --json from positional parsing
105
181
  const positional = args.filter((a) => a !== "--json");
@@ -115,6 +191,16 @@ async function run() {
115
191
  usage();
116
192
  return;
117
193
  }
194
+ if (cmd === "doctor") {
195
+ const result = await runDoctor(getFlag("--config"));
196
+ console.log(jsonMode ? JSON.stringify(result) : formatDoctor(result));
197
+ process.exitCode = result.ok ? 0 : 1;
198
+ return;
199
+ }
200
+ if (cmd === "setup-claude-desktop") {
201
+ setupClaudeDesktop();
202
+ return;
203
+ }
118
204
  const cliCheck = await checkCliAvailable();
119
205
  if (!cliCheck.available) {
120
206
  const msg = cliCheck.reason === "not_executable"
@@ -143,6 +229,21 @@ async function run() {
143
229
  case "list":
144
230
  print(await drive.list(requirePath(sub, "list <path>")));
145
231
  break;
232
+ case "tree":
233
+ print(await driveTree(drive, { path: sub && !sub.startsWith("--") ? sub : undefined, ...numFlags("depth", "limit"), foldersOnly: args.includes("--folders-only") || undefined, refresh: args.includes("--refresh") || undefined }));
234
+ break;
235
+ case "search":
236
+ print(await driveSearch(drive, {
237
+ path: sub && !sub.startsWith("--") ? sub : undefined,
238
+ query: getFlag("--query"), glob: getFlag("--glob"),
239
+ type: getFlag("--type"), mediaType: getFlag("--media-type"),
240
+ extensions: getFlag("--ext")?.split(","),
241
+ ...numFlags("limit", "offset"),
242
+ minSize: numFlag("--min-size"), maxSize: numFlag("--max-size"),
243
+ modifiedAfter: getFlag("--after"), modifiedBefore: getFlag("--before"),
244
+ sort: getFlag("--sort"), refresh: args.includes("--refresh") || undefined,
245
+ }));
246
+ break;
146
247
  case "info":
147
248
  print(await drive.info(requirePath(sub, "info <path> [--verbose]"), args.includes("--verbose")));
148
249
  break;
@@ -154,7 +255,7 @@ async function run() {
154
255
  }
155
256
  let local;
156
257
  try {
157
- local = validateLocalPath(rawLocal);
258
+ local = validateLocalPath(rawLocal, { scan: true });
158
259
  }
159
260
  catch (e) {
160
261
  console.error(e instanceof Error ? e.message : String(e));
@@ -256,6 +357,9 @@ async function run() {
256
357
  if (sub === "status") {
257
358
  print(await drive.shareStatus(requirePath(rest[0], "share status <path>")));
258
359
  }
360
+ else if (sub === "audit") {
361
+ print(await driveSharingAudit(drive, { path: pathOrRoot(rest[0]), refresh: args.includes("--refresh") }));
362
+ }
259
363
  else if (sub === "invite") {
260
364
  const [rawPath, rawEmail, role] = rest;
261
365
  if (!rawPath || !rawEmail || !role) {
@@ -324,6 +428,25 @@ async function run() {
324
428
  process.exit(1);
325
429
  }
326
430
  break;
431
+ case "usage":
432
+ print(await driveUsage(drive, {
433
+ path: pathOrRoot(sub),
434
+ top: intFlag("--top"),
435
+ olderThanDays: intFlag("--older-than"),
436
+ refresh: args.includes("--refresh"),
437
+ }));
438
+ break;
439
+ case "duplicates":
440
+ print(await driveFindDuplicates(drive, {
441
+ path: pathOrRoot(sub),
442
+ minSize: intFlag("--min-size"),
443
+ verify: args.includes("--verify"),
444
+ maxVerifyBytes: intFlag("--max-verify-bytes"),
445
+ maxVerifyTotalBytes: intFlag("--max-verify-total-bytes"),
446
+ limit: intFlag("--limit"),
447
+ refresh: args.includes("--refresh"),
448
+ }));
449
+ break;
327
450
  case "trash":
328
451
  if (sub === "empty") {
329
452
  if (!args.includes("--confirm")) {
@@ -345,6 +468,66 @@ async function run() {
345
468
  process.exit(1);
346
469
  }
347
470
  break;
471
+ case "sync-plan": {
472
+ const direction = getFlag("--direction"), compare = getFlag("--compare"), ignore = getFlag("--ignore"), limit = getFlag("--limit");
473
+ if (direction !== undefined && !["up", "down", "both"].includes(direction)) {
474
+ console.error("--direction must be up, down or both");
475
+ process.exit(1);
476
+ }
477
+ if (compare !== undefined && !["size-mtime", "sha1"].includes(compare)) {
478
+ console.error("--compare must be size-mtime or sha1");
479
+ process.exit(1);
480
+ }
481
+ if (!sub || !rest[0]) {
482
+ console.error("Usage: sync-plan <local> <remote> [--direction X] [--compare X] [--ignore g1,g2] [--limit N]");
483
+ process.exit(1);
484
+ }
485
+ print(await syncPlan(drive, {
486
+ localPath: sub, drivePath: rest[0], direction: direction, compare: compare,
487
+ ignore: ignore?.split(","), limit: limit !== undefined ? Number(limit) : undefined,
488
+ }));
489
+ break;
490
+ }
491
+ case "bulk-move": {
492
+ const dst = requirePath(sub, "bulk-move <dst-folder> <src>... [--confirm]");
493
+ const srcs = validateRemotePathList(rest.filter((r) => !r.startsWith("--")), "sources");
494
+ const plan = planBulkMove(srcs, dst, await loadListing(drive, foldersToList(srcs, dst)));
495
+ if (!args.includes("--confirm") || plan.problems.length) {
496
+ print({ applied: false, ...plan });
497
+ if (plan.problems.length)
498
+ process.exit(1);
499
+ console.error("Plan only. Pass --confirm to apply.");
500
+ break;
501
+ }
502
+ try {
503
+ await drive.bulkMove(srcs, dst);
504
+ }
505
+ finally {
506
+ invalidatePath(dst);
507
+ srcs.forEach(invalidatePath);
508
+ }
509
+ console.log(`Moved ${srcs.length} item(s) to ${dst}.`);
510
+ break;
511
+ }
512
+ case "bulk-trash": {
513
+ const paths = validateRemotePathList([sub, ...rest].filter((r) => r !== undefined && !r.startsWith("--")), "paths");
514
+ const plan = planBulkTrash(paths, await loadListing(drive, foldersToList(paths)));
515
+ if (!args.includes("--confirm") || plan.problems.length) {
516
+ print({ applied: false, ...plan });
517
+ if (plan.problems.length)
518
+ process.exit(1);
519
+ console.error("Plan only. Pass --confirm to apply.");
520
+ break;
521
+ }
522
+ try {
523
+ await drive.bulkTrash(paths);
524
+ }
525
+ finally {
526
+ paths.forEach(invalidatePath);
527
+ }
528
+ console.log(`Moved ${paths.length} item(s) to trash.`);
529
+ break;
530
+ }
348
531
  case "restore": {
349
532
  const restoreUid = getFlag("--uid");
350
533
  const restorePath = restoreUid && (!sub || sub.startsWith("--")) ? undefined : requirePath(sub, "restore <path>|--uid <uid>");
@@ -536,7 +719,7 @@ async function run() {
536
719
  const localPaths = [];
537
720
  for (const p of positionals) {
538
721
  try {
539
- localPaths.push(validateLocalPath(p));
722
+ localPaths.push(validateLocalPath(p, { scan: true }));
540
723
  }
541
724
  catch (e) {
542
725
  console.error(e instanceof Error ? e.message : String(e));