plum-control-mcp/src/transmission/tools.ts
Natalie 845f67d34a feat(@applications/plum-control-mcp): add blacktv remote control for black's HDMI TV
- black_* MCP tools drive mpv-on-DRM on black over SSH (mirrors transmission_*)
- black-tv.sh owns one mpv + IPC socket; display bring-up via nouveau atomic KMS
- boot-persistence systemd unit + nouveau atomic=1 modprobe.d
- README documents the black_* control surface

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:25:58 -07:00

129 lines
5.1 KiB
TypeScript

import { transmissionAdd, transmissionInfo, transmissionList, transmissionRemove } from "./client.ts";
import { searchTorrents } from "./search.ts";
export const TRANSMISSION_TOOLS = [
{
name: "torrent_search",
description: "Search for torrents across ThePirateBay, Nyaa, and 1337x (via FlareSolverr). Returns results with filename, source, size, seed/leech counts, and magnet links ready to pass to transmission_add. FlareSolverr must be running on localhost:8191 for 1337x results.",
inputSchema: {
type: "object" as const,
properties: {
query: {
type: "string" as const,
description: "Search query. For TV: 'show name sXX' or 'show name season N complete'. Add '1080p' for HD.",
},
limit: {
type: "number" as const,
description: "Max results to return (default 15, max 30).",
},
},
required: ["query"],
},
},
{
name: "transmission_add",
description: "Add one or more magnet links to Transmission on black. Pass magnets as an array. Returns the server response for each.",
inputSchema: {
type: "object" as const,
properties: {
magnets: {
type: "array" as const,
items: { type: "string" as const },
description: "Magnet URIs (magnet:?xt=urn:btih:...) to add.",
},
},
required: ["magnets"],
},
},
{
name: "transmission_list",
description: "List all torrents in Transmission on black with their ID, progress, status, and name. Optionally filter by a name substring.",
inputSchema: {
type: "object" as const,
properties: {
filter: {
type: "string" as const,
description: "Optional case-insensitive substring to filter torrent names.",
},
status: {
type: "string" as const,
enum: ["all", "downloading", "seeding", "idle", "stopped"] as const,
description: "Filter by status (default: all).",
},
},
},
},
{
name: "transmission_info",
description: "Get detailed info about a single torrent by its numeric ID.",
inputSchema: {
type: "object" as const,
properties: {
id: { type: "number" as const, description: "Torrent ID from transmission_list." },
},
required: ["id"],
},
},
{
name: "transmission_remove",
description: "Remove a torrent by ID. Set delete_data=true to also delete the downloaded files.",
inputSchema: {
type: "object" as const,
properties: {
id: { type: "number" as const, description: "Torrent ID from transmission_list." },
delete_data: { type: "boolean" as const, description: "Also delete downloaded files (default false)." },
},
required: ["id"],
},
},
] as const;
export function dispatchTransmission(name: string, args: Record<string, unknown>): unknown {
try {
switch (name) {
case "torrent_search": {
const query = args["query"];
if (typeof query !== "string" || query.trim().length === 0) throw new Error("query must be a non-empty string");
const rawLimit = args["limit"];
const limit = rawLimit === undefined ? 15 : typeof rawLimit === "number" ? Math.min(30, Math.max(1, Math.floor(rawLimit))) : (() => { throw new Error("limit must be a number"); })();
return searchTorrents(query.trim(), limit);
}
case "transmission_add": {
const magnets = args["magnets"];
if (!Array.isArray(magnets) || magnets.length === 0) throw new Error("magnets must be a non-empty array");
const results: Array<{ magnet: string; result: string }> = [];
for (const m of magnets) {
if (typeof m !== "string" || !m.startsWith("magnet:")) throw new Error(`invalid magnet: ${String(m).slice(0, 80)}`);
results.push({ magnet: m.slice(0, 80) + "…", result: transmissionAdd(m) });
}
return results;
}
case "transmission_list": {
const filter = typeof args["filter"] === "string" ? args["filter"].toLowerCase() : null;
const statusFilter = typeof args["status"] === "string" ? args["status"] : "all";
let rows = transmissionList();
if (filter) rows = rows.filter(r => r.name.toLowerCase().includes(filter));
if (statusFilter !== "all") {
const s = statusFilter.toLowerCase();
rows = rows.filter(r => r.status.toLowerCase().includes(s));
}
return rows;
}
case "transmission_info": {
const id = args["id"];
if (typeof id !== "number" || !Number.isFinite(id)) throw new Error("id must be a number");
return transmissionInfo(Math.floor(id));
}
case "transmission_remove": {
const id = args["id"];
if (typeof id !== "number" || !Number.isFinite(id)) throw new Error("id must be a number");
return transmissionRemove(Math.floor(id), args["delete_data"] === true);
}
default:
throw new Error(`unknown transmission tool: ${name}`);
}
} catch (err) {
if (err instanceof Error) throw err;
throw new Error(String(err));
}
}