plex-mcp 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +32 -5
  2. package/TODO.md +272 -0
  3. package/index.js +528 -115
  4. package/package.json +7 -4
package/README.md CHANGED
@@ -32,9 +32,9 @@ A Model Context Protocol (MCP) server for searching Plex media libraries using C
32
32
 
33
33
  ## Claude Desktop Configuration
34
34
 
35
- ### Option 1: Using npx (Recommended)
35
+ ### Option 1: Production (Using npx - Recommended)
36
36
 
37
- Add this configuration to your Claude Desktop settings:
37
+ Add this configuration to your Claude Desktop settings for the stable published version:
38
38
 
39
39
  ```json
40
40
  {
@@ -51,14 +51,14 @@ Add this configuration to your Claude Desktop settings:
51
51
  }
52
52
  ```
53
53
 
54
- ### Option 2: Local development
54
+ ### Option 2: Development (Local)
55
55
 
56
- For local development, add this configuration:
56
+ For development with your local code changes, add this configuration:
57
57
 
58
58
  ```json
59
59
  {
60
60
  "mcpServers": {
61
- "plex": {
61
+ "plex-dev": {
62
62
  "command": "node",
63
63
  "args": ["/path/to/your/plex-mcp/index.js"],
64
64
  "env": {
@@ -72,6 +72,33 @@ For local development, add this configuration:
72
72
 
73
73
  Replace `/path/to/your/plex-mcp/` with the actual path to this project directory.
74
74
 
75
+ ### Running Both Versions
76
+
77
+ You can configure both versions simultaneously by using different server names (`plex` and `plex-dev`):
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "plex": {
83
+ "command": "npx",
84
+ "args": ["plex-mcp"],
85
+ "env": {
86
+ "PLEX_URL": "http://your-plex-server:32400",
87
+ "PLEX_TOKEN": "your_plex_token"
88
+ }
89
+ },
90
+ "plex-dev": {
91
+ "command": "node",
92
+ "args": ["/path/to/your/plex-mcp/index.js"],
93
+ "env": {
94
+ "PLEX_URL": "http://your-plex-server:32400",
95
+ "PLEX_TOKEN": "your_plex_token"
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
75
102
  **Configuration Steps:**
76
103
  1. Open Claude Desktop settings (Cmd/Ctrl + ,)
77
104
  2. Navigate to the "MCP Servers" tab
package/TODO.md ADDED
@@ -0,0 +1,272 @@
1
+ # Plex MCP Server - TODO List
2
+
3
+ ## Recent Completions
4
+
5
+ ### ✅ Completed Items
6
+ - **Plex Item ID Investigation** - Identified correct field usage for playlist operations
7
+ - Confirmed `ratingKey` is the primary identifier for all playlist operations
8
+ - Documented ID field relationships (`ratingKey`, `key`, `machineIdentifier`)
9
+ - Found URI format conversion pattern for server operations
10
+ - **Playlist Operation Parameter Analysis** - Verified correct API parameter formats
11
+ - Confirmed `item_keys` array expects `ratingKey` values from search results
12
+ - Documented URI conversion: `ratingKey` → `server://localhost/com.plexapp.plugins.library/library/metadata/{key}`
13
+ - **Live Playlist Test Implementation** - Created comprehensive E2E test sequence
14
+ - Added playlist creation test with dynamic item search
15
+ - Added playlist item addition test with multiple items
16
+ - Added playlist browsing verification test
17
+ - Tests extract `ratingKey` from search results using `**ID: (\d+)**` pattern
18
+ - **Browse Playlist Bug Fix** - Fixed empty results issue in `handleBrowsePlaylist`
19
+ - Added missing pagination parameters (`X-Plex-Container-Start`, `X-Plex-Container-Size`)
20
+ - Added fallback to `/playlists/{id}/items` endpoint when main endpoint returns empty
21
+ - Added `leafCount` check to detect playlists that should have items
22
+
23
+ ## Unimplemented Plex API Features
24
+
25
+ ### Critical Missing APIs (High Priority)
26
+
27
+ #### Session Management
28
+ - [ ] **get_active_sessions** - List current "Now Playing" sessions
29
+ - Endpoint: `/status/sessions`
30
+ - Returns: Active playback sessions with user, client, media info
31
+ - [ ] **get_transcode_sessions** - List active transcoding operations
32
+ - Endpoint: `/transcode/sessions`
33
+ - Returns: Transcoding progress, quality settings, resource usage
34
+ - [ ] **terminate_session** - Stop/kill active playback sessions
35
+ - Endpoint: `/status/sessions/terminate`
36
+ - Action: Force stop sessions by session ID
37
+
38
+ #### Playback Control
39
+ - [ ] **control_playback** - Control playback (play/pause/stop/seek)
40
+ - Endpoint: `/player/playback/{command}`
41
+ - Commands: play, pause, stop, stepForward, stepBack, seekTo
42
+ - [ ] **start_playback** - Initiate playback on specific clients
43
+ - Endpoint: `/player/playback/playMedia`
44
+ - Parameters: media key, client ID, resume offset
45
+ - [ ] **remote_control** - Advanced remote control operations
46
+ - Endpoint: `/player/navigation/{command}`
47
+ - Commands: moveUp, moveDown, select, back, home
48
+
49
+ #### Client & Device Management
50
+ - [ ] **get_clients** - List available Plex clients/players
51
+ - Endpoint: `/clients`
52
+ - Returns: Client names, IDs, capabilities, online status
53
+ - [ ] **get_devices** - List all registered devices
54
+ - Endpoint: `/devices`
55
+ - Returns: Device info, last seen, platform details
56
+ - [ ] **get_servers** - List available Plex servers
57
+ - Endpoint: `/servers`
58
+ - Returns: Server list for multi-server setups
59
+
60
+ ### Server Administration (Medium Priority)
61
+
62
+ #### Server Info & Management
63
+ - [ ] **get_server_info** - Server capabilities and status
64
+ - Endpoint: `/`
65
+ - Returns: Version, capabilities, transcoder info, platform
66
+ - [ ] **get_server_preferences** - Server configuration settings
67
+ - Endpoint: `/:/prefs`
68
+ - Returns: All server preferences and settings
69
+ - [ ] **scan_library** - Trigger library content scan
70
+ - Endpoint: `/library/sections/{id}/refresh`
71
+ - Action: Force library scan for new content
72
+ - [ ] **refresh_metadata** - Force metadata refresh for items
73
+ - Endpoint: `/library/metadata/{id}/refresh`
74
+ - Action: Re-download metadata, artwork, etc.
75
+
76
+ #### User Management
77
+ - [ ] **get_users** - List server users and accounts
78
+ - Endpoint: `/accounts`
79
+ - Returns: User list, permissions, sharing status
80
+ - [ ] **get_user_activity** - User-specific activity logs
81
+ - Endpoint: `/status/sessions/history/all?accountID={id}`
82
+ - Returns: Per-user watch history and statistics
83
+
84
+ ### Content Discovery & Recommendations (Medium Priority)
85
+
86
+ #### Advanced Discovery
87
+ - [ ] **get_content_hubs** - Plex's recommendation engine
88
+ - Endpoint: `/hubs`
89
+ - Returns: Curated content recommendations, trending
90
+ - [ ] **get_discover_content** - Discover new content across libraries
91
+ - Endpoint: `/library/sections/all?discover=1`
92
+ - Returns: Cross-library content discovery
93
+ - [ ] **get_trending** - Trending content on Plex platform
94
+ - Endpoint: `/hubs/trending`
95
+ - Returns: Popular content across Plex network
96
+
97
+ #### Metadata Enhancement
98
+ - [ ] **get_genres** - Available genres across libraries
99
+ - Endpoint: `/library/sections/{id}/genre`
100
+ - Returns: Genre list with item counts
101
+ - [ ] **get_years** - Available years across libraries
102
+ - Endpoint: `/library/sections/{id}/year`
103
+ - Returns: Year list with item counts
104
+ - [ ] **get_studios** - Available studios/networks
105
+ - Endpoint: `/library/sections/{id}/studio`
106
+ - Returns: Studio/network list with item counts
107
+ - [ ] **get_directors** - Available directors/actors
108
+ - Endpoint: `/library/sections/{id}/director`
109
+ - Returns: People list with filmography counts
110
+
111
+ ### Media Management (Medium Priority)
112
+
113
+ #### Watch Status Management
114
+ - [ ] **mark_watched** - Mark items as watched
115
+ - Endpoint: `/:/scrobble?key={id}&identifier=com.plexapp.plugins.library`
116
+ - Action: Set watch status, update play count
117
+ - [ ] **mark_unwatched** - Mark items as unwatched
118
+ - Endpoint: `/:/unscrobble?key={id}&identifier=com.plexapp.plugins.library`
119
+ - Action: Remove watch status, reset play count
120
+ - [ ] **set_rating** - Rate content (stars/thumbs)
121
+ - Endpoint: `/:/rate?key={id}&rating={rating}&identifier=com.plexapp.plugins.library`
122
+ - Action: Set user rating for content
123
+
124
+ #### Library Maintenance
125
+ - [ ] **optimize_library** - Database optimization operations
126
+ - Endpoint: `/library/optimize`
127
+ - Action: Clean up database, optimize indexes
128
+ - [ ] **empty_trash** - Empty library trash/deleted items
129
+ - Endpoint: `/library/sections/{id}/emptyTrash`
130
+ - Action: Permanently delete trashed items
131
+ - [ ] **update_library** - Update library metadata
132
+ - Endpoint: `/library/sections/{id}/update`
133
+ - Action: Update library without full scan
134
+
135
+ ### Advanced Features (Low Priority)
136
+
137
+ #### Transcoding Management
138
+ - [ ] **get_transcoding_settings** - Transcoding preferences
139
+ - Endpoint: `/:/prefs?group=transcoder`
140
+ - Returns: Quality settings, codec preferences
141
+ - [ ] **optimize_content** - Content optimization for devices
142
+ - Endpoint: `/library/metadata/{id}/optimize`
143
+ - Action: Pre-transcode content for specific devices
144
+
145
+ #### Sync & Download
146
+ - [ ] **get_sync_items** - Sync queue and downloaded items
147
+ - Endpoint: `/sync/items`
148
+ - Returns: Sync status, download queue
149
+ - [ ] **download_media** - Download content for offline viewing
150
+ - Endpoint: `/sync/items`
151
+ - Action: Queue content for download/sync
152
+
153
+ #### Webhooks & Events
154
+ - [ ] **get_webhooks** - List configured webhooks
155
+ - Endpoint: `/:/webhooks`
156
+ - Returns: Webhook URLs and trigger events
157
+ - [ ] **listen_events** - Real-time event streaming
158
+ - Endpoint: `/:/events` (WebSocket/SSE)
159
+ - Returns: Live server events, playback updates
160
+
161
+ ## Missing Test Coverage
162
+
163
+ ### Unit Tests
164
+ - [ ] **Session management handlers** - Tests for new session control APIs
165
+ - [ ] **Playback control handlers** - Tests for media control functionality
166
+ - [ ] **Client management handlers** - Tests for device/client discovery
167
+ - [ ] **Server admin handlers** - Tests for administrative functions
168
+ - [ ] **User management handlers** - Tests for multi-user functionality
169
+ - [ ] **Content discovery handlers** - Tests for recommendation features
170
+ - [ ] **Watch status handlers** - Tests for marking watched/unwatched
171
+ - [ ] **Library management handlers** - Tests for scan/refresh operations
172
+
173
+ ### Integration Tests
174
+ - [ ] **Multi-library operations** - Cross-library search and discovery
175
+ - [ ] **Playlist management with new content** - Advanced playlist operations
176
+ - [ ] **User permission scenarios** - Multi-user access control
177
+ - [ ] **Server configuration changes** - Settings modification testing
178
+ - [ ] **Transcoding workflow** - End-to-end transcoding scenarios
179
+ - [ ] **Sync/download workflows** - Offline content management
180
+
181
+ ### Mock Data Enhancement
182
+ - [ ] **Session data mocks** - Active session responses
183
+ - [ ] **Client data mocks** - Available client/device responses
184
+ - [ ] **Server info mocks** - Server capabilities and status
185
+ - [ ] **User data mocks** - Multi-user account responses
186
+ - [ ] **Transcoding mocks** - Transcoding session responses
187
+ - [ ] **Webhook mocks** - Event and webhook responses
188
+
189
+ ## E2E Tests to Add
190
+
191
+ ### Real Server Integration
192
+ - [ ] **Live session monitoring** - Connect to real server, monitor active sessions
193
+ - Test with actual playback sessions
194
+ - Verify session data accuracy
195
+ - Test session termination
196
+ - [ ] **Live playback control** - Control actual Plex clients
197
+ - Test play/pause/stop commands
198
+ - Test seek functionality
199
+ - Test client discovery and selection
200
+ - [ ] **Live library management** - Real library operations
201
+ - Test library scanning
202
+ - Test metadata refresh
203
+ - Test library optimization
204
+ - [ ] **Live user scenarios** - Multi-user testing
205
+ - Test shared library access
206
+ - Test user-specific history
207
+ - Test permission boundaries
208
+
209
+ ### Cross-Platform Testing
210
+ - [ ] **Multiple client types** - Test with various Plex clients
211
+ - Desktop app, mobile app, web player
212
+ - Smart TV apps, streaming devices
213
+ - Verify control compatibility
214
+ - [ ] **Multiple server versions** - Test against different Plex server versions
215
+ - Latest stable, previous versions
216
+ - PlexPass vs free features
217
+ - API compatibility testing
218
+
219
+ ### Network Scenarios
220
+ - [ ] **SSL/TLS configurations** - Various security setups
221
+ - Self-signed certificates
222
+ - Valid SSL certificates
223
+ - Mixed HTTP/HTTPS environments
224
+ - [ ] **Remote access testing** - Plex relay and direct connections
225
+ - Remote server access via Plex.tv
226
+ - Direct IP connections
227
+ - VPN/tunnel scenarios
228
+ - [ ] **Performance testing** - Large library scenarios
229
+ - Libraries with 10k+ items
230
+ - Multiple concurrent operations
231
+ - Memory and CPU usage monitoring
232
+
233
+ ## Code Quality & Architecture
234
+
235
+ ### Error Handling Improvements
236
+ - [ ] **Granular error types** - Specific error classes for different failure modes
237
+ - [ ] **Retry mechanisms** - Automatic retry for transient failures
238
+ - [ ] **Circuit breaker pattern** - Fail-fast for consistently failing operations
239
+ - [ ] **Rate limiting** - Respect Plex server rate limits
240
+
241
+ ### Performance Optimizations
242
+ - [ ] **Response caching** - Cache frequently accessed data
243
+ - [ ] **Batch operations** - Combine multiple API calls when possible
244
+ - [ ] **Streaming responses** - Handle large datasets efficiently
245
+ - [ ] **Connection pooling** - Reuse HTTP connections
246
+
247
+ ### Documentation
248
+ - [ ] **API reference docs** - Complete documentation for all tools
249
+ - [ ] **Usage examples** - Real-world scenarios and code examples
250
+ - [ ] **Troubleshooting guide** - Common issues and solutions
251
+ - [ ] **Performance tuning** - Optimization recommendations
252
+
253
+ ### Security Enhancements
254
+ - [ ] **Token validation** - Verify Plex token format and permissions
255
+ - [ ] **SSL certificate validation** - Proper certificate handling
256
+ - [ ] **Input sanitization** - Validate all user inputs
257
+ - [ ] **Audit logging** - Log all administrative operations
258
+
259
+ ## Development Infrastructure
260
+
261
+ ### CI/CD Improvements
262
+ - [ ] **Automated testing** - Run full test suite on every commit
263
+ - [ ] **Code coverage tracking** - Monitor and improve test coverage
264
+ - [ ] **Performance benchmarks** - Track performance regressions
265
+ - [ ] **Security scanning** - Automated vulnerability detection
266
+
267
+ ### Development Tools
268
+ - [ ] **Mock Plex server** - Local development server for testing
269
+ - [ ] **Test data generator** - Generate realistic test datasets
270
+ - [ ] **Performance profiler** - Identify bottlenecks and optimize
271
+ - [ ] **Documentation generator** - Auto-generate API docs from code
272
+
package/index.js CHANGED
@@ -25,6 +25,14 @@ class PlexMCPServer {
25
25
  this.setupToolHandlers();
26
26
  }
27
27
 
28
+ getHttpsAgent() {
29
+ const verifySSL = process.env.PLEX_VERIFY_SSL !== 'false';
30
+ return new (require('https').Agent)({
31
+ rejectUnauthorized: verifySSL,
32
+ minVersion: 'TLSv1.2'
33
+ });
34
+ }
35
+
28
36
  setupToolHandlers() {
29
37
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
30
38
  return {
@@ -380,6 +388,25 @@ class PlexMCPServer {
380
388
  required: [],
381
389
  },
382
390
  },
391
+ {
392
+ name: "browse_playlist",
393
+ description: "Browse and view the contents of a specific playlist with full track metadata",
394
+ inputSchema: {
395
+ type: "object",
396
+ properties: {
397
+ playlist_id: {
398
+ type: "string",
399
+ description: "The ID of the playlist to browse",
400
+ },
401
+ limit: {
402
+ type: "number",
403
+ description: "Maximum number of items to return (default: 50)",
404
+ default: 50,
405
+ },
406
+ },
407
+ required: ["playlist_id"],
408
+ },
409
+ },
383
410
  {
384
411
  name: "create_playlist",
385
412
  description: "Create a new playlist on the Plex server. Note: Non-smart playlists require an initial item (item_key parameter) to be created successfully.",
@@ -429,6 +456,10 @@ class PlexMCPServer {
429
456
  required: ["playlist_id", "item_keys"],
430
457
  },
431
458
  },
459
+ // DISABLED: remove_from_playlist - PROBLEMATIC due to Plex API limitations
460
+ // This operation removes ALL instances of matching items, not just one
461
+ // Uncomment only after implementing safer removal patterns
462
+ /*
432
463
  {
433
464
  name: "remove_from_playlist",
434
465
  description: "Remove items from an existing playlist",
@@ -450,6 +481,7 @@ class PlexMCPServer {
450
481
  required: ["playlist_id", "item_keys"],
451
482
  },
452
483
  },
484
+ */
453
485
  {
454
486
  name: "delete_playlist",
455
487
  description: "Delete an existing playlist",
@@ -611,12 +643,15 @@ class PlexMCPServer {
611
643
  return await this.handleOnDeck(request.params.arguments);
612
644
  case "list_playlists":
613
645
  return await this.handleListPlaylists(request.params.arguments);
646
+ case "browse_playlist":
647
+ return await this.handleBrowsePlaylist(request.params.arguments);
614
648
  case "create_playlist":
615
649
  return await this.handleCreatePlaylist(request.params.arguments);
616
650
  case "add_to_playlist":
617
651
  return await this.handleAddToPlaylist(request.params.arguments);
618
- case "remove_from_playlist":
619
- return await this.handleRemoveFromPlaylist(request.params.arguments);
652
+ // DISABLED: remove_from_playlist - PROBLEMATIC operation
653
+ // case "remove_from_playlist":
654
+ // return await this.handleRemoveFromPlaylist(request.params.arguments);
620
655
  case "delete_playlist":
621
656
  return await this.handleDeletePlaylist(request.params.arguments);
622
657
  case "get_watched_status":
@@ -690,10 +725,7 @@ class PlexMCPServer {
690
725
 
691
726
  const response = await axios.get(searchUrl, {
692
727
  params,
693
- httpsAgent: new (require('https').Agent)({
694
- rejectUnauthorized: false,
695
- minVersion: 'TLSv1.2'
696
- })
728
+ httpsAgent: this.getHttpsAgent()
697
729
  });
698
730
 
699
731
  let results = this.parseSearchResults(response.data);
@@ -786,6 +818,12 @@ class PlexMCPServer {
786
818
  contentRating: item.contentRating,
787
819
  Media: item.Media,
788
820
  key: item.key,
821
+ ratingKey: item.ratingKey, // Critical: the unique identifier for playlist operations
822
+ // Additional hierarchical info for music tracks
823
+ parentTitle: item.parentTitle, // Album name
824
+ grandparentTitle: item.grandparentTitle, // Artist name
825
+ parentRatingKey: item.parentRatingKey, // Album ID
826
+ grandparentRatingKey: item.grandparentRatingKey, // Artist ID
789
827
  // Additional metadata for basic filters
790
828
  studio: item.studio,
791
829
  genres: item.Genre ? item.Genre.map(g => g.tag) : [],
@@ -807,8 +845,24 @@ class PlexMCPServer {
807
845
  formatted += ` - ${item.type}`;
808
846
  }
809
847
 
848
+ // Add artist/album info for music tracks
849
+ if (item.grandparentTitle && item.parentTitle) {
850
+ formatted += `\n Artist: ${item.grandparentTitle} | Album: ${item.parentTitle}`;
851
+ } else if (item.parentTitle) {
852
+ formatted += `\n Album/Show: ${item.parentTitle}`;
853
+ }
854
+
810
855
  if (item.rating) {
811
- formatted += ` - Rating: ${item.rating}`;
856
+ formatted += `\n Rating: ${item.rating}`;
857
+ }
858
+
859
+ if (item.duration) {
860
+ formatted += `\n Duration: ${this.formatDuration(item.duration)}`;
861
+ }
862
+
863
+ // CRITICAL: Show the ratingKey for playlist operations
864
+ if (item.ratingKey) {
865
+ formatted += `\n **ID: ${item.ratingKey}** (use this for playlists)`;
812
866
  }
813
867
 
814
868
  if (item.summary) {
@@ -819,6 +873,21 @@ class PlexMCPServer {
819
873
  }).join('\n\n');
820
874
  }
821
875
 
876
+ formatDuration(milliseconds) {
877
+ if (!milliseconds || milliseconds === 0) return 'Unknown';
878
+
879
+ const seconds = Math.floor(milliseconds / 1000);
880
+ const minutes = Math.floor(seconds / 60);
881
+ const hours = Math.floor(minutes / 60);
882
+
883
+ if (hours > 0) {
884
+ const remainingMinutes = minutes % 60;
885
+ return `${hours}h ${remainingMinutes}m`;
886
+ } else {
887
+ return `${minutes}m`;
888
+ }
889
+ }
890
+
822
891
  async handleBrowseLibraries(args) {
823
892
  try {
824
893
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
@@ -835,10 +904,7 @@ class PlexMCPServer {
835
904
 
836
905
  const response = await axios.get(librariesUrl, {
837
906
  params,
838
- httpsAgent: new (require('https').Agent)({
839
- rejectUnauthorized: false,
840
- minVersion: 'TLSv1.2'
841
- })
907
+ httpsAgent: this.getHttpsAgent()
842
908
  });
843
909
 
844
910
  const libraries = this.parseLibraries(response.data);
@@ -965,10 +1031,7 @@ class PlexMCPServer {
965
1031
 
966
1032
  const response = await axios.get(libraryUrl, {
967
1033
  params,
968
- httpsAgent: new (require('https').Agent)({
969
- rejectUnauthorized: false,
970
- minVersion: 'TLSv1.2'
971
- })
1034
+ httpsAgent: this.getHttpsAgent()
972
1035
  });
973
1036
 
974
1037
  let results = this.parseLibraryContent(response.data);
@@ -1090,10 +1153,7 @@ class PlexMCPServer {
1090
1153
 
1091
1154
  const response = await axios.get(recentUrl, {
1092
1155
  params,
1093
- httpsAgent: new (require('https').Agent)({
1094
- rejectUnauthorized: false,
1095
- minVersion: 'TLSv1.2'
1096
- })
1156
+ httpsAgent: this.getHttpsAgent()
1097
1157
  });
1098
1158
 
1099
1159
  const results = this.parseLibraryContent(response.data);
@@ -1191,10 +1251,7 @@ class PlexMCPServer {
1191
1251
 
1192
1252
  const response = await axios.get(historyUrl, {
1193
1253
  params,
1194
- httpsAgent: new (require('https').Agent)({
1195
- rejectUnauthorized: false,
1196
- minVersion: 'TLSv1.2'
1197
- })
1254
+ httpsAgent: this.getHttpsAgent()
1198
1255
  });
1199
1256
 
1200
1257
  const results = this.parseWatchHistory(response.data);
@@ -1314,10 +1371,7 @@ class PlexMCPServer {
1314
1371
 
1315
1372
  const response = await axios.get(onDeckUrl, {
1316
1373
  params,
1317
- httpsAgent: new (require('https').Agent)({
1318
- rejectUnauthorized: false,
1319
- minVersion: 'TLSv1.2'
1320
- })
1374
+ httpsAgent: this.getHttpsAgent()
1321
1375
  });
1322
1376
 
1323
1377
  const results = this.parseOnDeck(response.data);
@@ -1423,10 +1477,7 @@ class PlexMCPServer {
1423
1477
 
1424
1478
  const response = await axios.get(playlistsUrl, {
1425
1479
  params,
1426
- httpsAgent: new (require('https').Agent)({
1427
- rejectUnauthorized: false,
1428
- minVersion: 'TLSv1.2'
1429
- })
1480
+ httpsAgent: this.getHttpsAgent()
1430
1481
  });
1431
1482
 
1432
1483
  const playlists = this.parsePlaylists(response.data);
@@ -1456,6 +1507,154 @@ class PlexMCPServer {
1456
1507
  }
1457
1508
  }
1458
1509
 
1510
+ async handleBrowsePlaylist(args) {
1511
+ const { playlist_id, limit = 50 } = args;
1512
+
1513
+ try {
1514
+ const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1515
+ const plexToken = process.env.PLEX_TOKEN;
1516
+
1517
+ if (!plexToken) {
1518
+ throw new Error('PLEX_TOKEN environment variable is required');
1519
+ }
1520
+
1521
+ // First get playlist info
1522
+ const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1523
+ const response = await axios.get(playlistUrl, {
1524
+ params: {
1525
+ 'X-Plex-Token': plexToken,
1526
+ 'X-Plex-Container-Start': 0,
1527
+ 'X-Plex-Container-Size': limit || 50
1528
+ },
1529
+ httpsAgent: this.getHttpsAgent()
1530
+ });
1531
+
1532
+ const playlistData = response.data.MediaContainer;
1533
+ if (!playlistData || !playlistData.Metadata || playlistData.Metadata.length === 0) {
1534
+ return {
1535
+ content: [
1536
+ {
1537
+ type: "text",
1538
+ text: `Playlist with ID ${playlist_id} not found`,
1539
+ },
1540
+ ],
1541
+ };
1542
+ }
1543
+
1544
+ const playlist = playlistData.Metadata[0];
1545
+
1546
+ // Try to get items from the current response first
1547
+ let items = playlistData.Metadata[0].Metadata || [];
1548
+
1549
+ // If no items found, try the /items endpoint specifically
1550
+ if (items.length === 0 && playlist.leafCount && playlist.leafCount > 0) {
1551
+ try {
1552
+ const itemsUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1553
+ const itemsResponse = await axios.get(itemsUrl, {
1554
+ params: {
1555
+ 'X-Plex-Token': plexToken,
1556
+ 'X-Plex-Container-Start': 0,
1557
+ 'X-Plex-Container-Size': limit || 50
1558
+ },
1559
+ httpsAgent: this.getHttpsAgent()
1560
+ });
1561
+
1562
+ const itemsData = itemsResponse.data.MediaContainer;
1563
+ if (itemsData && itemsData.Metadata) {
1564
+ items = itemsData.Metadata;
1565
+ }
1566
+ } catch (itemsError) {
1567
+ console.error(`Failed to fetch playlist items via /items endpoint: ${itemsError.message}`);
1568
+ return {
1569
+ content: [
1570
+ {
1571
+ type: "text",
1572
+ text: `Error retrieving playlist items: ${itemsError.message}`,
1573
+ },
1574
+ ],
1575
+ };
1576
+ }
1577
+ }
1578
+
1579
+ // Limit results if specified
1580
+ const limitedItems = limit ? items.slice(0, limit) : items;
1581
+
1582
+ let resultText = `**${playlist.title}**`;
1583
+ if (playlist.smart) {
1584
+ resultText += ` (Smart Playlist)`;
1585
+ }
1586
+ resultText += `\n`;
1587
+ if (playlist.summary) {
1588
+ resultText += `${playlist.summary}\n`;
1589
+ }
1590
+ resultText += `Duration: ${this.formatDuration(playlist.duration || 0)}\n`;
1591
+ resultText += `Items: ${items.length}`;
1592
+ if (limit && items.length > limit) {
1593
+ resultText += ` (showing first ${limit})`;
1594
+ }
1595
+ resultText += `\n\n`;
1596
+
1597
+ if (limitedItems.length === 0) {
1598
+ resultText += `This playlist appears to be empty or items could not be retrieved.`;
1599
+ } else {
1600
+ resultText += limitedItems.map((item, index) => {
1601
+ let itemText = `${index + 1}. **${item.title}**`;
1602
+
1603
+ // Add artist/album info for music
1604
+ if (item.grandparentTitle && item.parentTitle) {
1605
+ itemText += `\n Artist: ${item.grandparentTitle}\n Album: ${item.parentTitle}`;
1606
+ } else if (item.parentTitle) {
1607
+ itemText += `\n Album/Show: ${item.parentTitle}`;
1608
+ }
1609
+
1610
+ // Add duration
1611
+ if (item.duration) {
1612
+ itemText += `\n Duration: ${this.formatDuration(item.duration)}`;
1613
+ }
1614
+
1615
+ // Add rating key for identification
1616
+ itemText += `\n ID: ${item.ratingKey}`;
1617
+
1618
+ // Add media type
1619
+ const mediaType = this.getMediaTypeFromItem(item);
1620
+ if (mediaType) {
1621
+ itemText += `\n Type: ${mediaType}`;
1622
+ }
1623
+
1624
+ return itemText;
1625
+ }).join('\n\n');
1626
+ }
1627
+
1628
+ return {
1629
+ content: [
1630
+ {
1631
+ type: "text",
1632
+ text: resultText,
1633
+ },
1634
+ ],
1635
+ };
1636
+ } catch (error) {
1637
+ return {
1638
+ content: [
1639
+ {
1640
+ type: "text",
1641
+ text: `Error browsing playlist: ${error.message}`,
1642
+ },
1643
+ ],
1644
+ isError: true,
1645
+ };
1646
+ }
1647
+ }
1648
+
1649
+ getMediaTypeFromItem(item) {
1650
+ if (item.type === 'track') return 'Music Track';
1651
+ if (item.type === 'episode') return 'TV Episode';
1652
+ if (item.type === 'movie') return 'Movie';
1653
+ if (item.type === 'artist') return 'Artist';
1654
+ if (item.type === 'album') return 'Album';
1655
+ return item.type || 'Unknown';
1656
+ }
1657
+
1459
1658
  parsePlaylists(data) {
1460
1659
  if (!data.MediaContainer || !data.MediaContainer.Metadata) {
1461
1660
  return [];
@@ -1540,10 +1739,7 @@ class PlexMCPServer {
1540
1739
  // First get server info to get machine identifier
1541
1740
  const serverResponse = await axios.get(`${plexUrl}/`, {
1542
1741
  headers: { 'X-Plex-Token': plexToken },
1543
- httpsAgent: new (require('https').Agent)({
1544
- rejectUnauthorized: false,
1545
- minVersion: 'TLSv1.2'
1546
- })
1742
+ httpsAgent: this.getHttpsAgent()
1547
1743
  });
1548
1744
 
1549
1745
  const machineIdentifier = serverResponse.data?.MediaContainer?.machineIdentifier;
@@ -1576,20 +1772,22 @@ class PlexMCPServer {
1576
1772
  headers: {
1577
1773
  'Content-Length': '0'
1578
1774
  },
1579
- httpsAgent: new (require('https').Agent)({
1580
- rejectUnauthorized: false,
1581
- minVersion: 'TLSv1.2'
1582
- })
1775
+ httpsAgent: this.getHttpsAgent()
1583
1776
  });
1584
1777
 
1585
1778
  // Get the created playlist info from the response
1586
1779
  const playlistData = response.data?.MediaContainer?.Metadata?.[0];
1587
1780
 
1588
- let resultText = `Successfully created ${smart ? 'smart ' : ''}playlist: **${title}**`;
1781
+ let resultText = `✅ Successfully created ${smart ? 'smart ' : ''}playlist: **${title}**`;
1589
1782
  if (playlistData) {
1590
- resultText += `\n Playlist ID: ${playlistData.ratingKey}`;
1783
+ resultText += `\n **Playlist ID: ${playlistData.ratingKey}** (use this ID for future operations)`;
1591
1784
  resultText += `\n Type: ${type}`;
1592
1785
  if (smart) resultText += `\n Smart Playlist: Yes`;
1786
+ if (item_key && !smart) {
1787
+ resultText += `\n Initial item added: ${item_key}`;
1788
+ }
1789
+ } else {
1790
+ resultText += `\n ⚠️ Playlist created but details not available - check your playlists`;
1593
1791
  }
1594
1792
 
1595
1793
  return {
@@ -1628,6 +1826,17 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1628
1826
  async handleAddToPlaylist(args) {
1629
1827
  const { playlist_id, item_keys } = args;
1630
1828
 
1829
+ // Input validation
1830
+ if (!playlist_id || typeof playlist_id !== 'string') {
1831
+ throw new Error('Valid playlist_id is required');
1832
+ }
1833
+ if (!item_keys || !Array.isArray(item_keys) || item_keys.length === 0) {
1834
+ throw new Error('item_keys must be a non-empty array');
1835
+ }
1836
+ if (item_keys.some(key => !key || typeof key !== 'string')) {
1837
+ throw new Error('All item_keys must be non-empty strings');
1838
+ }
1839
+
1631
1840
  try {
1632
1841
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1633
1842
  const plexToken = process.env.PLEX_TOKEN;
@@ -1636,21 +1845,103 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1636
1845
  throw new Error('PLEX_TOKEN environment variable is required');
1637
1846
  }
1638
1847
 
1848
+ // Get playlist info before adding items
1849
+ const playlistInfoUrl = `${plexUrl}/playlists/${playlist_id}`;
1850
+ const playlistItemsUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1851
+
1852
+ // Get playlist metadata (title, etc.)
1853
+ const playlistInfoResponse = await axios.get(playlistInfoUrl, {
1854
+ params: {
1855
+ 'X-Plex-Token': plexToken
1856
+ },
1857
+ httpsAgent: this.getHttpsAgent()
1858
+ });
1859
+
1860
+ const playlistInfo = playlistInfoResponse.data.MediaContainer;
1861
+ const playlistTitle = playlistInfo.Metadata && playlistInfo.Metadata[0]
1862
+ ? playlistInfo.Metadata[0].title : `Playlist ${playlist_id}`;
1863
+
1864
+ // Get current playlist items count
1865
+ let beforeCount = 0;
1866
+ try {
1867
+ const beforeResponse = await axios.get(playlistItemsUrl, {
1868
+ params: {
1869
+ 'X-Plex-Token': plexToken
1870
+ },
1871
+ httpsAgent: this.getHttpsAgent()
1872
+ });
1873
+ beforeCount = beforeResponse.data.MediaContainer?.totalSize || 0;
1874
+ } catch (error) {
1875
+ // If items endpoint fails, playlist might be empty
1876
+ beforeCount = 0;
1877
+ }
1878
+
1879
+ // Get server machine identifier for proper URI format
1880
+ const serverResponse = await axios.get(`${plexUrl}/`, {
1881
+ headers: { 'X-Plex-Token': plexToken },
1882
+ httpsAgent: this.getHttpsAgent()
1883
+ });
1884
+
1885
+ const machineIdentifier = serverResponse.data?.MediaContainer?.machineIdentifier;
1886
+ if (!machineIdentifier) {
1887
+ throw new Error('Could not get server machine identifier');
1888
+ }
1889
+
1639
1890
  const addUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1640
1891
  const params = {
1641
1892
  'X-Plex-Token': plexToken,
1642
- uri: item_keys.map(key => `server://localhost/com.plexapp.plugins.library/library/metadata/${key}`).join(',')
1893
+ uri: item_keys.map(key => `server://${machineIdentifier}/com.plexapp.plugins.library/library/metadata/${key}`).join(',')
1643
1894
  };
1644
1895
 
1645
1896
  const response = await axios.put(addUrl, null, {
1646
1897
  params,
1647
- httpsAgent: new (require('https').Agent)({
1648
- rejectUnauthorized: false,
1649
- minVersion: 'TLSv1.2'
1650
- })
1898
+ httpsAgent: this.getHttpsAgent()
1651
1899
  });
1652
1900
 
1653
- const resultText = `Successfully added ${item_keys.length} item(s) to playlist ${playlist_id}`;
1901
+ // Check if the PUT request was successful based on HTTP status
1902
+ const putSuccessful = response.status >= 200 && response.status < 300;
1903
+
1904
+ // Small delay to allow Plex server to update
1905
+ await new Promise(resolve => setTimeout(resolve, 100));
1906
+
1907
+ // Verify the addition by checking the playlist items again
1908
+ let afterCount = 0;
1909
+ try {
1910
+ const afterResponse = await axios.get(playlistItemsUrl, {
1911
+ params: {
1912
+ 'X-Plex-Token': plexToken
1913
+ },
1914
+ httpsAgent: this.getHttpsAgent()
1915
+ });
1916
+ afterCount = afterResponse.data.MediaContainer?.totalSize || 0;
1917
+ } catch (error) {
1918
+ // If items endpoint fails, playlist might be empty
1919
+ afterCount = 0;
1920
+ }
1921
+
1922
+ const actualAdded = afterCount - beforeCount;
1923
+ const attempted = item_keys.length;
1924
+
1925
+ let resultText = `Playlist "${playlistTitle}" update:\n`;
1926
+ resultText += `• Attempted to add: ${attempted} item(s)\n`;
1927
+ resultText += `• Actually added: ${actualAdded} item(s)\n`;
1928
+ resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n`;
1929
+
1930
+ // If HTTP request was successful but count didn't change,
1931
+ // it's likely the items already exist or are duplicates
1932
+ if (actualAdded === attempted) {
1933
+ resultText += `✅ All items added successfully!`;
1934
+ } else if (actualAdded > 0) {
1935
+ resultText += `⚠️ Partial success: ${attempted - actualAdded} item(s) may have been duplicates or invalid`;
1936
+ } else if (putSuccessful) {
1937
+ resultText += `✅ API request successful! Items may already exist in playlist or were duplicates.\n`;
1938
+ resultText += `ℹ️ This is normal behavior - Plex doesn't add duplicate items.`;
1939
+ } else {
1940
+ resultText += `❌ No items were added. This may indicate:\n`;
1941
+ resultText += ` - Invalid item IDs (use ratingKey from search results)\n`;
1942
+ resultText += ` - Items already exist in playlist\n`;
1943
+ resultText += ` - Permission issues`;
1944
+ }
1654
1945
 
1655
1946
  return {
1656
1947
  content: [
@@ -1661,11 +1952,27 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1661
1952
  ],
1662
1953
  };
1663
1954
  } catch (error) {
1955
+ // Enhanced error handling with specific error types
1956
+ let errorMessage = `Error adding items to playlist: ${error.message}`;
1957
+
1958
+ if (error.response) {
1959
+ const status = error.response.status;
1960
+ if (status === 404) {
1961
+ errorMessage = `Playlist with ID ${playlist_id} not found`;
1962
+ } else if (status === 401 || status === 403) {
1963
+ errorMessage = `Permission denied: Check your Plex token and server access`;
1964
+ } else if (status >= 500) {
1965
+ errorMessage = `Plex server error (${status}): ${error.message}`;
1966
+ }
1967
+ } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
1968
+ errorMessage = `Cannot connect to Plex server: Check PLEX_URL configuration`;
1969
+ }
1970
+
1664
1971
  return {
1665
1972
  content: [
1666
1973
  {
1667
1974
  type: "text",
1668
- text: `Error adding items to playlist: ${error.message}`,
1975
+ text: errorMessage,
1669
1976
  },
1670
1977
  ],
1671
1978
  isError: true,
@@ -1673,9 +1980,24 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1673
1980
  }
1674
1981
  }
1675
1982
 
1983
+ // DISABLED METHOD - PROBLEMATIC OPERATION
1984
+ // This method is currently disabled due to destructive Plex API behavior
1985
+ // It removes ALL instances of matching items, not just one instance
1986
+ // Use with extreme caution - consider implementing safer alternatives
1676
1987
  async handleRemoveFromPlaylist(args) {
1677
1988
  const { playlist_id, item_keys } = args;
1678
1989
 
1990
+ // Input validation
1991
+ if (!playlist_id || typeof playlist_id !== 'string') {
1992
+ throw new Error('Valid playlist_id is required');
1993
+ }
1994
+ if (!item_keys || !Array.isArray(item_keys) || item_keys.length === 0) {
1995
+ throw new Error('item_keys must be a non-empty array');
1996
+ }
1997
+ if (item_keys.some(key => !key || typeof key !== 'string')) {
1998
+ throw new Error('All item_keys must be non-empty strings');
1999
+ }
2000
+
1679
2001
  try {
1680
2002
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1681
2003
  const plexToken = process.env.PLEX_TOKEN;
@@ -1684,21 +2006,141 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1684
2006
  throw new Error('PLEX_TOKEN environment variable is required');
1685
2007
  }
1686
2008
 
2009
+ // Get playlist info before removing items
2010
+ const playlistInfoUrl = `${plexUrl}/playlists/${playlist_id}`;
2011
+ const playlistItemsUrl = `${plexUrl}/playlists/${playlist_id}/items`;
2012
+
2013
+ // Get playlist metadata (title, etc.)
2014
+ const playlistInfoResponse = await axios.get(playlistInfoUrl, {
2015
+ params: {
2016
+ 'X-Plex-Token': plexToken
2017
+ },
2018
+ httpsAgent: this.getHttpsAgent()
2019
+ });
2020
+
2021
+ const playlistInfo = playlistInfoResponse.data.MediaContainer;
2022
+ const playlistTitle = playlistInfo.Metadata && playlistInfo.Metadata[0]
2023
+ ? playlistInfo.Metadata[0].title : `Playlist ${playlist_id}`;
2024
+
2025
+ // Get current playlist items with their detailed information
2026
+ let beforeCount = 0;
2027
+ let playlistItems = [];
2028
+ try {
2029
+ const beforeResponse = await axios.get(playlistItemsUrl, {
2030
+ params: {
2031
+ 'X-Plex-Token': plexToken
2032
+ },
2033
+ httpsAgent: this.getHttpsAgent()
2034
+ });
2035
+ beforeCount = beforeResponse.data.MediaContainer?.totalSize || 0;
2036
+ playlistItems = beforeResponse.data.MediaContainer?.Metadata || [];
2037
+ } catch (error) {
2038
+ // If items endpoint fails, playlist might be empty
2039
+ beforeCount = 0;
2040
+ playlistItems = [];
2041
+ }
2042
+
2043
+ // Get server machine identifier for proper URI format
2044
+ const serverResponse = await axios.get(`${plexUrl}/`, {
2045
+ headers: { 'X-Plex-Token': plexToken },
2046
+ httpsAgent: this.getHttpsAgent()
2047
+ });
2048
+
2049
+ const machineIdentifier = serverResponse.data?.MediaContainer?.machineIdentifier;
2050
+ if (!machineIdentifier) {
2051
+ throw new Error('Could not get server machine identifier');
2052
+ }
2053
+
2054
+ // Find items to remove by matching ratingKeys to actual playlist positions
2055
+ const itemsToRemove = [];
2056
+ const itemKeysSet = new Set(item_keys);
2057
+
2058
+ playlistItems.forEach((item, index) => {
2059
+ if (itemKeysSet.has(item.ratingKey)) {
2060
+ itemsToRemove.push({
2061
+ ratingKey: item.ratingKey,
2062
+ position: index,
2063
+ title: item.title || 'Unknown'
2064
+ });
2065
+ }
2066
+ });
2067
+
2068
+ if (itemsToRemove.length === 0) {
2069
+ return {
2070
+ content: [
2071
+ {
2072
+ type: "text",
2073
+ text: `No matching items found in playlist "${playlistTitle}".\\nSpecified items may not exist in this playlist.`,
2074
+ },
2075
+ ],
2076
+ };
2077
+ }
2078
+
2079
+ // WARNING: Current Plex API behavior - this removes ALL instances of matching items
2080
+ // This is a limitation of the Plex API - there's no way to remove just specific instances
1687
2081
  const removeUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1688
2082
  const params = {
1689
2083
  'X-Plex-Token': plexToken,
1690
- uri: item_keys.map(key => `server://localhost/com.plexapp.plugins.library/library/metadata/${key}`).join(',')
2084
+ uri: itemsToRemove.map(item => `server://${machineIdentifier}/com.plexapp.plugins.library/library/metadata/${item.ratingKey}`).join(',')
1691
2085
  };
1692
2086
 
1693
2087
  const response = await axios.delete(removeUrl, {
1694
2088
  params,
1695
- httpsAgent: new (require('https').Agent)({
1696
- rejectUnauthorized: false,
1697
- minVersion: 'TLSv1.2'
1698
- })
2089
+ httpsAgent: this.getHttpsAgent()
1699
2090
  });
1700
2091
 
1701
- const resultText = `Successfully removed ${item_keys.length} item(s) from playlist ${playlist_id}`;
2092
+ // Check if the DELETE request was successful based on HTTP status
2093
+ const deleteSuccessful = response.status >= 200 && response.status < 300;
2094
+
2095
+ // Small delay to allow Plex server to update
2096
+ await new Promise(resolve => setTimeout(resolve, 100));
2097
+
2098
+ // Verify the removal by checking the playlist items again
2099
+ 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;
2111
+ }
2112
+
2113
+ const actualRemoved = beforeCount - afterCount;
2114
+ const attempted = item_keys.length;
2115
+
2116
+ let resultText = `Playlist "${playlistTitle}" update:\n`;
2117
+ resultText += `• Attempted to remove: ${attempted} item(s)\n`;
2118
+ resultText += `• Found in playlist: ${itemsToRemove.length} item(s)\n`;
2119
+ resultText += `• Actually removed: ${actualRemoved} item(s)\n`;
2120
+ resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n\n`;
2121
+
2122
+ // Add warning about Plex behavior
2123
+ if (itemsToRemove.length > 0) {
2124
+ resultText += `⚠️ **Important**: Plex removes ALL instances of matching items from playlists.\n`;
2125
+ resultText += `If you had duplicate tracks, all copies were removed.\n\n`;
2126
+ }
2127
+
2128
+ if (actualRemoved === attempted) {
2129
+ resultText += `✅ All items removed successfully!`;
2130
+ } else if (actualRemoved > 0) {
2131
+ resultText += `⚠️ Partial success: ${attempted - actualRemoved} item(s) were not found in the playlist`;
2132
+ } else if (deleteSuccessful && itemsToRemove.length > 0) {
2133
+ resultText += `✅ API request successful! Items were processed.\n`;
2134
+ resultText += `ℹ️ If count didn't change, items may have already been removed previously.`;
2135
+ } else if (deleteSuccessful) {
2136
+ resultText += `✅ API request successful! Items may not have been in the playlist.\n`;
2137
+ resultText += `ℹ️ This is normal behavior - Plex ignores requests to remove non-existent items.`;
2138
+ } else {
2139
+ resultText += `❌ No items were removed. This may indicate:\n`;
2140
+ resultText += ` - Invalid item IDs\n`;
2141
+ resultText += ` - Items not present in playlist\n`;
2142
+ resultText += ` - Permission issues`;
2143
+ }
1702
2144
 
1703
2145
  return {
1704
2146
  content: [
@@ -1709,11 +2151,27 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1709
2151
  ],
1710
2152
  };
1711
2153
  } catch (error) {
2154
+ // Enhanced error handling with specific error types
2155
+ let errorMessage = `Error removing items from playlist: ${error.message}`;
2156
+
2157
+ if (error.response) {
2158
+ const status = error.response.status;
2159
+ if (status === 404) {
2160
+ errorMessage = `Playlist with ID ${playlist_id} not found`;
2161
+ } else if (status === 401 || status === 403) {
2162
+ errorMessage = `Permission denied: Check your Plex token and server access`;
2163
+ } else if (status >= 500) {
2164
+ errorMessage = `Plex server error (${status}): ${error.message}`;
2165
+ }
2166
+ } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
2167
+ errorMessage = `Cannot connect to Plex server: Check PLEX_URL configuration`;
2168
+ }
2169
+
1712
2170
  return {
1713
2171
  content: [
1714
2172
  {
1715
2173
  type: "text",
1716
- text: `Error removing items from playlist: ${error.message}`,
2174
+ text: errorMessage,
1717
2175
  },
1718
2176
  ],
1719
2177
  isError: true,
@@ -1739,10 +2197,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1739
2197
 
1740
2198
  const response = await axios.delete(deleteUrl, {
1741
2199
  params,
1742
- httpsAgent: new (require('https').Agent)({
1743
- rejectUnauthorized: false,
1744
- minVersion: 'TLSv1.2'
1745
- })
2200
+ httpsAgent: this.getHttpsAgent()
1746
2201
  });
1747
2202
 
1748
2203
  const resultText = `Successfully deleted playlist ${playlist_id}`;
@@ -1795,10 +2250,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1795
2250
 
1796
2251
  const response = await axios.get(itemUrl, {
1797
2252
  params,
1798
- httpsAgent: new (require('https').Agent)({
1799
- rejectUnauthorized: false,
1800
- minVersion: 'TLSv1.2'
1801
- })
2253
+ httpsAgent: this.getHttpsAgent()
1802
2254
  });
1803
2255
 
1804
2256
  const item = response.data?.MediaContainer?.Metadata?.[0];
@@ -2242,10 +2694,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2242
2694
 
2243
2695
  const response = await axios.get(collectionsUrl, {
2244
2696
  params,
2245
- httpsAgent: new (require('https').Agent)({
2246
- rejectUnauthorized: false,
2247
- minVersion: 'TLSv1.2'
2248
- })
2697
+ httpsAgent: this.getHttpsAgent()
2249
2698
  });
2250
2699
 
2251
2700
  const collections = this.parseCollections(response.data);
@@ -2296,10 +2745,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2296
2745
 
2297
2746
  const response = await axios.get(collectionUrl, {
2298
2747
  params,
2299
- httpsAgent: new (require('https').Agent)({
2300
- rejectUnauthorized: false,
2301
- minVersion: 'TLSv1.2'
2302
- })
2748
+ httpsAgent: this.getHttpsAgent()
2303
2749
  });
2304
2750
 
2305
2751
  const results = this.parseLibraryContent(response.data);
@@ -2399,10 +2845,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2399
2845
 
2400
2846
  const response = await axios.get(mediaUrl, {
2401
2847
  params,
2402
- httpsAgent: new (require('https').Agent)({
2403
- rejectUnauthorized: false,
2404
- minVersion: 'TLSv1.2'
2405
- })
2848
+ httpsAgent: this.getHttpsAgent()
2406
2849
  });
2407
2850
 
2408
2851
  const item = response.data?.MediaContainer?.Metadata?.[0];
@@ -2691,10 +3134,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2691
3134
  // Get library information first
2692
3135
  const librariesResponse = await axios.get(`${plexUrl}/library/sections`, {
2693
3136
  params: { 'X-Plex-Token': plexToken },
2694
- httpsAgent: new (require('https').Agent)({
2695
- rejectUnauthorized: false,
2696
- minVersion: 'TLSv1.2'
2697
- })
3137
+ httpsAgent: this.getHttpsAgent()
2698
3138
  });
2699
3139
 
2700
3140
  const libraries = this.parseLibraries(librariesResponse.data);
@@ -2783,10 +3223,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2783
3223
  'X-Plex-Container-Start': offset,
2784
3224
  'X-Plex-Container-Size': batchSize
2785
3225
  },
2786
- httpsAgent: new (require('https').Agent)({
2787
- rejectUnauthorized: false,
2788
- minVersion: 'TLSv1.2'
2789
- })
3226
+ httpsAgent: this.getHttpsAgent()
2790
3227
  });
2791
3228
 
2792
3229
  const content = this.parseLibraryContent(contentResponse.data);
@@ -2827,10 +3264,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2827
3264
  try {
2828
3265
  const mediaResponse = await axios.get(`${plexUrl}${item.key}`, {
2829
3266
  params: { 'X-Plex-Token': plexToken },
2830
- httpsAgent: new (require('https').Agent)({
2831
- rejectUnauthorized: false,
2832
- minVersion: 'TLSv1.2'
2833
- })
3267
+ httpsAgent: this.getHttpsAgent()
2834
3268
  });
2835
3269
 
2836
3270
  const detailedItem = mediaResponse.data?.MediaContainer?.Metadata?.[0];
@@ -3059,10 +3493,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3059
3493
  } else {
3060
3494
  const librariesResponse = await axios.get(`${plexUrl}/library/sections`, {
3061
3495
  params: { 'X-Plex-Token': plexToken },
3062
- httpsAgent: new (require('https').Agent)({
3063
- rejectUnauthorized: false,
3064
- minVersion: 'TLSv1.2'
3065
- })
3496
+ httpsAgent: this.getHttpsAgent()
3066
3497
  });
3067
3498
 
3068
3499
  const allLibraries = this.parseLibraries(librariesResponse.data);
@@ -3165,10 +3596,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3165
3596
  try {
3166
3597
  const historyResponse = await axios.get(`${plexUrl}/status/sessions/history/all`, {
3167
3598
  params: historyParams,
3168
- httpsAgent: new (require('https').Agent)({
3169
- rejectUnauthorized: false,
3170
- minVersion: 'TLSv1.2'
3171
- })
3599
+ httpsAgent: this.getHttpsAgent()
3172
3600
  });
3173
3601
 
3174
3602
  const history = this.parseWatchHistory(historyResponse.data);
@@ -3249,10 +3677,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3249
3677
  query: trackName,
3250
3678
  type: 10 // Track type
3251
3679
  },
3252
- httpsAgent: new (require('https').Agent)({
3253
- rejectUnauthorized: false,
3254
- minVersion: 'TLSv1.2'
3255
- })
3680
+ httpsAgent: this.getHttpsAgent()
3256
3681
  });
3257
3682
 
3258
3683
  const tracks = this.parseSearchResults(searchResponse.data);
@@ -3260,10 +3685,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3260
3685
  if (track.key) {
3261
3686
  const trackDetailResponse = await axios.get(`${plexUrl}${track.key}`, {
3262
3687
  params: { 'X-Plex-Token': plexToken },
3263
- httpsAgent: new (require('https').Agent)({
3264
- rejectUnauthorized: false,
3265
- minVersion: 'TLSv1.2'
3266
- })
3688
+ httpsAgent: this.getHttpsAgent()
3267
3689
  });
3268
3690
 
3269
3691
  const trackDetail = trackDetailResponse.data?.MediaContainer?.Metadata?.[0];
@@ -3308,10 +3730,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3308
3730
  'X-Plex-Container-Size': 10,
3309
3731
  sort: 'addedAt:desc' // Recently added first
3310
3732
  },
3311
- httpsAgent: new (require('https').Agent)({
3312
- rejectUnauthorized: false,
3313
- minVersion: 'TLSv1.2'
3314
- })
3733
+ httpsAgent: this.getHttpsAgent()
3315
3734
  });
3316
3735
 
3317
3736
  const tracks = this.parseLibraryContent(genreSearchResponse.data);
@@ -3342,10 +3761,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3342
3761
  query: artist,
3343
3762
  type: 8 // Artist type
3344
3763
  },
3345
- httpsAgent: new (require('https').Agent)({
3346
- rejectUnauthorized: false,
3347
- minVersion: 'TLSv1.2'
3348
- })
3764
+ httpsAgent: this.getHttpsAgent()
3349
3765
  });
3350
3766
 
3351
3767
  const artists = this.parseSearchResults(artistSearchResponse.data);
@@ -3353,10 +3769,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3353
3769
  if (foundArtist.key) {
3354
3770
  const artistDetailResponse = await axios.get(`${plexUrl}${foundArtist.key}`, {
3355
3771
  params: { 'X-Plex-Token': plexToken },
3356
- httpsAgent: new (require('https').Agent)({
3357
- rejectUnauthorized: false,
3358
- minVersion: 'TLSv1.2'
3359
- })
3772
+ httpsAgent: this.getHttpsAgent()
3360
3773
  });
3361
3774
 
3362
3775
  const artistTracks = this.parseLibraryContent(artistDetailResponse.data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plex-mcp",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
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": {
@@ -8,9 +8,12 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node index.js",
11
- "test": "jest",
12
- "test:watch": "jest --watch",
13
- "test:coverage": "jest --coverage",
11
+ "test": "jest --testPathIgnorePatterns=/tests/e2e/",
12
+ "test:e2e": "jest --testPathPattern=e2e",
13
+ "test:e2e:analysis": "jest --testPathPattern=e2e --testNamePattern=\"(Playlist Behavior Analysis|Critical Remove Bug Investigation|Multiple Item Addition Analysis)\"",
14
+ "test:all": "jest",
15
+ "test:watch": "jest --watch --testPathIgnorePatterns=/tests/e2e/",
16
+ "test:coverage": "jest --coverage --testPathIgnorePatterns=/tests/e2e/",
14
17
  "prepublishOnly": "npm test",
15
18
  "version:patch": "npm version patch",
16
19
  "version:minor": "npm version minor",