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.
- package/.github/dependabot.yml +11 -0
- package/README.md +113 -14
- package/TODO.md +20 -0
- package/index.js +512 -135
- package/package.json +7 -5
|
@@ -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
|
-
|
|
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
|
|
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
|
|
59
|
+
### Option 2: Development (Local)
|
|
55
60
|
|
|
56
|
-
For local
|
|
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
|
-
###
|
|
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
|
-
|
|
648
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
1799
|
-
const
|
|
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
|
|
1807
|
-
const
|
|
1808
|
-
?
|
|
1809
|
-
|
|
1810
|
-
|
|
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
|
|
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
|
-
//
|
|
1824
|
-
const
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
1888
|
-
const
|
|
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
|
|
1896
|
-
const
|
|
1897
|
-
?
|
|
1898
|
-
|
|
1899
|
-
|
|
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:
|
|
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
|
-
//
|
|
1913
|
-
const
|
|
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
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
47
|
-
"
|
|
48
|
-
"
|
|
48
|
+
"@types/jest": "^29.5.14",
|
|
49
|
+
"axios-mock-adapter": "^2.1.0",
|
|
50
|
+
"jest": "^29.7.0"
|
|
49
51
|
}
|
|
50
52
|
}
|