diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml deleted file mode 100644 index 79737aa..0000000 --- a/.forgejo/workflows/release.yml +++ /dev/null @@ -1,68 +0,0 @@ -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/.forgejo/workflows/ci.yml b/.github/workflows/ci.yml similarity index 68% rename from .forgejo/workflows/ci.yml rename to .github/workflows/ci.yml index c1f6f9e..c764d29 100644 --- a/.forgejo/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,33 +12,33 @@ permissions: jobs: lint-commits: name: Lint commits - runs-on: docker - container: - image: docker.io/library/node:22 + runs-on: ubuntu-latest 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: docker - container: - image: docker.io/library/node:22 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - name: Install dependencies run: npm ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8a489af --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b33366..9e758a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,24 +2,6 @@ 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) - -### 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/LICENSE b/LICENSE index ee27d08..9d044e4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Deivid Soto +Copyright (c) 2026 buryni 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 deleted file mode 100644 index 2b7ca25..0000000 --- a/glama.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://glama.ai/mcp/schemas/server.json", - "maintainers": ["torrentclaw", "eividsoto"] -} diff --git a/package-lock.json b/package-lock.json index fdc30fa..e08ba81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@torrentclaw/mcp", - "version": "0.2.2", + "name": "torrentclaw-mcp", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@torrentclaw/mcp", - "version": "0.2.2", + "name": "torrentclaw-mcp", + "version": "0.1.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", @@ -5003,9 +5003,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/package.json b/package.json index fdece01..5f7a310 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@torrentclaw/mcp", - "version": "0.2.2", + "name": "torrentclaw-mcp", + "version": "0.1.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": { - "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": "git+https://github.com/torrentclaw/torrentclaw-mcp.git" + "url": "https://github.com/torrentclaw/torrentclaw-mcp" }, "homepage": "https://github.com/torrentclaw/torrentclaw-mcp#readme", "bugs": { diff --git a/scripts/test-season-filtering.ts b/scripts/test-season-filtering.ts deleted file mode 100755 index 43a644c..0000000 --- a/scripts/test-season-filtering.ts +++ /dev/null @@ -1,248 +0,0 @@ -#!/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/smithery.yaml b/smithery.yaml deleted file mode 100644 index 322550e..0000000 --- a/smithery.yaml +++ /dev/null @@ -1 +0,0 @@ -runtime: "typescript" diff --git a/src/formatters/content.ts b/src/formatters/content.ts index 1b42c74..2339259 100644 --- a/src/formatters/content.ts +++ b/src/formatters/content.ts @@ -83,8 +83,6 @@ function formatTorrent(t: TorrentInfo, compact?: boolean): string { export interface FormatOptions { compact?: boolean; - season?: number; - episode?: number; } function formatResult( @@ -104,39 +102,12 @@ function formatResult( } if (r.torrents.length > 0) { - // 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)`, - ); + 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)); } } else { lines.push(" No torrents available"); diff --git a/src/index.ts b/src/index.ts index 199899b..7353a14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,6 @@ 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(); @@ -38,7 +37,6 @@ 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 6cd4694..f6321fb 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -2,57 +2,6 @@ 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", @@ -63,7 +12,7 @@ Apply these practices to make results actionable and user-friendly.`, role: "user", content: { type: "text", - 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.`, + 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.`, }, }, ], @@ -73,23 +22,14 @@ Apply these practices to make results actionable and user-friendly.`, 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"), - season: z.number().optional().describe("Specific season number"), - }, - ({ title, season }) => ({ + { title: z.string().describe("TV show title to search for") }, + ({ title }) => ({ messages: [ { role: "user", content: { type: "text", - 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`, + 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.`, }, }, ], diff --git a/src/resources/presentation-guide.ts b/src/resources/presentation-guide.ts deleted file mode 100644 index f109909..0000000 --- a/src/resources/presentation-guide.ts +++ /dev/null @@ -1,137 +0,0 @@ -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 d3d6a28..d1772f2 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'). 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.", + "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').", { query: z .string() @@ -176,11 +176,7 @@ export function registerSearchContent( content: [ { type: "text", - text: formatSearchResults(data, { - compact: params.compact, - season: params.season, - episode: params.episode, - }), + text: formatSearchResults(data, { compact: params.compact }), }, ], }; diff --git a/tests/formatters/content.test.ts b/tests/formatters/content.test.ts index cf8ca08..4206386 100644 --- a/tests/formatters/content.test.ts +++ b/tests/formatters/content.test.ts @@ -745,27 +745,9 @@ 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, @@ -827,13 +809,7 @@ 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, @@ -1045,346 +1021,6 @@ 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 101b5f4..0d74126 100644 --- a/tests/prompts.test.ts +++ b/tests/prompts.test.ts @@ -26,11 +26,10 @@ describe("registerPrompts", () => { return { server, prompts }; } - it("registers 5 prompts", () => { + it("registers 4 prompts", () => { const { server, prompts } = createMockServer(); registerPrompts(server); - expect(prompts.size).toBe(5); - expect(prompts.has("presentation_guide")).toBe(true); + expect(prompts.size).toBe(4); 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 deleted file mode 100644 index 060ef55..0000000 --- a/tests/resources/presentation-guide.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -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"); - }); -});