mcp-arr-server 1.5.3 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -35
- package/dist/arr-client.d.ts +15 -129
- package/dist/arr-client.d.ts.map +1 -1
- package/dist/arr-client.js +33 -97
- package/dist/arr-client.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +458 -528
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -3,34 +3,34 @@
|
|
|
3
3
|
* MCP Server for *arr Media Management Suite
|
|
4
4
|
*
|
|
5
5
|
* Provides tools for managing Sonarr (TV), Radarr (Movies), Lidarr (Music),
|
|
6
|
-
*
|
|
6
|
+
* and Prowlarr (Indexers) through Claude Code.
|
|
7
7
|
*
|
|
8
8
|
* Environment variables:
|
|
9
9
|
* - SONARR_URL, SONARR_API_KEY
|
|
10
10
|
* - RADARR_URL, RADARR_API_KEY
|
|
11
11
|
* - LIDARR_URL, LIDARR_API_KEY
|
|
12
|
-
* - READARR_URL, READARR_API_KEY
|
|
13
12
|
* - PROWLARR_URL, PROWLARR_API_KEY
|
|
14
13
|
*/
|
|
15
14
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
17
17
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
18
|
-
import {
|
|
18
|
+
import { createServer } from "node:http";
|
|
19
|
+
import { SonarrClient, RadarrClient, LidarrClient, ProwlarrClient, } from "./arr-client.js";
|
|
19
20
|
import { trashClient } from "./trash-client.js";
|
|
21
|
+
const SERVER_VERSION = "1.6.2";
|
|
22
|
+
const TRANSPORT_MODE = (process.env.MCP_TRANSPORT || "stdio").toLowerCase();
|
|
23
|
+
const HTTP_HOST = process.env.HOST || "127.0.0.1";
|
|
24
|
+
const HTTP_PORT = Number(process.env.PORT || "3000");
|
|
25
|
+
const HTTP_PATH = process.env.MCP_PATH || "/mcp";
|
|
20
26
|
const services = [
|
|
21
27
|
{ name: 'sonarr', displayName: 'Sonarr (TV)', url: process.env.SONARR_URL, apiKey: process.env.SONARR_API_KEY },
|
|
22
28
|
{ name: 'radarr', displayName: 'Radarr (Movies)', url: process.env.RADARR_URL, apiKey: process.env.RADARR_API_KEY },
|
|
23
29
|
{ name: 'lidarr', displayName: 'Lidarr (Music)', url: process.env.LIDARR_URL, apiKey: process.env.LIDARR_API_KEY },
|
|
24
|
-
{ name: 'readarr', displayName: 'Readarr (Books)', url: process.env.READARR_URL, apiKey: process.env.READARR_API_KEY },
|
|
25
30
|
{ name: 'prowlarr', displayName: 'Prowlarr (Indexers)', url: process.env.PROWLARR_URL, apiKey: process.env.PROWLARR_API_KEY },
|
|
26
31
|
];
|
|
27
32
|
// Check which services are configured
|
|
28
33
|
const configuredServices = services.filter(s => s.url && s.apiKey);
|
|
29
|
-
if (configuredServices.length === 0) {
|
|
30
|
-
console.error("Error: No *arr services configured. Set at least one pair of URL and API_KEY environment variables.");
|
|
31
|
-
console.error("Example: SONARR_URL and SONARR_API_KEY");
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
34
|
// Initialize clients for configured services
|
|
35
35
|
const clients = {};
|
|
36
36
|
for (const service of configuredServices) {
|
|
@@ -45,9 +45,6 @@ for (const service of configuredServices) {
|
|
|
45
45
|
case 'lidarr':
|
|
46
46
|
clients.lidarr = new LidarrClient(config);
|
|
47
47
|
break;
|
|
48
|
-
case 'readarr':
|
|
49
|
-
clients.readarr = new ReadarrClient(config);
|
|
50
|
-
break;
|
|
51
48
|
case 'prowlarr':
|
|
52
49
|
clients.prowlarr = new ProwlarrClient(config);
|
|
53
50
|
break;
|
|
@@ -58,13 +55,43 @@ const TOOLS = [
|
|
|
58
55
|
// General tool available for all
|
|
59
56
|
{
|
|
60
57
|
name: "arr_status",
|
|
61
|
-
description:
|
|
58
|
+
description: configuredServices.length > 0
|
|
59
|
+
? `Get status of all configured *arr services. Currently configured: ${configuredServices.map(s => s.displayName).join(', ')}`
|
|
60
|
+
: "Get status of all supported *arr services. No local *arr services are currently configured, but TRaSH reference tools remain available.",
|
|
62
61
|
inputSchema: {
|
|
63
62
|
type: "object",
|
|
64
63
|
properties: {},
|
|
65
64
|
required: [],
|
|
66
65
|
},
|
|
67
66
|
},
|
|
67
|
+
{
|
|
68
|
+
name: "search",
|
|
69
|
+
description: "Search across configured *arr libraries plus TRaSH Guides reference profiles. This is the primary discovery tool for remote MCP clients such as ChatGPT.",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
query: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Natural-language search query",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ["query"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "fetch",
|
|
83
|
+
description: "Fetch a specific item returned by search. Accepts an opaque item id from the search tool.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
id: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Opaque result id returned by search",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ["id"],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
68
95
|
];
|
|
69
96
|
// Configuration review tools for each service
|
|
70
97
|
// These are added dynamically based on configured services
|
|
@@ -135,16 +162,27 @@ if (clients.radarr)
|
|
|
135
162
|
addConfigTools('radarr', 'Radarr (Movies)');
|
|
136
163
|
if (clients.lidarr)
|
|
137
164
|
addConfigTools('lidarr', 'Lidarr (Music)');
|
|
138
|
-
if (clients.readarr)
|
|
139
|
-
addConfigTools('readarr', 'Readarr (Books)');
|
|
140
165
|
// Sonarr tools
|
|
141
166
|
if (clients.sonarr) {
|
|
142
167
|
TOOLS.push({
|
|
143
168
|
name: "sonarr_get_series",
|
|
144
|
-
description: "Get
|
|
169
|
+
description: "Get TV series from Sonarr library with optional pagination and title filtering. Defaults to limit=25 to avoid very large responses. Use offset to fetch additional pages.",
|
|
145
170
|
inputSchema: {
|
|
146
171
|
type: "object",
|
|
147
|
-
properties: {
|
|
172
|
+
properties: {
|
|
173
|
+
limit: {
|
|
174
|
+
type: "number",
|
|
175
|
+
description: "Maximum number of series to return (default: 25, max: 100)",
|
|
176
|
+
},
|
|
177
|
+
offset: {
|
|
178
|
+
type: "number",
|
|
179
|
+
description: "Number of series to skip before returning results (default: 0)",
|
|
180
|
+
},
|
|
181
|
+
search: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "Optional case-insensitive title filter",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
148
186
|
required: [],
|
|
149
187
|
},
|
|
150
188
|
}, {
|
|
@@ -162,10 +200,19 @@ if (clients.sonarr) {
|
|
|
162
200
|
},
|
|
163
201
|
}, {
|
|
164
202
|
name: "sonarr_get_queue",
|
|
165
|
-
description: "Get Sonarr download queue",
|
|
203
|
+
description: "Get Sonarr download queue. Supports pagination with limit and offset.",
|
|
166
204
|
inputSchema: {
|
|
167
205
|
type: "object",
|
|
168
|
-
properties: {
|
|
206
|
+
properties: {
|
|
207
|
+
limit: {
|
|
208
|
+
type: "number",
|
|
209
|
+
description: "Maximum number of queue items to return (default: 25, max: 100)",
|
|
210
|
+
},
|
|
211
|
+
offset: {
|
|
212
|
+
type: "number",
|
|
213
|
+
description: "Number of queue items to skip before returning results (default: 0)",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
169
216
|
required: [],
|
|
170
217
|
},
|
|
171
218
|
}, {
|
|
@@ -225,6 +272,19 @@ if (clients.sonarr) {
|
|
|
225
272
|
},
|
|
226
273
|
required: ["episodeIds"],
|
|
227
274
|
},
|
|
275
|
+
}, {
|
|
276
|
+
name: "sonarr_refresh_series",
|
|
277
|
+
description: "Trigger a metadata refresh for a specific series in Sonarr",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
seriesId: {
|
|
282
|
+
type: "number",
|
|
283
|
+
description: "Series ID to refresh",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
required: ["seriesId"],
|
|
287
|
+
},
|
|
228
288
|
}, {
|
|
229
289
|
name: "sonarr_add_series",
|
|
230
290
|
description: "Add a TV series to Sonarr. Use sonarr_search first to find the tvdbId, and sonarr_get_root_folders / sonarr_get_quality_profiles to get valid values for rootFolderPath and qualityProfileId. Use sonarr_get_tags to get valid tag IDs.",
|
|
@@ -263,32 +323,29 @@ if (clients.sonarr) {
|
|
|
263
323
|
},
|
|
264
324
|
required: ["tvdbId", "title", "qualityProfileId", "rootFolderPath"],
|
|
265
325
|
},
|
|
266
|
-
}, {
|
|
267
|
-
name: "sonarr_get_root_folders",
|
|
268
|
-
description: "Get available root folders for Sonarr. Use this to find valid rootFolderPath values when adding a series.",
|
|
269
|
-
inputSchema: {
|
|
270
|
-
type: "object",
|
|
271
|
-
properties: {},
|
|
272
|
-
required: [],
|
|
273
|
-
},
|
|
274
|
-
}, {
|
|
275
|
-
name: "sonarr_get_quality_profiles",
|
|
276
|
-
description: "Get available quality profiles for Sonarr. Use this to find valid qualityProfileId values when adding a series.",
|
|
277
|
-
inputSchema: {
|
|
278
|
-
type: "object",
|
|
279
|
-
properties: {},
|
|
280
|
-
required: [],
|
|
281
|
-
},
|
|
282
326
|
});
|
|
283
327
|
}
|
|
284
328
|
// Radarr tools
|
|
285
329
|
if (clients.radarr) {
|
|
286
330
|
TOOLS.push({
|
|
287
331
|
name: "radarr_get_movies",
|
|
288
|
-
description: "Get
|
|
332
|
+
description: "Get movies from Radarr library with optional pagination and title filtering. Defaults to limit=25 to avoid very large responses. Use offset to fetch additional pages.",
|
|
289
333
|
inputSchema: {
|
|
290
334
|
type: "object",
|
|
291
|
-
properties: {
|
|
335
|
+
properties: {
|
|
336
|
+
limit: {
|
|
337
|
+
type: "number",
|
|
338
|
+
description: "Maximum number of movies to return (default: 25, max: 100)",
|
|
339
|
+
},
|
|
340
|
+
offset: {
|
|
341
|
+
type: "number",
|
|
342
|
+
description: "Number of movies to skip before returning results (default: 0)",
|
|
343
|
+
},
|
|
344
|
+
search: {
|
|
345
|
+
type: "string",
|
|
346
|
+
description: "Optional case-insensitive title filter",
|
|
347
|
+
},
|
|
348
|
+
},
|
|
292
349
|
required: [],
|
|
293
350
|
},
|
|
294
351
|
}, {
|
|
@@ -306,10 +363,19 @@ if (clients.radarr) {
|
|
|
306
363
|
},
|
|
307
364
|
}, {
|
|
308
365
|
name: "radarr_get_queue",
|
|
309
|
-
description: "Get Radarr download queue",
|
|
366
|
+
description: "Get Radarr download queue. Supports pagination with limit and offset.",
|
|
310
367
|
inputSchema: {
|
|
311
368
|
type: "object",
|
|
312
|
-
properties: {
|
|
369
|
+
properties: {
|
|
370
|
+
limit: {
|
|
371
|
+
type: "number",
|
|
372
|
+
description: "Maximum number of queue items to return (default: 25, max: 100)",
|
|
373
|
+
},
|
|
374
|
+
offset: {
|
|
375
|
+
type: "number",
|
|
376
|
+
description: "Number of queue items to skip before returning results (default: 0)",
|
|
377
|
+
},
|
|
378
|
+
},
|
|
313
379
|
required: [],
|
|
314
380
|
},
|
|
315
381
|
}, {
|
|
@@ -338,6 +404,19 @@ if (clients.radarr) {
|
|
|
338
404
|
},
|
|
339
405
|
required: ["movieId"],
|
|
340
406
|
},
|
|
407
|
+
}, {
|
|
408
|
+
name: "radarr_refresh_movie",
|
|
409
|
+
description: "Trigger a metadata refresh for a specific movie in Radarr",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
movieId: {
|
|
414
|
+
type: "number",
|
|
415
|
+
description: "Movie ID to refresh",
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
required: ["movieId"],
|
|
419
|
+
},
|
|
341
420
|
}, {
|
|
342
421
|
name: "radarr_add_movie",
|
|
343
422
|
description: "Add a movie to Radarr. Use radarr_search first to find the tmdbId, and radarr_get_root_folders / radarr_get_quality_profiles to get valid values. Use radarr_get_tags to get valid tag IDs.",
|
|
@@ -377,22 +456,6 @@ if (clients.radarr) {
|
|
|
377
456
|
},
|
|
378
457
|
required: ["tmdbId", "title", "qualityProfileId", "rootFolderPath"],
|
|
379
458
|
},
|
|
380
|
-
}, {
|
|
381
|
-
name: "radarr_get_root_folders",
|
|
382
|
-
description: "Get available root folders for Radarr. Use this to find valid rootFolderPath values when adding a movie.",
|
|
383
|
-
inputSchema: {
|
|
384
|
-
type: "object",
|
|
385
|
-
properties: {},
|
|
386
|
-
required: [],
|
|
387
|
-
},
|
|
388
|
-
}, {
|
|
389
|
-
name: "radarr_get_quality_profiles",
|
|
390
|
-
description: "Get available quality profiles for Radarr. Use this to find valid qualityProfileId values when adding a movie.",
|
|
391
|
-
inputSchema: {
|
|
392
|
-
type: "object",
|
|
393
|
-
properties: {},
|
|
394
|
-
required: [],
|
|
395
|
-
},
|
|
396
459
|
});
|
|
397
460
|
}
|
|
398
461
|
// Lidarr tools
|
|
@@ -420,10 +483,19 @@ if (clients.lidarr) {
|
|
|
420
483
|
},
|
|
421
484
|
}, {
|
|
422
485
|
name: "lidarr_get_queue",
|
|
423
|
-
description: "Get Lidarr download queue",
|
|
486
|
+
description: "Get Lidarr download queue. Supports pagination with limit and offset.",
|
|
424
487
|
inputSchema: {
|
|
425
488
|
type: "object",
|
|
426
|
-
properties: {
|
|
489
|
+
properties: {
|
|
490
|
+
limit: {
|
|
491
|
+
type: "number",
|
|
492
|
+
description: "Maximum number of queue items to return (default: 25, max: 100)",
|
|
493
|
+
},
|
|
494
|
+
offset: {
|
|
495
|
+
type: "number",
|
|
496
|
+
description: "Number of queue items to skip before returning results (default: 0)",
|
|
497
|
+
},
|
|
498
|
+
},
|
|
427
499
|
required: [],
|
|
428
500
|
},
|
|
429
501
|
}, {
|
|
@@ -542,154 +614,6 @@ if (clients.lidarr) {
|
|
|
542
614
|
},
|
|
543
615
|
});
|
|
544
616
|
}
|
|
545
|
-
// Readarr tools
|
|
546
|
-
if (clients.readarr) {
|
|
547
|
-
TOOLS.push({
|
|
548
|
-
name: "readarr_get_authors",
|
|
549
|
-
description: "Get all authors in Readarr library",
|
|
550
|
-
inputSchema: {
|
|
551
|
-
type: "object",
|
|
552
|
-
properties: {},
|
|
553
|
-
required: [],
|
|
554
|
-
},
|
|
555
|
-
}, {
|
|
556
|
-
name: "readarr_search",
|
|
557
|
-
description: "Search for authors by name. Returns results with foreignAuthorId needed for readarr_add_author.",
|
|
558
|
-
inputSchema: {
|
|
559
|
-
type: "object",
|
|
560
|
-
properties: {
|
|
561
|
-
term: {
|
|
562
|
-
type: "string",
|
|
563
|
-
description: "Search term (author name)",
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
required: ["term"],
|
|
567
|
-
},
|
|
568
|
-
}, {
|
|
569
|
-
name: "readarr_get_queue",
|
|
570
|
-
description: "Get Readarr download queue",
|
|
571
|
-
inputSchema: {
|
|
572
|
-
type: "object",
|
|
573
|
-
properties: {},
|
|
574
|
-
required: [],
|
|
575
|
-
},
|
|
576
|
-
}, {
|
|
577
|
-
name: "readarr_get_books",
|
|
578
|
-
description: "Get books for an author in Readarr. Shows which books are available and which are missing.",
|
|
579
|
-
inputSchema: {
|
|
580
|
-
type: "object",
|
|
581
|
-
properties: {
|
|
582
|
-
authorId: {
|
|
583
|
-
type: "number",
|
|
584
|
-
description: "Author ID to get books for",
|
|
585
|
-
},
|
|
586
|
-
},
|
|
587
|
-
required: ["authorId"],
|
|
588
|
-
},
|
|
589
|
-
}, {
|
|
590
|
-
name: "readarr_search_book",
|
|
591
|
-
description: "Trigger a search for a specific book to download",
|
|
592
|
-
inputSchema: {
|
|
593
|
-
type: "object",
|
|
594
|
-
properties: {
|
|
595
|
-
bookIds: {
|
|
596
|
-
type: "array",
|
|
597
|
-
items: { type: "number" },
|
|
598
|
-
description: "Book ID(s) to search for",
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
required: ["bookIds"],
|
|
602
|
-
},
|
|
603
|
-
}, {
|
|
604
|
-
name: "readarr_search_missing",
|
|
605
|
-
description: "Trigger a search for all missing books for an author",
|
|
606
|
-
inputSchema: {
|
|
607
|
-
type: "object",
|
|
608
|
-
properties: {
|
|
609
|
-
authorId: {
|
|
610
|
-
type: "number",
|
|
611
|
-
description: "Author ID to search missing books for",
|
|
612
|
-
},
|
|
613
|
-
},
|
|
614
|
-
required: ["authorId"],
|
|
615
|
-
},
|
|
616
|
-
}, {
|
|
617
|
-
name: "readarr_get_calendar",
|
|
618
|
-
description: "Get upcoming book releases from Readarr",
|
|
619
|
-
inputSchema: {
|
|
620
|
-
type: "object",
|
|
621
|
-
properties: {
|
|
622
|
-
days: {
|
|
623
|
-
type: "number",
|
|
624
|
-
description: "Number of days to look ahead (default: 30)",
|
|
625
|
-
},
|
|
626
|
-
},
|
|
627
|
-
required: [],
|
|
628
|
-
},
|
|
629
|
-
}, {
|
|
630
|
-
name: "readarr_add_author",
|
|
631
|
-
description: "Add an author to Readarr. Use readarr_search first to find the foreignAuthorId, and readarr_get_root_folders / readarr_get_quality_profiles / readarr_get_metadata_profiles to get valid values. Use readarr_get_tags to get valid tag IDs.",
|
|
632
|
-
inputSchema: {
|
|
633
|
-
type: "object",
|
|
634
|
-
properties: {
|
|
635
|
-
foreignAuthorId: {
|
|
636
|
-
type: "string",
|
|
637
|
-
description: "Foreign author ID from readarr_search results",
|
|
638
|
-
},
|
|
639
|
-
authorName: {
|
|
640
|
-
type: "string",
|
|
641
|
-
description: "Author name",
|
|
642
|
-
},
|
|
643
|
-
qualityProfileId: {
|
|
644
|
-
type: "number",
|
|
645
|
-
description: "Quality profile ID from readarr_get_quality_profiles",
|
|
646
|
-
},
|
|
647
|
-
metadataProfileId: {
|
|
648
|
-
type: "number",
|
|
649
|
-
description: "Metadata profile ID from readarr_get_metadata_profiles",
|
|
650
|
-
},
|
|
651
|
-
rootFolderPath: {
|
|
652
|
-
type: "string",
|
|
653
|
-
description: "Root folder path from readarr_get_root_folders",
|
|
654
|
-
},
|
|
655
|
-
monitored: {
|
|
656
|
-
type: "boolean",
|
|
657
|
-
description: "Whether to monitor the author (default: true)",
|
|
658
|
-
},
|
|
659
|
-
tags: {
|
|
660
|
-
type: "array",
|
|
661
|
-
items: { type: "number" },
|
|
662
|
-
description: "Array of tag IDs from readarr_get_tags (optional)",
|
|
663
|
-
},
|
|
664
|
-
},
|
|
665
|
-
required: ["foreignAuthorId", "authorName", "qualityProfileId", "metadataProfileId", "rootFolderPath"],
|
|
666
|
-
},
|
|
667
|
-
}, {
|
|
668
|
-
name: "readarr_get_root_folders",
|
|
669
|
-
description: "Get available root folders for Readarr. Use this to find valid rootFolderPath values when adding an author.",
|
|
670
|
-
inputSchema: {
|
|
671
|
-
type: "object",
|
|
672
|
-
properties: {},
|
|
673
|
-
required: [],
|
|
674
|
-
},
|
|
675
|
-
}, {
|
|
676
|
-
name: "readarr_get_quality_profiles",
|
|
677
|
-
description: "Get available quality profiles for Readarr. Use this to find valid qualityProfileId values when adding an author.",
|
|
678
|
-
inputSchema: {
|
|
679
|
-
type: "object",
|
|
680
|
-
properties: {},
|
|
681
|
-
required: [],
|
|
682
|
-
},
|
|
683
|
-
}, {
|
|
684
|
-
name: "readarr_get_metadata_profiles",
|
|
685
|
-
description: "Get available metadata profiles for Readarr. Use this to find valid metadataProfileId values when adding an author.",
|
|
686
|
-
inputSchema: {
|
|
687
|
-
type: "object",
|
|
688
|
-
properties: {},
|
|
689
|
-
required: [],
|
|
690
|
-
},
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
617
|
// Prowlarr tools
|
|
694
618
|
if (clients.prowlarr) {
|
|
695
619
|
TOOLS.push({
|
|
@@ -879,12 +803,188 @@ TOOLS.push({
|
|
|
879
803
|
// Create server instance
|
|
880
804
|
const server = new Server({
|
|
881
805
|
name: "mcp-arr",
|
|
882
|
-
version:
|
|
806
|
+
version: SERVER_VERSION,
|
|
883
807
|
}, {
|
|
884
808
|
capabilities: {
|
|
885
809
|
tools: {},
|
|
886
810
|
},
|
|
887
811
|
});
|
|
812
|
+
function buildResourceUrl(path) {
|
|
813
|
+
return `mcp-arr://${path}`;
|
|
814
|
+
}
|
|
815
|
+
function jsonText(data) {
|
|
816
|
+
return {
|
|
817
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function textError(message) {
|
|
821
|
+
return {
|
|
822
|
+
content: [{ type: "text", text: message }],
|
|
823
|
+
isError: true,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
async function runUnifiedSearch(query) {
|
|
827
|
+
const results = [];
|
|
828
|
+
const trimmedQuery = query.trim();
|
|
829
|
+
if (trimmedQuery.length === 0) {
|
|
830
|
+
return results;
|
|
831
|
+
}
|
|
832
|
+
const lowerQuery = trimmedQuery.toLowerCase();
|
|
833
|
+
for (const service of ["radarr", "sonarr"]) {
|
|
834
|
+
const profiles = await trashClient.listProfiles(service);
|
|
835
|
+
results.push(...profiles
|
|
836
|
+
.filter((profile) => profile.name.toLowerCase().includes(lowerQuery) ||
|
|
837
|
+
profile.description?.toLowerCase().includes(lowerQuery))
|
|
838
|
+
.slice(0, 8)
|
|
839
|
+
.map((profile) => ({
|
|
840
|
+
id: `trash-profile:${service}:${profile.name}`,
|
|
841
|
+
title: `${profile.name} (${service})`,
|
|
842
|
+
url: buildResourceUrl(`trash/profile/${service}/${encodeURIComponent(profile.name)}`),
|
|
843
|
+
type: "trash_profile",
|
|
844
|
+
service,
|
|
845
|
+
summary: profile.description?.replace(/<br>/g, " "),
|
|
846
|
+
})));
|
|
847
|
+
}
|
|
848
|
+
if (clients.sonarr) {
|
|
849
|
+
const series = await clients.sonarr.searchSeries(trimmedQuery);
|
|
850
|
+
results.push(...series.slice(0, 5).map((item) => ({
|
|
851
|
+
id: `arr:sonarr:series:${item.tvdbId}`,
|
|
852
|
+
title: `${item.title}${item.year ? ` (${item.year})` : ""}`,
|
|
853
|
+
url: buildResourceUrl(`arr/sonarr/series/${item.tvdbId}`),
|
|
854
|
+
type: "series",
|
|
855
|
+
service: "sonarr",
|
|
856
|
+
summary: item.overview?.slice(0, 220),
|
|
857
|
+
})));
|
|
858
|
+
}
|
|
859
|
+
if (clients.radarr) {
|
|
860
|
+
const movies = await clients.radarr.searchMovies(trimmedQuery);
|
|
861
|
+
results.push(...movies.slice(0, 5).map((item) => ({
|
|
862
|
+
id: `arr:radarr:movie:${item.tmdbId}`,
|
|
863
|
+
title: `${item.title}${item.year ? ` (${item.year})` : ""}`,
|
|
864
|
+
url: buildResourceUrl(`arr/radarr/movie/${item.tmdbId}`),
|
|
865
|
+
type: "movie",
|
|
866
|
+
service: "radarr",
|
|
867
|
+
summary: item.overview?.slice(0, 220),
|
|
868
|
+
})));
|
|
869
|
+
}
|
|
870
|
+
if (clients.lidarr) {
|
|
871
|
+
const artists = await clients.lidarr.searchArtists(trimmedQuery);
|
|
872
|
+
results.push(...artists.slice(0, 5).map((item) => ({
|
|
873
|
+
id: `arr:lidarr:artist:${item.foreignArtistId}`,
|
|
874
|
+
title: item.artistName || item.title,
|
|
875
|
+
url: buildResourceUrl(`arr/lidarr/artist/${item.foreignArtistId}`),
|
|
876
|
+
type: "artist",
|
|
877
|
+
service: "lidarr",
|
|
878
|
+
summary: item.overview?.slice(0, 220),
|
|
879
|
+
})));
|
|
880
|
+
}
|
|
881
|
+
return results;
|
|
882
|
+
}
|
|
883
|
+
async function fetchSearchEntry(id) {
|
|
884
|
+
const [kind, service, subtype, rawId] = id.split(":");
|
|
885
|
+
if (kind === "trash-profile" && (service === "radarr" || service === "sonarr")) {
|
|
886
|
+
const profile = await trashClient.getProfile(service, rawId);
|
|
887
|
+
if (!profile) {
|
|
888
|
+
throw new Error(`TRaSH profile '${rawId}' not found for ${service}`);
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
id,
|
|
892
|
+
title: `${profile.name} (${service})`,
|
|
893
|
+
url: buildResourceUrl(`trash/profile/${service}/${encodeURIComponent(profile.name)}`),
|
|
894
|
+
service,
|
|
895
|
+
type: "trash_profile",
|
|
896
|
+
data: {
|
|
897
|
+
name: profile.name,
|
|
898
|
+
description: profile.trash_description?.replace(/<br>/g, "\n"),
|
|
899
|
+
upgradeAllowed: profile.upgradeAllowed,
|
|
900
|
+
cutoff: profile.cutoff,
|
|
901
|
+
minFormatScore: profile.minFormatScore,
|
|
902
|
+
cutoffFormatScore: profile.cutoffFormatScore,
|
|
903
|
+
language: profile.language,
|
|
904
|
+
qualities: profile.items,
|
|
905
|
+
customFormats: Object.entries(profile.formatItems || {}).map(([name, trashId]) => ({
|
|
906
|
+
name,
|
|
907
|
+
trash_id: trashId,
|
|
908
|
+
})),
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (kind !== "arr") {
|
|
913
|
+
throw new Error(`Unsupported fetch id '${id}'`);
|
|
914
|
+
}
|
|
915
|
+
if (service === "sonarr" && subtype === "series" && clients.sonarr) {
|
|
916
|
+
const tvdbId = Number(rawId);
|
|
917
|
+
const matches = (await clients.sonarr.searchSeries(rawId)).filter((item) => item.tvdbId === tvdbId);
|
|
918
|
+
return {
|
|
919
|
+
id,
|
|
920
|
+
title: matches[0]?.title || rawId,
|
|
921
|
+
url: buildResourceUrl(`arr/sonarr/series/${rawId}`),
|
|
922
|
+
service,
|
|
923
|
+
type: subtype,
|
|
924
|
+
data: matches.slice(0, 10),
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
if (service === "radarr" && subtype === "movie" && clients.radarr) {
|
|
928
|
+
const tmdbId = Number(rawId);
|
|
929
|
+
const matches = (await clients.radarr.searchMovies(rawId)).filter((item) => item.tmdbId === tmdbId);
|
|
930
|
+
return {
|
|
931
|
+
id,
|
|
932
|
+
title: matches[0]?.title || rawId,
|
|
933
|
+
url: buildResourceUrl(`arr/radarr/movie/${rawId}`),
|
|
934
|
+
service,
|
|
935
|
+
type: subtype,
|
|
936
|
+
data: matches.slice(0, 10),
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
if (service === "lidarr" && subtype === "artist" && clients.lidarr) {
|
|
940
|
+
const matches = (await clients.lidarr.searchArtists(rawId)).filter((item) => item.foreignArtistId === rawId);
|
|
941
|
+
return {
|
|
942
|
+
id,
|
|
943
|
+
title: matches[0]?.artistName || matches[0]?.title || rawId,
|
|
944
|
+
url: buildResourceUrl(`arr/lidarr/artist/${rawId}`),
|
|
945
|
+
service,
|
|
946
|
+
type: subtype,
|
|
947
|
+
data: matches.slice(0, 10),
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
throw new Error(`Unsupported or unavailable fetch target '${id}'`);
|
|
951
|
+
}
|
|
952
|
+
async function getPaginatedQueue(client, args) {
|
|
953
|
+
const limit = Math.min(Math.max(Math.floor(args?.limit ?? 25), 1), 100);
|
|
954
|
+
const offset = Math.max(Math.floor(args?.offset ?? 0), 0);
|
|
955
|
+
const pageSize = 100;
|
|
956
|
+
const records = [];
|
|
957
|
+
let totalRecords = 0;
|
|
958
|
+
let page = 1;
|
|
959
|
+
while (true) {
|
|
960
|
+
const queuePage = await client.getQueue(page, pageSize);
|
|
961
|
+
totalRecords = queuePage.totalRecords;
|
|
962
|
+
records.push(...queuePage.records);
|
|
963
|
+
if (records.length >= totalRecords || queuePage.records.length === 0) {
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
page += 1;
|
|
967
|
+
}
|
|
968
|
+
const items = records.slice(offset, offset + limit).map((q) => ({
|
|
969
|
+
title: q.title,
|
|
970
|
+
status: q.status,
|
|
971
|
+
progress: q.size > 0 ? ((1 - q.sizeleft / q.size) * 100).toFixed(1) + "%" : "unknown",
|
|
972
|
+
timeLeft: q.timeleft,
|
|
973
|
+
downloadClient: q.downloadClient,
|
|
974
|
+
protocol: q.protocol,
|
|
975
|
+
trackedDownloadStatus: q.trackedDownloadStatus,
|
|
976
|
+
trackedDownloadState: q.trackedDownloadState,
|
|
977
|
+
}));
|
|
978
|
+
return {
|
|
979
|
+
total: totalRecords,
|
|
980
|
+
returned: items.length,
|
|
981
|
+
offset,
|
|
982
|
+
limit,
|
|
983
|
+
hasMore: offset + items.length < totalRecords,
|
|
984
|
+
nextOffset: offset + items.length < totalRecords ? offset + items.length : null,
|
|
985
|
+
items,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
888
988
|
// Handle list tools request
|
|
889
989
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
890
990
|
return { tools: TOOLS };
|
|
@@ -894,6 +994,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
894
994
|
const { name, arguments: args } = request.params;
|
|
895
995
|
try {
|
|
896
996
|
switch (name) {
|
|
997
|
+
case "search": {
|
|
998
|
+
const query = args.query;
|
|
999
|
+
const results = await runUnifiedSearch(query);
|
|
1000
|
+
return jsonText({ results });
|
|
1001
|
+
}
|
|
1002
|
+
case "fetch": {
|
|
1003
|
+
const id = args.id;
|
|
1004
|
+
const result = await fetchSearchEntry(id);
|
|
1005
|
+
return jsonText(result);
|
|
1006
|
+
}
|
|
897
1007
|
case "arr_status": {
|
|
898
1008
|
const statuses = {};
|
|
899
1009
|
for (const service of configuredServices) {
|
|
@@ -923,16 +1033,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
923
1033
|
statuses[service.name] = { configured: false };
|
|
924
1034
|
}
|
|
925
1035
|
}
|
|
926
|
-
return
|
|
927
|
-
content: [{ type: "text", text: JSON.stringify(statuses, null, 2) }],
|
|
928
|
-
};
|
|
1036
|
+
return jsonText(statuses);
|
|
929
1037
|
}
|
|
930
1038
|
// Dynamic config tool handlers
|
|
931
1039
|
// Quality Profiles
|
|
932
1040
|
case "sonarr_get_quality_profiles":
|
|
933
1041
|
case "radarr_get_quality_profiles":
|
|
934
|
-
case "lidarr_get_quality_profiles":
|
|
935
|
-
case "readarr_get_quality_profiles": {
|
|
1042
|
+
case "lidarr_get_quality_profiles": {
|
|
936
1043
|
const serviceName = name.split('_')[0];
|
|
937
1044
|
const client = clients[serviceName];
|
|
938
1045
|
if (!client)
|
|
@@ -966,8 +1073,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
966
1073
|
// Health checks
|
|
967
1074
|
case "sonarr_get_health":
|
|
968
1075
|
case "radarr_get_health":
|
|
969
|
-
case "lidarr_get_health":
|
|
970
|
-
case "readarr_get_health": {
|
|
1076
|
+
case "lidarr_get_health": {
|
|
971
1077
|
const serviceName = name.split('_')[0];
|
|
972
1078
|
const client = clients[serviceName];
|
|
973
1079
|
if (!client)
|
|
@@ -992,8 +1098,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
992
1098
|
// Root folders
|
|
993
1099
|
case "sonarr_get_root_folders":
|
|
994
1100
|
case "radarr_get_root_folders":
|
|
995
|
-
case "lidarr_get_root_folders":
|
|
996
|
-
case "readarr_get_root_folders": {
|
|
1101
|
+
case "lidarr_get_root_folders": {
|
|
997
1102
|
const serviceName = name.split('_')[0];
|
|
998
1103
|
const client = clients[serviceName];
|
|
999
1104
|
if (!client)
|
|
@@ -1019,8 +1124,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1019
1124
|
// Download clients
|
|
1020
1125
|
case "sonarr_get_download_clients":
|
|
1021
1126
|
case "radarr_get_download_clients":
|
|
1022
|
-
case "lidarr_get_download_clients":
|
|
1023
|
-
case "readarr_get_download_clients": {
|
|
1127
|
+
case "lidarr_get_download_clients": {
|
|
1024
1128
|
const serviceName = name.split('_')[0];
|
|
1025
1129
|
const client = clients[serviceName];
|
|
1026
1130
|
if (!client)
|
|
@@ -1049,8 +1153,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1049
1153
|
// Naming config
|
|
1050
1154
|
case "sonarr_get_naming":
|
|
1051
1155
|
case "radarr_get_naming":
|
|
1052
|
-
case "lidarr_get_naming":
|
|
1053
|
-
case "readarr_get_naming": {
|
|
1156
|
+
case "lidarr_get_naming": {
|
|
1054
1157
|
const serviceName = name.split('_')[0];
|
|
1055
1158
|
const client = clients[serviceName];
|
|
1056
1159
|
if (!client)
|
|
@@ -1066,8 +1169,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1066
1169
|
// Tags
|
|
1067
1170
|
case "sonarr_get_tags":
|
|
1068
1171
|
case "radarr_get_tags":
|
|
1069
|
-
case "lidarr_get_tags":
|
|
1070
|
-
case "readarr_get_tags": {
|
|
1172
|
+
case "lidarr_get_tags": {
|
|
1071
1173
|
const serviceName = name.split('_')[0];
|
|
1072
1174
|
const client = clients[serviceName];
|
|
1073
1175
|
if (!client)
|
|
@@ -1086,8 +1188,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1086
1188
|
// Comprehensive setup review
|
|
1087
1189
|
case "sonarr_review_setup":
|
|
1088
1190
|
case "radarr_review_setup":
|
|
1089
|
-
case "lidarr_review_setup":
|
|
1090
|
-
case "readarr_review_setup": {
|
|
1191
|
+
case "lidarr_review_setup": {
|
|
1091
1192
|
const serviceName = name.split('_')[0];
|
|
1092
1193
|
const client = clients[serviceName];
|
|
1093
1194
|
if (!client)
|
|
@@ -1105,14 +1206,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1105
1206
|
client.getTags(),
|
|
1106
1207
|
client.getIndexers(),
|
|
1107
1208
|
]);
|
|
1108
|
-
// For Lidarr
|
|
1209
|
+
// For Lidarr, also get metadata profiles
|
|
1109
1210
|
let metadataProfiles = null;
|
|
1110
1211
|
if (serviceName === 'lidarr' && clients.lidarr) {
|
|
1111
1212
|
metadataProfiles = await clients.lidarr.getMetadataProfiles();
|
|
1112
1213
|
}
|
|
1113
|
-
else if (serviceName === 'readarr' && clients.readarr) {
|
|
1114
|
-
metadataProfiles = await clients.readarr.getMetadataProfiles();
|
|
1115
|
-
}
|
|
1116
1214
|
const review = {
|
|
1117
1215
|
service: serviceName,
|
|
1118
1216
|
version: status.version,
|
|
@@ -1191,13 +1289,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1191
1289
|
case "sonarr_get_series": {
|
|
1192
1290
|
if (!clients.sonarr)
|
|
1193
1291
|
throw new Error("Sonarr not configured");
|
|
1194
|
-
const
|
|
1292
|
+
const { limit = 25, offset = 0, search } = args;
|
|
1293
|
+
const normalizedLimit = Math.max(1, Math.min(limit, 100));
|
|
1294
|
+
const normalizedOffset = Math.max(0, offset);
|
|
1295
|
+
const filter = search?.trim().toLowerCase();
|
|
1296
|
+
const allSeries = await clients.sonarr.getSeries();
|
|
1297
|
+
const filteredSeries = filter
|
|
1298
|
+
? allSeries.filter(s => s.title.toLowerCase().includes(filter))
|
|
1299
|
+
: allSeries;
|
|
1300
|
+
const pagedSeries = filteredSeries.slice(normalizedOffset, normalizedOffset + normalizedLimit);
|
|
1195
1301
|
return {
|
|
1196
1302
|
content: [{
|
|
1197
1303
|
type: "text",
|
|
1198
1304
|
text: JSON.stringify({
|
|
1199
|
-
|
|
1200
|
-
|
|
1305
|
+
total: allSeries.length,
|
|
1306
|
+
filteredCount: filteredSeries.length,
|
|
1307
|
+
returned: pagedSeries.length,
|
|
1308
|
+
offset: normalizedOffset,
|
|
1309
|
+
limit: normalizedLimit,
|
|
1310
|
+
hasMore: normalizedOffset + normalizedLimit < filteredSeries.length,
|
|
1311
|
+
nextOffset: normalizedOffset + normalizedLimit < filteredSeries.length
|
|
1312
|
+
? normalizedOffset + normalizedLimit
|
|
1313
|
+
: null,
|
|
1314
|
+
search: search ?? null,
|
|
1315
|
+
series: pagedSeries.map(s => ({
|
|
1201
1316
|
id: s.id,
|
|
1202
1317
|
title: s.title,
|
|
1203
1318
|
year: s.year,
|
|
@@ -1235,22 +1350,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1235
1350
|
case "sonarr_get_queue": {
|
|
1236
1351
|
if (!clients.sonarr)
|
|
1237
1352
|
throw new Error("Sonarr not configured");
|
|
1238
|
-
|
|
1239
|
-
return {
|
|
1240
|
-
content: [{
|
|
1241
|
-
type: "text",
|
|
1242
|
-
text: JSON.stringify({
|
|
1243
|
-
totalRecords: queue.totalRecords,
|
|
1244
|
-
items: queue.records.map(q => ({
|
|
1245
|
-
title: q.title,
|
|
1246
|
-
status: q.status,
|
|
1247
|
-
progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%',
|
|
1248
|
-
timeLeft: q.timeleft,
|
|
1249
|
-
downloadClient: q.downloadClient,
|
|
1250
|
-
})),
|
|
1251
|
-
}, null, 2),
|
|
1252
|
-
}],
|
|
1253
|
-
};
|
|
1353
|
+
return jsonText(await getPaginatedQueue(clients.sonarr, args));
|
|
1254
1354
|
}
|
|
1255
1355
|
case "sonarr_get_calendar": {
|
|
1256
1356
|
if (!clients.sonarr)
|
|
@@ -1318,6 +1418,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1318
1418
|
}],
|
|
1319
1419
|
};
|
|
1320
1420
|
}
|
|
1421
|
+
case "sonarr_refresh_series": {
|
|
1422
|
+
if (!clients.sonarr)
|
|
1423
|
+
throw new Error("Sonarr not configured");
|
|
1424
|
+
const seriesId = args.seriesId;
|
|
1425
|
+
const series = await clients.sonarr.getSeriesById(seriesId);
|
|
1426
|
+
const result = await clients.sonarr.refreshSeries(seriesId);
|
|
1427
|
+
return {
|
|
1428
|
+
content: [{
|
|
1429
|
+
type: "text",
|
|
1430
|
+
text: JSON.stringify({
|
|
1431
|
+
success: true,
|
|
1432
|
+
message: `Refresh triggered for series`,
|
|
1433
|
+
series: {
|
|
1434
|
+
id: series.id,
|
|
1435
|
+
title: series.title,
|
|
1436
|
+
year: series.year,
|
|
1437
|
+
},
|
|
1438
|
+
commandId: result.id,
|
|
1439
|
+
}, null, 2),
|
|
1440
|
+
}],
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1321
1443
|
case "sonarr_add_series": {
|
|
1322
1444
|
if (!clients.sonarr)
|
|
1323
1445
|
throw new Error("Sonarr not configured");
|
|
@@ -1338,39 +1460,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1338
1460
|
}],
|
|
1339
1461
|
};
|
|
1340
1462
|
}
|
|
1341
|
-
case "sonarr_get_root_folders": {
|
|
1342
|
-
if (!clients.sonarr)
|
|
1343
|
-
throw new Error("Sonarr not configured");
|
|
1344
|
-
const folders = await clients.sonarr.getRootFolders();
|
|
1345
|
-
return {
|
|
1346
|
-
content: [{
|
|
1347
|
-
type: "text",
|
|
1348
|
-
text: JSON.stringify(folders, null, 2),
|
|
1349
|
-
}],
|
|
1350
|
-
};
|
|
1351
|
-
}
|
|
1352
|
-
case "sonarr_get_quality_profiles": {
|
|
1353
|
-
if (!clients.sonarr)
|
|
1354
|
-
throw new Error("Sonarr not configured");
|
|
1355
|
-
const profiles = await clients.sonarr.getQualityProfiles();
|
|
1356
|
-
return {
|
|
1357
|
-
content: [{
|
|
1358
|
-
type: "text",
|
|
1359
|
-
text: JSON.stringify(profiles.map(p => ({ id: p.id, name: p.name })), null, 2),
|
|
1360
|
-
}],
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
1463
|
// Radarr handlers
|
|
1364
1464
|
case "radarr_get_movies": {
|
|
1365
1465
|
if (!clients.radarr)
|
|
1366
1466
|
throw new Error("Radarr not configured");
|
|
1367
|
-
const
|
|
1467
|
+
const { limit = 25, offset = 0, search } = args;
|
|
1468
|
+
const normalizedLimit = Math.max(1, Math.min(limit, 100));
|
|
1469
|
+
const normalizedOffset = Math.max(0, offset);
|
|
1470
|
+
const filter = search?.trim().toLowerCase();
|
|
1471
|
+
const allMovies = await clients.radarr.getMovies();
|
|
1472
|
+
const filteredMovies = filter
|
|
1473
|
+
? allMovies.filter(m => m.title.toLowerCase().includes(filter))
|
|
1474
|
+
: allMovies;
|
|
1475
|
+
const pagedMovies = filteredMovies.slice(normalizedOffset, normalizedOffset + normalizedLimit);
|
|
1368
1476
|
return {
|
|
1369
1477
|
content: [{
|
|
1370
1478
|
type: "text",
|
|
1371
1479
|
text: JSON.stringify({
|
|
1372
|
-
|
|
1373
|
-
|
|
1480
|
+
total: allMovies.length,
|
|
1481
|
+
filteredCount: filteredMovies.length,
|
|
1482
|
+
returned: pagedMovies.length,
|
|
1483
|
+
offset: normalizedOffset,
|
|
1484
|
+
limit: normalizedLimit,
|
|
1485
|
+
hasMore: normalizedOffset + normalizedLimit < filteredMovies.length,
|
|
1486
|
+
nextOffset: normalizedOffset + normalizedLimit < filteredMovies.length
|
|
1487
|
+
? normalizedOffset + normalizedLimit
|
|
1488
|
+
: null,
|
|
1489
|
+
search: search ?? null,
|
|
1490
|
+
movies: pagedMovies.map(m => ({
|
|
1374
1491
|
id: m.id,
|
|
1375
1492
|
title: m.title,
|
|
1376
1493
|
year: m.year,
|
|
@@ -1408,22 +1525,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1408
1525
|
case "radarr_get_queue": {
|
|
1409
1526
|
if (!clients.radarr)
|
|
1410
1527
|
throw new Error("Radarr not configured");
|
|
1411
|
-
|
|
1412
|
-
return {
|
|
1413
|
-
content: [{
|
|
1414
|
-
type: "text",
|
|
1415
|
-
text: JSON.stringify({
|
|
1416
|
-
totalRecords: queue.totalRecords,
|
|
1417
|
-
items: queue.records.map(q => ({
|
|
1418
|
-
title: q.title,
|
|
1419
|
-
status: q.status,
|
|
1420
|
-
progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%',
|
|
1421
|
-
timeLeft: q.timeleft,
|
|
1422
|
-
downloadClient: q.downloadClient,
|
|
1423
|
-
})),
|
|
1424
|
-
}, null, 2),
|
|
1425
|
-
}],
|
|
1426
|
-
};
|
|
1528
|
+
return jsonText(await getPaginatedQueue(clients.radarr, args));
|
|
1427
1529
|
}
|
|
1428
1530
|
case "radarr_get_calendar": {
|
|
1429
1531
|
if (!clients.radarr)
|
|
@@ -1452,6 +1554,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1452
1554
|
}],
|
|
1453
1555
|
};
|
|
1454
1556
|
}
|
|
1557
|
+
case "radarr_refresh_movie": {
|
|
1558
|
+
if (!clients.radarr)
|
|
1559
|
+
throw new Error("Radarr not configured");
|
|
1560
|
+
const movieId = args.movieId;
|
|
1561
|
+
const movie = await clients.radarr.getMovieById(movieId);
|
|
1562
|
+
const result = await clients.radarr.refreshMovie(movieId);
|
|
1563
|
+
return {
|
|
1564
|
+
content: [{
|
|
1565
|
+
type: "text",
|
|
1566
|
+
text: JSON.stringify({
|
|
1567
|
+
success: true,
|
|
1568
|
+
message: `Refresh triggered for movie`,
|
|
1569
|
+
movie: {
|
|
1570
|
+
id: movie.id,
|
|
1571
|
+
title: movie.title,
|
|
1572
|
+
year: movie.year,
|
|
1573
|
+
},
|
|
1574
|
+
commandId: result.id,
|
|
1575
|
+
}, null, 2),
|
|
1576
|
+
}],
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1455
1579
|
case "radarr_add_movie": {
|
|
1456
1580
|
if (!clients.radarr)
|
|
1457
1581
|
throw new Error("Radarr not configured");
|
|
@@ -1472,28 +1596,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1472
1596
|
}],
|
|
1473
1597
|
};
|
|
1474
1598
|
}
|
|
1475
|
-
case "radarr_get_root_folders": {
|
|
1476
|
-
if (!clients.radarr)
|
|
1477
|
-
throw new Error("Radarr not configured");
|
|
1478
|
-
const folders = await clients.radarr.getRootFolders();
|
|
1479
|
-
return {
|
|
1480
|
-
content: [{
|
|
1481
|
-
type: "text",
|
|
1482
|
-
text: JSON.stringify(folders, null, 2),
|
|
1483
|
-
}],
|
|
1484
|
-
};
|
|
1485
|
-
}
|
|
1486
|
-
case "radarr_get_quality_profiles": {
|
|
1487
|
-
if (!clients.radarr)
|
|
1488
|
-
throw new Error("Radarr not configured");
|
|
1489
|
-
const profiles = await clients.radarr.getQualityProfiles();
|
|
1490
|
-
return {
|
|
1491
|
-
content: [{
|
|
1492
|
-
type: "text",
|
|
1493
|
-
text: JSON.stringify(profiles.map(p => ({ id: p.id, name: p.name })), null, 2),
|
|
1494
|
-
}],
|
|
1495
|
-
};
|
|
1496
|
-
}
|
|
1497
1599
|
// Lidarr handlers
|
|
1498
1600
|
case "lidarr_get_artists": {
|
|
1499
1601
|
if (!clients.lidarr)
|
|
@@ -1543,22 +1645,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1543
1645
|
case "lidarr_get_queue": {
|
|
1544
1646
|
if (!clients.lidarr)
|
|
1545
1647
|
throw new Error("Lidarr not configured");
|
|
1546
|
-
|
|
1547
|
-
return {
|
|
1548
|
-
content: [{
|
|
1549
|
-
type: "text",
|
|
1550
|
-
text: JSON.stringify({
|
|
1551
|
-
totalRecords: queue.totalRecords,
|
|
1552
|
-
items: queue.records.map(q => ({
|
|
1553
|
-
title: q.title,
|
|
1554
|
-
status: q.status,
|
|
1555
|
-
progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%',
|
|
1556
|
-
timeLeft: q.timeleft,
|
|
1557
|
-
downloadClient: q.downloadClient,
|
|
1558
|
-
})),
|
|
1559
|
-
}, null, 2),
|
|
1560
|
-
}],
|
|
1561
|
-
};
|
|
1648
|
+
return jsonText(await getPaginatedQueue(clients.lidarr, args));
|
|
1562
1649
|
}
|
|
1563
1650
|
case "lidarr_get_albums": {
|
|
1564
1651
|
if (!clients.lidarr)
|
|
@@ -1694,199 +1781,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1694
1781
|
}],
|
|
1695
1782
|
};
|
|
1696
1783
|
}
|
|
1697
|
-
// Readarr handlers
|
|
1698
|
-
case "readarr_get_authors": {
|
|
1699
|
-
if (!clients.readarr)
|
|
1700
|
-
throw new Error("Readarr not configured");
|
|
1701
|
-
const authors = await clients.readarr.getAuthors();
|
|
1702
|
-
return {
|
|
1703
|
-
content: [{
|
|
1704
|
-
type: "text",
|
|
1705
|
-
text: JSON.stringify({
|
|
1706
|
-
count: authors.length,
|
|
1707
|
-
authors: authors.map(a => ({
|
|
1708
|
-
id: a.id,
|
|
1709
|
-
authorName: a.authorName,
|
|
1710
|
-
status: a.status,
|
|
1711
|
-
books: a.statistics?.bookFileCount + '/' + a.statistics?.totalBookCount,
|
|
1712
|
-
sizeOnDisk: formatBytes(a.statistics?.sizeOnDisk || 0),
|
|
1713
|
-
monitored: a.monitored,
|
|
1714
|
-
})),
|
|
1715
|
-
}, null, 2),
|
|
1716
|
-
}],
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
case "readarr_search": {
|
|
1720
|
-
if (!clients.readarr)
|
|
1721
|
-
throw new Error("Readarr not configured");
|
|
1722
|
-
const term = args.term;
|
|
1723
|
-
const results = await clients.readarr.searchAuthors(term);
|
|
1724
|
-
return {
|
|
1725
|
-
content: [{
|
|
1726
|
-
type: "text",
|
|
1727
|
-
text: JSON.stringify({
|
|
1728
|
-
count: results.length,
|
|
1729
|
-
results: results.slice(0, 10).map(r => ({
|
|
1730
|
-
title: r.title,
|
|
1731
|
-
foreignAuthorId: r.foreignAuthorId,
|
|
1732
|
-
overview: r.overview?.substring(0, 200) + (r.overview && r.overview.length > 200 ? '...' : ''),
|
|
1733
|
-
})),
|
|
1734
|
-
}, null, 2),
|
|
1735
|
-
}],
|
|
1736
|
-
};
|
|
1737
|
-
}
|
|
1738
|
-
case "readarr_get_queue": {
|
|
1739
|
-
if (!clients.readarr)
|
|
1740
|
-
throw new Error("Readarr not configured");
|
|
1741
|
-
const queue = await clients.readarr.getQueue();
|
|
1742
|
-
return {
|
|
1743
|
-
content: [{
|
|
1744
|
-
type: "text",
|
|
1745
|
-
text: JSON.stringify({
|
|
1746
|
-
totalRecords: queue.totalRecords,
|
|
1747
|
-
items: queue.records.map(q => ({
|
|
1748
|
-
title: q.title,
|
|
1749
|
-
status: q.status,
|
|
1750
|
-
progress: ((1 - q.sizeleft / q.size) * 100).toFixed(1) + '%',
|
|
1751
|
-
timeLeft: q.timeleft,
|
|
1752
|
-
downloadClient: q.downloadClient,
|
|
1753
|
-
})),
|
|
1754
|
-
}, null, 2),
|
|
1755
|
-
}],
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
case "readarr_get_books": {
|
|
1759
|
-
if (!clients.readarr)
|
|
1760
|
-
throw new Error("Readarr not configured");
|
|
1761
|
-
const authorId = args.authorId;
|
|
1762
|
-
const books = await clients.readarr.getBooks(authorId);
|
|
1763
|
-
return {
|
|
1764
|
-
content: [{
|
|
1765
|
-
type: "text",
|
|
1766
|
-
text: JSON.stringify({
|
|
1767
|
-
count: books.length,
|
|
1768
|
-
books: books.map(b => ({
|
|
1769
|
-
id: b.id,
|
|
1770
|
-
title: b.title,
|
|
1771
|
-
releaseDate: b.releaseDate,
|
|
1772
|
-
pageCount: b.pageCount,
|
|
1773
|
-
monitored: b.monitored,
|
|
1774
|
-
hasFile: b.statistics ? b.statistics.bookFileCount > 0 : false,
|
|
1775
|
-
sizeOnDisk: formatBytes(b.statistics?.sizeOnDisk || 0),
|
|
1776
|
-
grabbed: b.grabbed,
|
|
1777
|
-
})),
|
|
1778
|
-
}, null, 2),
|
|
1779
|
-
}],
|
|
1780
|
-
};
|
|
1781
|
-
}
|
|
1782
|
-
case "readarr_search_book": {
|
|
1783
|
-
if (!clients.readarr)
|
|
1784
|
-
throw new Error("Readarr not configured");
|
|
1785
|
-
const bookIds = args.bookIds;
|
|
1786
|
-
const result = await clients.readarr.searchBook(bookIds);
|
|
1787
|
-
return {
|
|
1788
|
-
content: [{
|
|
1789
|
-
type: "text",
|
|
1790
|
-
text: JSON.stringify({
|
|
1791
|
-
success: true,
|
|
1792
|
-
message: `Search triggered for ${bookIds.length} book(s)`,
|
|
1793
|
-
commandId: result.id,
|
|
1794
|
-
}, null, 2),
|
|
1795
|
-
}],
|
|
1796
|
-
};
|
|
1797
|
-
}
|
|
1798
|
-
case "readarr_search_missing": {
|
|
1799
|
-
if (!clients.readarr)
|
|
1800
|
-
throw new Error("Readarr not configured");
|
|
1801
|
-
const authorId = args.authorId;
|
|
1802
|
-
const result = await clients.readarr.searchMissingBooks(authorId);
|
|
1803
|
-
return {
|
|
1804
|
-
content: [{
|
|
1805
|
-
type: "text",
|
|
1806
|
-
text: JSON.stringify({
|
|
1807
|
-
success: true,
|
|
1808
|
-
message: `Search triggered for missing books`,
|
|
1809
|
-
commandId: result.id,
|
|
1810
|
-
}, null, 2),
|
|
1811
|
-
}],
|
|
1812
|
-
};
|
|
1813
|
-
}
|
|
1814
|
-
case "readarr_get_calendar": {
|
|
1815
|
-
if (!clients.readarr)
|
|
1816
|
-
throw new Error("Readarr not configured");
|
|
1817
|
-
const days = args?.days || 30;
|
|
1818
|
-
const start = new Date().toISOString().split('T')[0];
|
|
1819
|
-
const end = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
1820
|
-
const calendar = await clients.readarr.getCalendar(start, end);
|
|
1821
|
-
return {
|
|
1822
|
-
content: [{
|
|
1823
|
-
type: "text",
|
|
1824
|
-
text: JSON.stringify({
|
|
1825
|
-
count: calendar.length,
|
|
1826
|
-
books: calendar.map(b => ({
|
|
1827
|
-
id: b.id,
|
|
1828
|
-
title: b.title,
|
|
1829
|
-
authorId: b.authorId,
|
|
1830
|
-
releaseDate: b.releaseDate,
|
|
1831
|
-
monitored: b.monitored,
|
|
1832
|
-
})),
|
|
1833
|
-
}, null, 2),
|
|
1834
|
-
}],
|
|
1835
|
-
};
|
|
1836
|
-
}
|
|
1837
|
-
case "readarr_add_author": {
|
|
1838
|
-
if (!clients.readarr)
|
|
1839
|
-
throw new Error("Readarr not configured");
|
|
1840
|
-
const { foreignAuthorId, authorName, qualityProfileId, metadataProfileId, rootFolderPath, monitored, tags } = args;
|
|
1841
|
-
const added = await clients.readarr.addAuthor({
|
|
1842
|
-
foreignAuthorId, authorName, qualityProfileId, metadataProfileId, rootFolderPath, monitored, tags: tags ?? [],
|
|
1843
|
-
});
|
|
1844
|
-
return {
|
|
1845
|
-
content: [{
|
|
1846
|
-
type: "text",
|
|
1847
|
-
text: JSON.stringify({
|
|
1848
|
-
success: true,
|
|
1849
|
-
message: `Added "${added.authorName}" to Readarr`,
|
|
1850
|
-
id: added.id,
|
|
1851
|
-
path: added.path,
|
|
1852
|
-
monitored: added.monitored,
|
|
1853
|
-
}, null, 2),
|
|
1854
|
-
}],
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
case "readarr_get_root_folders": {
|
|
1858
|
-
if (!clients.readarr)
|
|
1859
|
-
throw new Error("Readarr not configured");
|
|
1860
|
-
const folders = await clients.readarr.getRootFolders();
|
|
1861
|
-
return {
|
|
1862
|
-
content: [{
|
|
1863
|
-
type: "text",
|
|
1864
|
-
text: JSON.stringify(folders, null, 2),
|
|
1865
|
-
}],
|
|
1866
|
-
};
|
|
1867
|
-
}
|
|
1868
|
-
case "readarr_get_quality_profiles": {
|
|
1869
|
-
if (!clients.readarr)
|
|
1870
|
-
throw new Error("Readarr not configured");
|
|
1871
|
-
const profiles = await clients.readarr.getQualityProfiles();
|
|
1872
|
-
return {
|
|
1873
|
-
content: [{
|
|
1874
|
-
type: "text",
|
|
1875
|
-
text: JSON.stringify(profiles.map(p => ({ id: p.id, name: p.name })), null, 2),
|
|
1876
|
-
}],
|
|
1877
|
-
};
|
|
1878
|
-
}
|
|
1879
|
-
case "readarr_get_metadata_profiles": {
|
|
1880
|
-
if (!clients.readarr)
|
|
1881
|
-
throw new Error("Readarr not configured");
|
|
1882
|
-
const profiles = await clients.readarr.getMetadataProfiles();
|
|
1883
|
-
return {
|
|
1884
|
-
content: [{
|
|
1885
|
-
type: "text",
|
|
1886
|
-
text: JSON.stringify(profiles.map(p => ({ id: p.id, name: p.name })), null, 2),
|
|
1887
|
-
}],
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
1784
|
// Prowlarr handlers
|
|
1891
1785
|
case "prowlarr_get_indexers": {
|
|
1892
1786
|
if (!clients.prowlarr)
|
|
@@ -2000,15 +1894,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2000
1894
|
results.lidarr = { error: e instanceof Error ? e.message : String(e) };
|
|
2001
1895
|
}
|
|
2002
1896
|
}
|
|
2003
|
-
if (clients.readarr) {
|
|
2004
|
-
try {
|
|
2005
|
-
const readarrResults = await clients.readarr.searchAuthors(term);
|
|
2006
|
-
results.readarr = { count: readarrResults.length, results: readarrResults.slice(0, 5) };
|
|
2007
|
-
}
|
|
2008
|
-
catch (e) {
|
|
2009
|
-
results.readarr = { error: e instanceof Error ? e.message : String(e) };
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
1897
|
return {
|
|
2013
1898
|
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
2014
1899
|
};
|
|
@@ -2339,11 +2224,56 @@ function formatBytes(bytes) {
|
|
|
2339
2224
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
2340
2225
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
2341
2226
|
}
|
|
2227
|
+
async function startHttpServer() {
|
|
2228
|
+
const transport = new StreamableHTTPServerTransport({
|
|
2229
|
+
sessionIdGenerator: undefined,
|
|
2230
|
+
});
|
|
2231
|
+
await server.connect(transport);
|
|
2232
|
+
const httpServer = createServer(async (req, res) => {
|
|
2233
|
+
if (!req.url) {
|
|
2234
|
+
res.statusCode = 400;
|
|
2235
|
+
res.end("Missing URL");
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
const requestUrl = new URL(req.url, `http://${req.headers.host || `${HTTP_HOST}:${HTTP_PORT}`}`);
|
|
2239
|
+
if (requestUrl.pathname === "/health" && req.method === "GET") {
|
|
2240
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2241
|
+
res.end(JSON.stringify({
|
|
2242
|
+
status: "ok",
|
|
2243
|
+
version: SERVER_VERSION,
|
|
2244
|
+
transport: "http",
|
|
2245
|
+
configuredServices: configuredServices.map((service) => service.name),
|
|
2246
|
+
}));
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (requestUrl.pathname !== HTTP_PATH) {
|
|
2250
|
+
res.statusCode = 404;
|
|
2251
|
+
res.end("Not found");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
try {
|
|
2255
|
+
await transport.handleRequest(req, res);
|
|
2256
|
+
}
|
|
2257
|
+
catch (error) {
|
|
2258
|
+
res.statusCode = 500;
|
|
2259
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
await new Promise((resolve, reject) => {
|
|
2263
|
+
httpServer.once("error", reject);
|
|
2264
|
+
httpServer.listen(HTTP_PORT, HTTP_HOST, () => resolve());
|
|
2265
|
+
});
|
|
2266
|
+
console.error(`*arr MCP server running over HTTP at http://${HTTP_HOST}:${HTTP_PORT}${HTTP_PATH}`);
|
|
2267
|
+
}
|
|
2342
2268
|
// Start the server
|
|
2343
2269
|
async function main() {
|
|
2270
|
+
if (TRANSPORT_MODE === "http") {
|
|
2271
|
+
await startHttpServer();
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2344
2274
|
const transport = new StdioServerTransport();
|
|
2345
2275
|
await server.connect(transport);
|
|
2346
|
-
console.error(`*arr MCP server running - configured services: ${configuredServices.map(s => s.name).join(', ')}`);
|
|
2276
|
+
console.error(`*arr MCP server running over stdio - configured services: ${configuredServices.map(s => s.name).join(', ') || 'none (TRaSH-only mode)'}`);
|
|
2347
2277
|
}
|
|
2348
2278
|
main().catch((error) => {
|
|
2349
2279
|
console.error("Fatal error:", error);
|