plex-mcp 0.0.2 → 0.0.4

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 (3) hide show
  1. package/TODO.md +252 -0
  2. package/index.js +304 -109
  3. package/package.json +6 -4
package/TODO.md ADDED
@@ -0,0 +1,252 @@
1
+ # Plex MCP Server - TODO List
2
+
3
+ ## Unimplemented Plex API Features
4
+
5
+ ### Critical Missing APIs (High Priority)
6
+
7
+ #### Session Management
8
+ - [ ] **get_active_sessions** - List current "Now Playing" sessions
9
+ - Endpoint: `/status/sessions`
10
+ - Returns: Active playback sessions with user, client, media info
11
+ - [ ] **get_transcode_sessions** - List active transcoding operations
12
+ - Endpoint: `/transcode/sessions`
13
+ - Returns: Transcoding progress, quality settings, resource usage
14
+ - [ ] **terminate_session** - Stop/kill active playback sessions
15
+ - Endpoint: `/status/sessions/terminate`
16
+ - Action: Force stop sessions by session ID
17
+
18
+ #### Playback Control
19
+ - [ ] **control_playback** - Control playback (play/pause/stop/seek)
20
+ - Endpoint: `/player/playback/{command}`
21
+ - Commands: play, pause, stop, stepForward, stepBack, seekTo
22
+ - [ ] **start_playback** - Initiate playback on specific clients
23
+ - Endpoint: `/player/playback/playMedia`
24
+ - Parameters: media key, client ID, resume offset
25
+ - [ ] **remote_control** - Advanced remote control operations
26
+ - Endpoint: `/player/navigation/{command}`
27
+ - Commands: moveUp, moveDown, select, back, home
28
+
29
+ #### Client & Device Management
30
+ - [ ] **get_clients** - List available Plex clients/players
31
+ - Endpoint: `/clients`
32
+ - Returns: Client names, IDs, capabilities, online status
33
+ - [ ] **get_devices** - List all registered devices
34
+ - Endpoint: `/devices`
35
+ - Returns: Device info, last seen, platform details
36
+ - [ ] **get_servers** - List available Plex servers
37
+ - Endpoint: `/servers`
38
+ - Returns: Server list for multi-server setups
39
+
40
+ ### Server Administration (Medium Priority)
41
+
42
+ #### Server Info & Management
43
+ - [ ] **get_server_info** - Server capabilities and status
44
+ - Endpoint: `/`
45
+ - Returns: Version, capabilities, transcoder info, platform
46
+ - [ ] **get_server_preferences** - Server configuration settings
47
+ - Endpoint: `/:/prefs`
48
+ - Returns: All server preferences and settings
49
+ - [ ] **scan_library** - Trigger library content scan
50
+ - Endpoint: `/library/sections/{id}/refresh`
51
+ - Action: Force library scan for new content
52
+ - [ ] **refresh_metadata** - Force metadata refresh for items
53
+ - Endpoint: `/library/metadata/{id}/refresh`
54
+ - Action: Re-download metadata, artwork, etc.
55
+
56
+ #### User Management
57
+ - [ ] **get_users** - List server users and accounts
58
+ - Endpoint: `/accounts`
59
+ - Returns: User list, permissions, sharing status
60
+ - [ ] **get_user_activity** - User-specific activity logs
61
+ - Endpoint: `/status/sessions/history/all?accountID={id}`
62
+ - Returns: Per-user watch history and statistics
63
+
64
+ ### Content Discovery & Recommendations (Medium Priority)
65
+
66
+ #### Advanced Discovery
67
+ - [ ] **get_content_hubs** - Plex's recommendation engine
68
+ - Endpoint: `/hubs`
69
+ - Returns: Curated content recommendations, trending
70
+ - [ ] **get_discover_content** - Discover new content across libraries
71
+ - Endpoint: `/library/sections/all?discover=1`
72
+ - Returns: Cross-library content discovery
73
+ - [ ] **get_trending** - Trending content on Plex platform
74
+ - Endpoint: `/hubs/trending`
75
+ - Returns: Popular content across Plex network
76
+
77
+ #### Metadata Enhancement
78
+ - [ ] **get_genres** - Available genres across libraries
79
+ - Endpoint: `/library/sections/{id}/genre`
80
+ - Returns: Genre list with item counts
81
+ - [ ] **get_years** - Available years across libraries
82
+ - Endpoint: `/library/sections/{id}/year`
83
+ - Returns: Year list with item counts
84
+ - [ ] **get_studios** - Available studios/networks
85
+ - Endpoint: `/library/sections/{id}/studio`
86
+ - Returns: Studio/network list with item counts
87
+ - [ ] **get_directors** - Available directors/actors
88
+ - Endpoint: `/library/sections/{id}/director`
89
+ - Returns: People list with filmography counts
90
+
91
+ ### Media Management (Medium Priority)
92
+
93
+ #### Watch Status Management
94
+ - [ ] **mark_watched** - Mark items as watched
95
+ - Endpoint: `/:/scrobble?key={id}&identifier=com.plexapp.plugins.library`
96
+ - Action: Set watch status, update play count
97
+ - [ ] **mark_unwatched** - Mark items as unwatched
98
+ - Endpoint: `/:/unscrobble?key={id}&identifier=com.plexapp.plugins.library`
99
+ - Action: Remove watch status, reset play count
100
+ - [ ] **set_rating** - Rate content (stars/thumbs)
101
+ - Endpoint: `/:/rate?key={id}&rating={rating}&identifier=com.plexapp.plugins.library`
102
+ - Action: Set user rating for content
103
+
104
+ #### Library Maintenance
105
+ - [ ] **optimize_library** - Database optimization operations
106
+ - Endpoint: `/library/optimize`
107
+ - Action: Clean up database, optimize indexes
108
+ - [ ] **empty_trash** - Empty library trash/deleted items
109
+ - Endpoint: `/library/sections/{id}/emptyTrash`
110
+ - Action: Permanently delete trashed items
111
+ - [ ] **update_library** - Update library metadata
112
+ - Endpoint: `/library/sections/{id}/update`
113
+ - Action: Update library without full scan
114
+
115
+ ### Advanced Features (Low Priority)
116
+
117
+ #### Transcoding Management
118
+ - [ ] **get_transcoding_settings** - Transcoding preferences
119
+ - Endpoint: `/:/prefs?group=transcoder`
120
+ - Returns: Quality settings, codec preferences
121
+ - [ ] **optimize_content** - Content optimization for devices
122
+ - Endpoint: `/library/metadata/{id}/optimize`
123
+ - Action: Pre-transcode content for specific devices
124
+
125
+ #### Sync & Download
126
+ - [ ] **get_sync_items** - Sync queue and downloaded items
127
+ - Endpoint: `/sync/items`
128
+ - Returns: Sync status, download queue
129
+ - [ ] **download_media** - Download content for offline viewing
130
+ - Endpoint: `/sync/items`
131
+ - Action: Queue content for download/sync
132
+
133
+ #### Webhooks & Events
134
+ - [ ] **get_webhooks** - List configured webhooks
135
+ - Endpoint: `/:/webhooks`
136
+ - Returns: Webhook URLs and trigger events
137
+ - [ ] **listen_events** - Real-time event streaming
138
+ - Endpoint: `/:/events` (WebSocket/SSE)
139
+ - Returns: Live server events, playback updates
140
+
141
+ ## Missing Test Coverage
142
+
143
+ ### Unit Tests
144
+ - [ ] **Session management handlers** - Tests for new session control APIs
145
+ - [ ] **Playback control handlers** - Tests for media control functionality
146
+ - [ ] **Client management handlers** - Tests for device/client discovery
147
+ - [ ] **Server admin handlers** - Tests for administrative functions
148
+ - [ ] **User management handlers** - Tests for multi-user functionality
149
+ - [ ] **Content discovery handlers** - Tests for recommendation features
150
+ - [ ] **Watch status handlers** - Tests for marking watched/unwatched
151
+ - [ ] **Library management handlers** - Tests for scan/refresh operations
152
+
153
+ ### Integration Tests
154
+ - [ ] **Multi-library operations** - Cross-library search and discovery
155
+ - [ ] **Playlist management with new content** - Advanced playlist operations
156
+ - [ ] **User permission scenarios** - Multi-user access control
157
+ - [ ] **Server configuration changes** - Settings modification testing
158
+ - [ ] **Transcoding workflow** - End-to-end transcoding scenarios
159
+ - [ ] **Sync/download workflows** - Offline content management
160
+
161
+ ### Mock Data Enhancement
162
+ - [ ] **Session data mocks** - Active session responses
163
+ - [ ] **Client data mocks** - Available client/device responses
164
+ - [ ] **Server info mocks** - Server capabilities and status
165
+ - [ ] **User data mocks** - Multi-user account responses
166
+ - [ ] **Transcoding mocks** - Transcoding session responses
167
+ - [ ] **Webhook mocks** - Event and webhook responses
168
+
169
+ ## E2E Tests to Add
170
+
171
+ ### Real Server Integration
172
+ - [ ] **Live session monitoring** - Connect to real server, monitor active sessions
173
+ - Test with actual playback sessions
174
+ - Verify session data accuracy
175
+ - Test session termination
176
+ - [ ] **Live playback control** - Control actual Plex clients
177
+ - Test play/pause/stop commands
178
+ - Test seek functionality
179
+ - Test client discovery and selection
180
+ - [ ] **Live library management** - Real library operations
181
+ - Test library scanning
182
+ - Test metadata refresh
183
+ - Test library optimization
184
+ - [ ] **Live user scenarios** - Multi-user testing
185
+ - Test shared library access
186
+ - Test user-specific history
187
+ - Test permission boundaries
188
+
189
+ ### Cross-Platform Testing
190
+ - [ ] **Multiple client types** - Test with various Plex clients
191
+ - Desktop app, mobile app, web player
192
+ - Smart TV apps, streaming devices
193
+ - Verify control compatibility
194
+ - [ ] **Multiple server versions** - Test against different Plex server versions
195
+ - Latest stable, previous versions
196
+ - PlexPass vs free features
197
+ - API compatibility testing
198
+
199
+ ### Network Scenarios
200
+ - [ ] **SSL/TLS configurations** - Various security setups
201
+ - Self-signed certificates
202
+ - Valid SSL certificates
203
+ - Mixed HTTP/HTTPS environments
204
+ - [ ] **Remote access testing** - Plex relay and direct connections
205
+ - Remote server access via Plex.tv
206
+ - Direct IP connections
207
+ - VPN/tunnel scenarios
208
+ - [ ] **Performance testing** - Large library scenarios
209
+ - Libraries with 10k+ items
210
+ - Multiple concurrent operations
211
+ - Memory and CPU usage monitoring
212
+
213
+ ## Code Quality & Architecture
214
+
215
+ ### Error Handling Improvements
216
+ - [ ] **Granular error types** - Specific error classes for different failure modes
217
+ - [ ] **Retry mechanisms** - Automatic retry for transient failures
218
+ - [ ] **Circuit breaker pattern** - Fail-fast for consistently failing operations
219
+ - [ ] **Rate limiting** - Respect Plex server rate limits
220
+
221
+ ### Performance Optimizations
222
+ - [ ] **Response caching** - Cache frequently accessed data
223
+ - [ ] **Batch operations** - Combine multiple API calls when possible
224
+ - [ ] **Streaming responses** - Handle large datasets efficiently
225
+ - [ ] **Connection pooling** - Reuse HTTP connections
226
+
227
+ ### Documentation
228
+ - [ ] **API reference docs** - Complete documentation for all tools
229
+ - [ ] **Usage examples** - Real-world scenarios and code examples
230
+ - [ ] **Troubleshooting guide** - Common issues and solutions
231
+ - [ ] **Performance tuning** - Optimization recommendations
232
+
233
+ ### Security Enhancements
234
+ - [ ] **Token validation** - Verify Plex token format and permissions
235
+ - [ ] **SSL certificate validation** - Proper certificate handling
236
+ - [ ] **Input sanitization** - Validate all user inputs
237
+ - [ ] **Audit logging** - Log all administrative operations
238
+
239
+ ## Development Infrastructure
240
+
241
+ ### CI/CD Improvements
242
+ - [ ] **Automated testing** - Run full test suite on every commit
243
+ - [ ] **Code coverage tracking** - Monitor and improve test coverage
244
+ - [ ] **Performance benchmarks** - Track performance regressions
245
+ - [ ] **Security scanning** - Automated vulnerability detection
246
+
247
+ ### Development Tools
248
+ - [ ] **Mock Plex server** - Local development server for testing
249
+ - [ ] **Test data generator** - Generate realistic test datasets
250
+ - [ ] **Performance profiler** - Identify bottlenecks and optimize
251
+ - [ ] **Documentation generator** - Auto-generate API docs from code
252
+
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.",
@@ -611,6 +638,8 @@ class PlexMCPServer {
611
638
  return await this.handleOnDeck(request.params.arguments);
612
639
  case "list_playlists":
613
640
  return await this.handleListPlaylists(request.params.arguments);
641
+ case "browse_playlist":
642
+ return await this.handleBrowsePlaylist(request.params.arguments);
614
643
  case "create_playlist":
615
644
  return await this.handleCreatePlaylist(request.params.arguments);
616
645
  case "add_to_playlist":
@@ -690,10 +719,7 @@ class PlexMCPServer {
690
719
 
691
720
  const response = await axios.get(searchUrl, {
692
721
  params,
693
- httpsAgent: new (require('https').Agent)({
694
- rejectUnauthorized: false,
695
- minVersion: 'TLSv1.2'
696
- })
722
+ httpsAgent: this.getHttpsAgent()
697
723
  });
698
724
 
699
725
  let results = this.parseSearchResults(response.data);
@@ -786,6 +812,12 @@ class PlexMCPServer {
786
812
  contentRating: item.contentRating,
787
813
  Media: item.Media,
788
814
  key: item.key,
815
+ ratingKey: item.ratingKey, // Critical: the unique identifier for playlist operations
816
+ // Additional hierarchical info for music tracks
817
+ parentTitle: item.parentTitle, // Album name
818
+ grandparentTitle: item.grandparentTitle, // Artist name
819
+ parentRatingKey: item.parentRatingKey, // Album ID
820
+ grandparentRatingKey: item.grandparentRatingKey, // Artist ID
789
821
  // Additional metadata for basic filters
790
822
  studio: item.studio,
791
823
  genres: item.Genre ? item.Genre.map(g => g.tag) : [],
@@ -807,8 +839,24 @@ class PlexMCPServer {
807
839
  formatted += ` - ${item.type}`;
808
840
  }
809
841
 
842
+ // Add artist/album info for music tracks
843
+ if (item.grandparentTitle && item.parentTitle) {
844
+ formatted += `\n Artist: ${item.grandparentTitle} | Album: ${item.parentTitle}`;
845
+ } else if (item.parentTitle) {
846
+ formatted += `\n Album/Show: ${item.parentTitle}`;
847
+ }
848
+
810
849
  if (item.rating) {
811
- formatted += ` - Rating: ${item.rating}`;
850
+ formatted += `\n Rating: ${item.rating}`;
851
+ }
852
+
853
+ if (item.duration) {
854
+ formatted += `\n Duration: ${this.formatDuration(item.duration)}`;
855
+ }
856
+
857
+ // CRITICAL: Show the ratingKey for playlist operations
858
+ if (item.ratingKey) {
859
+ formatted += `\n **ID: ${item.ratingKey}** (use this for playlists)`;
812
860
  }
813
861
 
814
862
  if (item.summary) {
@@ -819,6 +867,21 @@ class PlexMCPServer {
819
867
  }).join('\n\n');
820
868
  }
821
869
 
870
+ formatDuration(milliseconds) {
871
+ if (!milliseconds || milliseconds === 0) return 'Unknown';
872
+
873
+ const seconds = Math.floor(milliseconds / 1000);
874
+ const minutes = Math.floor(seconds / 60);
875
+ const hours = Math.floor(minutes / 60);
876
+
877
+ if (hours > 0) {
878
+ const remainingMinutes = minutes % 60;
879
+ return `${hours}h ${remainingMinutes}m`;
880
+ } else {
881
+ return `${minutes}m`;
882
+ }
883
+ }
884
+
822
885
  async handleBrowseLibraries(args) {
823
886
  try {
824
887
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
@@ -835,10 +898,7 @@ class PlexMCPServer {
835
898
 
836
899
  const response = await axios.get(librariesUrl, {
837
900
  params,
838
- httpsAgent: new (require('https').Agent)({
839
- rejectUnauthorized: false,
840
- minVersion: 'TLSv1.2'
841
- })
901
+ httpsAgent: this.getHttpsAgent()
842
902
  });
843
903
 
844
904
  const libraries = this.parseLibraries(response.data);
@@ -965,10 +1025,7 @@ class PlexMCPServer {
965
1025
 
966
1026
  const response = await axios.get(libraryUrl, {
967
1027
  params,
968
- httpsAgent: new (require('https').Agent)({
969
- rejectUnauthorized: false,
970
- minVersion: 'TLSv1.2'
971
- })
1028
+ httpsAgent: this.getHttpsAgent()
972
1029
  });
973
1030
 
974
1031
  let results = this.parseLibraryContent(response.data);
@@ -1090,10 +1147,7 @@ class PlexMCPServer {
1090
1147
 
1091
1148
  const response = await axios.get(recentUrl, {
1092
1149
  params,
1093
- httpsAgent: new (require('https').Agent)({
1094
- rejectUnauthorized: false,
1095
- minVersion: 'TLSv1.2'
1096
- })
1150
+ httpsAgent: this.getHttpsAgent()
1097
1151
  });
1098
1152
 
1099
1153
  const results = this.parseLibraryContent(response.data);
@@ -1191,10 +1245,7 @@ class PlexMCPServer {
1191
1245
 
1192
1246
  const response = await axios.get(historyUrl, {
1193
1247
  params,
1194
- httpsAgent: new (require('https').Agent)({
1195
- rejectUnauthorized: false,
1196
- minVersion: 'TLSv1.2'
1197
- })
1248
+ httpsAgent: this.getHttpsAgent()
1198
1249
  });
1199
1250
 
1200
1251
  const results = this.parseWatchHistory(response.data);
@@ -1314,10 +1365,7 @@ class PlexMCPServer {
1314
1365
 
1315
1366
  const response = await axios.get(onDeckUrl, {
1316
1367
  params,
1317
- httpsAgent: new (require('https').Agent)({
1318
- rejectUnauthorized: false,
1319
- minVersion: 'TLSv1.2'
1320
- })
1368
+ httpsAgent: this.getHttpsAgent()
1321
1369
  });
1322
1370
 
1323
1371
  const results = this.parseOnDeck(response.data);
@@ -1423,10 +1471,7 @@ class PlexMCPServer {
1423
1471
 
1424
1472
  const response = await axios.get(playlistsUrl, {
1425
1473
  params,
1426
- httpsAgent: new (require('https').Agent)({
1427
- rejectUnauthorized: false,
1428
- minVersion: 'TLSv1.2'
1429
- })
1474
+ httpsAgent: this.getHttpsAgent()
1430
1475
  });
1431
1476
 
1432
1477
  const playlists = this.parsePlaylists(response.data);
@@ -1456,6 +1501,120 @@ class PlexMCPServer {
1456
1501
  }
1457
1502
  }
1458
1503
 
1504
+ async handleBrowsePlaylist(args) {
1505
+ const { playlist_id, limit = 50 } = args;
1506
+
1507
+ try {
1508
+ const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1509
+ const plexToken = process.env.PLEX_TOKEN;
1510
+
1511
+ if (!plexToken) {
1512
+ throw new Error('PLEX_TOKEN environment variable is required');
1513
+ }
1514
+
1515
+ // First get playlist info
1516
+ const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1517
+ const response = await axios.get(playlistUrl, {
1518
+ params: {
1519
+ 'X-Plex-Token': plexToken
1520
+ },
1521
+ httpsAgent: this.getHttpsAgent()
1522
+ });
1523
+
1524
+ const playlistData = response.data.MediaContainer;
1525
+ if (!playlistData || !playlistData.Metadata || playlistData.Metadata.length === 0) {
1526
+ return {
1527
+ content: [
1528
+ {
1529
+ type: "text",
1530
+ text: `Playlist with ID ${playlist_id} not found or is empty`,
1531
+ },
1532
+ ],
1533
+ };
1534
+ }
1535
+
1536
+ const playlist = playlistData.Metadata[0];
1537
+ const items = playlistData.Metadata[0].Metadata || [];
1538
+
1539
+ // Limit results if specified
1540
+ const limitedItems = limit ? items.slice(0, limit) : items;
1541
+
1542
+ let resultText = `**${playlist.title}**`;
1543
+ if (playlist.smart) {
1544
+ resultText += ` (Smart Playlist)`;
1545
+ }
1546
+ resultText += `\n`;
1547
+ if (playlist.summary) {
1548
+ resultText += `${playlist.summary}\n`;
1549
+ }
1550
+ resultText += `Duration: ${this.formatDuration(playlist.duration || 0)}\n`;
1551
+ resultText += `Items: ${items.length}`;
1552
+ if (limit && items.length > limit) {
1553
+ resultText += ` (showing first ${limit})`;
1554
+ }
1555
+ resultText += `\n\n`;
1556
+
1557
+ if (limitedItems.length === 0) {
1558
+ resultText += `This playlist is empty.`;
1559
+ } else {
1560
+ resultText += limitedItems.map((item, index) => {
1561
+ let itemText = `${index + 1}. **${item.title}**`;
1562
+
1563
+ // Add artist/album info for music
1564
+ if (item.grandparentTitle && item.parentTitle) {
1565
+ itemText += `\n Artist: ${item.grandparentTitle}\n Album: ${item.parentTitle}`;
1566
+ } else if (item.parentTitle) {
1567
+ itemText += `\n Album/Show: ${item.parentTitle}`;
1568
+ }
1569
+
1570
+ // Add duration
1571
+ if (item.duration) {
1572
+ itemText += `\n Duration: ${this.formatDuration(item.duration)}`;
1573
+ }
1574
+
1575
+ // Add rating key for identification
1576
+ itemText += `\n ID: ${item.ratingKey}`;
1577
+
1578
+ // Add media type
1579
+ const mediaType = this.getMediaTypeFromItem(item);
1580
+ if (mediaType) {
1581
+ itemText += `\n Type: ${mediaType}`;
1582
+ }
1583
+
1584
+ return itemText;
1585
+ }).join('\n\n');
1586
+ }
1587
+
1588
+ return {
1589
+ content: [
1590
+ {
1591
+ type: "text",
1592
+ text: resultText,
1593
+ },
1594
+ ],
1595
+ };
1596
+ } catch (error) {
1597
+ return {
1598
+ content: [
1599
+ {
1600
+ type: "text",
1601
+ text: `Error browsing playlist: ${error.message}`,
1602
+ },
1603
+ ],
1604
+ isError: true,
1605
+ };
1606
+ }
1607
+ }
1608
+
1609
+ getMediaTypeFromItem(item) {
1610
+ if (item.type === 'track') return 'Music Track';
1611
+ if (item.type === 'episode') return 'TV Episode';
1612
+ if (item.type === 'movie') return 'Movie';
1613
+ if (item.type === 'artist') return 'Artist';
1614
+ if (item.type === 'album') return 'Album';
1615
+ return item.type || 'Unknown';
1616
+ }
1617
+
1459
1618
  parsePlaylists(data) {
1460
1619
  if (!data.MediaContainer || !data.MediaContainer.Metadata) {
1461
1620
  return [];
@@ -1540,10 +1699,7 @@ class PlexMCPServer {
1540
1699
  // First get server info to get machine identifier
1541
1700
  const serverResponse = await axios.get(`${plexUrl}/`, {
1542
1701
  headers: { 'X-Plex-Token': plexToken },
1543
- httpsAgent: new (require('https').Agent)({
1544
- rejectUnauthorized: false,
1545
- minVersion: 'TLSv1.2'
1546
- })
1702
+ httpsAgent: this.getHttpsAgent()
1547
1703
  });
1548
1704
 
1549
1705
  const machineIdentifier = serverResponse.data?.MediaContainer?.machineIdentifier;
@@ -1576,20 +1732,22 @@ class PlexMCPServer {
1576
1732
  headers: {
1577
1733
  'Content-Length': '0'
1578
1734
  },
1579
- httpsAgent: new (require('https').Agent)({
1580
- rejectUnauthorized: false,
1581
- minVersion: 'TLSv1.2'
1582
- })
1735
+ httpsAgent: this.getHttpsAgent()
1583
1736
  });
1584
1737
 
1585
1738
  // Get the created playlist info from the response
1586
1739
  const playlistData = response.data?.MediaContainer?.Metadata?.[0];
1587
1740
 
1588
- let resultText = `Successfully created ${smart ? 'smart ' : ''}playlist: **${title}**`;
1741
+ let resultText = `✅ Successfully created ${smart ? 'smart ' : ''}playlist: **${title}**`;
1589
1742
  if (playlistData) {
1590
- resultText += `\n Playlist ID: ${playlistData.ratingKey}`;
1743
+ resultText += `\n **Playlist ID: ${playlistData.ratingKey}** (use this ID for future operations)`;
1591
1744
  resultText += `\n Type: ${type}`;
1592
1745
  if (smart) resultText += `\n Smart Playlist: Yes`;
1746
+ if (item_key && !smart) {
1747
+ resultText += `\n Initial item added: ${item_key}`;
1748
+ }
1749
+ } else {
1750
+ resultText += `\n ⚠️ Playlist created but details not available - check your playlists`;
1593
1751
  }
1594
1752
 
1595
1753
  return {
@@ -1636,6 +1794,21 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1636
1794
  throw new Error('PLEX_TOKEN environment variable is required');
1637
1795
  }
1638
1796
 
1797
+ // Get playlist info before adding items
1798
+ const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1799
+ const beforeResponse = await axios.get(playlistUrl, {
1800
+ params: {
1801
+ 'X-Plex-Token': plexToken
1802
+ },
1803
+ httpsAgent: this.getHttpsAgent()
1804
+ });
1805
+
1806
+ const beforeData = beforeResponse.data.MediaContainer;
1807
+ const beforeCount = (beforeData.Metadata && beforeData.Metadata[0] && beforeData.Metadata[0].Metadata)
1808
+ ? beforeData.Metadata[0].Metadata.length : 0;
1809
+ const playlistTitle = beforeData.Metadata && beforeData.Metadata[0]
1810
+ ? beforeData.Metadata[0].title : `Playlist ${playlist_id}`;
1811
+
1639
1812
  const addUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1640
1813
  const params = {
1641
1814
  'X-Plex-Token': plexToken,
@@ -1644,13 +1817,39 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1644
1817
 
1645
1818
  const response = await axios.put(addUrl, null, {
1646
1819
  params,
1647
- httpsAgent: new (require('https').Agent)({
1648
- rejectUnauthorized: false,
1649
- minVersion: 'TLSv1.2'
1650
- })
1820
+ httpsAgent: this.getHttpsAgent()
1821
+ });
1822
+
1823
+ // Verify the addition by checking the playlist again
1824
+ const afterResponse = await axios.get(playlistUrl, {
1825
+ params: {
1826
+ 'X-Plex-Token': plexToken
1827
+ },
1828
+ httpsAgent: this.getHttpsAgent()
1651
1829
  });
1652
1830
 
1653
- const resultText = `Successfully added ${item_keys.length} item(s) to playlist ${playlist_id}`;
1831
+ const afterData = afterResponse.data.MediaContainer;
1832
+ const afterCount = (afterData.Metadata && afterData.Metadata[0] && afterData.Metadata[0].Metadata)
1833
+ ? afterData.Metadata[0].Metadata.length : 0;
1834
+
1835
+ const actualAdded = afterCount - beforeCount;
1836
+ const attempted = item_keys.length;
1837
+
1838
+ let resultText = `Playlist "${playlistTitle}" update:\n`;
1839
+ resultText += `• Attempted to add: ${attempted} item(s)\n`;
1840
+ resultText += `• Actually added: ${actualAdded} item(s)\n`;
1841
+ resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n`;
1842
+
1843
+ if (actualAdded === attempted) {
1844
+ resultText += `✅ All items added successfully!`;
1845
+ } else if (actualAdded > 0) {
1846
+ resultText += `⚠️ Partial success: ${attempted - actualAdded} item(s) may have been duplicates or invalid`;
1847
+ } else {
1848
+ resultText += `❌ No items were added. This may indicate:\n`;
1849
+ resultText += ` - Invalid item IDs (use ratingKey from search results)\n`;
1850
+ resultText += ` - Items already exist in playlist\n`;
1851
+ resultText += ` - Permission issues`;
1852
+ }
1654
1853
 
1655
1854
  return {
1656
1855
  content: [
@@ -1684,6 +1883,21 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1684
1883
  throw new Error('PLEX_TOKEN environment variable is required');
1685
1884
  }
1686
1885
 
1886
+ // Get playlist info before removing items
1887
+ const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1888
+ const beforeResponse = await axios.get(playlistUrl, {
1889
+ params: {
1890
+ 'X-Plex-Token': plexToken
1891
+ },
1892
+ httpsAgent: this.getHttpsAgent()
1893
+ });
1894
+
1895
+ const beforeData = beforeResponse.data.MediaContainer;
1896
+ const beforeCount = (beforeData.Metadata && beforeData.Metadata[0] && beforeData.Metadata[0].Metadata)
1897
+ ? beforeData.Metadata[0].Metadata.length : 0;
1898
+ const playlistTitle = beforeData.Metadata && beforeData.Metadata[0]
1899
+ ? beforeData.Metadata[0].title : `Playlist ${playlist_id}`;
1900
+
1687
1901
  const removeUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1688
1902
  const params = {
1689
1903
  'X-Plex-Token': plexToken,
@@ -1692,13 +1906,39 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1692
1906
 
1693
1907
  const response = await axios.delete(removeUrl, {
1694
1908
  params,
1695
- httpsAgent: new (require('https').Agent)({
1696
- rejectUnauthorized: false,
1697
- minVersion: 'TLSv1.2'
1698
- })
1909
+ httpsAgent: this.getHttpsAgent()
1910
+ });
1911
+
1912
+ // Verify the removal by checking the playlist again
1913
+ const afterResponse = await axios.get(playlistUrl, {
1914
+ params: {
1915
+ 'X-Plex-Token': plexToken
1916
+ },
1917
+ httpsAgent: this.getHttpsAgent()
1699
1918
  });
1700
1919
 
1701
- const resultText = `Successfully removed ${item_keys.length} item(s) from playlist ${playlist_id}`;
1920
+ const afterData = afterResponse.data.MediaContainer;
1921
+ const afterCount = (afterData.Metadata && afterData.Metadata[0] && afterData.Metadata[0].Metadata)
1922
+ ? afterData.Metadata[0].Metadata.length : 0;
1923
+
1924
+ const actualRemoved = beforeCount - afterCount;
1925
+ const attempted = item_keys.length;
1926
+
1927
+ let resultText = `Playlist "${playlistTitle}" update:\n`;
1928
+ resultText += `• Attempted to remove: ${attempted} item(s)\n`;
1929
+ resultText += `• Actually removed: ${actualRemoved} item(s)\n`;
1930
+ resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n`;
1931
+
1932
+ if (actualRemoved === attempted) {
1933
+ resultText += `✅ All items removed successfully!`;
1934
+ } else if (actualRemoved > 0) {
1935
+ resultText += `⚠️ Partial success: ${attempted - actualRemoved} item(s) were not found in the playlist`;
1936
+ } else {
1937
+ resultText += `❌ No items were removed. This may indicate:\n`;
1938
+ resultText += ` - Invalid item IDs\n`;
1939
+ resultText += ` - Items not present in playlist\n`;
1940
+ resultText += ` - Permission issues`;
1941
+ }
1702
1942
 
1703
1943
  return {
1704
1944
  content: [
@@ -1739,10 +1979,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1739
1979
 
1740
1980
  const response = await axios.delete(deleteUrl, {
1741
1981
  params,
1742
- httpsAgent: new (require('https').Agent)({
1743
- rejectUnauthorized: false,
1744
- minVersion: 'TLSv1.2'
1745
- })
1982
+ httpsAgent: this.getHttpsAgent()
1746
1983
  });
1747
1984
 
1748
1985
  const resultText = `Successfully deleted playlist ${playlist_id}`;
@@ -1795,10 +2032,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1795
2032
 
1796
2033
  const response = await axios.get(itemUrl, {
1797
2034
  params,
1798
- httpsAgent: new (require('https').Agent)({
1799
- rejectUnauthorized: false,
1800
- minVersion: 'TLSv1.2'
1801
- })
2035
+ httpsAgent: this.getHttpsAgent()
1802
2036
  });
1803
2037
 
1804
2038
  const item = response.data?.MediaContainer?.Metadata?.[0];
@@ -2242,10 +2476,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2242
2476
 
2243
2477
  const response = await axios.get(collectionsUrl, {
2244
2478
  params,
2245
- httpsAgent: new (require('https').Agent)({
2246
- rejectUnauthorized: false,
2247
- minVersion: 'TLSv1.2'
2248
- })
2479
+ httpsAgent: this.getHttpsAgent()
2249
2480
  });
2250
2481
 
2251
2482
  const collections = this.parseCollections(response.data);
@@ -2296,10 +2527,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2296
2527
 
2297
2528
  const response = await axios.get(collectionUrl, {
2298
2529
  params,
2299
- httpsAgent: new (require('https').Agent)({
2300
- rejectUnauthorized: false,
2301
- minVersion: 'TLSv1.2'
2302
- })
2530
+ httpsAgent: this.getHttpsAgent()
2303
2531
  });
2304
2532
 
2305
2533
  const results = this.parseLibraryContent(response.data);
@@ -2399,10 +2627,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2399
2627
 
2400
2628
  const response = await axios.get(mediaUrl, {
2401
2629
  params,
2402
- httpsAgent: new (require('https').Agent)({
2403
- rejectUnauthorized: false,
2404
- minVersion: 'TLSv1.2'
2405
- })
2630
+ httpsAgent: this.getHttpsAgent()
2406
2631
  });
2407
2632
 
2408
2633
  const item = response.data?.MediaContainer?.Metadata?.[0];
@@ -2691,10 +2916,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2691
2916
  // Get library information first
2692
2917
  const librariesResponse = await axios.get(`${plexUrl}/library/sections`, {
2693
2918
  params: { 'X-Plex-Token': plexToken },
2694
- httpsAgent: new (require('https').Agent)({
2695
- rejectUnauthorized: false,
2696
- minVersion: 'TLSv1.2'
2697
- })
2919
+ httpsAgent: this.getHttpsAgent()
2698
2920
  });
2699
2921
 
2700
2922
  const libraries = this.parseLibraries(librariesResponse.data);
@@ -2783,10 +3005,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2783
3005
  'X-Plex-Container-Start': offset,
2784
3006
  'X-Plex-Container-Size': batchSize
2785
3007
  },
2786
- httpsAgent: new (require('https').Agent)({
2787
- rejectUnauthorized: false,
2788
- minVersion: 'TLSv1.2'
2789
- })
3008
+ httpsAgent: this.getHttpsAgent()
2790
3009
  });
2791
3010
 
2792
3011
  const content = this.parseLibraryContent(contentResponse.data);
@@ -2827,10 +3046,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2827
3046
  try {
2828
3047
  const mediaResponse = await axios.get(`${plexUrl}${item.key}`, {
2829
3048
  params: { 'X-Plex-Token': plexToken },
2830
- httpsAgent: new (require('https').Agent)({
2831
- rejectUnauthorized: false,
2832
- minVersion: 'TLSv1.2'
2833
- })
3049
+ httpsAgent: this.getHttpsAgent()
2834
3050
  });
2835
3051
 
2836
3052
  const detailedItem = mediaResponse.data?.MediaContainer?.Metadata?.[0];
@@ -3059,10 +3275,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3059
3275
  } else {
3060
3276
  const librariesResponse = await axios.get(`${plexUrl}/library/sections`, {
3061
3277
  params: { 'X-Plex-Token': plexToken },
3062
- httpsAgent: new (require('https').Agent)({
3063
- rejectUnauthorized: false,
3064
- minVersion: 'TLSv1.2'
3065
- })
3278
+ httpsAgent: this.getHttpsAgent()
3066
3279
  });
3067
3280
 
3068
3281
  const allLibraries = this.parseLibraries(librariesResponse.data);
@@ -3165,10 +3378,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3165
3378
  try {
3166
3379
  const historyResponse = await axios.get(`${plexUrl}/status/sessions/history/all`, {
3167
3380
  params: historyParams,
3168
- httpsAgent: new (require('https').Agent)({
3169
- rejectUnauthorized: false,
3170
- minVersion: 'TLSv1.2'
3171
- })
3381
+ httpsAgent: this.getHttpsAgent()
3172
3382
  });
3173
3383
 
3174
3384
  const history = this.parseWatchHistory(historyResponse.data);
@@ -3249,10 +3459,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3249
3459
  query: trackName,
3250
3460
  type: 10 // Track type
3251
3461
  },
3252
- httpsAgent: new (require('https').Agent)({
3253
- rejectUnauthorized: false,
3254
- minVersion: 'TLSv1.2'
3255
- })
3462
+ httpsAgent: this.getHttpsAgent()
3256
3463
  });
3257
3464
 
3258
3465
  const tracks = this.parseSearchResults(searchResponse.data);
@@ -3260,10 +3467,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3260
3467
  if (track.key) {
3261
3468
  const trackDetailResponse = await axios.get(`${plexUrl}${track.key}`, {
3262
3469
  params: { 'X-Plex-Token': plexToken },
3263
- httpsAgent: new (require('https').Agent)({
3264
- rejectUnauthorized: false,
3265
- minVersion: 'TLSv1.2'
3266
- })
3470
+ httpsAgent: this.getHttpsAgent()
3267
3471
  });
3268
3472
 
3269
3473
  const trackDetail = trackDetailResponse.data?.MediaContainer?.Metadata?.[0];
@@ -3308,10 +3512,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3308
3512
  'X-Plex-Container-Size': 10,
3309
3513
  sort: 'addedAt:desc' // Recently added first
3310
3514
  },
3311
- httpsAgent: new (require('https').Agent)({
3312
- rejectUnauthorized: false,
3313
- minVersion: 'TLSv1.2'
3314
- })
3515
+ httpsAgent: this.getHttpsAgent()
3315
3516
  });
3316
3517
 
3317
3518
  const tracks = this.parseLibraryContent(genreSearchResponse.data);
@@ -3342,10 +3543,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3342
3543
  query: artist,
3343
3544
  type: 8 // Artist type
3344
3545
  },
3345
- httpsAgent: new (require('https').Agent)({
3346
- rejectUnauthorized: false,
3347
- minVersion: 'TLSv1.2'
3348
- })
3546
+ httpsAgent: this.getHttpsAgent()
3349
3547
  });
3350
3548
 
3351
3549
  const artists = this.parseSearchResults(artistSearchResponse.data);
@@ -3353,10 +3551,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3353
3551
  if (foundArtist.key) {
3354
3552
  const artistDetailResponse = await axios.get(`${plexUrl}${foundArtist.key}`, {
3355
3553
  params: { 'X-Plex-Token': plexToken },
3356
- httpsAgent: new (require('https').Agent)({
3357
- rejectUnauthorized: false,
3358
- minVersion: 'TLSv1.2'
3359
- })
3554
+ httpsAgent: this.getHttpsAgent()
3360
3555
  });
3361
3556
 
3362
3557
  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.0.4",
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,11 @@
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:all": "jest",
14
+ "test:watch": "jest --watch --testPathIgnorePatterns=/tests/e2e/",
15
+ "test:coverage": "jest --coverage --testPathIgnorePatterns=/tests/e2e/",
14
16
  "prepublishOnly": "npm test",
15
17
  "version:patch": "npm version patch",
16
18
  "version:minor": "npm version minor",