plex-mcp 0.0.4 → 0.2.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.
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "npm" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "daily"
package/README.md CHANGED
@@ -8,6 +8,8 @@ A Model Context Protocol (MCP) server for searching Plex media libraries using C
8
8
  - Filter by content type
9
9
  - Configurable result limits
10
10
  - Rich formatted results with metadata
11
+ - **Direct Plex authentication with OAuth flow**
12
+ - Support for both static tokens and interactive authentication
11
13
 
12
14
  ## Setup
13
15
 
@@ -16,25 +18,28 @@ A Model Context Protocol (MCP) server for searching Plex media libraries using C
16
18
  npm install
17
19
  ```
18
20
 
19
- 2. Configure your Plex connection:
20
- - Copy `.env.example` to `.env`
21
+ 2. Configure your Plex connection (two options):
22
+
23
+ **Option A: Interactive Authentication (Recommended)**
24
+ - Set your Plex server URL:
25
+ ```
26
+ PLEX_URL=http://your-plex-server:32400
27
+ ```
28
+ - Use the `authenticate_plex` tool for OAuth login (see Authentication section below)
29
+
30
+ **Option B: Static Token**
21
31
  - Set your Plex server URL and token:
22
32
  ```
23
33
  PLEX_URL=http://your-plex-server:32400
24
34
  PLEX_TOKEN=your_plex_token
25
35
  ```
26
-
27
- 3. Get your Plex token:
28
- - Log into your Plex account
29
- - Go to Settings > Account > Privacy
30
- - Click "Show" next to "Plex Pass Subscription"
31
- - Your token will be displayed
36
+ - Get your Plex token by visiting [Plex Token](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/)
32
37
 
33
38
  ## Claude Desktop Configuration
34
39
 
35
- ### Option 1: Using npx (Recommended)
40
+ ### Option 1: Production (Using npx - Recommended)
36
41
 
37
- Add this configuration to your Claude Desktop settings:
42
+ Add this configuration to your Claude Desktop settings for the stable published version:
38
43
 
39
44
  ```json
40
45
  {
@@ -51,14 +56,14 @@ Add this configuration to your Claude Desktop settings:
51
56
  }
52
57
  ```
53
58
 
54
- ### Option 2: Local development
59
+ ### Option 2: Development (Local)
55
60
 
56
- For local development, add this configuration:
61
+ For development with your local code changes, add this configuration:
57
62
 
58
63
  ```json
59
64
  {
60
65
  "mcpServers": {
61
- "plex": {
66
+ "plex-dev": {
62
67
  "command": "node",
63
68
  "args": ["/path/to/your/plex-mcp/index.js"],
64
69
  "env": {
@@ -72,6 +77,33 @@ For local development, add this configuration:
72
77
 
73
78
  Replace `/path/to/your/plex-mcp/` with the actual path to this project directory.
74
79
 
80
+ ### Running Both Versions
81
+
82
+ You can configure both versions simultaneously by using different server names (`plex` and `plex-dev`):
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "plex": {
88
+ "command": "npx",
89
+ "args": ["plex-mcp"],
90
+ "env": {
91
+ "PLEX_URL": "http://your-plex-server:32400",
92
+ "PLEX_TOKEN": "your_plex_token"
93
+ }
94
+ },
95
+ "plex-dev": {
96
+ "command": "node",
97
+ "args": ["/path/to/your/plex-mcp/index.js"],
98
+ "env": {
99
+ "PLEX_URL": "http://your-plex-server:32400",
100
+ "PLEX_TOKEN": "your_plex_token"
101
+ }
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
75
107
  **Configuration Steps:**
76
108
  1. Open Claude Desktop settings (Cmd/Ctrl + ,)
77
109
  2. Navigate to the "MCP Servers" tab
@@ -86,9 +118,76 @@ Run the MCP server standalone:
86
118
  node index.js
87
119
  ```
88
120
 
121
+ ## Authentication
122
+
123
+ The Plex MCP server supports two authentication methods:
124
+
125
+ ### 1. Interactive OAuth Authentication (Recommended)
126
+
127
+ Use the built-in OAuth flow for secure, interactive authentication:
128
+
129
+ 1. **Start Authentication:**
130
+ ```
131
+ Use the authenticate_plex tool
132
+ ```
133
+ This will provide you with a Plex login URL and pin ID.
134
+
135
+ 2. **Complete Login:**
136
+ - Open the provided URL in your browser
137
+ - Sign into your Plex account
138
+ - Grant access to the MCP application
139
+
140
+ 3. **Check Authentication Status:**
141
+ ```
142
+ Use the check_auth_status tool
143
+ ```
144
+ This confirms authentication completion and stores your token.
145
+
146
+ 4. **Clear Authentication (Optional):**
147
+ ```
148
+ Use the clear_auth tool
149
+ ```
150
+ This removes stored credentials if needed.
151
+
152
+ ### 2. Static Token Authentication
153
+
154
+ For automated setups or if you prefer manual token management:
155
+
156
+ 1. Obtain your Plex token from [Plex Support](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/)
157
+ 2. Set the `PLEX_TOKEN` environment variable
158
+ 3. All tools will automatically use this token
159
+
160
+ **Note:** The OAuth method takes precedence - if both are available, static tokens are used as fallback.
161
+
89
162
  ## MCP Tools
90
163
 
91
- ### search_plex
164
+ ### Authentication Tools
165
+
166
+ #### authenticate_plex
167
+ Start the Plex OAuth authentication flow.
168
+
169
+ **Parameters:** None
170
+
171
+ **Returns:** Login URL and pin ID for browser authentication.
172
+
173
+ #### check_auth_status
174
+ Check if OAuth authentication is complete and retrieve the token.
175
+
176
+ **Parameters:**
177
+ - `pin_id` (string, optional): Specific pin ID to check
178
+
179
+ **Returns:** Authentication status and success confirmation.
180
+
181
+ #### clear_auth
182
+ Clear stored authentication credentials.
183
+
184
+ **Parameters:** None
185
+
186
+ **Returns:** Confirmation of credential removal.
187
+
188
+ ### Content Tools
189
+
190
+ #### search_plex
92
191
 
93
192
  Search for content in your Plex libraries.
94
193
 
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
@@ -7,6 +7,84 @@ const {
7
7
  ListToolsRequestSchema,
8
8
  } = require("@modelcontextprotocol/sdk/types.js");
9
9
  const axios = require('axios');
10
+ const { PlexOauth } = require('plex-oauth');
11
+
12
+ class PlexAuthManager {
13
+ constructor() {
14
+ this.authToken = null;
15
+ this.plexOauth = null;
16
+ this.currentPinId = null;
17
+ }
18
+
19
+ async getAuthToken() {
20
+ // Try static token first
21
+ const staticToken = process.env.PLEX_TOKEN;
22
+ if (staticToken) {
23
+ return staticToken;
24
+ }
25
+
26
+ // Return stored OAuth token if available
27
+ if (this.authToken) {
28
+ return this.authToken;
29
+ }
30
+
31
+ throw new Error('No authentication token available. Please authenticate first using the authenticate_plex tool or set PLEX_TOKEN environment variable.');
32
+ }
33
+
34
+ initializeOAuth() {
35
+ if (this.plexOauth) {
36
+ return this.plexOauth;
37
+ }
38
+
39
+ const clientInfo = {
40
+ clientIdentifier: process.env.PLEX_CLIENT_ID || 'plex-mcp-client',
41
+ product: process.env.PLEX_PRODUCT || 'Plex MCP Server',
42
+ device: process.env.PLEX_DEVICE || 'MCP Server',
43
+ version: process.env.PLEX_VERSION || '1.0.0',
44
+ forwardUrl: process.env.PLEX_REDIRECT_URL || 'https://app.plex.tv/auth#!',
45
+ platform: process.env.PLEX_PLATFORM || 'Web'
46
+ };
47
+
48
+ this.plexOauth = new PlexOauth(clientInfo);
49
+ return this.plexOauth;
50
+ }
51
+
52
+ async requestAuthUrl() {
53
+ const oauth = this.initializeOAuth();
54
+ try {
55
+ const [hostedUILink, pinId] = await oauth.requestHostedLoginURL();
56
+ this.currentPinId = pinId;
57
+ return { loginUrl: hostedUILink, pinId };
58
+ } catch (error) {
59
+ throw new Error(`Failed to request authentication URL: ${error.message}`);
60
+ }
61
+ }
62
+
63
+ async checkAuthToken(pinId = null) {
64
+ const oauth = this.initializeOAuth();
65
+ const pin = pinId || this.currentPinId;
66
+
67
+ if (!pin) {
68
+ throw new Error('No pin ID available. Please request authentication first.');
69
+ }
70
+
71
+ try {
72
+ const authToken = await oauth.checkForAuthToken(pin);
73
+ if (authToken) {
74
+ this.authToken = authToken;
75
+ return authToken;
76
+ }
77
+ return null;
78
+ } catch (error) {
79
+ throw new Error(`Failed to check authentication token: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ clearAuth() {
84
+ this.authToken = null;
85
+ this.currentPinId = null;
86
+ }
87
+ }
10
88
 
11
89
  class PlexMCPServer {
12
90
  constructor() {
@@ -22,6 +100,7 @@ class PlexMCPServer {
22
100
  }
23
101
  );
24
102
 
103
+ this.authManager = new PlexAuthManager();
25
104
  this.setupToolHandlers();
26
105
  }
27
106
 
@@ -456,6 +535,10 @@ class PlexMCPServer {
456
535
  required: ["playlist_id", "item_keys"],
457
536
  },
458
537
  },
538
+ // DISABLED: remove_from_playlist - PROBLEMATIC due to Plex API limitations
539
+ // This operation removes ALL instances of matching items, not just one
540
+ // Uncomment only after implementing safer removal patterns
541
+ /*
459
542
  {
460
543
  name: "remove_from_playlist",
461
544
  description: "Remove items from an existing playlist",
@@ -477,6 +560,7 @@ class PlexMCPServer {
477
560
  required: ["playlist_id", "item_keys"],
478
561
  },
479
562
  },
563
+ */
480
564
  {
481
565
  name: "delete_playlist",
482
566
  description: "Delete an existing playlist",
@@ -618,6 +702,38 @@ class PlexMCPServer {
618
702
  required: [],
619
703
  },
620
704
  },
705
+ {
706
+ name: "authenticate_plex",
707
+ description: "Initiate Plex OAuth authentication flow to get user login URL",
708
+ inputSchema: {
709
+ type: "object",
710
+ properties: {},
711
+ required: [],
712
+ },
713
+ },
714
+ {
715
+ name: "check_auth_status",
716
+ description: "Check if Plex authentication is complete and retrieve the auth token",
717
+ inputSchema: {
718
+ type: "object",
719
+ properties: {
720
+ pin_id: {
721
+ type: "string",
722
+ description: "Optional pin ID to check. If not provided, uses the last requested pin.",
723
+ },
724
+ },
725
+ required: [],
726
+ },
727
+ },
728
+ {
729
+ name: "clear_auth",
730
+ description: "Clear stored authentication credentials",
731
+ inputSchema: {
732
+ type: "object",
733
+ properties: {},
734
+ required: [],
735
+ },
736
+ },
621
737
  ],
622
738
  };
623
739
  });
@@ -644,8 +760,9 @@ class PlexMCPServer {
644
760
  return await this.handleCreatePlaylist(request.params.arguments);
645
761
  case "add_to_playlist":
646
762
  return await this.handleAddToPlaylist(request.params.arguments);
647
- case "remove_from_playlist":
648
- return await this.handleRemoveFromPlaylist(request.params.arguments);
763
+ // DISABLED: remove_from_playlist - PROBLEMATIC operation
764
+ // case "remove_from_playlist":
765
+ // return await this.handleRemoveFromPlaylist(request.params.arguments);
649
766
  case "delete_playlist":
650
767
  return await this.handleDeletePlaylist(request.params.arguments);
651
768
  case "get_watched_status":
@@ -660,12 +777,132 @@ class PlexMCPServer {
660
777
  return await this.handleGetLibraryStats(request.params.arguments);
661
778
  case "get_listening_stats":
662
779
  return await this.handleGetListeningStats(request.params.arguments);
780
+ case "authenticate_plex":
781
+ return await this.handleAuthenticatePlex(request.params.arguments);
782
+ case "check_auth_status":
783
+ return await this.handleCheckAuthStatus(request.params.arguments);
784
+ case "clear_auth":
785
+ return await this.handleClearAuth(request.params.arguments);
663
786
  default:
664
787
  throw new Error(`Unknown tool: ${request.params.name}`);
665
788
  }
666
789
  });
667
790
  }
668
791
 
792
+ async handleAuthenticatePlex(args) {
793
+ try {
794
+ const { loginUrl, pinId } = await this.authManager.requestAuthUrl();
795
+
796
+ return {
797
+ content: [
798
+ {
799
+ type: "text",
800
+ text: `Plex Authentication Started
801
+
802
+ **Next Steps:**
803
+ 1. Open this URL in your browser: ${loginUrl}
804
+ 2. Sign into your Plex account
805
+ 3. After signing in, run the check_auth_status tool to complete authentication
806
+
807
+ **Pin ID:** ${pinId}
808
+
809
+ Note: Keep this pin ID if you want to check auth status manually later.`
810
+ }
811
+ ]
812
+ };
813
+ } catch (error) {
814
+ return {
815
+ content: [
816
+ {
817
+ type: "text",
818
+ text: `❌ Authentication Error: ${error.message}`
819
+ }
820
+ ],
821
+ isError: true
822
+ };
823
+ }
824
+ }
825
+
826
+ async handleCheckAuthStatus(args) {
827
+ const { pin_id } = args;
828
+
829
+ try {
830
+ const authToken = await this.authManager.checkAuthToken(pin_id);
831
+
832
+ if (authToken) {
833
+ return {
834
+ content: [
835
+ {
836
+ type: "text",
837
+ text: `✅ Plex Authentication Successful!
838
+
839
+ Your authentication token has been stored and will be used for all Plex API requests. You can now use all Plex tools without needing the PLEX_TOKEN environment variable.
840
+
841
+ **Note:** This token is stored only for this session. For persistent authentication, consider setting the PLEX_TOKEN environment variable.`
842
+ }
843
+ ]
844
+ };
845
+ } else {
846
+ return {
847
+ content: [
848
+ {
849
+ type: "text",
850
+ text: `⏳ Authentication Pending
851
+
852
+ The user has not yet completed the authentication process. Please:
853
+
854
+ 1. Make sure you've visited the login URL from the authenticate_plex tool
855
+ 2. Sign into your Plex account in the browser
856
+ 3. Try checking the auth status again in a few moments
857
+
858
+ You can run check_auth_status again to check if authentication is complete.`
859
+ }
860
+ ]
861
+ };
862
+ }
863
+ } catch (error) {
864
+ return {
865
+ content: [
866
+ {
867
+ type: "text",
868
+ text: `❌ Auth Status Check Error: ${error.message}`
869
+ }
870
+ ],
871
+ isError: true
872
+ };
873
+ }
874
+ }
875
+
876
+ async handleClearAuth(args) {
877
+ try {
878
+ this.authManager.clearAuth();
879
+
880
+ return {
881
+ content: [
882
+ {
883
+ type: "text",
884
+ text: `🔄 Authentication Cleared
885
+
886
+ All stored authentication credentials have been cleared. To use Plex tools again, you'll need to either:
887
+
888
+ 1. Set the PLEX_TOKEN environment variable, or
889
+ 2. Run the authenticate_plex tool to sign in again`
890
+ }
891
+ ]
892
+ };
893
+ } catch (error) {
894
+ return {
895
+ content: [
896
+ {
897
+ type: "text",
898
+ text: `❌ Clear Auth Error: ${error.message}`
899
+ }
900
+ ],
901
+ isError: true
902
+ };
903
+ }
904
+ }
905
+
669
906
  async handlePlexSearch(args) {
670
907
  const {
671
908
  query,
@@ -700,11 +937,7 @@ class PlexMCPServer {
700
937
 
701
938
  try {
702
939
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
703
- const plexToken = process.env.PLEX_TOKEN;
704
-
705
- if (!plexToken) {
706
- throw new Error('PLEX_TOKEN environment variable is required');
707
- }
940
+ const plexToken = await this.authManager.getAuthToken();
708
941
 
709
942
  const searchUrl = `${plexUrl}/search`;
710
943
  const params = {
@@ -885,11 +1118,7 @@ class PlexMCPServer {
885
1118
  async handleBrowseLibraries(args) {
886
1119
  try {
887
1120
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
888
- const plexToken = process.env.PLEX_TOKEN;
889
-
890
- if (!plexToken) {
891
- throw new Error('PLEX_TOKEN environment variable is required');
892
- }
1121
+ const plexToken = await this.authManager.getAuthToken();
893
1122
 
894
1123
  const librariesUrl = `${plexUrl}/library/sections`;
895
1124
  const params = {
@@ -1001,11 +1230,7 @@ class PlexMCPServer {
1001
1230
 
1002
1231
  try {
1003
1232
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1004
- const plexToken = process.env.PLEX_TOKEN;
1005
-
1006
- if (!plexToken) {
1007
- throw new Error('PLEX_TOKEN environment variable is required');
1008
- }
1233
+ const plexToken = await this.authManager.getAuthToken();
1009
1234
 
1010
1235
  const libraryUrl = `${plexUrl}/library/sections/${library_id}/all`;
1011
1236
  const params = {
@@ -1127,11 +1352,7 @@ class PlexMCPServer {
1127
1352
 
1128
1353
  try {
1129
1354
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1130
- const plexToken = process.env.PLEX_TOKEN;
1131
-
1132
- if (!plexToken) {
1133
- throw new Error('PLEX_TOKEN environment variable is required');
1134
- }
1355
+ const plexToken = await this.authManager.getAuthToken();
1135
1356
 
1136
1357
  let recentUrl;
1137
1358
  if (library_id) {
@@ -1227,11 +1448,7 @@ class PlexMCPServer {
1227
1448
 
1228
1449
  try {
1229
1450
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1230
- const plexToken = process.env.PLEX_TOKEN;
1231
-
1232
- if (!plexToken) {
1233
- throw new Error('PLEX_TOKEN environment variable is required');
1234
- }
1451
+ const plexToken = await this.authManager.getAuthToken();
1235
1452
 
1236
1453
  const historyUrl = `${plexUrl}/status/sessions/history/all`;
1237
1454
  const params = {
@@ -1351,11 +1568,7 @@ class PlexMCPServer {
1351
1568
 
1352
1569
  try {
1353
1570
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1354
- const plexToken = process.env.PLEX_TOKEN;
1355
-
1356
- if (!plexToken) {
1357
- throw new Error('PLEX_TOKEN environment variable is required');
1358
- }
1571
+ const plexToken = await this.authManager.getAuthToken();
1359
1572
 
1360
1573
  const onDeckUrl = `${plexUrl}/library/onDeck`;
1361
1574
  const params = {
@@ -1454,11 +1667,7 @@ class PlexMCPServer {
1454
1667
 
1455
1668
  try {
1456
1669
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1457
- const plexToken = process.env.PLEX_TOKEN;
1458
-
1459
- if (!plexToken) {
1460
- throw new Error('PLEX_TOKEN environment variable is required');
1461
- }
1670
+ const plexToken = await this.authManager.getAuthToken();
1462
1671
 
1463
1672
  const playlistsUrl = `${plexUrl}/playlists`;
1464
1673
  const params = {
@@ -1506,17 +1715,15 @@ class PlexMCPServer {
1506
1715
 
1507
1716
  try {
1508
1717
  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
- }
1718
+ const plexToken = await this.authManager.getAuthToken();
1514
1719
 
1515
1720
  // First get playlist info
1516
1721
  const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1517
1722
  const response = await axios.get(playlistUrl, {
1518
1723
  params: {
1519
- 'X-Plex-Token': plexToken
1724
+ 'X-Plex-Token': plexToken,
1725
+ 'X-Plex-Container-Start': 0,
1726
+ 'X-Plex-Container-Size': limit || 50
1520
1727
  },
1521
1728
  httpsAgent: this.getHttpsAgent()
1522
1729
  });
@@ -1527,14 +1734,46 @@ class PlexMCPServer {
1527
1734
  content: [
1528
1735
  {
1529
1736
  type: "text",
1530
- text: `Playlist with ID ${playlist_id} not found or is empty`,
1737
+ text: `Playlist with ID ${playlist_id} not found`,
1531
1738
  },
1532
1739
  ],
1533
1740
  };
1534
1741
  }
1535
1742
 
1536
1743
  const playlist = playlistData.Metadata[0];
1537
- const items = playlistData.Metadata[0].Metadata || [];
1744
+
1745
+ // Try to get items from the current response first
1746
+ let items = playlistData.Metadata[0].Metadata || [];
1747
+
1748
+ // If no items found, try the /items endpoint specifically
1749
+ if (items.length === 0 && playlist.leafCount && playlist.leafCount > 0) {
1750
+ try {
1751
+ const itemsUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1752
+ const itemsResponse = await axios.get(itemsUrl, {
1753
+ params: {
1754
+ 'X-Plex-Token': plexToken,
1755
+ 'X-Plex-Container-Start': 0,
1756
+ 'X-Plex-Container-Size': limit || 50
1757
+ },
1758
+ httpsAgent: this.getHttpsAgent()
1759
+ });
1760
+
1761
+ const itemsData = itemsResponse.data.MediaContainer;
1762
+ if (itemsData && itemsData.Metadata) {
1763
+ items = itemsData.Metadata;
1764
+ }
1765
+ } catch (itemsError) {
1766
+ console.error(`Failed to fetch playlist items via /items endpoint: ${itemsError.message}`);
1767
+ return {
1768
+ content: [
1769
+ {
1770
+ type: "text",
1771
+ text: `Error retrieving playlist items: ${itemsError.message}`,
1772
+ },
1773
+ ],
1774
+ };
1775
+ }
1776
+ }
1538
1777
 
1539
1778
  // Limit results if specified
1540
1779
  const limitedItems = limit ? items.slice(0, limit) : items;
@@ -1555,7 +1794,7 @@ class PlexMCPServer {
1555
1794
  resultText += `\n\n`;
1556
1795
 
1557
1796
  if (limitedItems.length === 0) {
1558
- resultText += `This playlist is empty.`;
1797
+ resultText += `This playlist appears to be empty or items could not be retrieved.`;
1559
1798
  } else {
1560
1799
  resultText += limitedItems.map((item, index) => {
1561
1800
  let itemText = `${index + 1}. **${item.title}**`;
@@ -1690,11 +1929,7 @@ class PlexMCPServer {
1690
1929
 
1691
1930
  try {
1692
1931
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1693
- const plexToken = process.env.PLEX_TOKEN;
1694
-
1695
- if (!plexToken) {
1696
- throw new Error('PLEX_TOKEN environment variable is required');
1697
- }
1932
+ const plexToken = await this.authManager.getAuthToken();
1698
1933
 
1699
1934
  // First get server info to get machine identifier
1700
1935
  const serverResponse = await axios.get(`${plexUrl}/`, {
@@ -1786,33 +2021,67 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1786
2021
  async handleAddToPlaylist(args) {
1787
2022
  const { playlist_id, item_keys } = args;
1788
2023
 
2024
+ // Input validation
2025
+ if (!playlist_id || typeof playlist_id !== 'string') {
2026
+ throw new Error('Valid playlist_id is required');
2027
+ }
2028
+ if (!item_keys || !Array.isArray(item_keys) || item_keys.length === 0) {
2029
+ throw new Error('item_keys must be a non-empty array');
2030
+ }
2031
+ if (item_keys.some(key => !key || typeof key !== 'string')) {
2032
+ throw new Error('All item_keys must be non-empty strings');
2033
+ }
2034
+
1789
2035
  try {
1790
2036
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1791
- const plexToken = process.env.PLEX_TOKEN;
1792
-
1793
- if (!plexToken) {
1794
- throw new Error('PLEX_TOKEN environment variable is required');
1795
- }
2037
+ const plexToken = await this.authManager.getAuthToken();
1796
2038
 
1797
2039
  // Get playlist info before adding items
1798
- const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1799
- const beforeResponse = await axios.get(playlistUrl, {
2040
+ const playlistInfoUrl = `${plexUrl}/playlists/${playlist_id}`;
2041
+ const playlistItemsUrl = `${plexUrl}/playlists/${playlist_id}/items`;
2042
+
2043
+ // Get playlist metadata (title, etc.)
2044
+ const playlistInfoResponse = await axios.get(playlistInfoUrl, {
1800
2045
  params: {
1801
2046
  'X-Plex-Token': plexToken
1802
2047
  },
1803
2048
  httpsAgent: this.getHttpsAgent()
1804
2049
  });
1805
2050
 
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}`;
2051
+ const playlistInfo = playlistInfoResponse.data.MediaContainer;
2052
+ const playlistTitle = playlistInfo.Metadata && playlistInfo.Metadata[0]
2053
+ ? playlistInfo.Metadata[0].title : `Playlist ${playlist_id}`;
2054
+
2055
+ // Get current playlist items count
2056
+ let beforeCount = 0;
2057
+ try {
2058
+ const beforeResponse = await axios.get(playlistItemsUrl, {
2059
+ params: {
2060
+ 'X-Plex-Token': plexToken
2061
+ },
2062
+ httpsAgent: this.getHttpsAgent()
2063
+ });
2064
+ beforeCount = beforeResponse.data.MediaContainer?.totalSize || 0;
2065
+ } catch (error) {
2066
+ // If items endpoint fails, playlist might be empty
2067
+ beforeCount = 0;
2068
+ }
2069
+
2070
+ // Get server machine identifier for proper URI format
2071
+ const serverResponse = await axios.get(`${plexUrl}/`, {
2072
+ headers: { 'X-Plex-Token': plexToken },
2073
+ httpsAgent: this.getHttpsAgent()
2074
+ });
2075
+
2076
+ const machineIdentifier = serverResponse.data?.MediaContainer?.machineIdentifier;
2077
+ if (!machineIdentifier) {
2078
+ throw new Error('Could not get server machine identifier');
2079
+ }
1811
2080
 
1812
2081
  const addUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1813
2082
  const params = {
1814
2083
  'X-Plex-Token': plexToken,
1815
- uri: item_keys.map(key => `server://localhost/com.plexapp.plugins.library/library/metadata/${key}`).join(',')
2084
+ uri: item_keys.map(key => `server://${machineIdentifier}/com.plexapp.plugins.library/library/metadata/${key}`).join(',')
1816
2085
  };
1817
2086
 
1818
2087
  const response = await axios.put(addUrl, null, {
@@ -1820,17 +2089,26 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1820
2089
  httpsAgent: this.getHttpsAgent()
1821
2090
  });
1822
2091
 
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
- });
2092
+ // Check if the PUT request was successful based on HTTP status
2093
+ const putSuccessful = response.status >= 200 && response.status < 300;
2094
+
2095
+ // Small delay to allow Plex server to update
2096
+ await new Promise(resolve => setTimeout(resolve, 100));
1830
2097
 
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;
2098
+ // Verify the addition 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
+ }
1834
2112
 
1835
2113
  const actualAdded = afterCount - beforeCount;
1836
2114
  const attempted = item_keys.length;
@@ -1840,10 +2118,15 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1840
2118
  resultText += `• Actually added: ${actualAdded} item(s)\n`;
1841
2119
  resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n`;
1842
2120
 
2121
+ // If HTTP request was successful but count didn't change,
2122
+ // it's likely the items already exist or are duplicates
1843
2123
  if (actualAdded === attempted) {
1844
2124
  resultText += `✅ All items added successfully!`;
1845
2125
  } else if (actualAdded > 0) {
1846
2126
  resultText += `⚠️ Partial success: ${attempted - actualAdded} item(s) may have been duplicates or invalid`;
2127
+ } else if (putSuccessful) {
2128
+ resultText += `✅ API request successful! Items may already exist in playlist or were duplicates.\n`;
2129
+ resultText += `ℹ️ This is normal behavior - Plex doesn't add duplicate items.`;
1847
2130
  } else {
1848
2131
  resultText += `❌ No items were added. This may indicate:\n`;
1849
2132
  resultText += ` - Invalid item IDs (use ratingKey from search results)\n`;
@@ -1860,11 +2143,27 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1860
2143
  ],
1861
2144
  };
1862
2145
  } catch (error) {
2146
+ // Enhanced error handling with specific error types
2147
+ let errorMessage = `Error adding items to playlist: ${error.message}`;
2148
+
2149
+ if (error.response) {
2150
+ const status = error.response.status;
2151
+ if (status === 404) {
2152
+ errorMessage = `Playlist with ID ${playlist_id} not found`;
2153
+ } else if (status === 401 || status === 403) {
2154
+ errorMessage = `Permission denied: Check your Plex token and server access`;
2155
+ } else if (status >= 500) {
2156
+ errorMessage = `Plex server error (${status}): ${error.message}`;
2157
+ }
2158
+ } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
2159
+ errorMessage = `Cannot connect to Plex server: Check PLEX_URL configuration`;
2160
+ }
2161
+
1863
2162
  return {
1864
2163
  content: [
1865
2164
  {
1866
2165
  type: "text",
1867
- text: `Error adding items to playlist: ${error.message}`,
2166
+ text: errorMessage,
1868
2167
  },
1869
2168
  ],
1870
2169
  isError: true,
@@ -1872,36 +2171,104 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1872
2171
  }
1873
2172
  }
1874
2173
 
2174
+ // DISABLED METHOD - PROBLEMATIC OPERATION
2175
+ // This method is currently disabled due to destructive Plex API behavior
2176
+ // It removes ALL instances of matching items, not just one instance
2177
+ // Use with extreme caution - consider implementing safer alternatives
1875
2178
  async handleRemoveFromPlaylist(args) {
1876
2179
  const { playlist_id, item_keys } = args;
1877
2180
 
2181
+ // Input validation
2182
+ if (!playlist_id || typeof playlist_id !== 'string') {
2183
+ throw new Error('Valid playlist_id is required');
2184
+ }
2185
+ if (!item_keys || !Array.isArray(item_keys) || item_keys.length === 0) {
2186
+ throw new Error('item_keys must be a non-empty array');
2187
+ }
2188
+ if (item_keys.some(key => !key || typeof key !== 'string')) {
2189
+ throw new Error('All item_keys must be non-empty strings');
2190
+ }
2191
+
1878
2192
  try {
1879
2193
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1880
- const plexToken = process.env.PLEX_TOKEN;
1881
-
1882
- if (!plexToken) {
1883
- throw new Error('PLEX_TOKEN environment variable is required');
1884
- }
2194
+ const plexToken = await this.authManager.getAuthToken();
1885
2195
 
1886
2196
  // Get playlist info before removing items
1887
- const playlistUrl = `${plexUrl}/playlists/${playlist_id}`;
1888
- const beforeResponse = await axios.get(playlistUrl, {
2197
+ const playlistInfoUrl = `${plexUrl}/playlists/${playlist_id}`;
2198
+ const playlistItemsUrl = `${plexUrl}/playlists/${playlist_id}/items`;
2199
+
2200
+ // Get playlist metadata (title, etc.)
2201
+ const playlistInfoResponse = await axios.get(playlistInfoUrl, {
1889
2202
  params: {
1890
2203
  'X-Plex-Token': plexToken
1891
2204
  },
1892
2205
  httpsAgent: this.getHttpsAgent()
1893
2206
  });
1894
2207
 
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}`;
2208
+ const playlistInfo = playlistInfoResponse.data.MediaContainer;
2209
+ const playlistTitle = playlistInfo.Metadata && playlistInfo.Metadata[0]
2210
+ ? playlistInfo.Metadata[0].title : `Playlist ${playlist_id}`;
2211
+
2212
+ // Get current playlist items with their detailed information
2213
+ let beforeCount = 0;
2214
+ let playlistItems = [];
2215
+ try {
2216
+ const beforeResponse = await axios.get(playlistItemsUrl, {
2217
+ params: {
2218
+ 'X-Plex-Token': plexToken
2219
+ },
2220
+ httpsAgent: this.getHttpsAgent()
2221
+ });
2222
+ beforeCount = beforeResponse.data.MediaContainer?.totalSize || 0;
2223
+ playlistItems = beforeResponse.data.MediaContainer?.Metadata || [];
2224
+ } catch (error) {
2225
+ // If items endpoint fails, playlist might be empty
2226
+ beforeCount = 0;
2227
+ playlistItems = [];
2228
+ }
2229
+
2230
+ // Get server machine identifier for proper URI format
2231
+ const serverResponse = await axios.get(`${plexUrl}/`, {
2232
+ headers: { 'X-Plex-Token': plexToken },
2233
+ httpsAgent: this.getHttpsAgent()
2234
+ });
2235
+
2236
+ const machineIdentifier = serverResponse.data?.MediaContainer?.machineIdentifier;
2237
+ if (!machineIdentifier) {
2238
+ throw new Error('Could not get server machine identifier');
2239
+ }
1900
2240
 
2241
+ // Find items to remove by matching ratingKeys to actual playlist positions
2242
+ const itemsToRemove = [];
2243
+ const itemKeysSet = new Set(item_keys);
2244
+
2245
+ playlistItems.forEach((item, index) => {
2246
+ if (itemKeysSet.has(item.ratingKey)) {
2247
+ itemsToRemove.push({
2248
+ ratingKey: item.ratingKey,
2249
+ position: index,
2250
+ title: item.title || 'Unknown'
2251
+ });
2252
+ }
2253
+ });
2254
+
2255
+ if (itemsToRemove.length === 0) {
2256
+ return {
2257
+ content: [
2258
+ {
2259
+ type: "text",
2260
+ text: `No matching items found in playlist "${playlistTitle}".\\nSpecified items may not exist in this playlist.`,
2261
+ },
2262
+ ],
2263
+ };
2264
+ }
2265
+
2266
+ // WARNING: Current Plex API behavior - this removes ALL instances of matching items
2267
+ // This is a limitation of the Plex API - there's no way to remove just specific instances
1901
2268
  const removeUrl = `${plexUrl}/playlists/${playlist_id}/items`;
1902
2269
  const params = {
1903
2270
  'X-Plex-Token': plexToken,
1904
- uri: item_keys.map(key => `server://localhost/com.plexapp.plugins.library/library/metadata/${key}`).join(',')
2271
+ uri: itemsToRemove.map(item => `server://${machineIdentifier}/com.plexapp.plugins.library/library/metadata/${item.ratingKey}`).join(',')
1905
2272
  };
1906
2273
 
1907
2274
  const response = await axios.delete(removeUrl, {
@@ -1909,30 +2276,52 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1909
2276
  httpsAgent: this.getHttpsAgent()
1910
2277
  });
1911
2278
 
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
- });
2279
+ // Check if the DELETE request was successful based on HTTP status
2280
+ const deleteSuccessful = response.status >= 200 && response.status < 300;
1919
2281
 
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;
2282
+ // Small delay to allow Plex server to update
2283
+ await new Promise(resolve => setTimeout(resolve, 100));
2284
+
2285
+ // Verify the removal by checking the playlist items again
2286
+ let afterCount = 0;
2287
+ try {
2288
+ const afterResponse = await axios.get(playlistItemsUrl, {
2289
+ params: {
2290
+ 'X-Plex-Token': plexToken
2291
+ },
2292
+ httpsAgent: this.getHttpsAgent()
2293
+ });
2294
+ afterCount = afterResponse.data.MediaContainer?.totalSize || 0;
2295
+ } catch (error) {
2296
+ // If items endpoint fails, playlist might be empty
2297
+ afterCount = 0;
2298
+ }
1923
2299
 
1924
2300
  const actualRemoved = beforeCount - afterCount;
1925
2301
  const attempted = item_keys.length;
1926
2302
 
1927
2303
  let resultText = `Playlist "${playlistTitle}" update:\n`;
1928
2304
  resultText += `• Attempted to remove: ${attempted} item(s)\n`;
2305
+ resultText += `• Found in playlist: ${itemsToRemove.length} item(s)\n`;
1929
2306
  resultText += `• Actually removed: ${actualRemoved} item(s)\n`;
1930
- resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n`;
2307
+ resultText += `• Playlist size: ${beforeCount} → ${afterCount} items\n\n`;
2308
+
2309
+ // Add warning about Plex behavior
2310
+ if (itemsToRemove.length > 0) {
2311
+ resultText += `⚠️ **Important**: Plex removes ALL instances of matching items from playlists.\n`;
2312
+ resultText += `If you had duplicate tracks, all copies were removed.\n\n`;
2313
+ }
1931
2314
 
1932
2315
  if (actualRemoved === attempted) {
1933
2316
  resultText += `✅ All items removed successfully!`;
1934
2317
  } else if (actualRemoved > 0) {
1935
2318
  resultText += `⚠️ Partial success: ${attempted - actualRemoved} item(s) were not found in the playlist`;
2319
+ } else if (deleteSuccessful && itemsToRemove.length > 0) {
2320
+ resultText += `✅ API request successful! Items were processed.\n`;
2321
+ resultText += `ℹ️ If count didn't change, items may have already been removed previously.`;
2322
+ } else if (deleteSuccessful) {
2323
+ resultText += `✅ API request successful! Items may not have been in the playlist.\n`;
2324
+ resultText += `ℹ️ This is normal behavior - Plex ignores requests to remove non-existent items.`;
1936
2325
  } else {
1937
2326
  resultText += `❌ No items were removed. This may indicate:\n`;
1938
2327
  resultText += ` - Invalid item IDs\n`;
@@ -1949,11 +2338,27 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1949
2338
  ],
1950
2339
  };
1951
2340
  } catch (error) {
2341
+ // Enhanced error handling with specific error types
2342
+ let errorMessage = `Error removing items from playlist: ${error.message}`;
2343
+
2344
+ if (error.response) {
2345
+ const status = error.response.status;
2346
+ if (status === 404) {
2347
+ errorMessage = `Playlist with ID ${playlist_id} not found`;
2348
+ } else if (status === 401 || status === 403) {
2349
+ errorMessage = `Permission denied: Check your Plex token and server access`;
2350
+ } else if (status >= 500) {
2351
+ errorMessage = `Plex server error (${status}): ${error.message}`;
2352
+ }
2353
+ } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
2354
+ errorMessage = `Cannot connect to Plex server: Check PLEX_URL configuration`;
2355
+ }
2356
+
1952
2357
  return {
1953
2358
  content: [
1954
2359
  {
1955
2360
  type: "text",
1956
- text: `Error removing items from playlist: ${error.message}`,
2361
+ text: errorMessage,
1957
2362
  },
1958
2363
  ],
1959
2364
  isError: true,
@@ -1966,11 +2371,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
1966
2371
 
1967
2372
  try {
1968
2373
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
1969
- const plexToken = process.env.PLEX_TOKEN;
1970
-
1971
- if (!plexToken) {
1972
- throw new Error('PLEX_TOKEN environment variable is required');
1973
- }
2374
+ const plexToken = await this.authManager.getAuthToken();
1974
2375
 
1975
2376
  const deleteUrl = `${plexUrl}/playlists/${playlist_id}`;
1976
2377
  const params = {
@@ -2010,11 +2411,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2010
2411
 
2011
2412
  try {
2012
2413
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
2013
- const plexToken = process.env.PLEX_TOKEN;
2014
-
2015
- if (!plexToken) {
2016
- throw new Error('PLEX_TOKEN environment variable is required');
2017
- }
2414
+ const plexToken = await this.authManager.getAuthToken();
2018
2415
 
2019
2416
  const statusResults = [];
2020
2417
 
@@ -2457,11 +2854,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2457
2854
 
2458
2855
  try {
2459
2856
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
2460
- const plexToken = process.env.PLEX_TOKEN;
2461
-
2462
- if (!plexToken) {
2463
- throw new Error('PLEX_TOKEN environment variable is required');
2464
- }
2857
+ const plexToken = await this.authManager.getAuthToken();
2465
2858
 
2466
2859
  let collectionsUrl;
2467
2860
  if (library_id) {
@@ -2511,11 +2904,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2511
2904
 
2512
2905
  try {
2513
2906
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
2514
- const plexToken = process.env.PLEX_TOKEN;
2515
-
2516
- if (!plexToken) {
2517
- throw new Error('PLEX_TOKEN environment variable is required');
2518
- }
2907
+ const plexToken = await this.authManager.getAuthToken();
2519
2908
 
2520
2909
  const collectionUrl = `${plexUrl}/library/collections/${collection_id}/children`;
2521
2910
  const params = {
@@ -2614,11 +3003,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2614
3003
 
2615
3004
  try {
2616
3005
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
2617
- const plexToken = process.env.PLEX_TOKEN;
2618
-
2619
- if (!plexToken) {
2620
- throw new Error('PLEX_TOKEN environment variable is required');
2621
- }
3006
+ const plexToken = await this.authManager.getAuthToken();
2622
3007
 
2623
3008
  const mediaUrl = `${plexUrl}/library/metadata/${item_key}`;
2624
3009
  const params = {
@@ -2907,11 +3292,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
2907
3292
 
2908
3293
  try {
2909
3294
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
2910
- const plexToken = process.env.PLEX_TOKEN;
2911
-
2912
- if (!plexToken) {
2913
- throw new Error('PLEX_TOKEN environment variable is required');
2914
- }
3295
+ const plexToken = await this.authManager.getAuthToken();
2915
3296
 
2916
3297
  // Get library information first
2917
3298
  const librariesResponse = await axios.get(`${plexUrl}/library/sections`, {
@@ -3262,11 +3643,7 @@ You can try creating the playlist manually in Plex and then use other MCP tools
3262
3643
 
3263
3644
  try {
3264
3645
  const plexUrl = process.env.PLEX_URL || 'http://localhost:32400';
3265
- const plexToken = process.env.PLEX_TOKEN;
3266
-
3267
- if (!plexToken) {
3268
- throw new Error('PLEX_TOKEN environment variable is required');
3269
- }
3646
+ const plexToken = await this.authManager.getAuthToken();
3270
3647
 
3271
3648
  // Auto-detect music libraries if not specified
3272
3649
  let musicLibraries = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plex-mcp",
3
- "version": "0.0.4",
3
+ "version": "0.2.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/",
@@ -40,11 +41,12 @@
40
41
  "type": "commonjs",
41
42
  "dependencies": {
42
43
  "@modelcontextprotocol/sdk": "^1.12.1",
43
- "axios": "^1.9.0"
44
+ "axios": "^1.9.0",
45
+ "plex-oauth": "^1.2.2"
44
46
  },
45
47
  "devDependencies": {
46
- "jest": "^29.7.0",
47
- "@types/jest": "^29.5.8",
48
- "axios-mock-adapter": "^1.22.0"
48
+ "@types/jest": "^29.5.14",
49
+ "axios-mock-adapter": "^2.1.0",
50
+ "jest": "^29.7.0"
49
51
  }
50
52
  }