plex-mcp 0.2.0 → 0.3.1
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/.github/workflows/ci.yml +2 -2
- package/README.md +83 -1
- package/index.js +485 -77
- package/package.json +10 -5
package/.github/workflows/ci.yml
CHANGED
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
|
|
13
13
|
strategy:
|
|
14
14
|
matrix:
|
|
15
|
-
node-version: [
|
|
15
|
+
node-version: [18.x, 20.x]
|
|
16
16
|
|
|
17
17
|
steps:
|
|
18
18
|
- name: Checkout code
|
|
@@ -82,4 +82,4 @@ jobs:
|
|
|
82
82
|
run: npm audit --audit-level=moderate
|
|
83
83
|
|
|
84
84
|
- name: Check for vulnerabilities
|
|
85
|
-
run: npm audit fix --dry-run
|
|
85
|
+
run: npm audit fix --dry-run
|
package/README.md
CHANGED
|
@@ -161,6 +161,16 @@ For automated setups or if you prefer manual token management:
|
|
|
161
161
|
|
|
162
162
|
## MCP Tools
|
|
163
163
|
|
|
164
|
+
### 🔴 Known Issues
|
|
165
|
+
|
|
166
|
+
**⚠️ Smart Playlist Creation (TEMPORARILY DISABLED)**
|
|
167
|
+
- The `create_smart_playlist` tool is currently disabled due to filter logic bugs
|
|
168
|
+
- Smart playlists were being created but with incorrect content and inflated metadata
|
|
169
|
+
- Use the regular `create_playlist` tool as an alternative
|
|
170
|
+
- Issue under investigation - will be re-enabled once fixed
|
|
171
|
+
|
|
172
|
+
### ✅ Working Tools
|
|
173
|
+
|
|
164
174
|
### Authentication Tools
|
|
165
175
|
|
|
166
176
|
#### authenticate_plex
|
|
@@ -203,4 +213,76 @@ Search for content in your Plex libraries.
|
|
|
203
213
|
"type": "movie",
|
|
204
214
|
"limit": 5
|
|
205
215
|
}
|
|
206
|
-
```
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### browse_libraries
|
|
219
|
+
List all available Plex libraries (Movies, TV Shows, Music, etc.)
|
|
220
|
+
|
|
221
|
+
#### browse_library
|
|
222
|
+
Browse content within a specific library with filtering and sorting options
|
|
223
|
+
|
|
224
|
+
#### get_recently_added
|
|
225
|
+
Get recently added content from Plex libraries
|
|
226
|
+
|
|
227
|
+
#### get_watch_history
|
|
228
|
+
Get playback history for the Plex server
|
|
229
|
+
|
|
230
|
+
#### get_on_deck
|
|
231
|
+
Get 'On Deck' items (continue watching) for users
|
|
232
|
+
|
|
233
|
+
### Playlist Tools
|
|
234
|
+
|
|
235
|
+
#### list_playlists
|
|
236
|
+
List all playlists on the Plex server
|
|
237
|
+
|
|
238
|
+
#### browse_playlist
|
|
239
|
+
Browse and view the contents of a specific playlist
|
|
240
|
+
|
|
241
|
+
#### create_playlist ✅
|
|
242
|
+
Create a new regular playlist (requires an initial item)
|
|
243
|
+
|
|
244
|
+
#### ~~create_smart_playlist~~ ❌ **DISABLED**
|
|
245
|
+
~~Create smart playlists with filter criteria~~ - Currently disabled due to filter logic bugs
|
|
246
|
+
|
|
247
|
+
#### add_to_playlist
|
|
248
|
+
Add items to an existing playlist
|
|
249
|
+
|
|
250
|
+
#### delete_playlist
|
|
251
|
+
Delete an existing playlist
|
|
252
|
+
|
|
253
|
+
### Media Information Tools
|
|
254
|
+
|
|
255
|
+
#### get_watched_status
|
|
256
|
+
Check watch status and progress for specific content items
|
|
257
|
+
|
|
258
|
+
#### get_collections
|
|
259
|
+
List all collections available on the Plex server
|
|
260
|
+
|
|
261
|
+
#### browse_collection
|
|
262
|
+
Browse content within a specific collection
|
|
263
|
+
|
|
264
|
+
#### get_media_info
|
|
265
|
+
Get detailed technical information about media files (codecs, bitrates, file sizes, etc.)
|
|
266
|
+
|
|
267
|
+
#### get_library_stats
|
|
268
|
+
Get comprehensive statistics about Plex libraries (storage usage, file counts, content breakdown, etc.)
|
|
269
|
+
|
|
270
|
+
#### get_listening_stats
|
|
271
|
+
Get detailed listening statistics and music recommendations based on play history
|
|
272
|
+
|
|
273
|
+
## Tool Status Summary
|
|
274
|
+
|
|
275
|
+
### ✅ Fully Working
|
|
276
|
+
- All authentication tools (`authenticate_plex`, `check_auth_status`, `clear_auth`)
|
|
277
|
+
- All search and browse tools (`search_plex`, `browse_libraries`, `browse_library`)
|
|
278
|
+
- All activity tools (`get_recently_added`, `get_watch_history`, `get_on_deck`)
|
|
279
|
+
- Regular playlist tools (`list_playlists`, `browse_playlist`, `create_playlist`, `add_to_playlist`, `delete_playlist`)
|
|
280
|
+
- All information tools (`get_watched_status`, `get_collections`, `browse_collection`, `get_media_info`, `get_library_stats`, `get_listening_stats`)
|
|
281
|
+
|
|
282
|
+
### ❌ Temporarily Disabled
|
|
283
|
+
- `create_smart_playlist` - Filter logic is broken, returns incorrect content with inflated metadata
|
|
284
|
+
|
|
285
|
+
### ⚠️ Known Limitations
|
|
286
|
+
- Smart playlist filtering system needs complete rework
|
|
287
|
+
- Some advanced filter combinations may not work as expected
|
|
288
|
+
- SSL certificate validation can be disabled with `PLEX_VERIFY_SSL=false` environment variable
|
package/index.js
CHANGED
|
@@ -8,12 +8,60 @@ const {
|
|
|
8
8
|
} = require("@modelcontextprotocol/sdk/types.js");
|
|
9
9
|
const axios = require('axios');
|
|
10
10
|
const { PlexOauth } = require('plex-oauth');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
11
14
|
|
|
12
15
|
class PlexAuthManager {
|
|
13
16
|
constructor() {
|
|
14
17
|
this.authToken = null;
|
|
15
18
|
this.plexOauth = null;
|
|
16
19
|
this.currentPinId = null;
|
|
20
|
+
this.tokenFilePath = path.join(os.homedir(), '.plex-mcp-token');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async loadPersistedToken() {
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync(this.tokenFilePath)) {
|
|
26
|
+
const tokenData = fs.readFileSync(this.tokenFilePath, 'utf8');
|
|
27
|
+
const parsed = JSON.parse(tokenData);
|
|
28
|
+
if (parsed.token && parsed.timestamp) {
|
|
29
|
+
// Check if token is less than 1 year old
|
|
30
|
+
const tokenAge = Date.now() - parsed.timestamp;
|
|
31
|
+
const oneYear = 365 * 24 * 60 * 60 * 1000;
|
|
32
|
+
if (tokenAge < oneYear) {
|
|
33
|
+
this.authToken = parsed.token;
|
|
34
|
+
return parsed.token;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// If there's any error reading the token, just continue without it
|
|
40
|
+
console.error('Error loading persisted token:', error.message);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async saveToken(token) {
|
|
46
|
+
try {
|
|
47
|
+
const tokenData = {
|
|
48
|
+
token: token,
|
|
49
|
+
timestamp: Date.now()
|
|
50
|
+
};
|
|
51
|
+
fs.writeFileSync(this.tokenFilePath, JSON.stringify(tokenData, null, 2), 'utf8');
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Error saving token:', error.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async clearPersistedToken() {
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(this.tokenFilePath)) {
|
|
60
|
+
fs.unlinkSync(this.tokenFilePath);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error clearing persisted token:', error.message);
|
|
64
|
+
}
|
|
17
65
|
}
|
|
18
66
|
|
|
19
67
|
async getAuthToken() {
|
|
@@ -28,6 +76,12 @@ class PlexAuthManager {
|
|
|
28
76
|
return this.authToken;
|
|
29
77
|
}
|
|
30
78
|
|
|
79
|
+
// Try to load persisted token
|
|
80
|
+
const persistedToken = await this.loadPersistedToken();
|
|
81
|
+
if (persistedToken) {
|
|
82
|
+
return persistedToken;
|
|
83
|
+
}
|
|
84
|
+
|
|
31
85
|
throw new Error('No authentication token available. Please authenticate first using the authenticate_plex tool or set PLEX_TOKEN environment variable.');
|
|
32
86
|
}
|
|
33
87
|
|
|
@@ -38,8 +92,8 @@ class PlexAuthManager {
|
|
|
38
92
|
|
|
39
93
|
const clientInfo = {
|
|
40
94
|
clientIdentifier: process.env.PLEX_CLIENT_ID || 'plex-mcp-client',
|
|
41
|
-
product: process.env.PLEX_PRODUCT || '
|
|
42
|
-
device: process.env.PLEX_DEVICE || '
|
|
95
|
+
product: process.env.PLEX_PRODUCT || 'PlexMCP',
|
|
96
|
+
device: process.env.PLEX_DEVICE || 'PlexMCP',
|
|
43
97
|
version: process.env.PLEX_VERSION || '1.0.0',
|
|
44
98
|
forwardUrl: process.env.PLEX_REDIRECT_URL || 'https://app.plex.tv/auth#!',
|
|
45
99
|
platform: process.env.PLEX_PLATFORM || 'Web'
|
|
@@ -72,6 +126,7 @@ class PlexAuthManager {
|
|
|
72
126
|
const authToken = await oauth.checkForAuthToken(pin);
|
|
73
127
|
if (authToken) {
|
|
74
128
|
this.authToken = authToken;
|
|
129
|
+
await this.saveToken(authToken);
|
|
75
130
|
return authToken;
|
|
76
131
|
}
|
|
77
132
|
return null;
|
|
@@ -80,9 +135,10 @@ class PlexAuthManager {
|
|
|
80
135
|
}
|
|
81
136
|
}
|
|
82
137
|
|
|
83
|
-
clearAuth() {
|
|
138
|
+
async clearAuth() {
|
|
84
139
|
this.authToken = null;
|
|
85
140
|
this.currentPinId = null;
|
|
141
|
+
await this.clearPersistedToken();
|
|
86
142
|
}
|
|
87
143
|
}
|
|
88
144
|
|
|
@@ -488,7 +544,7 @@ class PlexMCPServer {
|
|
|
488
544
|
},
|
|
489
545
|
{
|
|
490
546
|
name: "create_playlist",
|
|
491
|
-
description: "Create a new playlist on the Plex server.
|
|
547
|
+
description: "Create a new regular playlist on the Plex server. Requires an initial item (item_key parameter) to be created successfully. Smart playlists are not supported due to their complex filter requirements.",
|
|
492
548
|
inputSchema: {
|
|
493
549
|
type: "object",
|
|
494
550
|
properties: {
|
|
@@ -501,19 +557,76 @@ class PlexMCPServer {
|
|
|
501
557
|
enum: ["audio", "video", "photo"],
|
|
502
558
|
description: "The type of playlist to create",
|
|
503
559
|
},
|
|
504
|
-
smart: {
|
|
505
|
-
type: "boolean",
|
|
506
|
-
description: "Whether to create a smart playlist (default: false)",
|
|
507
|
-
default: false,
|
|
508
|
-
},
|
|
509
560
|
item_key: {
|
|
510
561
|
type: "string",
|
|
511
|
-
description: "The key of an initial item to add to the playlist. Required for
|
|
562
|
+
description: "The key of an initial item to add to the playlist. Required for playlist creation. Get item keys from search_plex or browse_library results.",
|
|
512
563
|
},
|
|
513
564
|
},
|
|
514
|
-
required: ["title", "type"],
|
|
565
|
+
required: ["title", "type", "item_key"],
|
|
515
566
|
},
|
|
516
567
|
},
|
|
568
|
+
// TEMPORARILY DISABLED - Smart playlist filtering is broken
|
|
569
|
+
// {
|
|
570
|
+
// name: "create_smart_playlist",
|
|
571
|
+
// description: "Create a new smart playlist with filter criteria. Smart playlists automatically populate based on specified conditions.",
|
|
572
|
+
// inputSchema: {
|
|
573
|
+
// type: "object",
|
|
574
|
+
// properties: {
|
|
575
|
+
// title: {
|
|
576
|
+
// type: "string",
|
|
577
|
+
// description: "The title/name for the new smart playlist",
|
|
578
|
+
// },
|
|
579
|
+
// type: {
|
|
580
|
+
// type: "string",
|
|
581
|
+
// enum: ["audio", "video", "photo"],
|
|
582
|
+
// description: "The type of content for the smart playlist",
|
|
583
|
+
// },
|
|
584
|
+
// library_id: {
|
|
585
|
+
// type: "string",
|
|
586
|
+
// description: "The library ID to create the smart playlist in. Use browse_libraries to get library IDs.",
|
|
587
|
+
// },
|
|
588
|
+
// filters: {
|
|
589
|
+
// type: "array",
|
|
590
|
+
// description: "Array of filter conditions for the smart playlist",
|
|
591
|
+
// items: {
|
|
592
|
+
// type: "object",
|
|
593
|
+
// properties: {
|
|
594
|
+
// field: {
|
|
595
|
+
// type: "string",
|
|
596
|
+
// enum: ["artist.title", "album.title", "track.title", "genre.tag", "year", "rating", "addedAt", "lastViewedAt", "viewCount"],
|
|
597
|
+
// description: "The field to filter on"
|
|
598
|
+
// },
|
|
599
|
+
// operator: {
|
|
600
|
+
// type: "string",
|
|
601
|
+
// enum: ["is", "isnot", "contains", "doesnotcontain", "beginswith", "endswith", "gt", "gte", "lt", "lte"],
|
|
602
|
+
// description: "The comparison operator"
|
|
603
|
+
// },
|
|
604
|
+
// value: {
|
|
605
|
+
// type: "string",
|
|
606
|
+
// description: "The value to compare against"
|
|
607
|
+
// }
|
|
608
|
+
// },
|
|
609
|
+
// required: ["field", "operator", "value"]
|
|
610
|
+
// },
|
|
611
|
+
// minItems: 1
|
|
612
|
+
// },
|
|
613
|
+
// sort: {
|
|
614
|
+
// type: "string",
|
|
615
|
+
// enum: ["artist.titleSort", "album.titleSort", "track.titleSort", "addedAt", "year", "rating", "lastViewedAt", "random"],
|
|
616
|
+
// description: "How to sort the smart playlist results (optional)",
|
|
617
|
+
// default: "artist.titleSort"
|
|
618
|
+
// },
|
|
619
|
+
// limit: {
|
|
620
|
+
// type: "integer",
|
|
621
|
+
// description: "Maximum number of items in the smart playlist (optional)",
|
|
622
|
+
// minimum: 1,
|
|
623
|
+
// maximum: 1000,
|
|
624
|
+
// default: 100
|
|
625
|
+
// }
|
|
626
|
+
// },
|
|
627
|
+
// required: ["title", "type", "library_id", "filters"],
|
|
628
|
+
// },
|
|
629
|
+
// },
|
|
517
630
|
{
|
|
518
631
|
name: "add_to_playlist",
|
|
519
632
|
description: "Add items to an existing playlist",
|
|
@@ -758,6 +871,9 @@ class PlexMCPServer {
|
|
|
758
871
|
return await this.handleBrowsePlaylist(request.params.arguments);
|
|
759
872
|
case "create_playlist":
|
|
760
873
|
return await this.handleCreatePlaylist(request.params.arguments);
|
|
874
|
+
// TEMPORARILY DISABLED - Smart playlist filtering is broken
|
|
875
|
+
// case "create_smart_playlist":
|
|
876
|
+
// return await this.handleCreateSmartPlaylist(request.params.arguments);
|
|
761
877
|
case "add_to_playlist":
|
|
762
878
|
return await this.handleAddToPlaylist(request.params.arguments);
|
|
763
879
|
// DISABLED: remove_from_playlist - PROBLEMATIC operation
|
|
@@ -800,13 +916,19 @@ class PlexMCPServer {
|
|
|
800
916
|
text: `Plex Authentication Started
|
|
801
917
|
|
|
802
918
|
**Next Steps:**
|
|
803
|
-
1. Open this URL in your browser:
|
|
804
|
-
|
|
805
|
-
|
|
919
|
+
1. Open this URL in your browser:
|
|
920
|
+
|
|
921
|
+
\`\`\`
|
|
922
|
+
${loginUrl.replace(/\[/g, '%5B').replace(/\]/g, '%5D').replace(/!/g, '%21')}
|
|
923
|
+
\`\`\`
|
|
924
|
+
|
|
925
|
+
2. Sign into your Plex account when prompted
|
|
926
|
+
3. **IMPORTANT:** After signing in, you MUST return here and run the \`check_auth_status\` tool to complete the authentication process
|
|
927
|
+
4. Only after running \`check_auth_status\` will your token be saved and ready for use
|
|
806
928
|
|
|
807
929
|
**Pin ID:** ${pinId}
|
|
808
930
|
|
|
809
|
-
|
|
931
|
+
⚠️ **Don't forget:** The authentication is not complete until you return and run \`check_auth_status\`!`
|
|
810
932
|
}
|
|
811
933
|
]
|
|
812
934
|
};
|
|
@@ -875,7 +997,7 @@ You can run check_auth_status again to check if authentication is complete.`
|
|
|
875
997
|
|
|
876
998
|
async handleClearAuth(args) {
|
|
877
999
|
try {
|
|
878
|
-
this.authManager.clearAuth();
|
|
1000
|
+
await this.authManager.clearAuth();
|
|
879
1001
|
|
|
880
1002
|
return {
|
|
881
1003
|
content: [
|
|
@@ -936,7 +1058,7 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
936
1058
|
} = args;
|
|
937
1059
|
|
|
938
1060
|
try {
|
|
939
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
1061
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
940
1062
|
const plexToken = await this.authManager.getAuthToken();
|
|
941
1063
|
|
|
942
1064
|
const searchUrl = `${plexUrl}/search`;
|
|
@@ -1117,7 +1239,7 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
1117
1239
|
|
|
1118
1240
|
async handleBrowseLibraries(args) {
|
|
1119
1241
|
try {
|
|
1120
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
1242
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
1121
1243
|
const plexToken = await this.authManager.getAuthToken();
|
|
1122
1244
|
|
|
1123
1245
|
const librariesUrl = `${plexUrl}/library/sections`;
|
|
@@ -1229,7 +1351,7 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
1229
1351
|
} = args;
|
|
1230
1352
|
|
|
1231
1353
|
try {
|
|
1232
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
1354
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
1233
1355
|
const plexToken = await this.authManager.getAuthToken();
|
|
1234
1356
|
|
|
1235
1357
|
const libraryUrl = `${plexUrl}/library/sections/${library_id}/all`;
|
|
@@ -1351,7 +1473,7 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
1351
1473
|
const { library_id, limit = 15, chunk_size = 10, chunk_offset = 0 } = args;
|
|
1352
1474
|
|
|
1353
1475
|
try {
|
|
1354
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
1476
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
1355
1477
|
const plexToken = await this.authManager.getAuthToken();
|
|
1356
1478
|
|
|
1357
1479
|
let recentUrl;
|
|
@@ -1447,7 +1569,7 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
1447
1569
|
const { limit = 20, account_id, chunk_size = 10, chunk_offset = 0 } = args;
|
|
1448
1570
|
|
|
1449
1571
|
try {
|
|
1450
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
1572
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
1451
1573
|
const plexToken = await this.authManager.getAuthToken();
|
|
1452
1574
|
|
|
1453
1575
|
const historyUrl = `${plexUrl}/status/sessions/history/all`;
|
|
@@ -1567,7 +1689,7 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
1567
1689
|
const { limit = 15 } = args;
|
|
1568
1690
|
|
|
1569
1691
|
try {
|
|
1570
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
1692
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
1571
1693
|
const plexToken = await this.authManager.getAuthToken();
|
|
1572
1694
|
|
|
1573
1695
|
const onDeckUrl = `${plexUrl}/library/onDeck`;
|
|
@@ -1666,7 +1788,7 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
1666
1788
|
const { playlist_type } = args;
|
|
1667
1789
|
|
|
1668
1790
|
try {
|
|
1669
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
1791
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
1670
1792
|
const plexToken = await this.authManager.getAuthToken();
|
|
1671
1793
|
|
|
1672
1794
|
const playlistsUrl = `${plexUrl}/playlists`;
|
|
@@ -1714,7 +1836,7 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
1714
1836
|
const { playlist_id, limit = 50 } = args;
|
|
1715
1837
|
|
|
1716
1838
|
try {
|
|
1717
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
1839
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
1718
1840
|
const plexToken = await this.authManager.getAuthToken();
|
|
1719
1841
|
|
|
1720
1842
|
// First get playlist info
|
|
@@ -1912,23 +2034,10 @@ All stored authentication credentials have been cleared. To use Plex tools again
|
|
|
1912
2034
|
}
|
|
1913
2035
|
|
|
1914
2036
|
async handleCreatePlaylist(args) {
|
|
1915
|
-
const { title, type, smart = false
|
|
1916
|
-
|
|
1917
|
-
// Validate that item_key is provided for non-smart playlists
|
|
1918
|
-
if (!smart && !item_key) {
|
|
1919
|
-
return {
|
|
1920
|
-
content: [
|
|
1921
|
-
{
|
|
1922
|
-
type: "text",
|
|
1923
|
-
text: `Error: Non-smart playlists require an initial item. Please provide an item_key parameter with the Plex item key to add to the playlist. You can get item keys by searching or browsing your library first.`,
|
|
1924
|
-
},
|
|
1925
|
-
],
|
|
1926
|
-
isError: true,
|
|
1927
|
-
};
|
|
1928
|
-
}
|
|
2037
|
+
const { title, type, item_key, smart = false } = args;
|
|
1929
2038
|
|
|
1930
2039
|
try {
|
|
1931
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
2040
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
1932
2041
|
const plexToken = await this.authManager.getAuthToken();
|
|
1933
2042
|
|
|
1934
2043
|
// First get server info to get machine identifier
|
|
@@ -2018,6 +2127,163 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2018
2127
|
}
|
|
2019
2128
|
}
|
|
2020
2129
|
|
|
2130
|
+
async handleCreateSmartPlaylist(args) {
|
|
2131
|
+
const { title, type, library_id, filters, sort = "artist.titleSort", limit = 100 } = args;
|
|
2132
|
+
|
|
2133
|
+
try {
|
|
2134
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
2135
|
+
const plexToken = await this.authManager.getAuthToken();
|
|
2136
|
+
|
|
2137
|
+
// Get server machine identifier
|
|
2138
|
+
const serverResponse = await axios.get(`${plexUrl}`, {
|
|
2139
|
+
headers: {
|
|
2140
|
+
'X-Plex-Token': plexToken,
|
|
2141
|
+
'Accept': 'application/json'
|
|
2142
|
+
},
|
|
2143
|
+
httpsAgent: this.getHttpsAgent()
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
const machineId = serverResponse.data.MediaContainer.machineIdentifier;
|
|
2147
|
+
|
|
2148
|
+
// Build query parameters from filters manually to avoid double encoding
|
|
2149
|
+
const queryParts = [`type=${type === 'audio' ? '10' : '1'}`];
|
|
2150
|
+
|
|
2151
|
+
filters.forEach(filter => {
|
|
2152
|
+
const field = this.mapFilterField(filter.field);
|
|
2153
|
+
const operator = this.mapFilterOperator(filter.operator);
|
|
2154
|
+
|
|
2155
|
+
if (operator === '=') {
|
|
2156
|
+
// Plex expects triple-encoded format: field%253D%3Dvalue
|
|
2157
|
+
const encodedField = encodeURIComponent(encodeURIComponent(field));
|
|
2158
|
+
const encodedValue = encodeURIComponent(encodeURIComponent(filter.value));
|
|
2159
|
+
queryParts.push(`${encodedField}%253D%3D${encodedValue}`);
|
|
2160
|
+
} else {
|
|
2161
|
+
queryParts.push(`${encodeURIComponent(field)}${operator}${encodeURIComponent(filter.value)}`);
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
// Build the URI in the format Plex expects
|
|
2166
|
+
const uri = `server://${machineId}/com.plexapp.plugins.library/library/sections/${library_id}/all?${queryParts.join('&')}`;
|
|
2167
|
+
|
|
2168
|
+
// Debug logging
|
|
2169
|
+
console.log('DEBUG: Generated URI:', uri);
|
|
2170
|
+
console.log('DEBUG: Query parts:', queryParts);
|
|
2171
|
+
|
|
2172
|
+
// Create smart playlist using POST to /playlists
|
|
2173
|
+
const createParams = new URLSearchParams();
|
|
2174
|
+
createParams.append('type', type);
|
|
2175
|
+
createParams.append('title', title);
|
|
2176
|
+
createParams.append('smart', '1');
|
|
2177
|
+
createParams.append('uri', uri);
|
|
2178
|
+
|
|
2179
|
+
const response = await axios.post(`${plexUrl}/playlists?${createParams.toString()}`, null, {
|
|
2180
|
+
headers: {
|
|
2181
|
+
'X-Plex-Token': plexToken,
|
|
2182
|
+
'Accept': 'application/json'
|
|
2183
|
+
},
|
|
2184
|
+
httpsAgent: this.getHttpsAgent()
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
const playlistData = response.data.MediaContainer.Metadata[0];
|
|
2188
|
+
|
|
2189
|
+
return {
|
|
2190
|
+
content: [
|
|
2191
|
+
{
|
|
2192
|
+
type: "text",
|
|
2193
|
+
text: `✅ **Smart Playlist Created Successfully**
|
|
2194
|
+
|
|
2195
|
+
**Playlist Details:**
|
|
2196
|
+
• **Name:** ${playlistData.title}
|
|
2197
|
+
• **Type:** ${playlistData.playlistType}
|
|
2198
|
+
• **Tracks:** ${playlistData.leafCount || 0}
|
|
2199
|
+
• **Duration:** ${playlistData.duration ? Math.round(playlistData.duration / 60000) + ' minutes' : 'Unknown'}
|
|
2200
|
+
• **ID:** ${playlistData.ratingKey}
|
|
2201
|
+
|
|
2202
|
+
**Filters Applied:**
|
|
2203
|
+
${filters.map(f => `• ${f.field} ${f.operator} "${f.value}"`).join('\n')}
|
|
2204
|
+
|
|
2205
|
+
The smart playlist has been created and is now available in your Plex library!`,
|
|
2206
|
+
},
|
|
2207
|
+
],
|
|
2208
|
+
};
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
// Enhanced error handling for smart playlists
|
|
2211
|
+
let errorMessage = `Error creating smart playlist: ${error.message}`;
|
|
2212
|
+
|
|
2213
|
+
if (error.response) {
|
|
2214
|
+
const status = error.response.status;
|
|
2215
|
+
if (status === 400) {
|
|
2216
|
+
errorMessage = `❌ **Smart Playlist Creation Failed (400 Bad Request)**
|
|
2217
|
+
|
|
2218
|
+
**Possible issues:**
|
|
2219
|
+
• Invalid filter criteria or field names
|
|
2220
|
+
• Unsupported operator for the field type
|
|
2221
|
+
• Library ID "${library_id}" not found or inaccessible
|
|
2222
|
+
• Filter values in wrong format
|
|
2223
|
+
|
|
2224
|
+
**Debug info:**
|
|
2225
|
+
• Status: ${status}
|
|
2226
|
+
• Filters attempted: ${filters.length}
|
|
2227
|
+
• Library ID: ${library_id}
|
|
2228
|
+
|
|
2229
|
+
**Suggestion:** Try with simpler filters first, or verify library_id with \`browse_libraries\`.`;
|
|
2230
|
+
} else if (status === 401 || status === 403) {
|
|
2231
|
+
errorMessage = `Permission denied: Check your Plex token and server access`;
|
|
2232
|
+
} else if (status >= 500) {
|
|
2233
|
+
errorMessage = `Plex server error (${status}): ${error.message}`;
|
|
2234
|
+
}
|
|
2235
|
+
} else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
2236
|
+
errorMessage = `Cannot connect to Plex server: Check PLEX_URL configuration`;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
return {
|
|
2240
|
+
content: [
|
|
2241
|
+
{
|
|
2242
|
+
type: "text",
|
|
2243
|
+
text: errorMessage,
|
|
2244
|
+
},
|
|
2245
|
+
],
|
|
2246
|
+
isError: true,
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// Helper functions for smart playlist field/operator mapping
|
|
2252
|
+
mapFilterField(field) {
|
|
2253
|
+
// Return the field as-is since Plex expects the full dotted notation
|
|
2254
|
+
return field;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
mapFilterOperator(operator) {
|
|
2258
|
+
const operatorMap = {
|
|
2259
|
+
'is': '=',
|
|
2260
|
+
'isnot': '!=',
|
|
2261
|
+
'contains': '=', // Plex uses = for contains on text fields
|
|
2262
|
+
'doesnotcontain': '!=',
|
|
2263
|
+
'beginswith': '=', // Plex uses = for text matching
|
|
2264
|
+
'endswith': '=',
|
|
2265
|
+
'gt': '>',
|
|
2266
|
+
'gte': '>=',
|
|
2267
|
+
'lt': '<',
|
|
2268
|
+
'lte': '<='
|
|
2269
|
+
};
|
|
2270
|
+
return operatorMap[operator] || operator;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
mapSortField(sort) {
|
|
2274
|
+
const sortMap = {
|
|
2275
|
+
'artist.titleSort': 'artist',
|
|
2276
|
+
'album.titleSort': 'album',
|
|
2277
|
+
'track.titleSort': 'title',
|
|
2278
|
+
'addedAt': 'addedAt',
|
|
2279
|
+
'year': 'year',
|
|
2280
|
+
'rating': 'userRating',
|
|
2281
|
+
'lastViewedAt': 'lastViewedAt',
|
|
2282
|
+
'random': 'random'
|
|
2283
|
+
};
|
|
2284
|
+
return sortMap[sort] || sort;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2021
2287
|
async handleAddToPlaylist(args) {
|
|
2022
2288
|
const { playlist_id, item_keys } = args;
|
|
2023
2289
|
|
|
@@ -2033,7 +2299,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2033
2299
|
}
|
|
2034
2300
|
|
|
2035
2301
|
try {
|
|
2036
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
2302
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
2037
2303
|
const plexToken = await this.authManager.getAuthToken();
|
|
2038
2304
|
|
|
2039
2305
|
// Get playlist info before adding items
|
|
@@ -2061,7 +2327,8 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2061
2327
|
},
|
|
2062
2328
|
httpsAgent: this.getHttpsAgent()
|
|
2063
2329
|
});
|
|
2064
|
-
|
|
2330
|
+
const beforeItems = beforeResponse.data.MediaContainer?.Metadata || [];
|
|
2331
|
+
beforeCount = beforeItems.length; // Use actual count of items instead of totalSize
|
|
2065
2332
|
} catch (error) {
|
|
2066
2333
|
// If items endpoint fails, playlist might be empty
|
|
2067
2334
|
beforeCount = 0;
|
|
@@ -2079,35 +2346,136 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2079
2346
|
}
|
|
2080
2347
|
|
|
2081
2348
|
const addUrl = `${plexUrl}/playlists/${playlist_id}/items`;
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2349
|
+
|
|
2350
|
+
// Try different batch approaches for multiple items
|
|
2351
|
+
let response;
|
|
2352
|
+
let batchMethod = '';
|
|
2353
|
+
|
|
2354
|
+
if (item_keys.length === 1) {
|
|
2355
|
+
// Single item - use existing proven method
|
|
2356
|
+
const params = {
|
|
2357
|
+
'X-Plex-Token': plexToken,
|
|
2358
|
+
uri: `server://${machineIdentifier}/com.plexapp.plugins.library/library/metadata/${item_keys[0]}`
|
|
2359
|
+
};
|
|
2360
|
+
response = await axios.put(addUrl, null, { params, httpsAgent: this.getHttpsAgent() });
|
|
2361
|
+
batchMethod = 'single';
|
|
2362
|
+
|
|
2363
|
+
} else {
|
|
2364
|
+
// Multiple items - use sequential individual adds (only reliable method)
|
|
2365
|
+
console.log(`Adding ${item_keys.length} items sequentially (batch operations are unreliable)...`);
|
|
2366
|
+
batchMethod = 'sequential-reliable';
|
|
2367
|
+
let sequentialCount = 0;
|
|
2368
|
+
const sequentialResults = [];
|
|
2369
|
+
|
|
2370
|
+
for (const itemKey of item_keys) {
|
|
2371
|
+
try {
|
|
2372
|
+
const singleParams = {
|
|
2373
|
+
'X-Plex-Token': plexToken,
|
|
2374
|
+
uri: `server://${machineIdentifier}/com.plexapp.plugins.library/library/metadata/${itemKey}`
|
|
2375
|
+
};
|
|
2376
|
+
|
|
2377
|
+
if (process.env.DEBUG_PLAYLISTS) {
|
|
2378
|
+
console.log(`Adding item ${itemKey} individually...`);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const singleResponse = await axios.put(addUrl, null, {
|
|
2382
|
+
params: singleParams,
|
|
2383
|
+
httpsAgent: this.getHttpsAgent(),
|
|
2384
|
+
timeout: 10000, // 10 second timeout
|
|
2385
|
+
validateStatus: function (status) {
|
|
2386
|
+
return status >= 200 && status < 300; // Only accept 2xx status codes
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
if (singleResponse.status >= 200 && singleResponse.status < 300) {
|
|
2391
|
+
sequentialCount++;
|
|
2392
|
+
sequentialResults.push({ itemKey, success: true });
|
|
2393
|
+
if (process.env.DEBUG_PLAYLISTS) {
|
|
2394
|
+
console.log(`✅ Successfully added item ${itemKey}`);
|
|
2395
|
+
}
|
|
2396
|
+
} else {
|
|
2397
|
+
sequentialResults.push({ itemKey, success: false, status: singleResponse.status });
|
|
2398
|
+
console.warn(`❌ Failed to add item ${itemKey}, status: ${singleResponse.status}`);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// Small delay between sequential operations for API stability
|
|
2402
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
2403
|
+
|
|
2404
|
+
} catch (seqError) {
|
|
2405
|
+
console.warn(`❌ Sequential add failed for item ${itemKey}:`, seqError.message);
|
|
2406
|
+
sequentialResults.push({ itemKey, success: false, error: seqError.message });
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// Create response for sequential operations
|
|
2411
|
+
response = {
|
|
2412
|
+
status: sequentialCount > 0 ? 200 : 400,
|
|
2413
|
+
data: {
|
|
2414
|
+
sequentialAdded: sequentialCount,
|
|
2415
|
+
sequentialResults: sequentialResults,
|
|
2416
|
+
totalRequested: item_keys.length
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
|
|
2420
|
+
if (process.env.DEBUG_PLAYLISTS) {
|
|
2421
|
+
console.log(`Sequential operation complete: ${sequentialCount}/${item_keys.length} items added successfully`);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2091
2424
|
|
|
2092
2425
|
// Check if the PUT request was successful based on HTTP status
|
|
2093
2426
|
const putSuccessful = response.status >= 200 && response.status < 300;
|
|
2094
2427
|
|
|
2095
|
-
//
|
|
2096
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2097
|
-
|
|
2098
|
-
// Verify the addition by checking the playlist items again
|
|
2428
|
+
// Verify the addition with retries due to Plex API reliability issues
|
|
2099
2429
|
let afterCount = 0;
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2430
|
+
let retryCount = 0;
|
|
2431
|
+
const maxRetries = 3;
|
|
2432
|
+
|
|
2433
|
+
while (retryCount <= maxRetries) {
|
|
2434
|
+
await new Promise(resolve => setTimeout(resolve, 300 * (retryCount + 1))); // Increasing delay
|
|
2435
|
+
|
|
2436
|
+
try {
|
|
2437
|
+
// Try both the items endpoint and playlist metadata endpoint
|
|
2438
|
+
const [itemsResponse, playlistResponse] = await Promise.allSettled([
|
|
2439
|
+
axios.get(playlistItemsUrl, {
|
|
2440
|
+
params: { 'X-Plex-Token': plexToken },
|
|
2441
|
+
httpsAgent: this.getHttpsAgent()
|
|
2442
|
+
}),
|
|
2443
|
+
axios.get(playlistInfoUrl, {
|
|
2444
|
+
params: { 'X-Plex-Token': plexToken },
|
|
2445
|
+
httpsAgent: this.getHttpsAgent()
|
|
2446
|
+
})
|
|
2447
|
+
]);
|
|
2448
|
+
|
|
2449
|
+
// Try to get count from items endpoint first
|
|
2450
|
+
if (itemsResponse.status === 'fulfilled' && itemsResponse.value?.data) {
|
|
2451
|
+
try {
|
|
2452
|
+
const items = itemsResponse.value.data.MediaContainer?.Metadata || [];
|
|
2453
|
+
afterCount = items.length;
|
|
2454
|
+
break; // Success, exit retry loop
|
|
2455
|
+
} catch (parseError) {
|
|
2456
|
+
console.warn('Error parsing items response:', parseError.message);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// Fall back to playlist metadata if items endpoint failed
|
|
2461
|
+
if (playlistResponse.status === 'fulfilled' && playlistResponse.value?.data) {
|
|
2462
|
+
try {
|
|
2463
|
+
const metadata = playlistResponse.value.data.MediaContainer?.Metadata?.[0];
|
|
2464
|
+
afterCount = parseInt(metadata?.leafCount || 0, 10) || 0;
|
|
2465
|
+
break; // Success, exit retry loop
|
|
2466
|
+
} catch (parseError) {
|
|
2467
|
+
console.warn('Error parsing playlist metadata:', parseError.message);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
retryCount++;
|
|
2473
|
+
if (retryCount > maxRetries) {
|
|
2474
|
+
console.warn(`Failed to get playlist count after ${maxRetries} retries:`, error.message);
|
|
2475
|
+
// If all retries failed, fall back to optimistic counting
|
|
2476
|
+
afterCount = beforeCount + (putSuccessful ? item_keys.length : 0);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2111
2479
|
}
|
|
2112
2480
|
|
|
2113
2481
|
const actualAdded = afterCount - beforeCount;
|
|
@@ -2118,6 +2486,46 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2118
2486
|
resultText += `• Actually added: ${actualAdded} item(s)\n`;
|
|
2119
2487
|
resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n`;
|
|
2120
2488
|
|
|
2489
|
+
// Show batch method for multiple items
|
|
2490
|
+
if (item_keys.length > 1) {
|
|
2491
|
+
const methodDescription = {
|
|
2492
|
+
'sequential-reliable': 'sequential individual adds (only reliable method for multiple items)'
|
|
2493
|
+
};
|
|
2494
|
+
resultText += `• Method used: ${methodDescription[batchMethod] || batchMethod}\n`;
|
|
2495
|
+
|
|
2496
|
+
// Show success summary for sequential operations
|
|
2497
|
+
if (response.data?.sequentialAdded !== undefined) {
|
|
2498
|
+
const successRate = ((response.data.sequentialAdded / item_keys.length) * 100).toFixed(0);
|
|
2499
|
+
resultText += `• Success rate: ${response.data.sequentialAdded}/${item_keys.length} items (${successRate}%)\n`;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// Show individual results in debug mode
|
|
2503
|
+
if (response.data?.sequentialResults && process.env.DEBUG_PLAYLISTS) {
|
|
2504
|
+
resultText += `• Individual results:\n`;
|
|
2505
|
+
response.data.sequentialResults.forEach(result => {
|
|
2506
|
+
const status = result.success ? '✅' : '❌';
|
|
2507
|
+
const detail = result.error ? ` (${result.error})` : result.status ? ` (HTTP ${result.status})` : '';
|
|
2508
|
+
resultText += ` ${status} ${result.itemKey}${detail}\n`;
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// Debug information
|
|
2514
|
+
if (process.env.DEBUG_PLAYLISTS) {
|
|
2515
|
+
resultText += `\nDEBUG INFO:\n`;
|
|
2516
|
+
resultText += `• Batch method used: ${batchMethod}\n`;
|
|
2517
|
+
resultText += `• PUT request status: ${response.status}\n`;
|
|
2518
|
+
resultText += `• PUT successful: ${putSuccessful}\n`;
|
|
2519
|
+
resultText += `• Before count: ${beforeCount}\n`;
|
|
2520
|
+
resultText += `• After count: ${afterCount}\n`;
|
|
2521
|
+
resultText += `• Retries needed: ${retryCount}\n`;
|
|
2522
|
+
resultText += `• Count verification method: ${retryCount > maxRetries ? 'fallback' : 'API'}\n`;
|
|
2523
|
+
resultText += `• Items requested: [${item_keys.join(', ')}]\n`;
|
|
2524
|
+
if (response.data?.sequentialAdded !== undefined) {
|
|
2525
|
+
resultText += `• Sequential adds successful: ${response.data.sequentialAdded}/${item_keys.length}\n`;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2121
2529
|
// If HTTP request was successful but count didn't change,
|
|
2122
2530
|
// it's likely the items already exist or are duplicates
|
|
2123
2531
|
if (actualAdded === attempted) {
|
|
@@ -2190,7 +2598,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2190
2598
|
}
|
|
2191
2599
|
|
|
2192
2600
|
try {
|
|
2193
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
2601
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
2194
2602
|
const plexToken = await this.authManager.getAuthToken();
|
|
2195
2603
|
|
|
2196
2604
|
// Get playlist info before removing items
|
|
@@ -2370,7 +2778,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2370
2778
|
const { playlist_id } = args;
|
|
2371
2779
|
|
|
2372
2780
|
try {
|
|
2373
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
2781
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
2374
2782
|
const plexToken = await this.authManager.getAuthToken();
|
|
2375
2783
|
|
|
2376
2784
|
const deleteUrl = `${plexUrl}/playlists/${playlist_id}`;
|
|
@@ -2410,7 +2818,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2410
2818
|
const { item_keys, account_id } = args;
|
|
2411
2819
|
|
|
2412
2820
|
try {
|
|
2413
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
2821
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
2414
2822
|
const plexToken = await this.authManager.getAuthToken();
|
|
2415
2823
|
|
|
2416
2824
|
const statusResults = [];
|
|
@@ -2627,7 +3035,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2627
3035
|
let height = 0;
|
|
2628
3036
|
|
|
2629
3037
|
if (media.height) {
|
|
2630
|
-
height = parseInt(media.height);
|
|
3038
|
+
height = parseInt(media.height, 10) || 0;
|
|
2631
3039
|
} else if (media.videoResolution) {
|
|
2632
3040
|
// Convert videoResolution string to height for comparison
|
|
2633
3041
|
switch (media.videoResolution) {
|
|
@@ -2712,7 +3120,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2712
3120
|
const totalSize = item.Media.reduce((total, media) => {
|
|
2713
3121
|
if (media.Part) {
|
|
2714
3122
|
return total + media.Part.reduce((partTotal, part) => {
|
|
2715
|
-
return partTotal + (part.size ? parseInt(part.size) / (1024 * 1024) : 0); // Convert to MB
|
|
3123
|
+
return partTotal + (part.size ? (parseInt(part.size, 10) || 0) / (1024 * 1024) : 0); // Convert to MB
|
|
2716
3124
|
}, 0);
|
|
2717
3125
|
}
|
|
2718
3126
|
return total;
|
|
@@ -2853,7 +3261,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2853
3261
|
const { library_id } = args;
|
|
2854
3262
|
|
|
2855
3263
|
try {
|
|
2856
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
3264
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
2857
3265
|
const plexToken = await this.authManager.getAuthToken();
|
|
2858
3266
|
|
|
2859
3267
|
let collectionsUrl;
|
|
@@ -2903,7 +3311,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
2903
3311
|
const { collection_id, sort = "titleSort", limit = 20, offset = 0 } = args;
|
|
2904
3312
|
|
|
2905
3313
|
try {
|
|
2906
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
3314
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
2907
3315
|
const plexToken = await this.authManager.getAuthToken();
|
|
2908
3316
|
|
|
2909
3317
|
const collectionUrl = `${plexUrl}/library/collections/${collection_id}/children`;
|
|
@@ -3002,7 +3410,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
3002
3410
|
const { item_key } = args;
|
|
3003
3411
|
|
|
3004
3412
|
try {
|
|
3005
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
3413
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
3006
3414
|
const plexToken = await this.authManager.getAuthToken();
|
|
3007
3415
|
|
|
3008
3416
|
const mediaUrl = `${plexUrl}/library/metadata/${item_key}`;
|
|
@@ -3221,7 +3629,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
3221
3629
|
}
|
|
3222
3630
|
|
|
3223
3631
|
if (part.size) {
|
|
3224
|
-
const sizeMB = Math.round(parseInt(part.size) / (1024 * 1024));
|
|
3632
|
+
const sizeMB = Math.round((parseInt(part.size, 10) || 0) / (1024 * 1024));
|
|
3225
3633
|
const sizeGB = (sizeMB / 1024).toFixed(2);
|
|
3226
3634
|
if (sizeMB > 1024) {
|
|
3227
3635
|
formatted += `\\n File Size: ${sizeGB} GB`;
|
|
@@ -3291,7 +3699,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
3291
3699
|
const { library_id, include_details = false } = args;
|
|
3292
3700
|
|
|
3293
3701
|
try {
|
|
3294
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
3702
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
3295
3703
|
const plexToken = await this.authManager.getAuthToken();
|
|
3296
3704
|
|
|
3297
3705
|
// Get library information first
|
|
@@ -3437,7 +3845,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
3437
3845
|
if (media.Part) {
|
|
3438
3846
|
for (const part of media.Part) {
|
|
3439
3847
|
if (part.size) {
|
|
3440
|
-
const sizeBytes = parseInt(part.size);
|
|
3848
|
+
const sizeBytes = parseInt(part.size, 10) || 0;
|
|
3441
3849
|
libraryStats.totalSize += sizeBytes;
|
|
3442
3850
|
|
|
3443
3851
|
if (includeDetails) {
|
|
@@ -3642,7 +4050,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
|
|
|
3642
4050
|
} = args;
|
|
3643
4051
|
|
|
3644
4052
|
try {
|
|
3645
|
-
const plexUrl = process.env.PLEX_URL || '
|
|
4053
|
+
const plexUrl = process.env.PLEX_URL || 'https://app.plex.tv';
|
|
3646
4054
|
const plexToken = await this.authManager.getAuthToken();
|
|
3647
4055
|
|
|
3648
4056
|
// Auto-detect music libraries if not specified
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plex-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server that enables Claude to query and manage Plex media libraries.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,13 +40,18 @@
|
|
|
40
40
|
"homepage": "https://github.com/vyb1ng/plex-mcp#readme",
|
|
41
41
|
"type": "commonjs",
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@modelcontextprotocol/sdk": "^1.12.
|
|
44
|
-
"axios": "^1.
|
|
45
|
-
"plex-oauth": "^1.
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.12.3",
|
|
44
|
+
"axios": "^1.10.0",
|
|
45
|
+
"plex-oauth": "^2.1.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/jest": "^29.5.14",
|
|
49
49
|
"axios-mock-adapter": "^2.1.0",
|
|
50
|
-
"jest": "^
|
|
50
|
+
"jest": "^30.0.0"
|
|
51
|
+
},
|
|
52
|
+
"overrides": {
|
|
53
|
+
"plex-oauth": {
|
|
54
|
+
"axios": "^1.10.0"
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
}
|