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.
@@ -12,7 +12,7 @@ jobs:
12
12
 
13
13
  strategy:
14
14
  matrix:
15
- node-version: [16.x, 18.x, 20.x]
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 || 'Plex MCP Server',
42
- device: process.env.PLEX_DEVICE || 'MCP Server',
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. Note: Non-smart playlists require an initial item (item_key parameter) to be created successfully.",
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 non-smart playlists, optional for smart playlists.",
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: ${loginUrl}
804
- 2. Sign into your Plex account
805
- 3. After signing in, run the check_auth_status tool to complete authentication
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
- Note: Keep this pin ID if you want to check auth status manually later.`
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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, item_key = null } = args;
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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
- beforeCount = beforeResponse.data.MediaContainer?.totalSize || 0;
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
- const params = {
2083
- 'X-Plex-Token': plexToken,
2084
- uri: item_keys.map(key => `server://${machineIdentifier}/com.plexapp.plugins.library/library/metadata/${key}`).join(',')
2085
- };
2086
-
2087
- const response = await axios.put(addUrl, null, {
2088
- params,
2089
- httpsAgent: this.getHttpsAgent()
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
- // Small delay to allow Plex server to update
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
- try {
2101
- const afterResponse = await axios.get(playlistItemsUrl, {
2102
- params: {
2103
- 'X-Plex-Token': plexToken
2104
- },
2105
- httpsAgent: this.getHttpsAgent()
2106
- });
2107
- afterCount = afterResponse.data.MediaContainer?.totalSize || 0;
2108
- } catch (error) {
2109
- // If items endpoint fails, playlist might be empty
2110
- afterCount = 0;
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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 || 'http://localhost:32400';
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.2.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.1",
44
- "axios": "^1.9.0",
45
- "plex-oauth": "^1.2.2"
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": "^29.7.0"
50
+ "jest": "^30.0.0"
51
+ },
52
+ "overrides": {
53
+ "plex-oauth": {
54
+ "axios": "^1.10.0"
55
+ }
51
56
  }
52
57
  }