proton-drive-mcp 1.0.24 → 1.0.26

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.25 — 2026-08-04
4
+
5
+ ### Added
6
+ - **`photos_list_albums`** — list all Proton Photos albums (`album list`); returns name, photoCount, isShared, creationTime
7
+ - **`photos_create_album`** — create a new empty album by name (`album create <name>`)
8
+ - **`photos_delete_album`** — delete an album with optional `--force` (delete even if non-empty) and `--save` (preserve photos in timeline) flags; requires `confirmed: true`
9
+ - **`photos_list_album_photos`** — list photos in an album by path; returns nodeUids (`album photos <albumPath>`)
10
+ - **`photos_add_to_album`** — add a photo from your library to an album (`album add-photo <albumPath> <photoPath>`)
11
+ - **`photos_remove_from_album`** — remove a photo from an album without deleting it from your library (`album remove-photo <albumPath> <photoPath>`)
12
+ - All 6 tools also available as CLI subcommands: `album list/create/delete/photos/add-photo/remove-photo`
13
+ - Album paths use `/albums/<name>` prefix; photo paths use `/photos/<filename>` prefix
14
+
3
15
  ## 1.0.24 — 2026-08-04
4
16
 
5
17
  ### Added
package/README.md CHANGED
@@ -24,12 +24,13 @@
24
24
 
25
25
  ---
26
26
 
27
- Give Claude Desktop (or any MCP client) full access to your Proton Drive: list folders, upload and download files, invite collaborators, manage sharing, and handle trash — all with end-to-end encryption intact. The same capabilities are available as a full CLI for scripting, backups, and cron.
27
+ Give Claude Desktop (or any MCP client) full access to your Proton Drive and Proton Photos: list folders, upload and download files, invite collaborators, manage sharing, handle trash, and manage photo albums — all with end-to-end encryption intact. The same capabilities are available as a full CLI for scripting, backups, and cron.
28
28
 
29
29
  ## What you get
30
30
 
31
31
  - **Claude manages your Proton Drive** — list, upload, download, move, share, trash, restore
32
- - **Full CLI** — same 23 operations, scriptable and pipeable, works in cron and shell scripts
32
+ - **Proton Photos album management** — list albums, create/delete albums, add and remove photos
33
+ - **Full CLI** — same 29 operations, scriptable and pipeable, works in cron and shell scripts
33
34
  - **Zero credential exposure** — auth is handled entirely by the official Proton Drive CLI; this MCP never touches your password or session token
34
35
  - **Shell injection safe** — all CLI calls use `execFile` with discrete argument arrays, never string interpolation
35
36
  - **Privacy-native** — end-to-end encryption is handled by Proton's own CLI; this server is just a thin MCP wrapper
@@ -231,6 +232,9 @@ proton-drive-cli share status /my-files/Projects
231
232
  ### Invitations
232
233
  `drive_list_invitations` · `drive_invitation_accept` · `drive_invitation_reject` · `drive_share_leave`
233
234
 
235
+ ### Photos
236
+ `photos_list_albums` · `photos_create_album` · `photos_delete_album` · `photos_list_album_photos` · `photos_add_to_album` · `photos_remove_from_album`
237
+
234
238
  ---
235
239
 
236
240
  ## Tool reference
@@ -260,6 +264,12 @@ proton-drive-cli share status /my-files/Projects
260
264
  | `drive_invitation_accept` | Accept a pending invitation | `uid` (from `drive_list_invitations`) |
261
265
  | `drive_invitation_reject` | Reject a pending invitation ⚠️ | `uid` (from `drive_list_invitations`) |
262
266
  | `drive_share_leave` | Leave a shared folder shared with you ⚠️ | `path` |
267
+ | `photos_list_albums` | List all Proton Photos albums | — |
268
+ | `photos_create_album` | Create a new empty album | `name` |
269
+ | `photos_delete_album` | Delete an album ⚠️ | `albumPath`, `confirmed: true`, `force?`, `save?` |
270
+ | `photos_list_album_photos` | List photos in an album | `albumPath` |
271
+ | `photos_add_to_album` | Add a photo from your library to an album | `albumPath`, `photoPath` |
272
+ | `photos_remove_from_album` | Remove a photo from an album (keeps it in library) ⚠️ | `albumPath`, `photoPath` |
263
273
 
264
274
  > ⚠️ **Destructive tools** require `confirmed: true`. Use `drive_list_trash` first so you know what will be deleted, then pass `confirmed: true` to proceed.
265
275
 
package/dist/cli.js CHANGED
@@ -52,6 +52,12 @@ Commands:
52
52
  trash list List trash contents
53
53
  trash empty Permanently delete all trash
54
54
  restore <path> Restore from trash
55
+ album list List all photo albums
56
+ album create <name> Create a new album
57
+ album delete <path> [--force] [--save] Delete an album
58
+ album photos <path> List photos in an album
59
+ album add-photo <album> <photo> Add a photo to an album
60
+ album remove-photo <album> <photo> Remove a photo from an album
55
61
 
56
62
  Flags:
57
63
  --json Machine-readable JSON output (one line)
@@ -304,6 +310,49 @@ async function run() {
304
310
  process.exit(1);
305
311
  }
306
312
  break;
313
+ case "album":
314
+ if (sub === "list" || sub === "ls") {
315
+ print(await drive.listAlbums());
316
+ }
317
+ else if (sub === "create") {
318
+ const albumName = rest[0];
319
+ if (!albumName) {
320
+ console.error("Usage: album create <name>");
321
+ process.exit(1);
322
+ }
323
+ await drive.createAlbum(albumName);
324
+ console.log(`Album created: ${albumName}`);
325
+ }
326
+ else if (sub === "delete") {
327
+ const albumPath = requirePath(rest[0], "album delete <path> [--force] [--save] --confirm");
328
+ if (!args.includes("--confirm")) {
329
+ console.error(`This will delete album: ${albumPath}\nPass --confirm to proceed.`);
330
+ process.exit(1);
331
+ }
332
+ await drive.deleteAlbum(albumPath, args.includes("--force"), args.includes("--save"));
333
+ console.log(`Album deleted: ${albumPath}`);
334
+ }
335
+ else if (sub === "photos") {
336
+ const albumPath = requirePath(rest[0], "album photos <path>");
337
+ print(await drive.listAlbumPhotos(albumPath));
338
+ }
339
+ else if (sub === "add-photo") {
340
+ const albumPath = requirePath(rest[0], "album add-photo <album-path> <photo-path>");
341
+ const photoPath = requirePath(rest[1], "album add-photo <album-path> <photo-path>");
342
+ await drive.addPhotoToAlbum(albumPath, photoPath);
343
+ console.log(`Added ${photoPath} to ${albumPath}`);
344
+ }
345
+ else if (sub === "remove-photo") {
346
+ const albumPath = requirePath(rest[0], "album remove-photo <album-path> <photo-path>");
347
+ const photoPath = requirePath(rest[1], "album remove-photo <album-path> <photo-path>");
348
+ await drive.removePhotoFromAlbum(albumPath, photoPath);
349
+ console.log(`Removed ${photoPath} from ${albumPath}`);
350
+ }
351
+ else {
352
+ console.error("Usage: album list | album create <name> | album delete <path> | album photos <path> | album add-photo <album> <photo> | album remove-photo <album> <photo>");
353
+ process.exit(1);
354
+ }
355
+ break;
307
356
  default:
308
357
  console.error(`Unknown command: ${cmd}`);
309
358
  usage();
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { createRequire } from "node:module";
6
6
  import { DriveService } from "./services/drive.js";
7
+ import { isMainModule } from "./utils/isMainModule.js";
7
8
  import { checkCliAvailable } from "./utils/subprocess.js";
8
9
  import { DriveCliNotFoundError, DriveCliError, DriveNotAuthenticatedError, DriveParseError, } from "./utils/errors.js";
9
10
  import { validateRemotePath, validateLocalPath, validateEmail, validateMessage } from "./utils/validation.js";
@@ -447,6 +448,130 @@ const TOOLS = [
447
448
  additionalProperties: false,
448
449
  },
449
450
  },
451
+ // Photos / Albums
452
+ {
453
+ name: "photos_list_albums",
454
+ description: "List all photo albums in Proton Photos. Requires authentication. " +
455
+ "Returns [{name, photoCount, isShared, creationTime?}]. " +
456
+ "Album paths are /albums/<name> — use the name from this list to build paths for other album tools. " +
457
+ "Do not use to list regular Drive folders — use drive_list instead.",
458
+ annotations: { readOnlyHint: true, idempotentHint: true },
459
+ inputSchema: { type: "object", properties: {}, required: [], additionalProperties: false },
460
+ },
461
+ {
462
+ name: "photos_create_album",
463
+ description: "Create a new empty photo album in Proton Photos. Requires authentication. " +
464
+ "Pass the album name (not a path) — the album is created at /albums/<name>. " +
465
+ "Fails if an album with that name already exists.",
466
+ annotations: { destructiveHint: false },
467
+ inputSchema: {
468
+ type: "object",
469
+ properties: {
470
+ name: {
471
+ type: "string",
472
+ description: "Name of the new album. E.g. 'Vacation 2024'. Must be non-empty.",
473
+ },
474
+ },
475
+ required: ["name"],
476
+ additionalProperties: false,
477
+ },
478
+ },
479
+ {
480
+ name: "photos_delete_album",
481
+ description: "Delete a Proton Photos album. Requires authentication and confirmed=true. " +
482
+ "By default refuses to delete an album that still contains photos — pass force=true to override. " +
483
+ "By default photos are removed from the album but kept in your timeline — pass save=true to explicitly preserve them in your timeline before deleting. " +
484
+ "albumPath must start with /albums/. " +
485
+ "Always show the user the album name and photo count (from photos_list_albums) before calling.",
486
+ annotations: { destructiveHint: true },
487
+ inputSchema: {
488
+ type: "object",
489
+ properties: {
490
+ albumPath: {
491
+ type: "string",
492
+ description: "Absolute path of the album to delete. Must start with /albums/. E.g. /albums/Vacation 2024",
493
+ },
494
+ confirmed: {
495
+ type: "boolean",
496
+ description: "Must be true. Confirms the user has acknowledged the deletion.",
497
+ },
498
+ force: {
499
+ type: "boolean",
500
+ description: "If true, delete even if the album still contains photos. Default false.",
501
+ },
502
+ save: {
503
+ type: "boolean",
504
+ description: "If true, save album photos to your timeline before deleting. Default false.",
505
+ },
506
+ },
507
+ required: ["albumPath", "confirmed"],
508
+ additionalProperties: false,
509
+ },
510
+ },
511
+ {
512
+ name: "photos_list_album_photos",
513
+ description: "List the photos in a Proton Photos album. Requires authentication. " +
514
+ "Returns [{nodeUid}] — photo node UIDs. " +
515
+ "albumPath must start with /albums/. " +
516
+ "To add or remove photos, use their Drive path under /photos/ (not the nodeUid).",
517
+ annotations: { readOnlyHint: true, idempotentHint: true },
518
+ inputSchema: {
519
+ type: "object",
520
+ properties: {
521
+ albumPath: {
522
+ type: "string",
523
+ description: "Absolute path of the album. Must start with /albums/. E.g. /albums/Vacation 2024",
524
+ },
525
+ },
526
+ required: ["albumPath"],
527
+ additionalProperties: false,
528
+ },
529
+ },
530
+ {
531
+ name: "photos_add_to_album",
532
+ description: "Add a photo from your Proton Photos library to an album. Requires authentication. " +
533
+ "albumPath must start with /albums/; photoPath must start with /photos/. " +
534
+ "The photo must already exist in your library — this does not upload new photos. " +
535
+ "Use photos_list_albums to find album paths.",
536
+ annotations: { destructiveHint: false },
537
+ inputSchema: {
538
+ type: "object",
539
+ properties: {
540
+ albumPath: {
541
+ type: "string",
542
+ description: "Absolute path of the album. Must start with /albums/. E.g. /albums/Vacation 2024",
543
+ },
544
+ photoPath: {
545
+ type: "string",
546
+ description: "Absolute path of the photo in your library. Must start with /photos/. E.g. /photos/IMG_001.jpg",
547
+ },
548
+ },
549
+ required: ["albumPath", "photoPath"],
550
+ additionalProperties: false,
551
+ },
552
+ },
553
+ {
554
+ name: "photos_remove_from_album",
555
+ description: "Remove a photo from a Proton Photos album without deleting it from your library. Requires authentication. " +
556
+ "albumPath must start with /albums/; photoPath must start with /photos/. " +
557
+ "The photo is removed from the album only — it stays in your timeline.",
558
+ annotations: { destructiveHint: true },
559
+ inputSchema: {
560
+ type: "object",
561
+ properties: {
562
+ albumPath: {
563
+ type: "string",
564
+ description: "Absolute path of the album. Must start with /albums/. E.g. /albums/Vacation 2024",
565
+ },
566
+ photoPath: {
567
+ type: "string",
568
+ description: "Absolute path of the photo in the album. Must start with /photos/. E.g. /photos/IMG_001.jpg",
569
+ },
570
+ },
571
+ required: ["albumPath", "photoPath"],
572
+ additionalProperties: false,
573
+ },
574
+ },
450
575
  // Sync-folder tools (requires PROTON_DRIVE_SYNC_PATH env var)
451
576
  {
452
577
  name: "drive_read_file",
@@ -625,6 +750,49 @@ export async function main() {
625
750
  await drive.shareLeave(leavePath);
626
751
  return ok({ message: `Left shared folder: ${leavePath}` });
627
752
  }
753
+ case "photos_list_albums":
754
+ return ok(await drive.listAlbums());
755
+ case "photos_create_album": {
756
+ if (typeof a.name !== "string" || !a.name.trim())
757
+ return fail("name must be a non-empty string");
758
+ await drive.createAlbum(a.name.trim());
759
+ return ok({ message: `Album created: ${a.name.trim()}` });
760
+ }
761
+ case "photos_delete_album": {
762
+ if (a.confirmed !== true)
763
+ return fail("photos_delete_album requires confirmed=true. Show the user the album name and photo count first.");
764
+ const albumDelPath = validateRemotePath(a.albumPath);
765
+ if (!albumDelPath.startsWith("/albums/"))
766
+ return fail("albumPath must start with /albums/");
767
+ await drive.deleteAlbum(albumDelPath, a.force === true, a.save === true);
768
+ return ok({ message: `Album deleted: ${albumDelPath}` });
769
+ }
770
+ case "photos_list_album_photos": {
771
+ const albumListPath = validateRemotePath(a.albumPath);
772
+ if (!albumListPath.startsWith("/albums/"))
773
+ return fail("albumPath must start with /albums/");
774
+ return ok(await drive.listAlbumPhotos(albumListPath));
775
+ }
776
+ case "photos_add_to_album": {
777
+ const addAlbumPath = validateRemotePath(a.albumPath);
778
+ const addPhotoPath = validateRemotePath(a.photoPath);
779
+ if (!addAlbumPath.startsWith("/albums/"))
780
+ return fail("albumPath must start with /albums/");
781
+ if (!addPhotoPath.startsWith("/photos/"))
782
+ return fail("photoPath must start with /photos/");
783
+ await drive.addPhotoToAlbum(addAlbumPath, addPhotoPath);
784
+ return ok({ message: `Added ${addPhotoPath} to ${addAlbumPath}` });
785
+ }
786
+ case "photos_remove_from_album": {
787
+ const remAlbumPath = validateRemotePath(a.albumPath);
788
+ const remPhotoPath = validateRemotePath(a.photoPath);
789
+ if (!remAlbumPath.startsWith("/albums/"))
790
+ return fail("albumPath must start with /albums/");
791
+ if (!remPhotoPath.startsWith("/photos/"))
792
+ return fail("photoPath must start with /photos/");
793
+ await drive.removePhotoFromAlbum(remAlbumPath, remPhotoPath);
794
+ return ok({ message: `Removed ${remPhotoPath} from ${remAlbumPath}` });
795
+ }
628
796
  case "drive_read_file": {
629
797
  const syncRoot = getSyncRoot();
630
798
  if (!syncRoot)
@@ -655,7 +823,9 @@ export async function main() {
655
823
  await server.connect(transport);
656
824
  logger.info(`proton-drive-mcp v${VERSION} running`);
657
825
  }
658
- main().catch((err) => {
659
- logger.error("Fatal:", err);
660
- process.exit(1);
661
- });
826
+ if (isMainModule(import.meta.url)) {
827
+ main().catch((err) => {
828
+ logger.error("Fatal:", err);
829
+ process.exit(1);
830
+ });
831
+ }
@@ -171,4 +171,45 @@ export class DriveService {
171
171
  async shareLeave(remotePath) {
172
172
  await this.run(["sharing", "leave", remotePath]);
173
173
  }
174
+ // Photos / Albums
175
+ async listAlbums() {
176
+ const result = await this.run(["album", "list"]);
177
+ if (result === null)
178
+ return [];
179
+ if (!Array.isArray(result))
180
+ throw new DriveParseError(`Expected array from album list, got: ${JSON.stringify(result).slice(0, 100)}`);
181
+ return result.map((item) => ({
182
+ name: String(item.name ?? ""),
183
+ photoCount: typeof item.photoCount === "number" ? item.photoCount : 0,
184
+ isShared: Boolean(item.isShared ?? false),
185
+ creationTime: typeof item.creationTime === "string" ? item.creationTime : undefined,
186
+ }));
187
+ }
188
+ async createAlbum(name) {
189
+ await this.run(["album", "create", name]);
190
+ }
191
+ async deleteAlbum(albumPath, force, save) {
192
+ const args = ["album", "delete", albumPath];
193
+ if (force)
194
+ args.push("--force");
195
+ if (save)
196
+ args.push("--save");
197
+ await this.run(args);
198
+ }
199
+ async listAlbumPhotos(albumPath) {
200
+ const result = await this.run(["album", "photos", albumPath]);
201
+ if (result === null)
202
+ return [];
203
+ if (!Array.isArray(result))
204
+ throw new DriveParseError(`Expected array from album photos, got: ${JSON.stringify(result).slice(0, 100)}`);
205
+ return result.map((item) => ({
206
+ nodeUid: String(item.nodeUid ?? item.uid ?? ""),
207
+ }));
208
+ }
209
+ async addPhotoToAlbum(albumPath, photoPath) {
210
+ await this.run(["album", "add-photo", albumPath, photoPath]);
211
+ }
212
+ async removePhotoFromAlbum(albumPath, photoPath) {
213
+ await this.run(["album", "remove-photo", albumPath, photoPath]);
214
+ }
174
215
  }
@@ -0,0 +1,18 @@
1
+ import { fileURLToPath } from "node:url";
2
+ /**
3
+ * Returns true when this module is the direct entry point (i.e. the script
4
+ * passed to `node`), false when it is being imported as a library.
5
+ *
6
+ * Equivalent to `require.main === module` in CommonJS.
7
+ */
8
+ export function isMainModule(importMetaUrl) {
9
+ const entryPoint = process.argv[1];
10
+ if (!entryPoint)
11
+ return false;
12
+ try {
13
+ return fileURLToPath(importMetaUrl) === entryPoint;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
package/glama.json CHANGED
@@ -102,6 +102,30 @@
102
102
  {
103
103
  "name": "drive_share_leave",
104
104
  "description": "Leave a Proton Drive folder that was shared with you by another user."
105
+ },
106
+ {
107
+ "name": "photos_list_albums",
108
+ "description": "List all photo albums in Proton Photos."
109
+ },
110
+ {
111
+ "name": "photos_create_album",
112
+ "description": "Create a new empty photo album in Proton Photos."
113
+ },
114
+ {
115
+ "name": "photos_delete_album",
116
+ "description": "Delete a Proton Photos album."
117
+ },
118
+ {
119
+ "name": "photos_list_album_photos",
120
+ "description": "List the photos in a Proton Photos album."
121
+ },
122
+ {
123
+ "name": "photos_add_to_album",
124
+ "description": "Add a photo from your Proton Photos library to an album."
125
+ },
126
+ {
127
+ "name": "photos_remove_from_album",
128
+ "description": "Remove a photo from a Proton Photos album."
105
129
  }
106
130
  ]
107
131
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "proton-drive-mcp",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
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",
7
7
  "exports": {
8
8
  ".": "./dist/index.js",
9
+ "./services/drive": "./dist/services/drive.js",
9
10
  "./package.json": "./package.json"
10
11
  },
11
12
  "bin": {
package/smithery.yaml CHANGED
@@ -35,6 +35,12 @@ tools:
35
35
  - drive_invitation_accept
36
36
  - drive_invitation_reject
37
37
  - drive_share_leave
38
+ - photos_list_albums
39
+ - photos_create_album
40
+ - photos_delete_album
41
+ - photos_list_album_photos
42
+ - photos_add_to_album
43
+ - photos_remove_from_album
38
44
  startCommand:
39
45
  type: stdio
40
46
  configSchema: