@thelord/mcp-arr 1.0.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/LICENSE +21 -0
- package/README.md +363 -0
- package/dist/arr-client.d.ts +784 -0
- package/dist/arr-client.d.ts.map +1 -0
- package/dist/arr-client.js +487 -0
- package/dist/arr-client.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1947 -0
- package/dist/index.js.map +1 -0
- package/dist/lingarr-client.d.ts +316 -0
- package/dist/lingarr-client.d.ts.map +1 -0
- package/dist/lingarr-client.js +320 -0
- package/dist/lingarr-client.js.map +1 -0
- package/dist/lingarr-handlers.d.ts +40 -0
- package/dist/lingarr-handlers.d.ts.map +1 -0
- package/dist/lingarr-handlers.js +347 -0
- package/dist/lingarr-handlers.js.map +1 -0
- package/dist/lingarr-tools.d.ts +19 -0
- package/dist/lingarr-tools.d.ts.map +1 -0
- package/dist/lingarr-tools.js +271 -0
- package/dist/lingarr-tools.js.map +1 -0
- package/dist/trash-client.d.ts +121 -0
- package/dist/trash-client.d.ts.map +1 -0
- package/dist/trash-client.js +287 -0
- package/dist/trash-client.js.map +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1947 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server for *arr Media Management Suite
|
|
4
|
+
*
|
|
5
|
+
* Provides tools for managing Sonarr (TV), Radarr (Movies), Lidarr (Music),
|
|
6
|
+
* Readarr (Books), and Prowlarr (Indexers) through Claude Code.
|
|
7
|
+
*
|
|
8
|
+
* Environment variables:
|
|
9
|
+
* - SONARR_URL, SONARR_API_KEY
|
|
10
|
+
* - RADARR_URL, RADARR_API_KEY
|
|
11
|
+
* - LIDARR_URL, LIDARR_API_KEY
|
|
12
|
+
* - READARR_URL, READARR_API_KEY
|
|
13
|
+
* - PROWLARR_URL, PROWLARR_API_KEY
|
|
14
|
+
*/
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
18
|
+
import { SonarrClient, RadarrClient, LidarrClient, ReadarrClient, ProwlarrClient, } from "./arr-client.js";
|
|
19
|
+
import { trashClient } from "./trash-client.js";
|
|
20
|
+
import { LingarrClient } from "./lingarr-client.js";
|
|
21
|
+
import { getLingarrTools } from "./lingarr-tools.js";
|
|
22
|
+
import { handleLingarrTool, getLingarrStatus } from "./lingarr-handlers.js";
|
|
23
|
+
const services = [
|
|
24
|
+
{ name: 'sonarr', displayName: 'Sonarr (TV)', url: process.env.SONARR_URL, apiKey: process.env.SONARR_API_KEY },
|
|
25
|
+
{ name: 'radarr', displayName: 'Radarr (Movies)', url: process.env.RADARR_URL, apiKey: process.env.RADARR_API_KEY },
|
|
26
|
+
{ name: 'lidarr', displayName: 'Lidarr (Music)', url: process.env.LIDARR_URL, apiKey: process.env.LIDARR_API_KEY },
|
|
27
|
+
{ name: 'readarr', displayName: 'Readarr (Books)', url: process.env.READARR_URL, apiKey: process.env.READARR_API_KEY },
|
|
28
|
+
{ name: 'prowlarr', displayName: 'Prowlarr (Indexers)', url: process.env.PROWLARR_URL, apiKey: process.env.PROWLARR_API_KEY },
|
|
29
|
+
{ name: 'lingarr', displayName: 'Lingarr (Subtitles)', url: process.env.LINGARR_URL, apiKey: process.env.LINGARR_API_KEY },
|
|
30
|
+
];
|
|
31
|
+
// Check which services are configured
|
|
32
|
+
const configuredServices = services.filter(s => s.url && s.apiKey);
|
|
33
|
+
if (configuredServices.length === 0) {
|
|
34
|
+
console.error("Error: No *arr services configured. Set at least one pair of URL and API_KEY environment variables.");
|
|
35
|
+
console.error("Example: SONARR_URL and SONARR_API_KEY");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
// Initialize clients for configured services
|
|
39
|
+
const clients = {};
|
|
40
|
+
for (const service of configuredServices) {
|
|
41
|
+
const config = { url: service.url, apiKey: service.apiKey };
|
|
42
|
+
switch (service.name) {
|
|
43
|
+
case 'sonarr':
|
|
44
|
+
clients.sonarr = new SonarrClient(config);
|
|
45
|
+
break;
|
|
46
|
+
case 'radarr':
|
|
47
|
+
clients.radarr = new RadarrClient(config);
|
|
48
|
+
break;
|
|
49
|
+
case 'lidarr':
|
|
50
|
+
clients.lidarr = new LidarrClient(config);
|
|
51
|
+
break;
|
|
52
|
+
case 'readarr':
|
|
53
|
+
clients.readarr = new ReadarrClient(config);
|
|
54
|
+
break;
|
|
55
|
+
case 'prowlarr':
|
|
56
|
+
clients.prowlarr = new ProwlarrClient(config);
|
|
57
|
+
break;
|
|
58
|
+
case 'lingarr':
|
|
59
|
+
clients.lingarr = new LingarrClient(config);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Build tools based on configured services
|
|
64
|
+
const TOOLS = [
|
|
65
|
+
// General tool available for all
|
|
66
|
+
{
|
|
67
|
+
name: "arr_status",
|
|
68
|
+
description: `Get status of all configured *arr services. Currently configured: ${configuredServices.map(s => s.displayName).join(', ')}`,
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {},
|
|
72
|
+
required: [],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
// Configuration review tools for each service
|
|
77
|
+
// These are added dynamically based on configured services
|
|
78
|
+
// Helper function to create config tools for a service
|
|
79
|
+
function addConfigTools(serviceName, displayName) {
|
|
80
|
+
TOOLS.push({
|
|
81
|
+
name: `${serviceName}_get_quality_profiles`,
|
|
82
|
+
description: `Get detailed quality profiles from ${displayName}. Shows allowed qualities, upgrade settings, and custom format scores.`,
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {},
|
|
86
|
+
required: [],
|
|
87
|
+
},
|
|
88
|
+
}, {
|
|
89
|
+
name: `${serviceName}_get_health`,
|
|
90
|
+
description: `Get health check warnings and issues from ${displayName}. Shows any problems detected by the application.`,
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {},
|
|
94
|
+
required: [],
|
|
95
|
+
},
|
|
96
|
+
}, {
|
|
97
|
+
name: `${serviceName}_get_root_folders`,
|
|
98
|
+
description: `Get root folders and storage info from ${displayName}. Shows paths, free space, and unmapped folders.`,
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {},
|
|
102
|
+
required: [],
|
|
103
|
+
},
|
|
104
|
+
}, {
|
|
105
|
+
name: `${serviceName}_get_download_clients`,
|
|
106
|
+
description: `Get download client configurations from ${displayName}. Shows configured clients and their settings.`,
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {},
|
|
110
|
+
required: [],
|
|
111
|
+
},
|
|
112
|
+
}, {
|
|
113
|
+
name: `${serviceName}_get_naming`,
|
|
114
|
+
description: `Get file naming configuration from ${displayName}. Shows naming patterns for files and folders.`,
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {},
|
|
118
|
+
required: [],
|
|
119
|
+
},
|
|
120
|
+
}, {
|
|
121
|
+
name: `${serviceName}_get_tags`,
|
|
122
|
+
description: `Get all tags defined in ${displayName}. Tags can be used to organize and filter content.`,
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {},
|
|
126
|
+
required: [],
|
|
127
|
+
},
|
|
128
|
+
}, {
|
|
129
|
+
name: `${serviceName}_review_setup`,
|
|
130
|
+
description: `Get comprehensive configuration review for ${displayName}. Returns all settings for analysis: quality profiles, download clients, naming, storage, indexers, health warnings, and more. Use this to analyze the setup and suggest improvements.`,
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {},
|
|
134
|
+
required: [],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Add config tools for each configured service (except Prowlarr which has different config)
|
|
139
|
+
if (clients.sonarr)
|
|
140
|
+
addConfigTools('sonarr', 'Sonarr (TV)');
|
|
141
|
+
if (clients.radarr)
|
|
142
|
+
addConfigTools('radarr', 'Radarr (Movies)');
|
|
143
|
+
if (clients.lidarr)
|
|
144
|
+
addConfigTools('lidarr', 'Lidarr (Music)');
|
|
145
|
+
if (clients.readarr)
|
|
146
|
+
addConfigTools('readarr', 'Readarr (Books)');
|
|
147
|
+
// Sonarr tools
|
|
148
|
+
if (clients.sonarr) {
|
|
149
|
+
TOOLS.push({
|
|
150
|
+
name: "sonarr_get_series",
|
|
151
|
+
description: "Get all TV series in Sonarr library",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {},
|
|
155
|
+
required: [],
|
|
156
|
+
},
|
|
157
|
+
}, {
|
|
158
|
+
name: "sonarr_search",
|
|
159
|
+
description: "Search for TV series to add to Sonarr",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
term: {
|
|
164
|
+
type: "string",
|
|
165
|
+
description: "Search term (show name)",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
required: ["term"],
|
|
169
|
+
},
|
|
170
|
+
}, {
|
|
171
|
+
name: "sonarr_get_queue",
|
|
172
|
+
description: "Get Sonarr download queue",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {},
|
|
176
|
+
required: [],
|
|
177
|
+
},
|
|
178
|
+
}, {
|
|
179
|
+
name: "sonarr_get_calendar",
|
|
180
|
+
description: "Get upcoming TV episodes from Sonarr",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
days: {
|
|
185
|
+
type: "number",
|
|
186
|
+
description: "Number of days to look ahead (default: 7)",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
required: [],
|
|
190
|
+
},
|
|
191
|
+
}, {
|
|
192
|
+
name: "sonarr_get_episodes",
|
|
193
|
+
description: "Get episodes for a TV series. Shows which episodes are available and which are missing.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
seriesId: {
|
|
198
|
+
type: "number",
|
|
199
|
+
description: "Series ID to get episodes for",
|
|
200
|
+
},
|
|
201
|
+
seasonNumber: {
|
|
202
|
+
type: "number",
|
|
203
|
+
description: "Optional: filter to a specific season",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
required: ["seriesId"],
|
|
207
|
+
},
|
|
208
|
+
}, {
|
|
209
|
+
name: "sonarr_search_missing",
|
|
210
|
+
description: "Trigger a search for all missing episodes in a series",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: {
|
|
214
|
+
seriesId: {
|
|
215
|
+
type: "number",
|
|
216
|
+
description: "Series ID to search for missing episodes",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
required: ["seriesId"],
|
|
220
|
+
},
|
|
221
|
+
}, {
|
|
222
|
+
name: "sonarr_search_episode",
|
|
223
|
+
description: "Trigger a search for specific episode(s)",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
episodeIds: {
|
|
228
|
+
type: "array",
|
|
229
|
+
items: { type: "number" },
|
|
230
|
+
description: "Episode ID(s) to search for",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
required: ["episodeIds"],
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Radarr tools
|
|
238
|
+
if (clients.radarr) {
|
|
239
|
+
TOOLS.push({
|
|
240
|
+
name: "radarr_get_movies",
|
|
241
|
+
description: "Get all movies in Radarr library",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {},
|
|
245
|
+
required: [],
|
|
246
|
+
},
|
|
247
|
+
}, {
|
|
248
|
+
name: "radarr_search",
|
|
249
|
+
description: "Search for movies to add to Radarr",
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
term: {
|
|
254
|
+
type: "string",
|
|
255
|
+
description: "Search term (movie name)",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
required: ["term"],
|
|
259
|
+
},
|
|
260
|
+
}, {
|
|
261
|
+
name: "radarr_get_queue",
|
|
262
|
+
description: "Get Radarr download queue",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {},
|
|
266
|
+
required: [],
|
|
267
|
+
},
|
|
268
|
+
}, {
|
|
269
|
+
name: "radarr_get_calendar",
|
|
270
|
+
description: "Get upcoming movie releases from Radarr",
|
|
271
|
+
inputSchema: {
|
|
272
|
+
type: "object",
|
|
273
|
+
properties: {
|
|
274
|
+
days: {
|
|
275
|
+
type: "number",
|
|
276
|
+
description: "Number of days to look ahead (default: 30)",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
required: [],
|
|
280
|
+
},
|
|
281
|
+
}, {
|
|
282
|
+
name: "radarr_search_movie",
|
|
283
|
+
description: "Trigger a search to download a movie that's already in your library",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
movieId: {
|
|
288
|
+
type: "number",
|
|
289
|
+
description: "Movie ID to search for",
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
required: ["movieId"],
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
// Lidarr tools
|
|
297
|
+
if (clients.lidarr) {
|
|
298
|
+
TOOLS.push({
|
|
299
|
+
name: "lidarr_get_artists",
|
|
300
|
+
description: "Get all artists in Lidarr library",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {},
|
|
304
|
+
required: [],
|
|
305
|
+
},
|
|
306
|
+
}, {
|
|
307
|
+
name: "lidarr_search",
|
|
308
|
+
description: "Search for artists to add to Lidarr",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
term: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "Search term (artist name)",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
required: ["term"],
|
|
318
|
+
},
|
|
319
|
+
}, {
|
|
320
|
+
name: "lidarr_get_queue",
|
|
321
|
+
description: "Get Lidarr download queue",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {},
|
|
325
|
+
required: [],
|
|
326
|
+
},
|
|
327
|
+
}, {
|
|
328
|
+
name: "lidarr_get_albums",
|
|
329
|
+
description: "Get albums for an artist in Lidarr. Shows which albums are available and which are missing.",
|
|
330
|
+
inputSchema: {
|
|
331
|
+
type: "object",
|
|
332
|
+
properties: {
|
|
333
|
+
artistId: {
|
|
334
|
+
type: "number",
|
|
335
|
+
description: "Artist ID to get albums for",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
required: ["artistId"],
|
|
339
|
+
},
|
|
340
|
+
}, {
|
|
341
|
+
name: "lidarr_search_album",
|
|
342
|
+
description: "Trigger a search for a specific album to download",
|
|
343
|
+
inputSchema: {
|
|
344
|
+
type: "object",
|
|
345
|
+
properties: {
|
|
346
|
+
albumId: {
|
|
347
|
+
type: "number",
|
|
348
|
+
description: "Album ID to search for",
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
required: ["albumId"],
|
|
352
|
+
},
|
|
353
|
+
}, {
|
|
354
|
+
name: "lidarr_search_missing",
|
|
355
|
+
description: "Trigger a search for all missing albums for an artist",
|
|
356
|
+
inputSchema: {
|
|
357
|
+
type: "object",
|
|
358
|
+
properties: {
|
|
359
|
+
artistId: {
|
|
360
|
+
type: "number",
|
|
361
|
+
description: "Artist ID to search missing albums for",
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
required: ["artistId"],
|
|
365
|
+
},
|
|
366
|
+
}, {
|
|
367
|
+
name: "lidarr_get_calendar",
|
|
368
|
+
description: "Get upcoming album releases from Lidarr",
|
|
369
|
+
inputSchema: {
|
|
370
|
+
type: "object",
|
|
371
|
+
properties: {
|
|
372
|
+
days: {
|
|
373
|
+
type: "number",
|
|
374
|
+
description: "Number of days to look ahead (default: 30)",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
required: [],
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
// Readarr tools
|
|
382
|
+
if (clients.readarr) {
|
|
383
|
+
TOOLS.push({
|
|
384
|
+
name: "readarr_get_authors",
|
|
385
|
+
description: "Get all authors in Readarr library",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {},
|
|
389
|
+
required: [],
|
|
390
|
+
},
|
|
391
|
+
}, {
|
|
392
|
+
name: "readarr_search",
|
|
393
|
+
description: "Search for authors to add to Readarr",
|
|
394
|
+
inputSchema: {
|
|
395
|
+
type: "object",
|
|
396
|
+
properties: {
|
|
397
|
+
term: {
|
|
398
|
+
type: "string",
|
|
399
|
+
description: "Search term (author name)",
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
required: ["term"],
|
|
403
|
+
},
|
|
404
|
+
}, {
|
|
405
|
+
name: "readarr_get_queue",
|
|
406
|
+
description: "Get Readarr download queue",
|
|
407
|
+
inputSchema: {
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: {},
|
|
410
|
+
required: [],
|
|
411
|
+
},
|
|
412
|
+
}, {
|
|
413
|
+
name: "readarr_get_books",
|
|
414
|
+
description: "Get books for an author in Readarr. Shows which books are available and which are missing.",
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {
|
|
418
|
+
authorId: {
|
|
419
|
+
type: "number",
|
|
420
|
+
description: "Author ID to get books for",
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
required: ["authorId"],
|
|
424
|
+
},
|
|
425
|
+
}, {
|
|
426
|
+
name: "readarr_search_book",
|
|
427
|
+
description: "Trigger a search for a specific book to download",
|
|
428
|
+
inputSchema: {
|
|
429
|
+
type: "object",
|
|
430
|
+
properties: {
|
|
431
|
+
bookIds: {
|
|
432
|
+
type: "array",
|
|
433
|
+
items: { type: "number" },
|
|
434
|
+
description: "Book ID(s) to search for",
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
required: ["bookIds"],
|
|
438
|
+
},
|
|
439
|
+
}, {
|
|
440
|
+
name: "readarr_search_missing",
|
|
441
|
+
description: "Trigger a search for all missing books for an author",
|
|
442
|
+
inputSchema: {
|
|
443
|
+
type: "object",
|
|
444
|
+
properties: {
|
|
445
|
+
authorId: {
|
|
446
|
+
type: "number",
|
|
447
|
+
description: "Author ID to search missing books for",
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
required: ["authorId"],
|
|
451
|
+
},
|
|
452
|
+
}, {
|
|
453
|
+
name: "readarr_get_calendar",
|
|
454
|
+
description: "Get upcoming book releases from Readarr",
|
|
455
|
+
inputSchema: {
|
|
456
|
+
type: "object",
|
|
457
|
+
properties: {
|
|
458
|
+
days: {
|
|
459
|
+
type: "number",
|
|
460
|
+
description: "Number of days to look ahead (default: 30)",
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
required: [],
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
// Prowlarr tools
|
|
468
|
+
if (clients.prowlarr) {
|
|
469
|
+
TOOLS.push({
|
|
470
|
+
name: "prowlarr_get_indexers",
|
|
471
|
+
description: "Get all configured indexers in Prowlarr",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: "object",
|
|
474
|
+
properties: {},
|
|
475
|
+
required: [],
|
|
476
|
+
},
|
|
477
|
+
}, {
|
|
478
|
+
name: "prowlarr_search",
|
|
479
|
+
description: "Search across all Prowlarr indexers",
|
|
480
|
+
inputSchema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
query: {
|
|
484
|
+
type: "string",
|
|
485
|
+
description: "Search query",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
required: ["query"],
|
|
489
|
+
},
|
|
490
|
+
}, {
|
|
491
|
+
name: "prowlarr_test_indexers",
|
|
492
|
+
description: "Test all indexers and return their health status",
|
|
493
|
+
inputSchema: {
|
|
494
|
+
type: "object",
|
|
495
|
+
properties: {},
|
|
496
|
+
required: [],
|
|
497
|
+
},
|
|
498
|
+
}, {
|
|
499
|
+
name: "prowlarr_get_stats",
|
|
500
|
+
description: "Get indexer statistics (queries, grabs, failures)",
|
|
501
|
+
inputSchema: {
|
|
502
|
+
type: "object",
|
|
503
|
+
properties: {},
|
|
504
|
+
required: [],
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
// Lingarr tools - imported from separate module to reduce merge conflicts
|
|
509
|
+
if (clients.lingarr) {
|
|
510
|
+
TOOLS.push(...getLingarrTools());
|
|
511
|
+
}
|
|
512
|
+
// Cross-service search tool
|
|
513
|
+
TOOLS.push({
|
|
514
|
+
name: "arr_search_all",
|
|
515
|
+
description: "Search across all configured *arr services for any media",
|
|
516
|
+
inputSchema: {
|
|
517
|
+
type: "object",
|
|
518
|
+
properties: {
|
|
519
|
+
term: {
|
|
520
|
+
type: "string",
|
|
521
|
+
description: "Search term",
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
required: ["term"],
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
// TRaSH Guides tools (always available - no *arr config required)
|
|
528
|
+
TOOLS.push({
|
|
529
|
+
name: "trash_list_profiles",
|
|
530
|
+
description: "List available TRaSH Guides quality profiles for Radarr or Sonarr. Shows recommended profiles for different use cases (1080p, 4K, Remux, etc.)",
|
|
531
|
+
inputSchema: {
|
|
532
|
+
type: "object",
|
|
533
|
+
properties: {
|
|
534
|
+
service: {
|
|
535
|
+
type: "string",
|
|
536
|
+
enum: ["radarr", "sonarr"],
|
|
537
|
+
description: "Which service to get profiles for",
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
required: ["service"],
|
|
541
|
+
},
|
|
542
|
+
}, {
|
|
543
|
+
name: "trash_get_profile",
|
|
544
|
+
description: "Get a specific TRaSH Guides quality profile with all custom format scores, quality settings, and implementation details",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
service: {
|
|
549
|
+
type: "string",
|
|
550
|
+
enum: ["radarr", "sonarr"],
|
|
551
|
+
description: "Which service",
|
|
552
|
+
},
|
|
553
|
+
profile: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Profile name (e.g., 'remux-web-1080p', 'uhd-bluray-web', 'hd-bluray-web')",
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
required: ["service", "profile"],
|
|
559
|
+
},
|
|
560
|
+
}, {
|
|
561
|
+
name: "trash_list_custom_formats",
|
|
562
|
+
description: "List available TRaSH Guides custom formats. Can filter by category: hdr, audio, resolution, source, streaming, anime, unwanted, release, language",
|
|
563
|
+
inputSchema: {
|
|
564
|
+
type: "object",
|
|
565
|
+
properties: {
|
|
566
|
+
service: {
|
|
567
|
+
type: "string",
|
|
568
|
+
enum: ["radarr", "sonarr"],
|
|
569
|
+
description: "Which service",
|
|
570
|
+
},
|
|
571
|
+
category: {
|
|
572
|
+
type: "string",
|
|
573
|
+
description: "Optional filter by category",
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
required: ["service"],
|
|
577
|
+
},
|
|
578
|
+
}, {
|
|
579
|
+
name: "trash_get_naming",
|
|
580
|
+
description: "Get TRaSH Guides recommended naming conventions for your media server (Plex, Emby, Jellyfin, or standard)",
|
|
581
|
+
inputSchema: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
service: {
|
|
585
|
+
type: "string",
|
|
586
|
+
enum: ["radarr", "sonarr"],
|
|
587
|
+
description: "Which service",
|
|
588
|
+
},
|
|
589
|
+
mediaServer: {
|
|
590
|
+
type: "string",
|
|
591
|
+
enum: ["plex", "emby", "jellyfin", "standard"],
|
|
592
|
+
description: "Which media server you use",
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
required: ["service", "mediaServer"],
|
|
596
|
+
},
|
|
597
|
+
}, {
|
|
598
|
+
name: "trash_get_quality_sizes",
|
|
599
|
+
description: "Get TRaSH Guides recommended min/max/preferred sizes for each quality level",
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: "object",
|
|
602
|
+
properties: {
|
|
603
|
+
service: {
|
|
604
|
+
type: "string",
|
|
605
|
+
enum: ["radarr", "sonarr"],
|
|
606
|
+
description: "Which service",
|
|
607
|
+
},
|
|
608
|
+
type: {
|
|
609
|
+
type: "string",
|
|
610
|
+
description: "Content type: 'movie', 'anime' for Radarr; 'series', 'anime' for Sonarr",
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
required: ["service"],
|
|
614
|
+
},
|
|
615
|
+
}, {
|
|
616
|
+
name: "trash_compare_profile",
|
|
617
|
+
description: "Compare your quality profile against TRaSH Guides recommendations. Shows missing custom formats, scoring differences, and quality settings. Requires the corresponding *arr service to be configured.",
|
|
618
|
+
inputSchema: {
|
|
619
|
+
type: "object",
|
|
620
|
+
properties: {
|
|
621
|
+
service: {
|
|
622
|
+
type: "string",
|
|
623
|
+
enum: ["radarr", "sonarr"],
|
|
624
|
+
description: "Which service",
|
|
625
|
+
},
|
|
626
|
+
profileId: {
|
|
627
|
+
type: "number",
|
|
628
|
+
description: "Your quality profile ID to compare",
|
|
629
|
+
},
|
|
630
|
+
trashProfile: {
|
|
631
|
+
type: "string",
|
|
632
|
+
description: "TRaSH profile name to compare against",
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
required: ["service", "profileId", "trashProfile"],
|
|
636
|
+
},
|
|
637
|
+
}, {
|
|
638
|
+
name: "trash_compare_naming",
|
|
639
|
+
description: "Compare your naming configuration against TRaSH Guides recommendations. Requires the corresponding *arr service to be configured.",
|
|
640
|
+
inputSchema: {
|
|
641
|
+
type: "object",
|
|
642
|
+
properties: {
|
|
643
|
+
service: {
|
|
644
|
+
type: "string",
|
|
645
|
+
enum: ["radarr", "sonarr"],
|
|
646
|
+
description: "Which service",
|
|
647
|
+
},
|
|
648
|
+
mediaServer: {
|
|
649
|
+
type: "string",
|
|
650
|
+
enum: ["plex", "emby", "jellyfin", "standard"],
|
|
651
|
+
description: "Which media server you use",
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
required: ["service", "mediaServer"],
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
// Create server instance
|
|
658
|
+
const server = new Server({
|
|
659
|
+
name: "mcp-arr",
|
|
660
|
+
version: "1.0.0",
|
|
661
|
+
}, {
|
|
662
|
+
capabilities: {
|
|
663
|
+
tools: {},
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
// Handle list tools request
|
|
667
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
668
|
+
return { tools: TOOLS };
|
|
669
|
+
});
|
|
670
|
+
// Handle tool calls
|
|
671
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
672
|
+
const { name, arguments: args } = request.params;
|
|
673
|
+
try {
|
|
674
|
+
// Delegate Lingarr tools to separate handler module
|
|
675
|
+
const lingarrResult = await handleLingarrTool(name, (args || {}), clients.lingarr);
|
|
676
|
+
if (lingarrResult) {
|
|
677
|
+
return lingarrResult;
|
|
678
|
+
}
|
|
679
|
+
switch (name) {
|
|
680
|
+
case "arr_status": {
|
|
681
|
+
const statuses = {};
|
|
682
|
+
for (const service of configuredServices) {
|
|
683
|
+
try {
|
|
684
|
+
if (service.name === 'lingarr' && clients.lingarr) {
|
|
685
|
+
// Lingarr status handled by separate module
|
|
686
|
+
statuses[service.name] = await getLingarrStatus(clients.lingarr);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
const client = clients[service.name];
|
|
690
|
+
if (client) {
|
|
691
|
+
const status = await client.getStatus();
|
|
692
|
+
statuses[service.name] = {
|
|
693
|
+
configured: true,
|
|
694
|
+
connected: true,
|
|
695
|
+
version: status.version,
|
|
696
|
+
appName: status.appName,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
statuses[service.name] = {
|
|
703
|
+
configured: true,
|
|
704
|
+
connected: false,
|
|
705
|
+
error: error instanceof Error ? error.message : String(error),
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Add unconfigured services
|
|
710
|
+
for (const service of services) {
|
|
711
|
+
if (!statuses[service.name]) {
|
|
712
|
+
statuses[service.name] = { configured: false };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
content: [{ type: "text", text: JSON.stringify(statuses, null, 2) }],
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
// Dynamic config tool handlers
|
|
720
|
+
// Quality Profiles
|
|
721
|
+
case "sonarr_get_quality_profiles":
|
|
722
|
+
case "radarr_get_quality_profiles":
|
|
723
|
+
case "lidarr_get_quality_profiles":
|
|
724
|
+
case "readarr_get_quality_profiles": {
|
|
725
|
+
const serviceName = name.split('_')[0];
|
|
726
|
+
const client = clients[serviceName];
|
|
727
|
+
if (!client)
|
|
728
|
+
throw new Error(`${serviceName} not configured`);
|
|
729
|
+
const profiles = await client.getQualityProfiles();
|
|
730
|
+
return {
|
|
731
|
+
content: [{
|
|
732
|
+
type: "text",
|
|
733
|
+
text: JSON.stringify({
|
|
734
|
+
count: profiles.length,
|
|
735
|
+
profiles: profiles.map((p) => ({
|
|
736
|
+
id: p.id,
|
|
737
|
+
name: p.name,
|
|
738
|
+
upgradeAllowed: p.upgradeAllowed,
|
|
739
|
+
cutoff: p.cutoff,
|
|
740
|
+
allowedQualities: p.items
|
|
741
|
+
.filter((i) => i.allowed)
|
|
742
|
+
.map((i) => i.quality?.name || i.name || (i.items?.map((q) => q.quality.name).join(', ')))
|
|
743
|
+
.filter(Boolean),
|
|
744
|
+
customFormats: p.formatItems?.filter((f) => f.score !== 0).map((f) => ({
|
|
745
|
+
name: f.name,
|
|
746
|
+
score: f.score,
|
|
747
|
+
})) || [],
|
|
748
|
+
minFormatScore: p.minFormatScore,
|
|
749
|
+
cutoffFormatScore: p.cutoffFormatScore,
|
|
750
|
+
})),
|
|
751
|
+
}, null, 2),
|
|
752
|
+
}],
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
// Health checks
|
|
756
|
+
case "sonarr_get_health":
|
|
757
|
+
case "radarr_get_health":
|
|
758
|
+
case "lidarr_get_health":
|
|
759
|
+
case "readarr_get_health": {
|
|
760
|
+
const serviceName = name.split('_')[0];
|
|
761
|
+
const client = clients[serviceName];
|
|
762
|
+
if (!client)
|
|
763
|
+
throw new Error(`${serviceName} not configured`);
|
|
764
|
+
const health = await client.getHealth();
|
|
765
|
+
return {
|
|
766
|
+
content: [{
|
|
767
|
+
type: "text",
|
|
768
|
+
text: JSON.stringify({
|
|
769
|
+
issueCount: health.length,
|
|
770
|
+
issues: health.map((h) => ({
|
|
771
|
+
source: h.source,
|
|
772
|
+
type: h.type,
|
|
773
|
+
message: h.message,
|
|
774
|
+
wikiUrl: h.wikiUrl,
|
|
775
|
+
})),
|
|
776
|
+
status: health.length === 0 ? 'healthy' : 'issues detected',
|
|
777
|
+
}, null, 2),
|
|
778
|
+
}],
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
// Root folders
|
|
782
|
+
case "sonarr_get_root_folders":
|
|
783
|
+
case "radarr_get_root_folders":
|
|
784
|
+
case "lidarr_get_root_folders":
|
|
785
|
+
case "readarr_get_root_folders": {
|
|
786
|
+
const serviceName = name.split('_')[0];
|
|
787
|
+
const client = clients[serviceName];
|
|
788
|
+
if (!client)
|
|
789
|
+
throw new Error(`${serviceName} not configured`);
|
|
790
|
+
const folders = await client.getRootFoldersDetailed();
|
|
791
|
+
return {
|
|
792
|
+
content: [{
|
|
793
|
+
type: "text",
|
|
794
|
+
text: JSON.stringify({
|
|
795
|
+
count: folders.length,
|
|
796
|
+
folders: folders.map((f) => ({
|
|
797
|
+
id: f.id,
|
|
798
|
+
path: f.path,
|
|
799
|
+
accessible: f.accessible,
|
|
800
|
+
freeSpace: formatBytes(f.freeSpace),
|
|
801
|
+
freeSpaceBytes: f.freeSpace,
|
|
802
|
+
unmappedFolders: f.unmappedFolders?.length || 0,
|
|
803
|
+
})),
|
|
804
|
+
}, null, 2),
|
|
805
|
+
}],
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
// Download clients
|
|
809
|
+
case "sonarr_get_download_clients":
|
|
810
|
+
case "radarr_get_download_clients":
|
|
811
|
+
case "lidarr_get_download_clients":
|
|
812
|
+
case "readarr_get_download_clients": {
|
|
813
|
+
const serviceName = name.split('_')[0];
|
|
814
|
+
const client = clients[serviceName];
|
|
815
|
+
if (!client)
|
|
816
|
+
throw new Error(`${serviceName} not configured`);
|
|
817
|
+
const downloadClients = await client.getDownloadClients();
|
|
818
|
+
return {
|
|
819
|
+
content: [{
|
|
820
|
+
type: "text",
|
|
821
|
+
text: JSON.stringify({
|
|
822
|
+
count: downloadClients.length,
|
|
823
|
+
clients: downloadClients.map((c) => ({
|
|
824
|
+
id: c.id,
|
|
825
|
+
name: c.name,
|
|
826
|
+
implementation: c.implementationName,
|
|
827
|
+
protocol: c.protocol,
|
|
828
|
+
enabled: c.enable,
|
|
829
|
+
priority: c.priority,
|
|
830
|
+
removeCompletedDownloads: c.removeCompletedDownloads,
|
|
831
|
+
removeFailedDownloads: c.removeFailedDownloads,
|
|
832
|
+
tags: c.tags,
|
|
833
|
+
})),
|
|
834
|
+
}, null, 2),
|
|
835
|
+
}],
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
// Naming config
|
|
839
|
+
case "sonarr_get_naming":
|
|
840
|
+
case "radarr_get_naming":
|
|
841
|
+
case "lidarr_get_naming":
|
|
842
|
+
case "readarr_get_naming": {
|
|
843
|
+
const serviceName = name.split('_')[0];
|
|
844
|
+
const client = clients[serviceName];
|
|
845
|
+
if (!client)
|
|
846
|
+
throw new Error(`${serviceName} not configured`);
|
|
847
|
+
const naming = await client.getNamingConfig();
|
|
848
|
+
return {
|
|
849
|
+
content: [{
|
|
850
|
+
type: "text",
|
|
851
|
+
text: JSON.stringify(naming, null, 2),
|
|
852
|
+
}],
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
// Tags
|
|
856
|
+
case "sonarr_get_tags":
|
|
857
|
+
case "radarr_get_tags":
|
|
858
|
+
case "lidarr_get_tags":
|
|
859
|
+
case "readarr_get_tags": {
|
|
860
|
+
const serviceName = name.split('_')[0];
|
|
861
|
+
const client = clients[serviceName];
|
|
862
|
+
if (!client)
|
|
863
|
+
throw new Error(`${serviceName} not configured`);
|
|
864
|
+
const tags = await client.getTags();
|
|
865
|
+
return {
|
|
866
|
+
content: [{
|
|
867
|
+
type: "text",
|
|
868
|
+
text: JSON.stringify({
|
|
869
|
+
count: tags.length,
|
|
870
|
+
tags: tags.map((t) => ({ id: t.id, label: t.label })),
|
|
871
|
+
}, null, 2),
|
|
872
|
+
}],
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
// Comprehensive setup review
|
|
876
|
+
case "sonarr_review_setup":
|
|
877
|
+
case "radarr_review_setup":
|
|
878
|
+
case "lidarr_review_setup":
|
|
879
|
+
case "readarr_review_setup": {
|
|
880
|
+
const serviceName = name.split('_')[0];
|
|
881
|
+
const client = clients[serviceName];
|
|
882
|
+
if (!client)
|
|
883
|
+
throw new Error(`${serviceName} not configured`);
|
|
884
|
+
// Gather all configuration data
|
|
885
|
+
const [status, health, qualityProfiles, qualityDefinitions, downloadClients, naming, mediaManagement, rootFolders, tags, indexers] = await Promise.all([
|
|
886
|
+
client.getStatus(),
|
|
887
|
+
client.getHealth(),
|
|
888
|
+
client.getQualityProfiles(),
|
|
889
|
+
client.getQualityDefinitions(),
|
|
890
|
+
client.getDownloadClients(),
|
|
891
|
+
client.getNamingConfig(),
|
|
892
|
+
client.getMediaManagement(),
|
|
893
|
+
client.getRootFoldersDetailed(),
|
|
894
|
+
client.getTags(),
|
|
895
|
+
client.getIndexers(),
|
|
896
|
+
]);
|
|
897
|
+
// For Lidarr/Readarr, also get metadata profiles
|
|
898
|
+
let metadataProfiles = null;
|
|
899
|
+
if (serviceName === 'lidarr' && clients.lidarr) {
|
|
900
|
+
metadataProfiles = await clients.lidarr.getMetadataProfiles();
|
|
901
|
+
}
|
|
902
|
+
else if (serviceName === 'readarr' && clients.readarr) {
|
|
903
|
+
metadataProfiles = await clients.readarr.getMetadataProfiles();
|
|
904
|
+
}
|
|
905
|
+
const review = {
|
|
906
|
+
service: serviceName,
|
|
907
|
+
version: status.version,
|
|
908
|
+
appName: status.appName,
|
|
909
|
+
platform: {
|
|
910
|
+
os: status.osName,
|
|
911
|
+
isDocker: status.isDocker,
|
|
912
|
+
},
|
|
913
|
+
health: {
|
|
914
|
+
issueCount: health.length,
|
|
915
|
+
issues: health,
|
|
916
|
+
},
|
|
917
|
+
storage: {
|
|
918
|
+
rootFolders: rootFolders.map((f) => ({
|
|
919
|
+
path: f.path,
|
|
920
|
+
accessible: f.accessible,
|
|
921
|
+
freeSpace: formatBytes(f.freeSpace),
|
|
922
|
+
freeSpaceBytes: f.freeSpace,
|
|
923
|
+
unmappedFolderCount: f.unmappedFolders?.length || 0,
|
|
924
|
+
})),
|
|
925
|
+
},
|
|
926
|
+
qualityProfiles: qualityProfiles.map((p) => ({
|
|
927
|
+
id: p.id,
|
|
928
|
+
name: p.name,
|
|
929
|
+
upgradeAllowed: p.upgradeAllowed,
|
|
930
|
+
cutoff: p.cutoff,
|
|
931
|
+
allowedQualities: p.items
|
|
932
|
+
.filter((i) => i.allowed)
|
|
933
|
+
.map((i) => i.quality?.name || i.name || (i.items?.map((q) => q.quality.name).join(', ')))
|
|
934
|
+
.filter(Boolean),
|
|
935
|
+
customFormatsWithScores: p.formatItems?.filter((f) => f.score !== 0).length || 0,
|
|
936
|
+
minFormatScore: p.minFormatScore,
|
|
937
|
+
})),
|
|
938
|
+
qualityDefinitions: qualityDefinitions.map((d) => ({
|
|
939
|
+
quality: d.quality.name,
|
|
940
|
+
minSize: d.minSize + ' MB/min',
|
|
941
|
+
maxSize: d.maxSize === 0 ? 'unlimited' : d.maxSize + ' MB/min',
|
|
942
|
+
preferredSize: d.preferredSize + ' MB/min',
|
|
943
|
+
})),
|
|
944
|
+
downloadClients: downloadClients.map((c) => ({
|
|
945
|
+
name: c.name,
|
|
946
|
+
type: c.implementationName,
|
|
947
|
+
protocol: c.protocol,
|
|
948
|
+
enabled: c.enable,
|
|
949
|
+
priority: c.priority,
|
|
950
|
+
})),
|
|
951
|
+
indexers: indexers.map((i) => ({
|
|
952
|
+
name: i.name,
|
|
953
|
+
protocol: i.protocol,
|
|
954
|
+
enableRss: i.enableRss,
|
|
955
|
+
enableAutomaticSearch: i.enableAutomaticSearch,
|
|
956
|
+
enableInteractiveSearch: i.enableInteractiveSearch,
|
|
957
|
+
priority: i.priority,
|
|
958
|
+
})),
|
|
959
|
+
naming: naming,
|
|
960
|
+
mediaManagement: {
|
|
961
|
+
recycleBin: mediaManagement.recycleBin || 'not set',
|
|
962
|
+
recycleBinCleanupDays: mediaManagement.recycleBinCleanupDays,
|
|
963
|
+
downloadPropersAndRepacks: mediaManagement.downloadPropersAndRepacks,
|
|
964
|
+
deleteEmptyFolders: mediaManagement.deleteEmptyFolders,
|
|
965
|
+
copyUsingHardlinks: mediaManagement.copyUsingHardlinks,
|
|
966
|
+
importExtraFiles: mediaManagement.importExtraFiles,
|
|
967
|
+
extraFileExtensions: mediaManagement.extraFileExtensions,
|
|
968
|
+
},
|
|
969
|
+
tags: tags.map((t) => t.label),
|
|
970
|
+
...(metadataProfiles && { metadataProfiles }),
|
|
971
|
+
};
|
|
972
|
+
return {
|
|
973
|
+
content: [{
|
|
974
|
+
type: "text",
|
|
975
|
+
text: JSON.stringify(review, null, 2),
|
|
976
|
+
}],
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
// Sonarr handlers
|
|
980
|
+
case "sonarr_get_series": {
|
|
981
|
+
if (!clients.sonarr)
|
|
982
|
+
throw new Error("Sonarr not configured");
|
|
983
|
+
const series = await clients.sonarr.getSeries();
|
|
984
|
+
return {
|
|
985
|
+
content: [{
|
|
986
|
+
type: "text",
|
|
987
|
+
text: JSON.stringify({
|
|
988
|
+
count: series.length,
|
|
989
|
+
series: series.map(s => ({
|
|
990
|
+
id: s.id,
|
|
991
|
+
title: s.title,
|
|
992
|
+
year: s.year,
|
|
993
|
+
status: s.status,
|
|
994
|
+
network: s.network,
|
|
995
|
+
seasons: s.statistics?.seasonCount,
|
|
996
|
+
episodes: s.statistics?.episodeFileCount + '/' + s.statistics?.totalEpisodeCount,
|
|
997
|
+
sizeOnDisk: formatBytes(s.statistics?.sizeOnDisk || 0),
|
|
998
|
+
monitored: s.monitored,
|
|
999
|
+
})),
|
|
1000
|
+
}, null, 2),
|
|
1001
|
+
}],
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
case "sonarr_search": {
|
|
1005
|
+
if (!clients.sonarr)
|
|
1006
|
+
throw new Error("Sonarr not configured");
|
|
1007
|
+
const term = args.term;
|
|
1008
|
+
const results = await clients.sonarr.searchSeries(term);
|
|
1009
|
+
return {
|
|
1010
|
+
content: [{
|
|
1011
|
+
type: "text",
|
|
1012
|
+
text: JSON.stringify({
|
|
1013
|
+
count: results.length,
|
|
1014
|
+
results: results.slice(0, 10).map(r => ({
|
|
1015
|
+
title: r.title,
|
|
1016
|
+
year: r.year,
|
|
1017
|
+
tvdbId: r.tvdbId,
|
|
1018
|
+
overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''),
|
|
1019
|
+
})),
|
|
1020
|
+
}, null, 2),
|
|
1021
|
+
}],
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
case "sonarr_get_queue": {
|
|
1025
|
+
if (!clients.sonarr)
|
|
1026
|
+
throw new Error("Sonarr not configured");
|
|
1027
|
+
const queue = await clients.sonarr.getQueue();
|
|
1028
|
+
return {
|
|
1029
|
+
content: [{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: JSON.stringify({
|
|
1032
|
+
totalRecords: queue.totalRecords,
|
|
1033
|
+
items: queue.records.map(q => ({
|
|
1034
|
+
title: q.title,
|
|
1035
|
+
status: q.status,
|
|
1036
|
+
progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%',
|
|
1037
|
+
timeLeft: q.timeleft,
|
|
1038
|
+
downloadClient: q.downloadClient,
|
|
1039
|
+
})),
|
|
1040
|
+
}, null, 2),
|
|
1041
|
+
}],
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
case "sonarr_get_calendar": {
|
|
1045
|
+
if (!clients.sonarr)
|
|
1046
|
+
throw new Error("Sonarr not configured");
|
|
1047
|
+
const days = args?.days || 7;
|
|
1048
|
+
const start = new Date().toISOString().split('T')[0];
|
|
1049
|
+
const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
1050
|
+
const calendar = await clients.sonarr.getCalendar(start, end);
|
|
1051
|
+
return {
|
|
1052
|
+
content: [{ type: "text", text: JSON.stringify(calendar, null, 2) }],
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
case "sonarr_get_episodes": {
|
|
1056
|
+
if (!clients.sonarr)
|
|
1057
|
+
throw new Error("Sonarr not configured");
|
|
1058
|
+
const { seriesId, seasonNumber } = args;
|
|
1059
|
+
const episodes = await clients.sonarr.getEpisodes(seriesId, seasonNumber);
|
|
1060
|
+
return {
|
|
1061
|
+
content: [{
|
|
1062
|
+
type: "text",
|
|
1063
|
+
text: JSON.stringify({
|
|
1064
|
+
count: episodes.length,
|
|
1065
|
+
episodes: episodes.map(e => ({
|
|
1066
|
+
id: e.id,
|
|
1067
|
+
seasonNumber: e.seasonNumber,
|
|
1068
|
+
episodeNumber: e.episodeNumber,
|
|
1069
|
+
title: e.title,
|
|
1070
|
+
airDate: e.airDate,
|
|
1071
|
+
hasFile: e.hasFile,
|
|
1072
|
+
monitored: e.monitored,
|
|
1073
|
+
})),
|
|
1074
|
+
}, null, 2),
|
|
1075
|
+
}],
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
case "sonarr_search_missing": {
|
|
1079
|
+
if (!clients.sonarr)
|
|
1080
|
+
throw new Error("Sonarr not configured");
|
|
1081
|
+
const seriesId = args.seriesId;
|
|
1082
|
+
const result = await clients.sonarr.searchMissing(seriesId);
|
|
1083
|
+
return {
|
|
1084
|
+
content: [{
|
|
1085
|
+
type: "text",
|
|
1086
|
+
text: JSON.stringify({
|
|
1087
|
+
success: true,
|
|
1088
|
+
message: `Search triggered for missing episodes`,
|
|
1089
|
+
commandId: result.id,
|
|
1090
|
+
}, null, 2),
|
|
1091
|
+
}],
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
case "sonarr_search_episode": {
|
|
1095
|
+
if (!clients.sonarr)
|
|
1096
|
+
throw new Error("Sonarr not configured");
|
|
1097
|
+
const episodeIds = args.episodeIds;
|
|
1098
|
+
const result = await clients.sonarr.searchEpisode(episodeIds);
|
|
1099
|
+
return {
|
|
1100
|
+
content: [{
|
|
1101
|
+
type: "text",
|
|
1102
|
+
text: JSON.stringify({
|
|
1103
|
+
success: true,
|
|
1104
|
+
message: `Search triggered for ${episodeIds.length} episode(s)`,
|
|
1105
|
+
commandId: result.id,
|
|
1106
|
+
}, null, 2),
|
|
1107
|
+
}],
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
// Radarr handlers
|
|
1111
|
+
case "radarr_get_movies": {
|
|
1112
|
+
if (!clients.radarr)
|
|
1113
|
+
throw new Error("Radarr not configured");
|
|
1114
|
+
const movies = await clients.radarr.getMovies();
|
|
1115
|
+
return {
|
|
1116
|
+
content: [{
|
|
1117
|
+
type: "text",
|
|
1118
|
+
text: JSON.stringify({
|
|
1119
|
+
count: movies.length,
|
|
1120
|
+
movies: movies.map(m => ({
|
|
1121
|
+
id: m.id,
|
|
1122
|
+
title: m.title,
|
|
1123
|
+
year: m.year,
|
|
1124
|
+
status: m.status,
|
|
1125
|
+
hasFile: m.hasFile,
|
|
1126
|
+
sizeOnDisk: formatBytes(m.sizeOnDisk),
|
|
1127
|
+
monitored: m.monitored,
|
|
1128
|
+
studio: m.studio,
|
|
1129
|
+
})),
|
|
1130
|
+
}, null, 2),
|
|
1131
|
+
}],
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
case "radarr_search": {
|
|
1135
|
+
if (!clients.radarr)
|
|
1136
|
+
throw new Error("Radarr not configured");
|
|
1137
|
+
const term = args.term;
|
|
1138
|
+
const results = await clients.radarr.searchMovies(term);
|
|
1139
|
+
return {
|
|
1140
|
+
content: [{
|
|
1141
|
+
type: "text",
|
|
1142
|
+
text: JSON.stringify({
|
|
1143
|
+
count: results.length,
|
|
1144
|
+
results: results.slice(0, 10).map(r => ({
|
|
1145
|
+
title: r.title,
|
|
1146
|
+
year: r.year,
|
|
1147
|
+
tmdbId: r.tmdbId,
|
|
1148
|
+
imdbId: r.imdbId,
|
|
1149
|
+
overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''),
|
|
1150
|
+
})),
|
|
1151
|
+
}, null, 2),
|
|
1152
|
+
}],
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
case "radarr_get_queue": {
|
|
1156
|
+
if (!clients.radarr)
|
|
1157
|
+
throw new Error("Radarr not configured");
|
|
1158
|
+
const queue = await clients.radarr.getQueue();
|
|
1159
|
+
return {
|
|
1160
|
+
content: [{
|
|
1161
|
+
type: "text",
|
|
1162
|
+
text: JSON.stringify({
|
|
1163
|
+
totalRecords: queue.totalRecords,
|
|
1164
|
+
items: queue.records.map(q => ({
|
|
1165
|
+
title: q.title,
|
|
1166
|
+
status: q.status,
|
|
1167
|
+
progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%',
|
|
1168
|
+
timeLeft: q.timeleft,
|
|
1169
|
+
downloadClient: q.downloadClient,
|
|
1170
|
+
})),
|
|
1171
|
+
}, null, 2),
|
|
1172
|
+
}],
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
case "radarr_get_calendar": {
|
|
1176
|
+
if (!clients.radarr)
|
|
1177
|
+
throw new Error("Radarr not configured");
|
|
1178
|
+
const days = args?.days || 30;
|
|
1179
|
+
const start = new Date().toISOString().split('T')[0];
|
|
1180
|
+
const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
1181
|
+
const calendar = await clients.radarr.getCalendar(start, end);
|
|
1182
|
+
return {
|
|
1183
|
+
content: [{ type: "text", text: JSON.stringify(calendar, null, 2) }],
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
case "radarr_search_movie": {
|
|
1187
|
+
if (!clients.radarr)
|
|
1188
|
+
throw new Error("Radarr not configured");
|
|
1189
|
+
const movieId = args.movieId;
|
|
1190
|
+
const result = await clients.radarr.searchMovie(movieId);
|
|
1191
|
+
return {
|
|
1192
|
+
content: [{
|
|
1193
|
+
type: "text",
|
|
1194
|
+
text: JSON.stringify({
|
|
1195
|
+
success: true,
|
|
1196
|
+
message: `Search triggered for movie`,
|
|
1197
|
+
commandId: result.id,
|
|
1198
|
+
}, null, 2),
|
|
1199
|
+
}],
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
// Lidarr handlers
|
|
1203
|
+
case "lidarr_get_artists": {
|
|
1204
|
+
if (!clients.lidarr)
|
|
1205
|
+
throw new Error("Lidarr not configured");
|
|
1206
|
+
const artists = await clients.lidarr.getArtists();
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{
|
|
1209
|
+
type: "text",
|
|
1210
|
+
text: JSON.stringify({
|
|
1211
|
+
count: artists.length,
|
|
1212
|
+
artists: artists.map(a => ({
|
|
1213
|
+
id: a.id,
|
|
1214
|
+
artistName: a.artistName,
|
|
1215
|
+
status: a.status,
|
|
1216
|
+
albums: a.statistics?.albumCount,
|
|
1217
|
+
tracks: a.statistics?.trackFileCount + '/' + a.statistics?.totalTrackCount,
|
|
1218
|
+
sizeOnDisk: formatBytes(a.statistics?.sizeOnDisk || 0),
|
|
1219
|
+
monitored: a.monitored,
|
|
1220
|
+
})),
|
|
1221
|
+
}, null, 2),
|
|
1222
|
+
}],
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
case "lidarr_search": {
|
|
1226
|
+
if (!clients.lidarr)
|
|
1227
|
+
throw new Error("Lidarr not configured");
|
|
1228
|
+
const term = args.term;
|
|
1229
|
+
const results = await clients.lidarr.searchArtists(term);
|
|
1230
|
+
return {
|
|
1231
|
+
content: [{
|
|
1232
|
+
type: "text",
|
|
1233
|
+
text: JSON.stringify({
|
|
1234
|
+
count: results.length,
|
|
1235
|
+
results: results.slice(0, 10).map(r => ({
|
|
1236
|
+
title: r.title,
|
|
1237
|
+
foreignArtistId: r.foreignArtistId,
|
|
1238
|
+
overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''),
|
|
1239
|
+
})),
|
|
1240
|
+
}, null, 2),
|
|
1241
|
+
}],
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
case "lidarr_get_queue": {
|
|
1245
|
+
if (!clients.lidarr)
|
|
1246
|
+
throw new Error("Lidarr not configured");
|
|
1247
|
+
const queue = await clients.lidarr.getQueue();
|
|
1248
|
+
return {
|
|
1249
|
+
content: [{
|
|
1250
|
+
type: "text",
|
|
1251
|
+
text: JSON.stringify({
|
|
1252
|
+
totalRecords: queue.totalRecords,
|
|
1253
|
+
items: queue.records.map(q => ({
|
|
1254
|
+
title: q.title,
|
|
1255
|
+
status: q.status,
|
|
1256
|
+
progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%',
|
|
1257
|
+
timeLeft: q.timeleft,
|
|
1258
|
+
downloadClient: q.downloadClient,
|
|
1259
|
+
})),
|
|
1260
|
+
}, null, 2),
|
|
1261
|
+
}],
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
case "lidarr_get_albums": {
|
|
1265
|
+
if (!clients.lidarr)
|
|
1266
|
+
throw new Error("Lidarr not configured");
|
|
1267
|
+
const artistId = args.artistId;
|
|
1268
|
+
const albums = await clients.lidarr.getAlbums(artistId);
|
|
1269
|
+
return {
|
|
1270
|
+
content: [{
|
|
1271
|
+
type: "text",
|
|
1272
|
+
text: JSON.stringify({
|
|
1273
|
+
count: albums.length,
|
|
1274
|
+
albums: albums.map(a => ({
|
|
1275
|
+
id: a.id,
|
|
1276
|
+
title: a.title,
|
|
1277
|
+
releaseDate: a.releaseDate,
|
|
1278
|
+
albumType: a.albumType,
|
|
1279
|
+
monitored: a.monitored,
|
|
1280
|
+
tracks: a.statistics ? `${a.statistics.trackFileCount}/${a.statistics.totalTrackCount}` : 'unknown',
|
|
1281
|
+
sizeOnDisk: formatBytes(a.statistics?.sizeOnDisk || 0),
|
|
1282
|
+
percentComplete: a.statistics?.percentOfTracks || 0,
|
|
1283
|
+
grabbed: a.grabbed,
|
|
1284
|
+
})),
|
|
1285
|
+
}, null, 2),
|
|
1286
|
+
}],
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
case "lidarr_search_album": {
|
|
1290
|
+
if (!clients.lidarr)
|
|
1291
|
+
throw new Error("Lidarr not configured");
|
|
1292
|
+
const albumId = args.albumId;
|
|
1293
|
+
const result = await clients.lidarr.searchAlbum(albumId);
|
|
1294
|
+
return {
|
|
1295
|
+
content: [{
|
|
1296
|
+
type: "text",
|
|
1297
|
+
text: JSON.stringify({
|
|
1298
|
+
success: true,
|
|
1299
|
+
message: `Search triggered for album`,
|
|
1300
|
+
commandId: result.id,
|
|
1301
|
+
}, null, 2),
|
|
1302
|
+
}],
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
case "lidarr_search_missing": {
|
|
1306
|
+
if (!clients.lidarr)
|
|
1307
|
+
throw new Error("Lidarr not configured");
|
|
1308
|
+
const artistId = args.artistId;
|
|
1309
|
+
const result = await clients.lidarr.searchMissingAlbums(artistId);
|
|
1310
|
+
return {
|
|
1311
|
+
content: [{
|
|
1312
|
+
type: "text",
|
|
1313
|
+
text: JSON.stringify({
|
|
1314
|
+
success: true,
|
|
1315
|
+
message: `Search triggered for missing albums`,
|
|
1316
|
+
commandId: result.id,
|
|
1317
|
+
}, null, 2),
|
|
1318
|
+
}],
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
case "lidarr_get_calendar": {
|
|
1322
|
+
if (!clients.lidarr)
|
|
1323
|
+
throw new Error("Lidarr not configured");
|
|
1324
|
+
const days = args?.days || 30;
|
|
1325
|
+
const start = new Date().toISOString().split('T')[0];
|
|
1326
|
+
const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
1327
|
+
const calendar = await clients.lidarr.getCalendar(start, end);
|
|
1328
|
+
return {
|
|
1329
|
+
content: [{
|
|
1330
|
+
type: "text",
|
|
1331
|
+
text: JSON.stringify({
|
|
1332
|
+
count: calendar.length,
|
|
1333
|
+
albums: calendar.map(a => ({
|
|
1334
|
+
id: a.id,
|
|
1335
|
+
title: a.title,
|
|
1336
|
+
artistId: a.artistId,
|
|
1337
|
+
releaseDate: a.releaseDate,
|
|
1338
|
+
albumType: a.albumType,
|
|
1339
|
+
monitored: a.monitored,
|
|
1340
|
+
})),
|
|
1341
|
+
}, null, 2),
|
|
1342
|
+
}],
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
// Readarr handlers
|
|
1346
|
+
case "readarr_get_authors": {
|
|
1347
|
+
if (!clients.readarr)
|
|
1348
|
+
throw new Error("Readarr not configured");
|
|
1349
|
+
const authors = await clients.readarr.getAuthors();
|
|
1350
|
+
return {
|
|
1351
|
+
content: [{
|
|
1352
|
+
type: "text",
|
|
1353
|
+
text: JSON.stringify({
|
|
1354
|
+
count: authors.length,
|
|
1355
|
+
authors: authors.map(a => ({
|
|
1356
|
+
id: a.id,
|
|
1357
|
+
authorName: a.authorName,
|
|
1358
|
+
status: a.status,
|
|
1359
|
+
books: a.statistics?.bookFileCount + '/' + a.statistics?.totalBookCount,
|
|
1360
|
+
sizeOnDisk: formatBytes(a.statistics?.sizeOnDisk || 0),
|
|
1361
|
+
monitored: a.monitored,
|
|
1362
|
+
})),
|
|
1363
|
+
}, null, 2),
|
|
1364
|
+
}],
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
case "readarr_search": {
|
|
1368
|
+
if (!clients.readarr)
|
|
1369
|
+
throw new Error("Readarr not configured");
|
|
1370
|
+
const term = args.term;
|
|
1371
|
+
const results = await clients.readarr.searchAuthors(term);
|
|
1372
|
+
return {
|
|
1373
|
+
content: [{
|
|
1374
|
+
type: "text",
|
|
1375
|
+
text: JSON.stringify({
|
|
1376
|
+
count: results.length,
|
|
1377
|
+
results: results.slice(0, 10).map(r => ({
|
|
1378
|
+
title: r.title,
|
|
1379
|
+
foreignAuthorId: r.foreignAuthorId,
|
|
1380
|
+
overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''),
|
|
1381
|
+
})),
|
|
1382
|
+
}, null, 2),
|
|
1383
|
+
}],
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
case "readarr_get_queue": {
|
|
1387
|
+
if (!clients.readarr)
|
|
1388
|
+
throw new Error("Readarr not configured");
|
|
1389
|
+
const queue = await clients.readarr.getQueue();
|
|
1390
|
+
return {
|
|
1391
|
+
content: [{
|
|
1392
|
+
type: "text",
|
|
1393
|
+
text: JSON.stringify({
|
|
1394
|
+
totalRecords: queue.totalRecords,
|
|
1395
|
+
items: queue.records.map(q => ({
|
|
1396
|
+
title: q.title,
|
|
1397
|
+
status: q.status,
|
|
1398
|
+
progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%',
|
|
1399
|
+
timeLeft: q.timeleft,
|
|
1400
|
+
downloadClient: q.downloadClient,
|
|
1401
|
+
})),
|
|
1402
|
+
}, null, 2),
|
|
1403
|
+
}],
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
case "readarr_get_books": {
|
|
1407
|
+
if (!clients.readarr)
|
|
1408
|
+
throw new Error("Readarr not configured");
|
|
1409
|
+
const authorId = args.authorId;
|
|
1410
|
+
const books = await clients.readarr.getBooks(authorId);
|
|
1411
|
+
return {
|
|
1412
|
+
content: [{
|
|
1413
|
+
type: "text",
|
|
1414
|
+
text: JSON.stringify({
|
|
1415
|
+
count: books.length,
|
|
1416
|
+
books: books.map(b => ({
|
|
1417
|
+
id: b.id,
|
|
1418
|
+
title: b.title,
|
|
1419
|
+
releaseDate: b.releaseDate,
|
|
1420
|
+
pageCount: b.pageCount,
|
|
1421
|
+
monitored: b.monitored,
|
|
1422
|
+
hasFile: b.statistics ? b.statistics.bookFileCount > 0 : false,
|
|
1423
|
+
sizeOnDisk: formatBytes(b.statistics?.sizeOnDisk || 0),
|
|
1424
|
+
grabbed: b.grabbed,
|
|
1425
|
+
})),
|
|
1426
|
+
}, null, 2),
|
|
1427
|
+
}],
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
case "readarr_search_book": {
|
|
1431
|
+
if (!clients.readarr)
|
|
1432
|
+
throw new Error("Readarr not configured");
|
|
1433
|
+
const bookIds = args.bookIds;
|
|
1434
|
+
const result = await clients.readarr.searchBook(bookIds);
|
|
1435
|
+
return {
|
|
1436
|
+
content: [{
|
|
1437
|
+
type: "text",
|
|
1438
|
+
text: JSON.stringify({
|
|
1439
|
+
success: true,
|
|
1440
|
+
message: `Search triggered for ${bookIds.length} book(s)`,
|
|
1441
|
+
commandId: result.id,
|
|
1442
|
+
}, null, 2),
|
|
1443
|
+
}],
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
case "readarr_search_missing": {
|
|
1447
|
+
if (!clients.readarr)
|
|
1448
|
+
throw new Error("Readarr not configured");
|
|
1449
|
+
const authorId = args.authorId;
|
|
1450
|
+
const result = await clients.readarr.searchMissingBooks(authorId);
|
|
1451
|
+
return {
|
|
1452
|
+
content: [{
|
|
1453
|
+
type: "text",
|
|
1454
|
+
text: JSON.stringify({
|
|
1455
|
+
success: true,
|
|
1456
|
+
message: `Search triggered for missing books`,
|
|
1457
|
+
commandId: result.id,
|
|
1458
|
+
}, null, 2),
|
|
1459
|
+
}],
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
case "readarr_get_calendar": {
|
|
1463
|
+
if (!clients.readarr)
|
|
1464
|
+
throw new Error("Readarr not configured");
|
|
1465
|
+
const days = args?.days || 30;
|
|
1466
|
+
const start = new Date().toISOString().split('T')[0];
|
|
1467
|
+
const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
1468
|
+
const calendar = await clients.readarr.getCalendar(start, end);
|
|
1469
|
+
return {
|
|
1470
|
+
content: [{
|
|
1471
|
+
type: "text",
|
|
1472
|
+
text: JSON.stringify({
|
|
1473
|
+
count: calendar.length,
|
|
1474
|
+
books: calendar.map(b => ({
|
|
1475
|
+
id: b.id,
|
|
1476
|
+
title: b.title,
|
|
1477
|
+
authorId: b.authorId,
|
|
1478
|
+
releaseDate: b.releaseDate,
|
|
1479
|
+
monitored: b.monitored,
|
|
1480
|
+
})),
|
|
1481
|
+
}, null, 2),
|
|
1482
|
+
}],
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
// Prowlarr handlers
|
|
1486
|
+
case "prowlarr_get_indexers": {
|
|
1487
|
+
if (!clients.prowlarr)
|
|
1488
|
+
throw new Error("Prowlarr not configured");
|
|
1489
|
+
const indexers = await clients.prowlarr.getIndexers();
|
|
1490
|
+
return {
|
|
1491
|
+
content: [{
|
|
1492
|
+
type: "text",
|
|
1493
|
+
text: JSON.stringify({
|
|
1494
|
+
count: indexers.length,
|
|
1495
|
+
indexers: indexers.map(i => ({
|
|
1496
|
+
id: i.id,
|
|
1497
|
+
name: i.name,
|
|
1498
|
+
protocol: i.protocol,
|
|
1499
|
+
enableRss: i.enableRss,
|
|
1500
|
+
enableAutomaticSearch: i.enableAutomaticSearch,
|
|
1501
|
+
enableInteractiveSearch: i.enableInteractiveSearch,
|
|
1502
|
+
priority: i.priority,
|
|
1503
|
+
})),
|
|
1504
|
+
}, null, 2),
|
|
1505
|
+
}],
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
case "prowlarr_search": {
|
|
1509
|
+
if (!clients.prowlarr)
|
|
1510
|
+
throw new Error("Prowlarr not configured");
|
|
1511
|
+
const query = args.query;
|
|
1512
|
+
const results = await clients.prowlarr.search(query);
|
|
1513
|
+
return {
|
|
1514
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
case "prowlarr_test_indexers": {
|
|
1518
|
+
if (!clients.prowlarr)
|
|
1519
|
+
throw new Error("Prowlarr not configured");
|
|
1520
|
+
const results = await clients.prowlarr.testAllIndexers();
|
|
1521
|
+
const indexers = await clients.prowlarr.getIndexers();
|
|
1522
|
+
const indexerMap = new Map(indexers.map(i => [i.id, i.name]));
|
|
1523
|
+
return {
|
|
1524
|
+
content: [{
|
|
1525
|
+
type: "text",
|
|
1526
|
+
text: JSON.stringify({
|
|
1527
|
+
count: results.length,
|
|
1528
|
+
indexers: results.map(r => ({
|
|
1529
|
+
id: r.id,
|
|
1530
|
+
name: indexerMap.get(r.id) || 'Unknown',
|
|
1531
|
+
isValid: r.isValid,
|
|
1532
|
+
errors: r.validationFailures.map(f => f.errorMessage),
|
|
1533
|
+
})),
|
|
1534
|
+
healthy: results.filter(r => r.isValid).length,
|
|
1535
|
+
failed: results.filter(r => !r.isValid).length,
|
|
1536
|
+
}, null, 2),
|
|
1537
|
+
}],
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
case "prowlarr_get_stats": {
|
|
1541
|
+
if (!clients.prowlarr)
|
|
1542
|
+
throw new Error("Prowlarr not configured");
|
|
1543
|
+
const stats = await clients.prowlarr.getIndexerStats();
|
|
1544
|
+
return {
|
|
1545
|
+
content: [{
|
|
1546
|
+
type: "text",
|
|
1547
|
+
text: JSON.stringify({
|
|
1548
|
+
count: stats.indexers.length,
|
|
1549
|
+
indexers: stats.indexers.map(s => ({
|
|
1550
|
+
name: s.indexerName,
|
|
1551
|
+
queries: s.numberOfQueries,
|
|
1552
|
+
grabs: s.numberOfGrabs,
|
|
1553
|
+
failedQueries: s.numberOfFailedQueries,
|
|
1554
|
+
failedGrabs: s.numberOfFailedGrabs,
|
|
1555
|
+
avgResponseTime: s.averageResponseTime + 'ms',
|
|
1556
|
+
})),
|
|
1557
|
+
totals: {
|
|
1558
|
+
queries: stats.indexers.reduce((sum, s) => sum + s.numberOfQueries, 0),
|
|
1559
|
+
grabs: stats.indexers.reduce((sum, s) => sum + s.numberOfGrabs, 0),
|
|
1560
|
+
failedQueries: stats.indexers.reduce((sum, s) => sum + s.numberOfFailedQueries, 0),
|
|
1561
|
+
failedGrabs: stats.indexers.reduce((sum, s) => sum + s.numberOfFailedGrabs, 0),
|
|
1562
|
+
},
|
|
1563
|
+
}, null, 2),
|
|
1564
|
+
}],
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
// Cross-service search
|
|
1568
|
+
case "arr_search_all": {
|
|
1569
|
+
const term = args.term;
|
|
1570
|
+
const results = {};
|
|
1571
|
+
if (clients.sonarr) {
|
|
1572
|
+
try {
|
|
1573
|
+
const sonarrResults = await clients.sonarr.searchSeries(term);
|
|
1574
|
+
results.sonarr = { count: sonarrResults.length, results: sonarrResults.slice(0, 5) };
|
|
1575
|
+
}
|
|
1576
|
+
catch (e) {
|
|
1577
|
+
results.sonarr = { error: e instanceof Error ? e.message : String(e) };
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (clients.radarr) {
|
|
1581
|
+
try {
|
|
1582
|
+
const radarrResults = await clients.radarr.searchMovies(term);
|
|
1583
|
+
results.radarr = { count: radarrResults.length, results: radarrResults.slice(0, 5) };
|
|
1584
|
+
}
|
|
1585
|
+
catch (e) {
|
|
1586
|
+
results.radarr = { error: e instanceof Error ? e.message : String(e) };
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (clients.lidarr) {
|
|
1590
|
+
try {
|
|
1591
|
+
const lidarrResults = await clients.lidarr.searchArtists(term);
|
|
1592
|
+
results.lidarr = { count: lidarrResults.length, results: lidarrResults.slice(0, 5) };
|
|
1593
|
+
}
|
|
1594
|
+
catch (e) {
|
|
1595
|
+
results.lidarr = { error: e instanceof Error ? e.message : String(e) };
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (clients.readarr) {
|
|
1599
|
+
try {
|
|
1600
|
+
const readarrResults = await clients.readarr.searchAuthors(term);
|
|
1601
|
+
results.readarr = { count: readarrResults.length, results: readarrResults.slice(0, 5) };
|
|
1602
|
+
}
|
|
1603
|
+
catch (e) {
|
|
1604
|
+
results.readarr = { error: e instanceof Error ? e.message : String(e) };
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
// TRaSH Guides handlers
|
|
1612
|
+
case "trash_list_profiles": {
|
|
1613
|
+
const service = args.service;
|
|
1614
|
+
const profiles = await trashClient.listProfiles(service);
|
|
1615
|
+
return {
|
|
1616
|
+
content: [{
|
|
1617
|
+
type: "text",
|
|
1618
|
+
text: JSON.stringify({
|
|
1619
|
+
service,
|
|
1620
|
+
count: profiles.length,
|
|
1621
|
+
profiles: profiles.map(p => ({
|
|
1622
|
+
name: p.name,
|
|
1623
|
+
description: p.description?.replace(/<br>/g, ' ') || 'No description',
|
|
1624
|
+
})),
|
|
1625
|
+
usage: "Use trash_get_profile to see full details for a specific profile",
|
|
1626
|
+
}, null, 2),
|
|
1627
|
+
}],
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
case "trash_get_profile": {
|
|
1631
|
+
const { service, profile: profileName } = args;
|
|
1632
|
+
const profile = await trashClient.getProfile(service, profileName);
|
|
1633
|
+
if (!profile) {
|
|
1634
|
+
return {
|
|
1635
|
+
content: [{
|
|
1636
|
+
type: "text",
|
|
1637
|
+
text: JSON.stringify({
|
|
1638
|
+
error: `Profile '${profileName}' not found for ${service}`,
|
|
1639
|
+
hint: "Use trash_list_profiles to see available profiles",
|
|
1640
|
+
}, null, 2),
|
|
1641
|
+
}],
|
|
1642
|
+
isError: true,
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
content: [{
|
|
1647
|
+
type: "text",
|
|
1648
|
+
text: JSON.stringify({
|
|
1649
|
+
name: profile.name,
|
|
1650
|
+
description: profile.trash_description?.replace(/<br>/g, '\n'),
|
|
1651
|
+
trash_id: profile.trash_id,
|
|
1652
|
+
upgradeAllowed: profile.upgradeAllowed,
|
|
1653
|
+
cutoff: profile.cutoff,
|
|
1654
|
+
minFormatScore: profile.minFormatScore,
|
|
1655
|
+
cutoffFormatScore: profile.cutoffFormatScore,
|
|
1656
|
+
language: profile.language,
|
|
1657
|
+
qualities: profile.items.map(i => ({
|
|
1658
|
+
name: i.name,
|
|
1659
|
+
allowed: i.allowed,
|
|
1660
|
+
items: i.items,
|
|
1661
|
+
})),
|
|
1662
|
+
customFormats: Object.entries(profile.formatItems || {}).map(([name, trashId]) => ({
|
|
1663
|
+
name,
|
|
1664
|
+
trash_id: trashId,
|
|
1665
|
+
})),
|
|
1666
|
+
}, null, 2),
|
|
1667
|
+
}],
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
case "trash_list_custom_formats": {
|
|
1671
|
+
const { service, category } = args;
|
|
1672
|
+
const formats = await trashClient.listCustomFormats(service, category);
|
|
1673
|
+
return {
|
|
1674
|
+
content: [{
|
|
1675
|
+
type: "text",
|
|
1676
|
+
text: JSON.stringify({
|
|
1677
|
+
service,
|
|
1678
|
+
category: category || 'all',
|
|
1679
|
+
count: formats.length,
|
|
1680
|
+
formats: formats.slice(0, 50).map(f => ({
|
|
1681
|
+
name: f.name,
|
|
1682
|
+
categories: f.categories,
|
|
1683
|
+
defaultScore: f.defaultScore,
|
|
1684
|
+
})),
|
|
1685
|
+
note: formats.length > 50 ? `Showing first 50 of ${formats.length}. Use category filter to narrow results.` : undefined,
|
|
1686
|
+
availableCategories: ['hdr', 'audio', 'resolution', 'source', 'streaming', 'anime', 'unwanted', 'release', 'language'],
|
|
1687
|
+
}, null, 2),
|
|
1688
|
+
}],
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
case "trash_get_naming": {
|
|
1692
|
+
const { service, mediaServer } = args;
|
|
1693
|
+
const naming = await trashClient.getNaming(service);
|
|
1694
|
+
if (!naming) {
|
|
1695
|
+
return {
|
|
1696
|
+
content: [{
|
|
1697
|
+
type: "text",
|
|
1698
|
+
text: JSON.stringify({ error: `Could not fetch naming conventions for ${service}` }, null, 2),
|
|
1699
|
+
}],
|
|
1700
|
+
isError: true,
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
// Map media server to naming key
|
|
1704
|
+
const serverMap = {
|
|
1705
|
+
plex: { folder: 'plex-imdb', file: 'plex-imdb' },
|
|
1706
|
+
emby: { folder: 'emby-imdb', file: 'emby-imdb' },
|
|
1707
|
+
jellyfin: { folder: 'jellyfin-imdb', file: 'jellyfin-imdb' },
|
|
1708
|
+
standard: { folder: 'default', file: 'standard' },
|
|
1709
|
+
};
|
|
1710
|
+
const keys = serverMap[mediaServer] || serverMap.standard;
|
|
1711
|
+
return {
|
|
1712
|
+
content: [{
|
|
1713
|
+
type: "text",
|
|
1714
|
+
text: JSON.stringify({
|
|
1715
|
+
service,
|
|
1716
|
+
mediaServer,
|
|
1717
|
+
recommended: {
|
|
1718
|
+
folder: naming.folder[keys.folder] || naming.folder.default,
|
|
1719
|
+
file: naming.file[keys.file] || naming.file.standard,
|
|
1720
|
+
...(naming.season && { season: naming.season[keys.folder] || naming.season.default }),
|
|
1721
|
+
...(naming.series && { series: naming.series[keys.folder] || naming.series.default }),
|
|
1722
|
+
},
|
|
1723
|
+
allFolderOptions: Object.keys(naming.folder),
|
|
1724
|
+
allFileOptions: Object.keys(naming.file),
|
|
1725
|
+
}, null, 2),
|
|
1726
|
+
}],
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
case "trash_get_quality_sizes": {
|
|
1730
|
+
const { service, type } = args;
|
|
1731
|
+
const sizes = await trashClient.getQualitySizes(service, type);
|
|
1732
|
+
return {
|
|
1733
|
+
content: [{
|
|
1734
|
+
type: "text",
|
|
1735
|
+
text: JSON.stringify({
|
|
1736
|
+
service,
|
|
1737
|
+
type: type || 'all',
|
|
1738
|
+
profiles: sizes.map(s => ({
|
|
1739
|
+
type: s.type,
|
|
1740
|
+
qualities: s.qualities.map(q => ({
|
|
1741
|
+
quality: q.quality,
|
|
1742
|
+
min: q.min + ' MB/min',
|
|
1743
|
+
preferred: q.preferred === 1999 ? 'unlimited' : q.preferred + ' MB/min',
|
|
1744
|
+
max: q.max === 2000 ? 'unlimited' : q.max + ' MB/min',
|
|
1745
|
+
})),
|
|
1746
|
+
})),
|
|
1747
|
+
}, null, 2),
|
|
1748
|
+
}],
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
case "trash_compare_profile": {
|
|
1752
|
+
const { service, profileId, trashProfile } = args;
|
|
1753
|
+
// Get client
|
|
1754
|
+
const client = service === 'radarr' ? clients.radarr : clients.sonarr;
|
|
1755
|
+
if (!client) {
|
|
1756
|
+
return {
|
|
1757
|
+
content: [{
|
|
1758
|
+
type: "text",
|
|
1759
|
+
text: JSON.stringify({ error: `${service} not configured. Cannot compare profiles.` }, null, 2),
|
|
1760
|
+
}],
|
|
1761
|
+
isError: true,
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
// Fetch both profiles
|
|
1765
|
+
const [userProfiles, trashProfileData] = await Promise.all([
|
|
1766
|
+
client.getQualityProfiles(),
|
|
1767
|
+
trashClient.getProfile(service, trashProfile),
|
|
1768
|
+
]);
|
|
1769
|
+
const userProfile = userProfiles.find(p => p.id === profileId);
|
|
1770
|
+
if (!userProfile) {
|
|
1771
|
+
return {
|
|
1772
|
+
content: [{
|
|
1773
|
+
type: "text",
|
|
1774
|
+
text: JSON.stringify({
|
|
1775
|
+
error: `Profile ID ${profileId} not found`,
|
|
1776
|
+
availableProfiles: userProfiles.map(p => ({ id: p.id, name: p.name })),
|
|
1777
|
+
}, null, 2),
|
|
1778
|
+
}],
|
|
1779
|
+
isError: true,
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
if (!trashProfileData) {
|
|
1783
|
+
return {
|
|
1784
|
+
content: [{
|
|
1785
|
+
type: "text",
|
|
1786
|
+
text: JSON.stringify({
|
|
1787
|
+
error: `TRaSH profile '${trashProfile}' not found`,
|
|
1788
|
+
hint: "Use trash_list_profiles to see available profiles",
|
|
1789
|
+
}, null, 2),
|
|
1790
|
+
}],
|
|
1791
|
+
isError: true,
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
// Compare qualities
|
|
1795
|
+
const userQualities = new Set(userProfile.items
|
|
1796
|
+
.filter(i => i.allowed)
|
|
1797
|
+
.map(i => i.quality?.name || i.name)
|
|
1798
|
+
.filter((n) => n !== undefined));
|
|
1799
|
+
const trashQualities = new Set(trashProfileData.items
|
|
1800
|
+
.filter(i => i.allowed)
|
|
1801
|
+
.map(i => i.name));
|
|
1802
|
+
const qualityComparison = {
|
|
1803
|
+
matching: [...userQualities].filter(q => trashQualities.has(q)),
|
|
1804
|
+
missingFromYours: [...trashQualities].filter(q => !userQualities.has(q)),
|
|
1805
|
+
extraInYours: [...userQualities].filter(q => !trashQualities.has(q)),
|
|
1806
|
+
};
|
|
1807
|
+
// Compare custom formats
|
|
1808
|
+
const userCFNames = new Set((userProfile.formatItems || [])
|
|
1809
|
+
.filter(f => f.score !== 0)
|
|
1810
|
+
.map(f => f.name));
|
|
1811
|
+
const trashCFNames = new Set(Object.keys(trashProfileData.formatItems || {}));
|
|
1812
|
+
const cfComparison = {
|
|
1813
|
+
matching: [...userCFNames].filter(cf => trashCFNames.has(cf)),
|
|
1814
|
+
missingFromYours: [...trashCFNames].filter(cf => !userCFNames.has(cf)),
|
|
1815
|
+
extraInYours: [...userCFNames].filter(cf => !trashCFNames.has(cf)),
|
|
1816
|
+
};
|
|
1817
|
+
return {
|
|
1818
|
+
content: [{
|
|
1819
|
+
type: "text",
|
|
1820
|
+
text: JSON.stringify({
|
|
1821
|
+
yourProfile: {
|
|
1822
|
+
name: userProfile.name,
|
|
1823
|
+
id: userProfile.id,
|
|
1824
|
+
upgradeAllowed: userProfile.upgradeAllowed,
|
|
1825
|
+
cutoff: userProfile.cutoff,
|
|
1826
|
+
},
|
|
1827
|
+
trashProfile: {
|
|
1828
|
+
name: trashProfileData.name,
|
|
1829
|
+
upgradeAllowed: trashProfileData.upgradeAllowed,
|
|
1830
|
+
cutoff: trashProfileData.cutoff,
|
|
1831
|
+
},
|
|
1832
|
+
qualityComparison,
|
|
1833
|
+
customFormatComparison: cfComparison,
|
|
1834
|
+
recommendations: [
|
|
1835
|
+
...(qualityComparison.missingFromYours.length > 0
|
|
1836
|
+
? [`Enable these qualities: ${qualityComparison.missingFromYours.join(', ')}`]
|
|
1837
|
+
: []),
|
|
1838
|
+
...(cfComparison.missingFromYours.length > 0
|
|
1839
|
+
? [`Add these custom formats: ${cfComparison.missingFromYours.slice(0, 5).join(', ')}${cfComparison.missingFromYours.length > 5 ? ` and ${cfComparison.missingFromYours.length - 5} more` : ''}`]
|
|
1840
|
+
: []),
|
|
1841
|
+
...(userProfile.upgradeAllowed !== trashProfileData.upgradeAllowed
|
|
1842
|
+
? [`Set upgradeAllowed to ${trashProfileData.upgradeAllowed}`]
|
|
1843
|
+
: []),
|
|
1844
|
+
],
|
|
1845
|
+
}, null, 2),
|
|
1846
|
+
}],
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
case "trash_compare_naming": {
|
|
1850
|
+
const { service, mediaServer } = args;
|
|
1851
|
+
// Get client
|
|
1852
|
+
const client = service === 'radarr' ? clients.radarr : clients.sonarr;
|
|
1853
|
+
if (!client) {
|
|
1854
|
+
return {
|
|
1855
|
+
content: [{
|
|
1856
|
+
type: "text",
|
|
1857
|
+
text: JSON.stringify({ error: `${service} not configured. Cannot compare naming.` }, null, 2),
|
|
1858
|
+
}],
|
|
1859
|
+
isError: true,
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
// Fetch both
|
|
1863
|
+
const [userNaming, trashNaming] = await Promise.all([
|
|
1864
|
+
client.getNamingConfig(),
|
|
1865
|
+
trashClient.getNaming(service),
|
|
1866
|
+
]);
|
|
1867
|
+
if (!trashNaming) {
|
|
1868
|
+
return {
|
|
1869
|
+
content: [{
|
|
1870
|
+
type: "text",
|
|
1871
|
+
text: JSON.stringify({ error: `Could not fetch TRaSH naming for ${service}` }, null, 2),
|
|
1872
|
+
}],
|
|
1873
|
+
isError: true,
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
// Map media server to naming key
|
|
1877
|
+
const serverMap = {
|
|
1878
|
+
plex: { folder: 'plex-imdb', file: 'plex-imdb' },
|
|
1879
|
+
emby: { folder: 'emby-imdb', file: 'emby-imdb' },
|
|
1880
|
+
jellyfin: { folder: 'jellyfin-imdb', file: 'jellyfin-imdb' },
|
|
1881
|
+
standard: { folder: 'default', file: 'standard' },
|
|
1882
|
+
};
|
|
1883
|
+
const keys = serverMap[mediaServer] || serverMap.standard;
|
|
1884
|
+
const recommendedFolder = trashNaming.folder[keys.folder] || trashNaming.folder.default;
|
|
1885
|
+
const recommendedFile = trashNaming.file[keys.file] || trashNaming.file.standard;
|
|
1886
|
+
// Extract user's current naming (field names vary by service)
|
|
1887
|
+
const namingRecord = userNaming;
|
|
1888
|
+
const userFolder = namingRecord.movieFolderFormat ||
|
|
1889
|
+
namingRecord.seriesFolderFormat ||
|
|
1890
|
+
namingRecord.standardMovieFormat;
|
|
1891
|
+
const userFile = namingRecord.standardMovieFormat ||
|
|
1892
|
+
namingRecord.standardEpisodeFormat;
|
|
1893
|
+
return {
|
|
1894
|
+
content: [{
|
|
1895
|
+
type: "text",
|
|
1896
|
+
text: JSON.stringify({
|
|
1897
|
+
mediaServer,
|
|
1898
|
+
yourNaming: {
|
|
1899
|
+
folder: userFolder,
|
|
1900
|
+
file: userFile,
|
|
1901
|
+
},
|
|
1902
|
+
trashRecommended: {
|
|
1903
|
+
folder: recommendedFolder,
|
|
1904
|
+
file: recommendedFile,
|
|
1905
|
+
},
|
|
1906
|
+
folderMatch: userFolder === recommendedFolder,
|
|
1907
|
+
fileMatch: userFile === recommendedFile,
|
|
1908
|
+
recommendations: [
|
|
1909
|
+
...(userFolder !== recommendedFolder ? [`Update folder format to: ${recommendedFolder}`] : []),
|
|
1910
|
+
...(userFile !== recommendedFile ? [`Update file format to: ${recommendedFile}`] : []),
|
|
1911
|
+
],
|
|
1912
|
+
}, null, 2),
|
|
1913
|
+
}],
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
default:
|
|
1917
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
catch (error) {
|
|
1921
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1922
|
+
return {
|
|
1923
|
+
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
1924
|
+
isError: true,
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
// Helper function to format bytes
|
|
1929
|
+
function formatBytes(bytes) {
|
|
1930
|
+
if (bytes === 0)
|
|
1931
|
+
return '0 B';
|
|
1932
|
+
const k = 1024;
|
|
1933
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
1934
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1935
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1936
|
+
}
|
|
1937
|
+
// Start the server
|
|
1938
|
+
async function main() {
|
|
1939
|
+
const transport = new StdioServerTransport();
|
|
1940
|
+
await server.connect(transport);
|
|
1941
|
+
console.error(`*arr MCP server running - configured services: ${configuredServices.map(s => s.name).join(', ')}`);
|
|
1942
|
+
}
|
|
1943
|
+
main().catch((error) => {
|
|
1944
|
+
console.error("Fatal error:", error);
|
|
1945
|
+
process.exit(1);
|
|
1946
|
+
});
|
|
1947
|
+
//# sourceMappingURL=index.js.map
|