plex-mcp 0.0.1 → 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.
- package/README.md +50 -1
- package/TODO.md +252 -0
- package/index.js +304 -109
- package/package.json +18 -9
- package/.claude/settings.local.json +0 -17
- package/README.txt~ +0 -198
- package/package-lock.json~ +0 -4715
- package/package.json~ +0 -25
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:
|
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 +=
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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 =
|
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:
|
1648
|
-
|
1649
|
-
|
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
|
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:
|
1696
|
-
|
1697
|
-
|
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
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
3357
|
-
rejectUnauthorized: false,
|
3358
|
-
minVersion: 'TLSv1.2'
|
3359
|
-
})
|
3554
|
+
httpsAgent: this.getHttpsAgent()
|
3360
3555
|
});
|
3361
3556
|
|
3362
3557
|
const artistTracks = this.parseLibraryContent(artistDetailResponse.data);
|