proton-drive-mcp 1.0.30 → 1.0.32

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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.32 — 2026-08-21
4
+
5
+ Closed the last disclosed gap from v1.0.31 — tested `drive_share_invite` for real, against the user's own Gmail address, through the actual MCP JSON-RPC server. Found a real bug in the process.
6
+
7
+ ### Fixed
8
+ - **`drive_share_status` only read the `members` field, ignoring `protonInvitations` and `nonProtonInvitations` entirely.** Confirmed live: inviting a non-Proton address (a Gmail account) is filed by the CLI under `nonProtonInvitations`, not `members` — so a real, successfully-sent invite was completely invisible from this tool. `isShared` correctly reported `true`, but `members` came back empty, giving zero visibility into who the item was actually shared with. `members` now merges accepted members with both pending-invitation sources, each entry tagged `status: "accepted" | "pending"` so callers can still tell them apart. `drive_share_revoke` was separately confirmed to correctly cancel a pending invitation, not just remove accepted access, so no fix was needed there.
9
+
10
+ ### Verified live (closing v1.0.31's disclosed gaps)
11
+ - `drive_share_invite` — sent a real invitation via the actual MCP server (not just the CLI wrapper), to a real external email.
12
+ - `drive_share_revoke` — confirmed it cancels a pending (not-yet-accepted) invitation as well as accepted access.
13
+
14
+ ### Still not independently verified
15
+ - `drive_list_invitations`'s field mapping — still no real pending invitation exists from the *invitee's* side to test against (this release only tested from the inviter's side).
16
+ - `invitation_accept`/`invitation_reject`, `share_leave` — require a second real account to be on the receiving end.
17
+ - Sync-folder tools, `drive_empty_trash` — same reasons as v1.0.31.
18
+
19
+ ## 1.0.31 — 2026-08-21
20
+
21
+ A second, more skeptical pass over v1.0.30 — re-verified everything live rather than trusting the previous session's own reasoning, and tested the actual MCP JSON-RPC protocol layer directly (not just the `DriveService` class underneath it), plus recursive multi-file transfers.
22
+
23
+ ### Fixed
24
+ - **`drive_download`'s tool description was misleading.** `localPath` is a destination *folder* — auto-created if missing, with the downloaded item placed inside it under its original remote name — not the file's exact final path. The old description said "must already exist," which is also simply false (confirmed live: the CLI creates it). An agent following the old description could reasonably expect a file at the literal `localPath` given and be confused to find a directory there instead.
25
+
26
+ ### Verified live, no bug found (ruling out a false alarm)
27
+ - **`photos_upload`'s "non-photo files are silently skipped" claim is accurate.** An initial test using a `.jpg`-named text file *failed* rather than skipped — looked like a bug at first. Root cause: media-type detection is extension-based, so the mislabeled file was sniffed as `image/jpeg`, passed the photo-type check, and correctly failed at the actual upload stage since the content wasn't a valid image. A genuinely non-photo file (`.txt`) correctly produces `skippedItems: 1`, exactly as documented.
28
+ - **The real MCP server (JSON-RPC over stdio), not just the CLI wrapper.** Sent actual `tools/call` protocol messages to the compiled server for `drive_list` and `drive_share_status` — both correct, matching the CLI-wrapper output exactly. (One `drive_list` call failed transiently on a rapid-fire multi-request batch; immediately reproducible as a one-off, not a code bug — retried clean.)
29
+ - **Recursive folder upload/download** (nested subfolder, 3 files) — round-tripped correctly, structure preserved, transfer counts accurate.
30
+
31
+ ### Still not independently verified (disclosed, not swept under the rug)
32
+ - `drive_list_invitations`'s field mapping — no pending invitation existed to test against; the fix is based on the SDK's TypeScript type definition, not an empirically observed response.
33
+ - Any sharing operation requiring a second real account (`share_invite`, `share_revoke` against a real member, `invitation_accept`/`reject`, `share_leave`) — correctly not exercised against a real third party without their consent.
34
+ - `drive_read_file`/`drive_write_file` (the `PROTON_DRIVE_SYNC_PATH`-gated tools) — not configured in this environment.
35
+ - `drive_empty_trash` — deliberately not executed for real; the test account's actual Trash held real, unrelated user data that a live run would have destroyed.
36
+
3
37
  ## 1.0.30 — 2026-08-21
4
38
 
5
39
  **Root cause of this release:** every prior "verification" of this package (v1.0.24 through v1.0.29) diffed the CLI's *command source* against GitHub tags — confirming the right argv gets built — but never actually ran the CLI and inspected its *response* JSON. This release does that, live, against a real authenticated account. It found that several response-shape assumptions were wrong from day one, in ways no amount of source-reading would have caught, because the exact wire format (Result-wrapped names, nested fields, a CLI-side `undefined`-text quirk) isn't visible from the command classes — it only shows up in the actual output.
package/dist/index.js CHANGED
@@ -154,10 +154,11 @@ const TOOLS = [
154
154
  {
155
155
  name: "drive_download",
156
156
  description: "Download a file or folder from Proton Drive to the local filesystem. Requires authentication. " +
157
+ "localPath is a destination FOLDER, not the file's exact final path — the CLI creates it automatically if missing and places the downloaded item inside it under its original remote name. " +
158
+ "E.g. downloading /my-files/report.pdf with localPath '/tmp/out' produces /tmp/out/report.pdf, not /tmp/out itself as a file — confirmed live against the real CLI (v0.8.0). " +
157
159
  "For folders, downloads recursively. " +
158
160
  "Conflict strategies are set separately for files and folders (CLI v0.8.0+) — both default to 'skip'. " +
159
- "Fails if the local parent directory does not exist. " +
160
- "Returns {downloaded} count. " +
161
+ "Returns {downloaded} count — not the actual local path; construct it as localPath + the remote item's basename if you need it. " +
161
162
  "Do not use to move files within Drive (use drive_move) or to read a small text file's contents (use drive_read_file if PROTON_DRIVE_SYNC_PATH is set).",
162
163
  annotations: { openWorldHint: true },
163
164
  inputSchema: {
@@ -169,7 +170,7 @@ const TOOLS = [
169
170
  },
170
171
  localPath: {
171
172
  type: "string",
172
- description: "Absolute local destination path (must start with '/'). Parent directory must already exist.",
173
+ description: "Absolute local DESTINATION FOLDER (must start with '/'), not the file's final path. Created automatically if it doesn't exist. The downloaded item is placed inside it, keeping its original remote name.",
173
174
  },
174
175
  fileConflictStrategy: {
175
176
  type: "string",
@@ -283,8 +284,9 @@ const TOOLS = [
283
284
  {
284
285
  name: "drive_share_status",
285
286
  description: "Return the current sharing state of a Proton Drive path. Requires authentication. " +
286
- "Returns {isShared: boolean, members: [{email, role, addedAt?}], shareUrl?}. " +
287
- "Always call this before drive_share_invite (to avoid duplicate invitations) and before drive_share_revoke (to confirm the member email). " +
287
+ "Returns {isShared: boolean, members: [{email, role, addedAt?, status: 'accepted'|'pending'}], shareUrl?}. " +
288
+ "members includes both accepted access and pending invitations that haven't been accepted yet (including invites sent to non-Proton addresses, e.g. Gmail) — check the status field to tell them apart. " +
289
+ "Always call this before drive_share_invite (to avoid duplicate invitations) and before drive_share_revoke (to confirm the member email — revoke also cancels pending invitations, not just accepted access). " +
288
290
  "Do not call this to modify sharing — it is read-only.",
289
291
  annotations: { readOnlyHint: true, idempotentHint: true },
290
292
  inputSchema: {
@@ -188,23 +188,30 @@ export class DriveService {
188
188
  // isShared/email/addedAt/shareUrl field; those were all wrong names.
189
189
  // When nothing is shared, the CLI prints literal "undefined" (see
190
190
  // subprocess.ts) which now resolves to `result === null` here.
191
+ //
192
+ // `members` merges all three sources (accepted members + both invitation
193
+ // kinds), each tagged accepted/pending. Confirmed live: inviting a
194
+ // non-Proton email (e.g. a Gmail address) files it under
195
+ // nonProtonInvitations, not members — reading only `members` made a real,
196
+ // successfully-sent invite completely invisible from this tool.
191
197
  async shareStatus(remotePath) {
192
198
  const result = await this.run(["sharing", "status", remotePath]);
193
199
  const r = (result ?? {});
194
200
  const VALID_ROLES = new Set(["viewer", "editor", "admin"]);
195
- const members = Array.isArray(r.members)
196
- ? r.members.map((m) => ({
197
- email: String(m.inviteeEmail ?? ""),
198
- role: (VALID_ROLES.has(String(m.role)) ? String(m.role) : "viewer"),
199
- addedAt: typeof m.invitationTime === "string" ? m.invitationTime : undefined,
200
- }))
201
- : [];
202
- const protonInvitationCount = Array.isArray(r.protonInvitations) ? r.protonInvitations.length : 0;
203
- const nonProtonInvitationCount = Array.isArray(r.nonProtonInvitations) ? r.nonProtonInvitations.length : 0;
201
+ const toShareMember = (m, status) => ({
202
+ email: String(m.inviteeEmail ?? ""),
203
+ role: (VALID_ROLES.has(String(m.role)) ? String(m.role) : "viewer"),
204
+ addedAt: typeof m.invitationTime === "string" ? m.invitationTime : undefined,
205
+ status,
206
+ });
207
+ const accepted = Array.isArray(r.members) ? r.members.map((m) => toShareMember(m, "accepted")) : [];
208
+ const protonPending = Array.isArray(r.protonInvitations) ? r.protonInvitations.map((m) => toShareMember(m, "pending")) : [];
209
+ const nonProtonPending = Array.isArray(r.nonProtonInvitations) ? r.nonProtonInvitations.map((m) => toShareMember(m, "pending")) : [];
210
+ const members = [...accepted, ...protonPending, ...nonProtonPending];
204
211
  const urlAccess = (r.urlAccess ?? undefined);
205
212
  return {
206
213
  path: remotePath,
207
- isShared: members.length > 0 || protonInvitationCount > 0 || nonProtonInvitationCount > 0 || !!urlAccess,
214
+ isShared: members.length > 0 || !!urlAccess,
208
215
  members,
209
216
  shareUrl: typeof urlAccess?.url === "string" ? urlAccess.url : undefined,
210
217
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proton-drive-mcp",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "MCP server and CLI that gives Claude full access to Proton Drive — upload, download, share, and manage your end-to-end encrypted files without leaving the conversation.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",