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/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
- * Readarr (Books), and Prowlarr (Indexers) through Claude Code.
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 { SonarrClient, RadarrClient, LidarrClient, ReadarrClient, ProwlarrClient, } from "./arr-client.js";
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: `Get status of all configured *arr services. Currently configured: ${configuredServices.map(s => s.displayName).join(', ')}`,
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 all TV series in Sonarr library",
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 all movies in Radarr library",
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: "1.0.0",
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/Readarr, also get metadata profiles
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 series = await clients.sonarr.getSeries();
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
- count: series.length,
1200
- series: series.map(s => ({
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
- const queue = await clients.sonarr.getQueue();
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 movies = await clients.radarr.getMovies();
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
- count: movies.length,
1373
- movies: movies.map(m => ({
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
- const queue = await clients.radarr.getQueue();
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
- const queue = await clients.lidarr.getQueue();
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);