plex-mcp 0.0.4 → 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 +20 -0
  3. package/index.js +263 -45
  4. package/package.json +2 -1
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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Plex MCP Server - TODO List
2
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
+
3
23
  ## Unimplemented Plex API Features
4
24
 
5
25
  ### Critical Missing APIs (High Priority)
package/index.js CHANGED
@@ -456,6 +456,10 @@ class PlexMCPServer {
456
456
  required: ["playlist_id", "item_keys"],
457
457
  },
458
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
+ /*
459
463
  {
460
464
  name: "remove_from_playlist",
461
465
  description: "Remove items from an existing playlist",
@@ -477,6 +481,7 @@ class PlexMCPServer {
477
481
  required: ["playlist_id", "item_keys"],
478
482
  },
479
483
  },
484
+ */
480
485
  {
481
486
  name: "delete_playlist",
482
487
  description: "Delete an existing playlist",
@@ -644,8 +649,9 @@ class PlexMCPServer {
644
649
  return await this.handleCreatePlaylist(request.params.arguments);
645
650
  case "add_to_playlist":
646
651
  return await this.handleAddToPlaylist(request.params.arguments);
647
- case "remove_from_playlist":
648
- 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);
649
655
  case "delete_playlist":
650
656
  return await this.handleDeletePlaylist(request.params.arguments);
651
657
  case "get_watched_status":
@@ -1516,7 +1522,9 @@ class PlexMCPServer {
1516
1522
  const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1517
1523
  const response = await axios.get(playlistUrl, {
1518
1524
  params: {
1519
- 'X-Plex-Token': plexToken
1525
+ 'X-Plex-Token': plexToken,
1526
+ 'X-Plex-Container-Start': 0,
1527
+ 'X-Plex-Container-Size': limit || 50
1520
1528
  },
1521
1529
  httpsAgent: this.getHttpsAgent()
1522
1530
  });
@@ -1527,14 +1535,46 @@ class PlexMCPServer {
1527
1535
  content: [
1528
1536
  {
1529
1537
  type: "text",
1530
- text: `Playlist with ID ${playlist_id} not found or is empty`,
1538
+ text: `Playlist with ID ${playlist_id} not found`,
1531
1539
  },
1532
1540
  ],
1533
1541
  };
1534
1542
  }
1535
1543
 
1536
1544
  const playlist = playlistData.Metadata[0];
1537
- const items = playlistData.Metadata[0].Metadata || [];
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
+ }
1538
1578
 
1539
1579
  // Limit results if specified
1540
1580
  const limitedItems = limit ? items.slice(0, limit) : items;
@@ -1555,7 +1595,7 @@ class PlexMCPServer {
1555
1595
  resultText += `\n\n`;
1556
1596
 
1557
1597
  if (limitedItems.length === 0) {
1558
- resultText += `This playlist is empty.`;
1598
+ resultText += `This playlist appears to be empty or items could not be retrieved.`;
1559
1599
  } else {
1560
1600
  resultText += limitedItems.map((item, index) => {
1561
1601
  let itemText = `${index + 1}. **${item.title}**`;
@@ -1786,6 +1826,17 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1786
1826
  async handleAddToPlaylist(args) {
1787
1827
  const { playlist_id, item_keys } = args;
1788
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
+
1789
1840
  try {
1790
1841
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1791
1842
  const plexToken = process.env.PLEX_TOKEN;
@@ -1795,24 +1846,51 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1795
1846
  }
1796
1847
 
1797
1848
  // Get playlist info before adding items
1798
- const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1799
- const beforeResponse = await axios.get(playlistUrl, {
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, {
1800
1854
  params: {
1801
1855
  'X-Plex-Token': plexToken
1802
1856
  },
1803
1857
  httpsAgent: this.getHttpsAgent()
1804
1858
  });
1805
1859
 
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}`;
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
+ }
1811
1889
 
1812
1890
  const addUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1813
1891
  const params = {
1814
1892
  'X-Plex-Token': plexToken,
1815
- 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(',')
1816
1894
  };
1817
1895
 
1818
1896
  const response = await axios.put(addUrl, null, {
@@ -1820,17 +1898,26 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1820
1898
  httpsAgent: this.getHttpsAgent()
1821
1899
  });
1822
1900
 
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()
1829
- });
1901
+ // Check if the PUT request was successful based on HTTP status
1902
+ const putSuccessful = response.status >= 200 && response.status < 300;
1830
1903
 
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;
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
+ }
1834
1921
 
1835
1922
  const actualAdded = afterCount - beforeCount;
1836
1923
  const attempted = item_keys.length;
@@ -1840,10 +1927,15 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1840
1927
  resultText += `• Actually added: ${actualAdded} item(s)\n`;
1841
1928
  resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n`;
1842
1929
 
1930
+ // If HTTP request was successful but count didn't change,
1931
+ // it's likely the items already exist or are duplicates
1843
1932
  if (actualAdded === attempted) {
1844
1933
  resultText += `✅ All items added successfully!`;
1845
1934
  } else if (actualAdded > 0) {
1846
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.`;
1847
1939
  } else {
1848
1940
  resultText += `❌ No items were added. This may indicate:\n`;
1849
1941
  resultText += ` - Invalid item IDs (use ratingKey from search results)\n`;
@@ -1860,11 +1952,27 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1860
1952
  ],
1861
1953
  };
1862
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
+
1863
1971
  return {
1864
1972
  content: [
1865
1973
  {
1866
1974
  type: "text",
1867
- text: `Error adding items to playlist: ${error.message}`,
1975
+ text: errorMessage,
1868
1976
  },
1869
1977
  ],
1870
1978
  isError: true,
@@ -1872,9 +1980,24 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1872
1980
  }
1873
1981
  }
1874
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
1875
1987
  async handleRemoveFromPlaylist(args) {
1876
1988
  const { playlist_id, item_keys } = args;
1877
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
+
1878
2001
  try {
1879
2002
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1880
2003
  const plexToken = process.env.PLEX_TOKEN;
@@ -1884,24 +2007,81 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1884
2007
  }
1885
2008
 
1886
2009
  // Get playlist info before removing items
1887
- const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1888
- const beforeResponse = await axios.get(playlistUrl, {
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, {
1889
2015
  params: {
1890
2016
  'X-Plex-Token': plexToken
1891
2017
  },
1892
2018
  httpsAgent: this.getHttpsAgent()
1893
2019
  });
1894
2020
 
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}`;
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
+ }
1900
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
1901
2081
  const removeUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1902
2082
  const params = {
1903
2083
  'X-Plex-Token': plexToken,
1904
- 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(',')
1905
2085
  };
1906
2086
 
1907
2087
  const response = await axios.delete(removeUrl, {
@@ -1909,30 +2089,52 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1909
2089
  httpsAgent: this.getHttpsAgent()
1910
2090
  });
1911
2091
 
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()
1918
- });
2092
+ // Check if the DELETE request was successful based on HTTP status
2093
+ const deleteSuccessful = response.status >= 200 && response.status < 300;
1919
2094
 
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;
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
+ }
1923
2112
 
1924
2113
  const actualRemoved = beforeCount - afterCount;
1925
2114
  const attempted = item_keys.length;
1926
2115
 
1927
2116
  let resultText = `Playlist "${playlistTitle}" update:\n`;
1928
2117
  resultText += `• Attempted to remove: ${attempted} item(s)\n`;
2118
+ resultText += `• Found in playlist: ${itemsToRemove.length} item(s)\n`;
1929
2119
  resultText += `• Actually removed: ${actualRemoved} item(s)\n`;
1930
- resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\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
+ }
1931
2127
 
1932
2128
  if (actualRemoved === attempted) {
1933
2129
  resultText += `✅ All items removed successfully!`;
1934
2130
  } else if (actualRemoved > 0) {
1935
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.`;
1936
2138
  } else {
1937
2139
  resultText += `❌ No items were removed. This may indicate:\n`;
1938
2140
  resultText += ` - Invalid item IDs\n`;
@@ -1949,11 +2151,27 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1949
2151
  ],
1950
2152
  };
1951
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
+
1952
2170
  return {
1953
2171
  content: [
1954
2172
  {
1955
2173
  type: "text",
1956
- text: `Error removing items from playlist: ${error.message}`,
2174
+ text: errorMessage,
1957
2175
  },
1958
2176
  ],
1959
2177
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plex-mcp",
3
- "version": "0.0.4",
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": {
@@ -10,6 +10,7 @@
10
10
  "start": "node index.js",
11
11
  "test": "jest --testPathIgnorePatterns=/tests/e2e/",
12
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)\"",
13
14
  "test:all": "jest",
14
15
  "test:watch": "jest --watch --testPathIgnorePatterns=/tests/e2e/",
15
16
  "test:coverage": "jest --coverage --testPathIgnorePatterns=/tests/e2e/",