From d48b91f5549d0d0358133c8989935ed3c909b1b1 Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Thu, 12 Feb 2026 18:44:22 +0100 Subject: [PATCH 01/11] feat(mcp): add season filtering and presentation guide for better UX --- scripts/test-season-filtering.ts | 248 ++++++++++++++ src/formatters/content.ts | 41 ++- src/index.ts | 2 + src/prompts.ts | 68 +++- src/resources/presentation-guide.ts | 137 ++++++++ src/tools/search-content.ts | 8 +- tests/formatters/content.test.ts | 372 ++++++++++++++++++++- tests/prompts.test.ts | 5 +- tests/resources/presentation-guide.test.ts | 78 +++++ 9 files changed, 941 insertions(+), 18 deletions(-) create mode 100755 scripts/test-season-filtering.ts create mode 100644 src/resources/presentation-guide.ts create mode 100644 tests/resources/presentation-guide.test.ts diff --git a/scripts/test-season-filtering.ts b/scripts/test-season-filtering.ts new file mode 100755 index 0000000..43a644c --- /dev/null +++ b/scripts/test-season-filtering.ts @@ -0,0 +1,248 @@ +#!/usr/bin/env tsx +/** + * Script de prueba para verificar el filtrado por temporada/episodio + * + * Uso: + * npx tsx scripts/test-season-filtering.ts + */ + +import { formatSearchResults } from "../src/formatters/content.js"; +import type { SearchResponse, TorrentInfo } from "../src/types.js"; + +// Colores para la consola +const colors = { + reset: "\x1b[0m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", +}; + +function log(color: keyof typeof colors, message: string) { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function header(message: string) { + console.log("\n" + "=".repeat(80)); + log("cyan", message); + console.log("=".repeat(80)); +} + +function createTorrent( + season: number | null, + episode: number | null, + quality: string, + score: number, + seeders: number, +): TorrentInfo { + return { + infoHash: "a".repeat(40), + rawTitle: null, + quality, + codec: "x264", + sourceType: "WEB-DL", + sizeBytes: "1073741824", + seeders, + leechers: 0, + magnetUrl: `magnet:?xt=urn:btih:${"a".repeat(40)}`, + torrentUrl: null, + source: "test", + qualityScore: score, + uploadedAt: null, + languages: [], + audioCodec: null, + hdrType: null, + releaseGroup: null, + isProper: null, + isRepack: null, + isRemastered: null, + season, + episode, + audioTracks: null, + subtitleTracks: null, + videoInfo: null, + scanStatus: null, + }; +} + +function createResponse(torrents: TorrentInfo[]): SearchResponse { + return { + total: 1, + page: 1, + pageSize: 10, + results: [ + { + id: 1, + imdbId: "tt1234567", + tmdbId: "12345", + contentType: "show", + title: "Test Show", + titleOriginal: null, + year: 2024, + overview: + "Una serie de prueba para verificar el filtrado por temporada", + posterUrl: null, + backdropUrl: null, + genres: ["Drama", "Action"], + ratingImdb: "8.5", + ratingTmdb: "8.2", + contentUrl: "https://torrentclaw.com/shows/test-show-1", + hasTorrents: true, + torrents, + }, + ], + }; +} + +// Test 1: Caso original del bug - Temporada 4 con mejores torrents en temporadas anteriores +header("TEST 1: BΓΊsqueda de temporada especΓ­fica (caso del bug original)"); +log( + "yellow", + "Escenario: Serie con temporadas 1-4, donde T1-T3 tienen mejor calidad que T4", +); +log("yellow", "BΓΊsqueda: season=4"); + +const test1Torrents = [ + createTorrent(1, null, "1080p", 90, 100), // Mejor score, pero T1 + createTorrent(2, null, "1080p", 85, 80), // Segundo mejor, pero T2 + createTorrent(3, null, "1080p", 80, 60), // Tercero, pero T3 + createTorrent(4, null, "720p", 70, 40), // T4 - score bajo + createTorrent(4, null, "1080p", 75, 30), // T4 - score medio +]; + +const test1Response = createResponse(test1Torrents); + +console.log("\nπŸ“Œ SIN filtro (comportamiento anterior):"); +console.log(formatSearchResults(test1Response)); + +console.log("\nβœ… CON filtro season=4 (comportamiento corregido):"); +console.log(formatSearchResults(test1Response, { season: 4 })); + +log("green", "βœ“ Debe mostrar SOLO los 2 torrents de temporada 4"); +log("green", 'βœ“ Debe indicar "2 matching, 5 total"'); + +// Test 2: Filtrado por episodio especΓ­fico +header("TEST 2: BΓΊsqueda de episodio especΓ­fico"); +log("yellow", "Escenario: MΓΊltiples episodios de la misma temporada"); +log("yellow", "BΓΊsqueda: season=2, episode=5"); + +const test2Torrents = [ + createTorrent(2, 3, "1080p", 90, 100), + createTorrent(2, 5, "1080p", 85, 80), // Episodio buscado + createTorrent(2, 5, "720p", 70, 60), // Episodio buscado + createTorrent(2, 7, "1080p", 88, 90), +]; + +const test2Response = createResponse(test2Torrents); + +console.log("\nβœ… CON filtro season=2, episode=5:"); +console.log(formatSearchResults(test2Response, { season: 2, episode: 5 })); + +log("green", "βœ“ Debe mostrar SOLO los 2 torrents de S02E05"); +log("green", 'βœ“ Debe indicar "2 matching, 4 total"'); + +// Test 3: Temporada no disponible +header("TEST 3: Temporada no disponible"); +log("yellow", "Escenario: Buscar temporada que no existe"); +log("yellow", "BΓΊsqueda: season=10"); + +const test3Torrents = [ + createTorrent(1, null, "1080p", 90, 100), + createTorrent(2, null, "1080p", 85, 80), + createTorrent(3, null, "720p", 75, 60), +]; + +const test3Response = createResponse(test3Torrents); + +console.log("\nβœ… CON filtro season=10 (no existe):"); +console.log(formatSearchResults(test3Response, { season: 10 })); + +log("green", 'βœ“ Debe mostrar mensaje "No torrents available for season 10"'); +log("green", 'βœ“ Debe indicar "3 torrents available for other seasons"'); + +// Test 4: Packs de temporada vs episodios individuales +header("TEST 4: Packs de temporada completa vs episodios individuales"); +log("yellow", "Escenario: Mezcla de packs completos y episodios individuales"); +log("yellow", "BΓΊsqueda: season=1, episode=5"); + +const test4Torrents = [ + createTorrent(1, null, "1080p", 95, 100), // Pack completo T1 + createTorrent(1, 1, "1080p", 90, 80), // Episodio 1 + createTorrent(1, 5, "1080p", 85, 70), // Episodio 5 (buscado) + createTorrent(1, 5, "720p", 75, 60), // Episodio 5 (buscado) + createTorrent(1, 10, "1080p", 88, 65), // Episodio 10 +]; + +const test4Response = createResponse(test4Torrents); + +console.log("\nπŸ“Œ CON filtro season=1 (solo temporada):"); +console.log(formatSearchResults(test4Response, { season: 1 })); + +log("green", "βœ“ Debe mostrar todos los torrents de T1 (5 torrents)"); + +console.log("\nβœ… CON filtro season=1, episode=5 (especΓ­fico):"); +console.log(formatSearchResults(test4Response, { season: 1, episode: 5 })); + +log("green", "βœ“ Debe mostrar SOLO episodios S01E05 (2 torrents)"); +log("green", "βœ“ NO debe mostrar el pack completo"); + +// Test 5: MΓ‘s de 5 torrents de la misma temporada +header("TEST 5: MΓ‘s de 5 torrents de la misma temporada"); +log("yellow", "Escenario: 8 torrents disponibles de la temporada 3"); +log("yellow", "BΓΊsqueda: season=3"); + +const test5Torrents = [ + createTorrent(3, null, "2160p", 100, 150), + createTorrent(3, null, "1080p", 95, 140), + createTorrent(3, null, "1080p", 90, 130), + createTorrent(3, null, "720p", 85, 120), + createTorrent(3, null, "1080p", 80, 110), + createTorrent(3, null, "720p", 75, 100), + createTorrent(3, null, "480p", 70, 90), + createTorrent(3, null, "720p", 65, 80), +]; + +const test5Response = createResponse(test5Torrents); + +console.log("\nβœ… CON filtro season=3:"); +console.log(formatSearchResults(test5Response, { season: 3 })); + +log("green", "βœ“ Debe mostrar mΓ‘ximo 5 torrents (los de mejor score)"); +log("green", 'βœ“ Debe indicar "8 matching, 8 total, top 5"'); +log("green", "βœ“ Primer torrent debe ser 2160p (score: 100)"); + +// Test 6: Sin filtro (comportamiento original debe mantenerse) +header("TEST 6: Sin filtro de temporada (regresiΓ³n)"); +log("yellow", "Escenario: BΓΊsqueda sin especificar temporada"); +log("yellow", "BΓΊsqueda: sin season ni episode"); + +const test6Torrents = [ + createTorrent(1, null, "1080p", 70, 50), + createTorrent(2, null, "1080p", 85, 80), + createTorrent(3, null, "2160p", 95, 100), + createTorrent(4, null, "720p", 60, 40), +]; + +const test6Response = createResponse(test6Torrents); + +console.log("\nβœ… SIN filtro:"); +console.log(formatSearchResults(test6Response)); + +log("green", "βœ“ Debe mostrar top 4 torrents ordenados por score"); +log("green", "βœ“ Primero: T3 2160p (score: 95)"); +log("green", "βœ“ Último: T4 720p (score: 60)"); +log("green", 'βœ“ Debe indicar "4 total, top 4"'); + +// Resumen +header("RESUMEN DE PRUEBAS"); +log("cyan", "Todos los tests manuales ejecutados."); +log( + "cyan", + "Verifica que los resultados coincidan con las expectativas marcadas con βœ“", +); +console.log("\nPara ejecutar tests automatizados:"); +log("blue", " npm test -- tests/formatters/content.test.ts"); +console.log("\nPara ver cobertura completa:"); +log("blue", " npm test"); +console.log(); diff --git a/src/formatters/content.ts b/src/formatters/content.ts index 2339259..1b42c74 100644 --- a/src/formatters/content.ts +++ b/src/formatters/content.ts @@ -83,6 +83,8 @@ function formatTorrent(t: TorrentInfo, compact?: boolean): string { export interface FormatOptions { compact?: boolean; + season?: number; + episode?: number; } function formatResult( @@ -102,12 +104,39 @@ function formatResult( } if (r.torrents.length > 0) { - const top = r.torrents - .sort((a, b) => (b.qualityScore ?? 0) - (a.qualityScore ?? 0)) - .slice(0, 5); - lines.push(` Torrents (${r.torrents.length} total, top ${top.length}):`); - for (const t of top) { - lines.push(formatTorrent(t, opts?.compact)); + // Filter torrents by season/episode if specified + let filteredTorrents = r.torrents; + if (opts?.season !== undefined) { + filteredTorrents = filteredTorrents.filter( + (t) => t.season === opts.season, + ); + if (opts?.episode !== undefined) { + filteredTorrents = filteredTorrents.filter( + (t) => t.episode === opts.episode, + ); + } + } + + if (filteredTorrents.length > 0) { + const top = filteredTorrents + .sort((a, b) => (b.qualityScore ?? 0) - (a.qualityScore ?? 0)) + .slice(0, 5); + const totalMsg = + filteredTorrents.length !== r.torrents.length + ? `${filteredTorrents.length} matching, ${r.torrents.length} total` + : `${r.torrents.length} total`; + lines.push(` Torrents (${totalMsg}, top ${top.length}):`); + for (const t of top) { + lines.push(formatTorrent(t, opts?.compact)); + } + } else { + const seasonEpStr = + opts?.episode !== undefined + ? `S${String(opts.season).padStart(2, "0")}E${String(opts.episode).padStart(2, "0")}` + : `season ${opts?.season}`; + lines.push( + ` No torrents available for ${seasonEpStr} (${r.torrents.length} torrents available for other seasons)`, + ); } } else { lines.push(" No torrents available"); diff --git a/src/index.ts b/src/index.ts index 7353a14..199899b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import { registerAutocomplete } from "./tools/autocomplete.js"; import { registerTrackInteraction } from "./tools/track-interaction.js"; import { registerScanRequest } from "./tools/scan-request.js"; import { registerStatsResource } from "./resources/stats.js"; +import { registerPresentationGuideResource } from "./resources/presentation-guide.js"; import { registerPrompts } from "./prompts.js"; const client = new TorrentClawClient(); @@ -37,6 +38,7 @@ registerScanRequest(server, client); // Register resources registerStatsResource(server, client); +registerPresentationGuideResource(server); // Register prompts registerPrompts(server); diff --git a/src/prompts.ts b/src/prompts.ts index f6321fb..6cd4694 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -2,6 +2,57 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; export function registerPrompts(server: McpServer): void { + server.prompt( + "presentation_guide", + "Guide for presenting torrent search results in a user-friendly format", + {}, + () => ({ + messages: [ + { + role: "user", + content: { + type: "text", + text: `When presenting torrent search results to users, follow these best practices: + +1. **Magnet Links**: Always make magnet links clickable using markdown format: + - Format: [πŸ“₯ Download](magnet:?xt=urn:btih:HASH...) + - Or: [🧲 Magnet Link](magnet:?xt=urn:btih:HASH...) + - Never show raw magnet URIs without making them clickable + +2. **Content URL**: Include the TorrentClaw content URL for browsing all seasons/episodes: + - Format: [πŸ”— View all seasons on TorrentClaw](https://torrentclaw.com/shows/...) + - This allows users to explore other seasons/episodes + +3. **Presentation Format**: Use clear, readable formatting: + - Group by episode/season for TV shows + - Show quality, size, and seeder count prominently + - Highlight torrents with active seeders + - Warn if torrents have 0 seeders + +4. **Example Format for TV Shows**: + **EntrevΓ­as - Temporada 4** + + **Episodio 1** (S04E01) + - 720p HDTV β€’ 879 MB β€’ 6 seeders [πŸ“₯ Download](magnet:?xt=...) + + **Episodio 2** (S04E02) + - 1080p WEB-DL β€’ 2.5 GB β€’ 0 seeders ⚠️ [πŸ“₯ Download](magnet:?xt=...) + - 720p HDTV β€’ 976 MB β€’ 1 seeder [πŸ“₯ Download](magnet:?xt=...) + + [πŸ”— View all seasons on TorrentClaw](URL) + +5. **Helpful Information**: + - Recommend torrents with more seeders + - Suggest alternatives if requested season/episode has no seeders + - Offer to search for different quality if user wants + +Apply these practices to make results actionable and user-friendly.`, + }, + }, + ], + }), + ); + server.prompt( "search_movie", "Search for a movie by title and get torrent download options", @@ -12,7 +63,7 @@ export function registerPrompts(server: McpServer): void { role: "user", content: { type: "text", - text: `Search for the movie "${title}" using search_content with type="movie". Present the results showing: title, year, ratings, and the top torrents sorted by quality score with their magnet links. If results are found, also call get_watch_providers with the content_id to check streaming availability.`, + text: `Search for the movie "${title}" using search_content with type="movie". Present the results with clickable magnet links using markdown format [πŸ“₯ Download](magnet:...), include the content URL for more details, and show quality/size/seeders clearly. If results are found, also call get_watch_providers with the content_id to check streaming availability.`, }, }, ], @@ -22,14 +73,23 @@ export function registerPrompts(server: McpServer): void { server.prompt( "search_show", "Search for a TV show by title and get torrent download options", - { title: z.string().describe("TV show title to search for") }, - ({ title }) => ({ + { + title: z.string().describe("TV show title to search for"), + season: z.number().optional().describe("Specific season number"), + }, + ({ title, season }) => ({ messages: [ { role: "user", content: { type: "text", - text: `Search for the TV show "${title}" using search_content with type="show". Present the results showing: title, year, ratings, and the top torrents sorted by quality score with their magnet links.`, + text: `Search for the TV show "${title}" using search_content with type="show"${season ? ` and season=${season}` : ""}. Present results grouped by episode with: +- Episode identifier (e.g., S04E01) +- Quality, size, and seeder count +- Clickable magnet links using markdown: [πŸ“₯ Download](magnet:...) +- Content URL for browsing all seasons: [πŸ”— View all seasons](URL) +- Recommendations for torrents with most seeders +- Warnings if torrents have 0 seeders`, }, }, ], diff --git a/src/resources/presentation-guide.ts b/src/resources/presentation-guide.ts new file mode 100644 index 0000000..f109909 --- /dev/null +++ b/src/resources/presentation-guide.ts @@ -0,0 +1,137 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +export function registerPresentationGuideResource(server: McpServer): void { + server.resource( + "presentation-guide", + "torrentclaw://presentation-guide", + { + description: + "Best practices for presenting torrent search results to users in a user-friendly, actionable format", + mimeType: "text/markdown", + }, + async (uri) => { + const guide = `# TorrentClaw Results Presentation Guide + +## Critical Requirements + +When presenting torrent search results to users, you MUST follow these practices to make results actionable and user-friendly: + +### 1. Clickable Magnet Links + +**ALWAYS** make magnet links clickable using markdown format: + +βœ… **CORRECT**: +- [πŸ“₯ Download](magnet:?xt=urn:btih:41159dc60579839533e04796df0e96bfa4864cb4&...) +- [🧲 Magnet](magnet:?xt=urn:btih:41159dc60579839533e04796df0e96bfa4864cb4&...) + +❌ **INCORRECT**: +- magnet:?xt=urn:btih:41159dc60579839533e04796df0e96bfa4864cb4 (not clickable) +- Showing only the info hash without full magnet URI + +### 2. Content URL for Browsing + +**ALWAYS** include the TorrentClaw content URL so users can explore all seasons/episodes: + +βœ… **CORRECT**: +[πŸ”— View all seasons and episodes on TorrentClaw](https://torrentclaw.com/shows/entrevias-2022-91260) + +This allows users to: +- Browse all available seasons +- See all torrents for each episode +- Explore different quality options + +### 3. User-Friendly Presentation Format + +**For TV Shows** (especially when searching by season): + +\`\`\`markdown +### EntrevΓ­as - Temporada 4 + +**Episodio 1** (S04E01) +- 720p HDTV β€’ 879 MB β€’ 6 seeders β€’ [πŸ“₯ Download](magnet:?xt=urn:btih:...) + +**Episodio 2** (S04E02) +- 1080p WEB-DL β€’ 2.5 GB β€’ 0 seeders ⚠️ No active seeders β€’ [πŸ“₯ Download](magnet:?xt=urn:btih:...) +- 720p HDTV β€’ 976 MB β€’ 1 seeder β€’ [πŸ“₯ Download](magnet:?xt=urn:btih:...) + +**Episodio 3** (S04E03) +- 720p HDTV β€’ 795 MB β€’ 2 seeders β€’ [πŸ“₯ Download](magnet:?xt=urn:btih:...) + +[πŸ”— View all seasons on TorrentClaw](https://torrentclaw.com/shows/...) +\`\`\` + +**For Movies**: + +\`\`\`markdown +### Inception (2010) +IMDb: 8.8 | TMDB: 8.4 + +**Available Torrents:** + +1. **2160p BluRay** β€’ 15.2 GB β€’ 147 seeders β€’ [πŸ“₯ Download](magnet:?xt=...) +2. **1080p BluRay** β€’ 2.0 GB β€’ 847 seeders ⭐ Recommended β€’ [πŸ“₯ Download](magnet:?xt=...) +3. **720p WEB-DL** β€’ 1.2 GB β€’ 234 seeders β€’ [πŸ“₯ Download](magnet:?xt=...) + +[πŸ”— View on TorrentClaw](https://torrentclaw.com/movies/...) +\`\`\` + +### 4. Helpful User Guidance + +Provide context and recommendations: + +- βœ… Recommend torrents with most seeders +- βœ… Warn when torrents have 0 seeders: "⚠️ No active seeders" +- βœ… Mark best option: "⭐ Recommended" (based on seeders + quality) +- βœ… Suggest alternatives if requested season has no seeders +- βœ… Offer to search different quality/season + +### 5. What NOT to Do + +❌ **Never** present results in plain text tables without clickable links +❌ **Never** show truncated magnet links +❌ **Never** omit the content URL +❌ **Never** show info hashes without the full magnet URI +❌ **Never** present results without indicating seeder count + +### 6. Example of Good vs Bad Presentation + +**❌ BAD** (what user reported as not practical): +\`\`\` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Episodio β”‚ Calidad β”‚ TamaΓ±o β”‚ Seeders β”‚ Magnet β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ S04E01 β”‚ 720p HDTV β”‚ 879 MB β”‚ 6 seeders β”‚ magnet:?xt=urn:btih:41159dc... β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +\`\`\` + +**βœ… GOOD**: +\`\`\`markdown +**S04E01** +720p HDTV β€’ 879 MB β€’ 6 seeders β€’ [πŸ“₯ Download](magnet:?xt=urn:btih:41159dc60579839533e04796df0e96bfa4864cb4&...) + +[πŸ”— View all episodes on TorrentClaw](https://torrentclaw.com/shows/entrevias-2022-91260) +\`\`\` + +## Summary + +The key is to make results **actionable**: users should be able to: +1. Click magnet links to start downloading immediately +2. Click content URL to explore more options +3. Quickly identify which torrents are best (seeders) +4. Understand warnings (no seeders) + +**Remember**: You're not just displaying data, you're helping users take action. +`; + + return { + contents: [ + { + uri: uri.href, + mimeType: "text/markdown", + text: guide, + }, + ], + }; + }, + ); +} diff --git a/src/tools/search-content.ts b/src/tools/search-content.ts index d1772f2..d3d6a28 100644 --- a/src/tools/search-content.ts +++ b/src/tools/search-content.ts @@ -10,7 +10,7 @@ export function registerSearchContent( ): void { server.tool( "search_content", - "Search for movies and TV shows by title, genre, year, rating, or quality. Returns matching content with metadata (title, year, genres, IMDb/TMDB ratings) and torrent download options (magnet links, quality, seeders, file size). This is the primary tool β€” use it first when a user asks to find, download, or learn about a movie or TV show. Results include a content_id needed by get_watch_providers and get_credits. For TV shows, you can filter by season/episode. Season/episode can also be auto-detected from the query (e.g. 'Bluey s01e05').", + "Search for movies and TV shows by title, genre, year, rating, or quality. Returns matching content with metadata (title, year, genres, IMDb/TMDB ratings) and torrent download options (magnet links, quality, seeders, file size). This is the primary tool β€” use it first when a user asks to find, download, or learn about a movie or TV show. Results include a content_id needed by get_watch_providers and get_credits. For TV shows, you can filter by season/episode. Season/episode can also be auto-detected from the query (e.g. 'Bluey s01e05'). IMPORTANT: When presenting results to users, make magnet links clickable using markdown format [Download](magnet:?xt=...), include the contentUrl for browsing all seasons/episodes, and present the information in a user-friendly format rather than raw tables.", { query: z .string() @@ -176,7 +176,11 @@ export function registerSearchContent( content: [ { type: "text", - text: formatSearchResults(data, { compact: params.compact }), + text: formatSearchResults(data, { + compact: params.compact, + season: params.season, + episode: params.episode, + }), }, ], }; diff --git a/tests/formatters/content.test.ts b/tests/formatters/content.test.ts index 4206386..cf8ca08 100644 --- a/tests/formatters/content.test.ts +++ b/tests/formatters/content.test.ts @@ -745,9 +745,27 @@ describe("formatSearchResults", () => { season: null, episode: null, audioTracks: [ - { lang: "en", codec: "aac", channels: "5.1", title: "English", default: true }, - { lang: "es", codec: "aac", channels: "5.1", title: "Spanish", default: false }, - { lang: "en", codec: "ac3", channels: "2.0", title: "Commentary", default: false }, + { + lang: "en", + codec: "aac", + channels: "5.1", + title: "English", + default: true, + }, + { + lang: "es", + codec: "aac", + channels: "5.1", + title: "Spanish", + default: false, + }, + { + lang: "en", + codec: "ac3", + channels: "2.0", + title: "Commentary", + default: false, + }, ], subtitleTracks: null, videoInfo: null, @@ -809,7 +827,13 @@ describe("formatSearchResults", () => { season: null, episode: null, audioTracks: [ - { lang: null, codec: null, channels: null, title: null, default: null }, + { + lang: null, + codec: null, + channels: null, + title: null, + default: null, + }, ], subtitleTracks: null, videoInfo: null, @@ -1021,6 +1045,346 @@ describe("formatSearchResults", () => { expect(text).toContain("Stream: Netflix, Disney+"); expect(text).toContain("Free: Tubi"); }); + + it("filters torrents by season when specified", () => { + const response: SearchResponse = { + total: 1, + page: 1, + pageSize: 10, + results: [ + { + id: 1, + imdbId: null, + tmdbId: null, + contentType: "show", + title: "Test Show", + titleOriginal: null, + year: 2024, + overview: null, + posterUrl: null, + backdropUrl: null, + genres: null, + ratingImdb: null, + ratingTmdb: null, + contentUrl: null, + hasTorrents: true, + torrents: [ + { + infoHash: "a".repeat(40), + rawTitle: null, + quality: "1080p", + codec: "x264", + sourceType: "WEB-DL", + sizeBytes: "1073741824", + seeders: 100, + leechers: 5, + magnetUrl: null, + torrentUrl: null, + source: "test", + qualityScore: 90, + uploadedAt: null, + languages: [], + audioCodec: null, + hdrType: null, + releaseGroup: null, + isProper: null, + isRepack: null, + isRemastered: null, + season: 1, + episode: null, + audioTracks: null, + subtitleTracks: null, + videoInfo: null, + scanStatus: null, + }, + { + infoHash: "b".repeat(40), + rawTitle: null, + quality: "720p", + codec: "x264", + sourceType: "WEB-DL", + sizeBytes: "536870912", + seeders: 50, + leechers: 2, + magnetUrl: null, + torrentUrl: null, + source: "test", + qualityScore: 70, + uploadedAt: null, + languages: [], + audioCodec: null, + hdrType: null, + releaseGroup: null, + isProper: null, + isRepack: null, + isRemastered: null, + season: 4, + episode: null, + audioTracks: null, + subtitleTracks: null, + videoInfo: null, + scanStatus: null, + }, + { + infoHash: "c".repeat(40), + rawTitle: null, + quality: "1080p", + codec: "x265", + sourceType: "WEB-DL", + sizeBytes: "805306368", + seeders: 75, + leechers: 3, + magnetUrl: null, + torrentUrl: null, + source: "test", + qualityScore: 85, + uploadedAt: null, + languages: [], + audioCodec: null, + hdrType: null, + releaseGroup: null, + isProper: null, + isRepack: null, + isRemastered: null, + season: 4, + episode: null, + audioTracks: null, + subtitleTracks: null, + videoInfo: null, + scanStatus: null, + }, + ], + }, + ], + }; + + // Filter by season 4 + const text = formatSearchResults(response, { season: 4 }); + expect(text).toContain("2 matching, 3 total"); + // Should show season 4 torrents (quality scores 70 and 85) + expect(text).toContain("Score: 85"); + expect(text).toContain("Score: 70"); + // Should NOT show season 1 torrent (quality score 90) + expect(text).not.toContain("Score: 90"); + }); + + it("filters torrents by season and episode when both specified", () => { + const response: SearchResponse = { + total: 1, + page: 1, + pageSize: 10, + results: [ + { + id: 1, + imdbId: null, + tmdbId: null, + contentType: "show", + title: "Test Show", + titleOriginal: null, + year: 2024, + overview: null, + posterUrl: null, + backdropUrl: null, + genres: null, + ratingImdb: null, + ratingTmdb: null, + contentUrl: null, + hasTorrents: true, + torrents: [ + { + infoHash: "a".repeat(40), + rawTitle: null, + quality: "1080p", + codec: "x264", + sourceType: "WEB-DL", + sizeBytes: "1073741824", + seeders: 100, + leechers: 5, + magnetUrl: null, + torrentUrl: null, + source: "test", + qualityScore: 90, + uploadedAt: null, + languages: [], + audioCodec: null, + hdrType: null, + releaseGroup: null, + isProper: null, + isRepack: null, + isRemastered: null, + season: 2, + episode: 5, + audioTracks: null, + subtitleTracks: null, + videoInfo: null, + scanStatus: null, + }, + { + infoHash: "b".repeat(40), + rawTitle: null, + quality: "720p", + codec: "x264", + sourceType: "WEB-DL", + sizeBytes: "536870912", + seeders: 50, + leechers: 2, + magnetUrl: null, + torrentUrl: null, + source: "test", + qualityScore: 70, + uploadedAt: null, + languages: [], + audioCodec: null, + hdrType: null, + releaseGroup: null, + isProper: null, + isRepack: null, + isRemastered: null, + season: 2, + episode: 3, + audioTracks: null, + subtitleTracks: null, + videoInfo: null, + scanStatus: null, + }, + ], + }, + ], + }; + + // Filter by season 2, episode 5 + const text = formatSearchResults(response, { season: 2, episode: 5 }); + expect(text).toContain("1 matching, 2 total"); + // Should show only S02E05 torrent + expect(text).toContain("Score: 90"); + expect(text).toContain("S02E05"); + // Should NOT show S02E03 + expect(text).not.toContain("Score: 70"); + }); + + it("shows message when no torrents match the season filter", () => { + const response: SearchResponse = { + total: 1, + page: 1, + pageSize: 10, + results: [ + { + id: 1, + imdbId: null, + tmdbId: null, + contentType: "show", + title: "Test Show", + titleOriginal: null, + year: 2024, + overview: null, + posterUrl: null, + backdropUrl: null, + genres: null, + ratingImdb: null, + ratingTmdb: null, + contentUrl: null, + hasTorrents: true, + torrents: [ + { + infoHash: "a".repeat(40), + rawTitle: null, + quality: "1080p", + codec: "x264", + sourceType: "WEB-DL", + sizeBytes: "1073741824", + seeders: 100, + leechers: 5, + magnetUrl: null, + torrentUrl: null, + source: "test", + qualityScore: 90, + uploadedAt: null, + languages: [], + audioCodec: null, + hdrType: null, + releaseGroup: null, + isProper: null, + isRepack: null, + isRemastered: null, + season: 1, + episode: null, + audioTracks: null, + subtitleTracks: null, + videoInfo: null, + scanStatus: null, + }, + ], + }, + ], + }; + + // Filter by season 4 (not available) + const text = formatSearchResults(response, { season: 4 }); + expect(text).toContain("No torrents available for season 4"); + expect(text).toContain("1 torrents available for other seasons"); + }); + + it("shows message when no torrents match the season+episode filter", () => { + const response: SearchResponse = { + total: 1, + page: 1, + pageSize: 10, + results: [ + { + id: 1, + imdbId: null, + tmdbId: null, + contentType: "show", + title: "Test Show", + titleOriginal: null, + year: 2024, + overview: null, + posterUrl: null, + backdropUrl: null, + genres: null, + ratingImdb: null, + ratingTmdb: null, + contentUrl: null, + hasTorrents: true, + torrents: [ + { + infoHash: "a".repeat(40), + rawTitle: null, + quality: "1080p", + codec: "x264", + sourceType: "WEB-DL", + sizeBytes: "1073741824", + seeders: 100, + leechers: 5, + magnetUrl: null, + torrentUrl: null, + source: "test", + qualityScore: 90, + uploadedAt: null, + languages: [], + audioCodec: null, + hdrType: null, + releaseGroup: null, + isProper: null, + isRepack: null, + isRemastered: null, + season: 2, + episode: 5, + audioTracks: null, + subtitleTracks: null, + videoInfo: null, + scanStatus: null, + }, + ], + }, + ], + }; + + // Filter by S04E01 (not available) + const text = formatSearchResults(response, { season: 4, episode: 1 }); + expect(text).toContain("No torrents available for S04E01"); + expect(text).toContain("1 torrents available for other seasons"); + }); }); describe("formatPopularResults", () => { diff --git a/tests/prompts.test.ts b/tests/prompts.test.ts index 0d74126..101b5f4 100644 --- a/tests/prompts.test.ts +++ b/tests/prompts.test.ts @@ -26,10 +26,11 @@ describe("registerPrompts", () => { return { server, prompts }; } - it("registers 4 prompts", () => { + it("registers 5 prompts", () => { const { server, prompts } = createMockServer(); registerPrompts(server); - expect(prompts.size).toBe(4); + expect(prompts.size).toBe(5); + expect(prompts.has("presentation_guide")).toBe(true); expect(prompts.has("search_movie")).toBe(true); expect(prompts.has("search_show")).toBe(true); expect(prompts.has("whats_new")).toBe(true); diff --git a/tests/resources/presentation-guide.test.ts b/tests/resources/presentation-guide.test.ts new file mode 100644 index 0000000..060ef55 --- /dev/null +++ b/tests/resources/presentation-guide.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from "vitest"; +import { createMockServer } from "../helpers.js"; +import { registerPresentationGuideResource } from "../../src/resources/presentation-guide.js"; + +describe("presentation-guide resource", () => { + it("returns markdown guide with best practices", async () => { + const { server, getResourceHandler } = createMockServer(); + registerPresentationGuideResource(server); + + const handler = getResourceHandler("torrentclaw://presentation-guide"); + const result = await handler({ href: "torrentclaw://presentation-guide" }); + + expect(result.contents).toHaveLength(1); + expect(result.contents[0].mimeType).toBe("text/markdown"); + const text = result.contents[0].text; + + // Check for key sections + expect(text).toContain("# TorrentClaw Results Presentation Guide"); + expect(text).toContain("## Critical Requirements"); + expect(text).toContain("### 1. Clickable Magnet Links"); + expect(text).toContain("### 2. Content URL for Browsing"); + expect(text).toContain("### 3. User-Friendly Presentation Format"); + + // Check for markdown examples + expect(text).toContain("[πŸ“₯ Download](magnet:"); + expect(text).toContain("[πŸ”— View"); + + // Check for good vs bad examples + expect(text).toContain("❌ BAD"); + expect(text).toContain("βœ… GOOD"); + + // Check for warnings about seeders + expect(text).toContain("⚠️ No active seeders"); + expect(text).toContain("⭐ Recommended"); + }); + + it("provides guidance for TV shows", async () => { + const { server, getResourceHandler } = createMockServer(); + registerPresentationGuideResource(server); + + const handler = getResourceHandler("torrentclaw://presentation-guide"); + const result = await handler({ href: "torrentclaw://presentation-guide" }); + + const text = result.contents[0].text; + expect(text).toContain("**For TV Shows**"); + expect(text).toContain("S04E01"); + expect(text).toContain("EntrevΓ­as"); + }); + + it("provides guidance for movies", async () => { + const { server, getResourceHandler } = createMockServer(); + registerPresentationGuideResource(server); + + const handler = getResourceHandler("torrentclaw://presentation-guide"); + const result = await handler({ href: "torrentclaw://presentation-guide" }); + + const text = result.contents[0].text; + expect(text).toContain("**For Movies**"); + expect(text).toContain("Inception"); + expect(text).toContain("BluRay"); + }); + + it("warns against bad practices", async () => { + const { server, getResourceHandler } = createMockServer(); + registerPresentationGuideResource(server); + + const handler = getResourceHandler("torrentclaw://presentation-guide"); + const result = await handler({ href: "torrentclaw://presentation-guide" }); + + const text = result.contents[0].text; + + // Check for warnings + expect(text).toContain("### 5. What NOT to Do"); + expect(text).toContain("without clickable links"); + expect(text).toContain("truncated magnet links"); + expect(text).toContain("omit the content URL"); + }); +}); From f0603cae601694db730f59697388d1be2f5858af Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Thu, 12 Feb 2026 18:44:42 +0100 Subject: [PATCH 02/11] chore(release): 0.2.0 --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e758a9..f9f553b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [0.2.0](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.1.0...v0.2.0) (2026-02-12) + +### Features + +- **mcp:** add season filtering and presentation guide for better UX ([7844b83](https://github.com/torrentclaw/torrentclaw-mcp/commit/7844b83eb3f49323e7b64ca4c4e09868bbce2dd9)) + ## 0.1.0 (2026-02-12) ### Features diff --git a/package-lock.json b/package-lock.json index e08ba81..782f861 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "torrentclaw-mcp", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "torrentclaw-mcp", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", diff --git a/package.json b/package.json index 5f7a310..fe3dc79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "torrentclaw-mcp", - "version": "0.1.0", + "version": "0.2.0", "description": "MCP server for TorrentClaw β€” search and discover movies and TV shows with torrent downloads, magnet links, streaming availability, and cast/crew metadata", "type": "module", "bin": { From b12946117286003ea0c40d015728c5ee88931abe Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Thu, 12 Feb 2026 22:33:52 +0100 Subject: [PATCH 03/11] chore: rename package to @torrentclaw/mcp for npm organization --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe3dc79..3a13c68 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "torrentclaw-mcp", + "name": "@torrentclaw/mcp", "version": "0.2.0", "description": "MCP server for TorrentClaw β€” search and discover movies and TV shows with torrent downloads, magnet links, streaming availability, and cast/crew metadata", "type": "module", From 0c4f7f1356cb280c0fc012a39cb5f0ca35e8ffcf Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Thu, 12 Feb 2026 22:34:00 +0100 Subject: [PATCH 04/11] chore(release): 0.2.1 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f553b..79f9278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [0.2.1](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.2.0...v0.2.1) (2026-02-12) + ## [0.2.0](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.1.0...v0.2.0) (2026-02-12) ### Features diff --git a/package-lock.json b/package-lock.json index 782f861..993fc4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "torrentclaw-mcp", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "torrentclaw-mcp", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", diff --git a/package.json b/package.json index 3a13c68..a18aab4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@torrentclaw/mcp", - "version": "0.2.0", + "version": "0.2.1", "description": "MCP server for TorrentClaw β€” search and discover movies and TV shows with torrent downloads, magnet links, streaming availability, and cast/crew metadata", "type": "module", "bin": { From b3ca57aeb7240c1551a1127d4544ffabe0303946 Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Thu, 12 Feb 2026 22:48:14 +0100 Subject: [PATCH 05/11] chore: normalize package.json format --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a18aab4..67722d9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "MCP server for TorrentClaw β€” search and discover movies and TV shows with torrent downloads, magnet links, streaming availability, and cast/crew metadata", "type": "module", "bin": { - "torrentclaw-mcp": "./build/index.js" + "torrentclaw-mcp": "build/index.js" }, "main": "./build/index.js", "files": [ @@ -48,7 +48,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/torrentclaw/torrentclaw-mcp" + "url": "git+https://github.com/torrentclaw/torrentclaw-mcp.git" }, "homepage": "https://github.com/torrentclaw/torrentclaw-mcp#readme", "bugs": { From 964c111b7819be438a03d40176072506902d4556 Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Mon, 16 Feb 2026 22:01:58 +0100 Subject: [PATCH 06/11] feat: add glama.json and smithery.yaml for directory listings Add configuration files for Glama.ai and Smithery.ai MCP directories to enable automatic indexing and discovery. --- glama.json | 4 ++++ smithery.yaml | 1 + 2 files changed, 5 insertions(+) create mode 100644 glama.json create mode 100644 smithery.yaml diff --git a/glama.json b/glama.json new file mode 100644 index 0000000..2c089d5 --- /dev/null +++ b/glama.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://glama.ai/mcp/schemas/server.json", + "maintainers": ["torrentclaw"] +} diff --git a/smithery.yaml b/smithery.yaml new file mode 100644 index 0000000..322550e --- /dev/null +++ b/smithery.yaml @@ -0,0 +1 @@ +runtime: "typescript" From 08c67e6e9ee2aaf476f8e0c31fcaaeb73d2d2182 Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Mon, 16 Feb 2026 22:21:56 +0100 Subject: [PATCH 07/11] fix: update qs to 6.15.0 to resolve CVE denial of service vulnerability --- package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 993fc4f..2dc5a1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "torrentclaw-mcp", + "name": "@torrentclaw/mcp", "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "torrentclaw-mcp", + "name": "@torrentclaw/mcp", "version": "0.2.1", "license": "MIT", "dependencies": { @@ -5003,9 +5003,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" From 635ad4ca424fce9fcb481b57959f58cb23f61bac Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Mon, 16 Feb 2026 22:22:52 +0100 Subject: [PATCH 08/11] chore(release): 0.2.2 --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f9278..6b33366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [0.2.2](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.2.1...v0.2.2) (2026-02-16) + +### Features + +- add glama.json and smithery.yaml for directory listings ([06865a2](https://github.com/torrentclaw/torrentclaw-mcp/commit/06865a2abda420567af7fb3d5046b29ea7de6060)) + +### Bug Fixes + +- update qs to 6.15.0 to resolve CVE denial of service vulnerability ([657910a](https://github.com/torrentclaw/torrentclaw-mcp/commit/657910ad357f14f651d1af1afec0c0cda64513f5)) + ## [0.2.1](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.2.0...v0.2.1) (2026-02-12) ## [0.2.0](https://github.com/torrentclaw/torrentclaw-mcp/compare/v0.1.0...v0.2.0) (2026-02-12) diff --git a/package-lock.json b/package-lock.json index 2dc5a1f..fdc30fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@torrentclaw/mcp", - "version": "0.2.1", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@torrentclaw/mcp", - "version": "0.2.1", + "version": "0.2.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", diff --git a/package.json b/package.json index 67722d9..fdece01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@torrentclaw/mcp", - "version": "0.2.1", + "version": "0.2.2", "description": "MCP server for TorrentClaw β€” search and discover movies and TV shows with torrent downloads, magnet links, streaming availability, and cast/crew metadata", "type": "module", "bin": { From f46cbc5156de37743f965aab2ccf9cc48144c79c Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Sat, 28 Mar 2026 12:00:21 +0100 Subject: [PATCH 09/11] chore: update maintainer info in LICENSE and glama.json --- LICENSE | 2 +- glama.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 9d044e4..ee27d08 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 buryni +Copyright (c) 2026 Deivid Soto Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/glama.json b/glama.json index 2c089d5..2b7ca25 100644 --- a/glama.json +++ b/glama.json @@ -1,4 +1,4 @@ { "$schema": "https://glama.ai/mcp/schemas/server.json", - "maintainers": ["torrentclaw"] + "maintainers": ["torrentclaw", "eividsoto"] } From ffac6fe22ad87e34fe7220f88fae216968d15e91 Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Wed, 27 May 2026 15:45:38 +0200 Subject: [PATCH 10/11] ci: port workflows from .github/ to .forgejo/ (Forgejo Actions) GitHub torrentclaw org is shadow-banned; CI is hosted at git.torrentclaw.com now. Move workflows into the runner's natively-watched .forgejo/workflows/ tree and adapt steps to run in the available 'docker'-labeled Forgejo runner without GitHub-only tooling (gh CLI, third-party marketplace actions). - Use container: image to ship the toolchain (no actions/setup-* needed). - Drop GitHub-only marketplace actions in favour of upstream installers invoked over curl/apt. - Where a workflow created a GitHub Release (release.yml), substitute the step with a curl call against the Forgejo Releases API (POST /repos///releases). --- {.github => .forgejo}/workflows/ci.yml | 22 ++++----- .forgejo/workflows/release.yml | 68 ++++++++++++++++++++++++++ .github/workflows/release.yml | 41 ---------------- 3 files changed, 79 insertions(+), 52 deletions(-) rename {.github => .forgejo}/workflows/ci.yml (68%) create mode 100644 .forgejo/workflows/release.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.forgejo/workflows/ci.yml similarity index 68% rename from .github/workflows/ci.yml rename to .forgejo/workflows/ci.yml index c764d29..c1f6f9e 100644 --- a/.github/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -12,33 +12,33 @@ permissions: jobs: lint-commits: name: Lint commits - runs-on: ubuntu-latest + runs-on: docker + container: + image: docker.io/library/node:22 if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: "22" - - name: Install dependencies run: npm ci - name: Validate conventional commits - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + run: | + npx commitlint \ + --from ${{ github.event.pull_request.base.sha }} \ + --to ${{ github.event.pull_request.head.sha }} \ + --verbose build-and-test: name: Build & test - runs-on: ubuntu-latest + runs-on: docker + container: + image: docker.io/library/node:22 steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - - name: Install dependencies run: npm ci diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..145d200 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,68 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: docker + container: + image: docker.io/library/node:22 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Test + run: npm test + + - name: Publish to npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + # Set authToken explicitly β€” actions/setup-node was the only consumer + # of registry-url + NODE_AUTH_TOKEN, and we dropped it for Forgejo + # compat. The literal npmjs registry stays the same. + cat > ~/.npmrc </dev/null || echo "") + if [ -n "$prev" ]; then + notes=$(git log --pretty=format:'- %s' "${prev}..${TAG}") + else + notes=$(git log --pretty=format:'- %s' "${TAG}") + fi + body=$(jq -n --arg t "$TAG" --arg n "$notes" \ + '{tag_name:$t, name:$t, body:$n, draft:false, prerelease:false}') + curl -sSf -X POST "$FORGEJO_API/repos/$REPO/releases" \ + -H "Authorization: token $FORGEJO_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$body" >/dev/null || \ + echo "Release may already exist for $TAG" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 8a489af..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-node@v4 - with: - node-version: "22" - registry-url: "https://registry.npmjs.org" - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Test - run: npm test - - - name: Publish to npm - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: GitHub Release - uses: softprops/action-gh-release@v2 - with: - generate_release_notes: true From a5f867eeea393b0bbef41150c21ca7e235366e92 Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Wed, 27 May 2026 15:58:47 +0200 Subject: [PATCH 11/11] refactor(ci): point Forgejo URLs at torrentclaw org (post-transfer) Repos were transferred from the deivid user to a dedicated torrentclaw organisation; the workflows reference the org path. --- .forgejo/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 145d200..79737aa 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -46,7 +46,7 @@ jobs: env: FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} FORGEJO_API: http://forgejo:3000/api/v1 - REPO: deivid/torrentclaw-mcp + REPO: torrentclaw/torrentclaw-mcp TAG: ${{ github.ref_name }} run: | set -euo pipefail