mpv-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +123 -0
  2. package/dist/config.d.ts +12 -0
  3. package/dist/config.js +30 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +52 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/services/media-scanner.d.ts +19 -0
  9. package/dist/services/media-scanner.js +61 -0
  10. package/dist/services/media-scanner.js.map +1 -0
  11. package/dist/services/mpv-ipc.d.ts +23 -0
  12. package/dist/services/mpv-ipc.js +112 -0
  13. package/dist/services/mpv-ipc.js.map +1 -0
  14. package/dist/services/mpv-process.d.ts +9 -0
  15. package/dist/services/mpv-process.js +73 -0
  16. package/dist/services/mpv-process.js.map +1 -0
  17. package/dist/tools/browse.d.ts +3 -0
  18. package/dist/tools/browse.js +86 -0
  19. package/dist/tools/browse.js.map +1 -0
  20. package/dist/tools/navigation.d.ts +3 -0
  21. package/dist/tools/navigation.js +62 -0
  22. package/dist/tools/navigation.js.map +1 -0
  23. package/dist/tools/play.d.ts +4 -0
  24. package/dist/tools/play.js +82 -0
  25. package/dist/tools/play.js.map +1 -0
  26. package/dist/tools/playback.d.ts +3 -0
  27. package/dist/tools/playback.js +69 -0
  28. package/dist/tools/playback.js.map +1 -0
  29. package/dist/tools/playlist.d.ts +4 -0
  30. package/dist/tools/playlist.js +174 -0
  31. package/dist/tools/playlist.js.map +1 -0
  32. package/dist/tools/seek.d.ts +3 -0
  33. package/dist/tools/seek.js +57 -0
  34. package/dist/tools/seek.js.map +1 -0
  35. package/dist/tools/status.d.ts +3 -0
  36. package/dist/tools/status.js +58 -0
  37. package/dist/tools/status.js.map +1 -0
  38. package/dist/tools/volume.d.ts +3 -0
  39. package/dist/tools/volume.js +43 -0
  40. package/dist/tools/volume.js.map +1 -0
  41. package/dist/tools/youtube.d.ts +4 -0
  42. package/dist/tools/youtube.js +148 -0
  43. package/dist/tools/youtube.js.map +1 -0
  44. package/dist/utils/format.d.ts +7 -0
  45. package/dist/utils/format.js +34 -0
  46. package/dist/utils/format.js.map +1 -0
  47. package/dist/utils/fuzzy-match.d.ts +16 -0
  48. package/dist/utils/fuzzy-match.js +38 -0
  49. package/dist/utils/fuzzy-match.js.map +1 -0
  50. package/dist/utils/path.d.ts +2 -0
  51. package/dist/utils/path.js +5 -0
  52. package/dist/utils/path.js.map +1 -0
  53. package/package.json +47 -0
@@ -0,0 +1,148 @@
1
+ import { execFile } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+ import { z } from "zod";
5
+ import { DOWNLOAD_DIR } from "../config.js";
6
+ export function registerYoutubeTools(server, ipc, scanner) {
7
+ server.registerTool("mpv_youtube", {
8
+ title: "Play from YouTube",
9
+ description: "Search YouTube and stream the result through mpv. Accepts a search query or a direct YouTube URL.",
10
+ inputSchema: {
11
+ query: z
12
+ .string()
13
+ .min(1)
14
+ .describe("YouTube search term or URL"),
15
+ results: z
16
+ .number()
17
+ .int()
18
+ .min(1)
19
+ .max(10)
20
+ .default(1)
21
+ .optional()
22
+ .describe("Number of search results to queue (default 1, max 10)"),
23
+ },
24
+ annotations: {
25
+ readOnlyHint: false,
26
+ destructiveHint: false,
27
+ idempotentHint: false,
28
+ openWorldHint: true,
29
+ },
30
+ }, async ({ query, results }) => {
31
+ try {
32
+ const count = results ?? 1;
33
+ const isUrl = query.includes("youtube.com/") || query.includes("youtu.be/");
34
+ const target = isUrl ? query : `ytdl://ytsearch${count}:${query}`;
35
+ await ipc.command(["loadfile", target, "replace"]);
36
+ await delay(2000); // YouTube resolution takes longer than local files
37
+ const title = await ipc
38
+ .getProperty("media-title")
39
+ .catch(() => null);
40
+ if (title) {
41
+ const lines = [`Streaming from YouTube: ${title}`];
42
+ if (!isUrl && count > 1) {
43
+ lines.push(`(queued top ${count} results)`);
44
+ }
45
+ return { content: [{ type: "text", text: lines.join("\n") }] };
46
+ }
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: isUrl
52
+ ? "Loading YouTube URL... (may take a moment to buffer)"
53
+ : `Searching YouTube for "${query}"... (playback will start shortly)`,
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ catch (err) {
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: `YouTube playback failed: ${err instanceof Error ? err.message : err}`,
64
+ },
65
+ ],
66
+ isError: true,
67
+ };
68
+ }
69
+ });
70
+ server.registerTool("mpv_download", {
71
+ title: "Download from YouTube",
72
+ description: "Download audio or video from YouTube. Specify format: \"audio\" for MP3, \"video\" for full video.",
73
+ inputSchema: {
74
+ query: z
75
+ .string()
76
+ .min(1)
77
+ .describe("YouTube URL or search term"),
78
+ format: z
79
+ .enum(["audio", "video"])
80
+ .describe("\"audio\" saves MP3 only, \"video\" saves full video"),
81
+ },
82
+ annotations: {
83
+ readOnlyHint: false,
84
+ destructiveHint: false,
85
+ idempotentHint: false,
86
+ openWorldHint: true,
87
+ },
88
+ }, async ({ query, format }) => {
89
+ try {
90
+ const fmt = format;
91
+ const isUrl = query.includes("youtube.com/") || query.includes("youtu.be/");
92
+ const target = isUrl ? query : `ytsearch:${query}`;
93
+ if (!existsSync(DOWNLOAD_DIR)) {
94
+ mkdirSync(DOWNLOAD_DIR, { recursive: true });
95
+ }
96
+ const args = [];
97
+ if (fmt === "audio") {
98
+ args.push("-x", "--audio-format", "mp3", "--audio-quality", "0", "-o", join(DOWNLOAD_DIR, "%(artist,uploader)s - %(title)s.%(ext)s"));
99
+ }
100
+ else {
101
+ args.push("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best", "-o", join(DOWNLOAD_DIR, "%(artist,uploader)s - %(title)s.%(ext)s"));
102
+ }
103
+ args.push("--no-playlist", "--restrict-filenames", target);
104
+ // First, get the title so we can report what's downloading
105
+ const info = await runYtDlp(["--print", "title", "--no-playlist", target]);
106
+ const title = info.trim() || query;
107
+ const result = await runYtDlp(args);
108
+ // Invalidate the media scanner cache so the new file shows up
109
+ scanner.invalidateCache();
110
+ const typeLabel = fmt === "audio" ? "audio (MP3)" : "video";
111
+ return {
112
+ content: [
113
+ {
114
+ type: "text",
115
+ text: `Downloaded ${typeLabel}: ${title}\nSaved to: ${DOWNLOAD_DIR}`,
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ catch (err) {
121
+ return {
122
+ content: [
123
+ {
124
+ type: "text",
125
+ text: `Download failed: ${err instanceof Error ? err.message : err}`,
126
+ },
127
+ ],
128
+ isError: true,
129
+ };
130
+ }
131
+ });
132
+ }
133
+ function delay(ms) {
134
+ return new Promise((r) => setTimeout(r, ms));
135
+ }
136
+ function runYtDlp(args) {
137
+ return new Promise((resolve, reject) => {
138
+ execFile("yt-dlp", args, { timeout: 120_000 }, (err, stdout, stderr) => {
139
+ if (err) {
140
+ reject(new Error(stderr || err.message));
141
+ }
142
+ else {
143
+ resolve(stdout);
144
+ }
145
+ });
146
+ });
147
+ }
148
+ //# sourceMappingURL=youtube.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"youtube.js","sourceRoot":"","sources":["../../src/tools/youtube.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEhD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,UAAU,oBAAoB,CAClC,MAAiB,EACjB,GAAW,EACX,OAAqB;IAErB,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;QACE,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,mGAAmG;QACrG,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,4BAA4B,CAAC;YACzC,OAAO,EAAE,CAAC;iBACP,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,EAAE,CAAC;iBACP,OAAO,CAAC,CAAC,CAAC;iBACV,QAAQ,EAAE;iBACV,QAAQ,CAAC,uDAAuD,CAAC;SACrE;QACD,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,KAAK;YACrB,aAAa,EAAE,IAAI;SACpB;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,CAAC;YAC3B,MAAM,KAAK,GACT,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAChE,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,kBAAkB,KAAK,IAAI,KAAK,EAAE,CAAC;YAElE,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;YACnD,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,mDAAmD;YAEtE,MAAM,KAAK,GAAG,MAAM,GAAG;iBACpB,WAAW,CAAC,aAAa,CAAC;iBAC1B,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAErB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,KAAK,GAAG,CAAC,2BAA2B,KAAK,EAAE,CAAC,CAAC;gBACnD,IAAI,CAAC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;oBACxB,KAAK,CAAC,IAAI,CAAC,eAAe,KAAK,WAAW,CAAC,CAAC;gBAC9C,CAAC;gBACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1E,CAAC;YAED,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,KAAK;4BACT,CAAC,CAAC,sDAAsD;4BACxD,CAAC,CAAC,0BAA0B,KAAK,oCAAoC;qBACxE;iBACF;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,4BAA4B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE;qBAC7E;iBACF;gBACD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EACT,oGAAoG;QACtG,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,4BAA4B,CAAC;YACzC,MAAM,EAAE,CAAC;iBACN,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;iBACxB,QAAQ,CAAC,sDAAsD,CAAC;SACpE;QACD,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,KAAK;YACrB,aAAa,EAAE,IAAI;SACpB;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAC1B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,CAAC;YACnB,MAAM,KAAK,GACT,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAChE,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,KAAK,EAAE,CAAC;YAEnD,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC9B,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,CAAC;YAED,MAAM,IAAI,GAAa,EAAE,CAAC;YAE1B,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;gBACpB,IAAI,CAAC,IAAI,CACP,IAAI,EACJ,gBAAgB,EAAE,KAAK,EACvB,iBAAiB,EAAE,GAAG,EACtB,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,yCAAyC,CAAC,CACpE,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,IAAI,CACP,IAAI,EAAE,0DAA0D,EAChE,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,yCAAyC,CAAC,CACpE,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,sBAAsB,EAAE,MAAM,CAAC,CAAC;YAE3D,2DAA2D;YAC3D,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;YAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC;YAEnC,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;YAEpC,8DAA8D;YAC9D,OAAO,CAAC,eAAe,EAAE,CAAC;YAE1B,MAAM,SAAS,GAAG,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC;YAC5D,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,cAAc,SAAS,KAAK,KAAK,eAAe,YAAY,EAAE;qBACrE;iBACF;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,oBAAoB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE;qBACrE;iBACF;gBACD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,QAAQ,CAAC,IAAc;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACrE,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,MAAM,CAAC,CAAC;YAClB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** Format seconds into MM:SS or H:MM:SS */
2
+ export declare function formatDuration(seconds: number | null | undefined): string;
3
+ /** Parse a seek target string into seconds. Supports "90", "1:30", "1:02:30", "+10", "-30" */
4
+ export declare function parseSeekTarget(target: string): {
5
+ seconds: number;
6
+ mode: "absolute" | "relative";
7
+ };
@@ -0,0 +1,34 @@
1
+ /** Format seconds into MM:SS or H:MM:SS */
2
+ export function formatDuration(seconds) {
3
+ if (seconds == null || seconds < 0)
4
+ return "--:--";
5
+ const s = Math.floor(seconds);
6
+ const h = Math.floor(s / 3600);
7
+ const m = Math.floor((s % 3600) / 60);
8
+ const sec = s % 60;
9
+ const pad = (n) => n.toString().padStart(2, "0");
10
+ return h > 0 ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`;
11
+ }
12
+ /** Parse a seek target string into seconds. Supports "90", "1:30", "1:02:30", "+10", "-30" */
13
+ export function parseSeekTarget(target) {
14
+ const trimmed = target.trim();
15
+ // Relative seek: "+10" or "-30"
16
+ if (trimmed.startsWith("+") || trimmed.startsWith("-")) {
17
+ return { seconds: parseFloat(trimmed), mode: "relative" };
18
+ }
19
+ // Timestamp: "1:30" or "1:02:30"
20
+ if (trimmed.includes(":")) {
21
+ const parts = trimmed.split(":").map(Number);
22
+ let total = 0;
23
+ if (parts.length === 3) {
24
+ total = parts[0] * 3600 + parts[1] * 60 + parts[2];
25
+ }
26
+ else if (parts.length === 2) {
27
+ total = parts[0] * 60 + parts[1];
28
+ }
29
+ return { seconds: total, mode: "absolute" };
30
+ }
31
+ // Plain number: absolute seconds
32
+ return { seconds: parseFloat(trimmed), mode: "absolute" };
33
+ }
34
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,MAAM,UAAU,cAAc,CAAC,OAAkC;IAC/D,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IACnD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC;IACnB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACzD,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAE9B,gCAAgC;IAChC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC5D,CAAC;IAED,iCAAiC;IACjC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IAED,iCAAiC;IACjC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,16 @@
1
+ export interface ScoredResult<T> {
2
+ item: T;
3
+ score: number;
4
+ }
5
+ /**
6
+ * Score how well `query` matches `text`.
7
+ * Returns 0 for no match, higher for better matches.
8
+ *
9
+ * Scoring:
10
+ * - Exact substring match: 100
11
+ * - All query words present: 60 + (10 * word count bonus)
12
+ * - Partial word matches: 10 per matched word
13
+ */
14
+ export declare function fuzzyScore(query: string, text: string): number;
15
+ /** Search items by scoring a text field, return top N sorted by score descending */
16
+ export declare function fuzzySearch<T>(items: T[], query: string, getText: (item: T) => string, limit?: number): ScoredResult<T>[];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Score how well `query` matches `text`.
3
+ * Returns 0 for no match, higher for better matches.
4
+ *
5
+ * Scoring:
6
+ * - Exact substring match: 100
7
+ * - All query words present: 60 + (10 * word count bonus)
8
+ * - Partial word matches: 10 per matched word
9
+ */
10
+ export function fuzzyScore(query, text) {
11
+ const q = query.toLowerCase().trim();
12
+ const t = text.toLowerCase();
13
+ if (!q)
14
+ return 0;
15
+ // Exact substring match is the strongest signal
16
+ if (t.includes(q))
17
+ return 100;
18
+ // Split query into words and check each
19
+ const words = q.split(/\s+/).filter(Boolean);
20
+ if (words.length === 0)
21
+ return 0;
22
+ const matched = words.filter((w) => t.includes(w));
23
+ if (matched.length === words.length) {
24
+ // All words present
25
+ return 60 + matched.length * 10;
26
+ }
27
+ // Partial matches
28
+ return matched.length * 10;
29
+ }
30
+ /** Search items by scoring a text field, return top N sorted by score descending */
31
+ export function fuzzySearch(items, query, getText, limit = 10) {
32
+ return items
33
+ .map((item) => ({ item, score: fuzzyScore(query, getText(item)) }))
34
+ .filter((r) => r.score > 0)
35
+ .sort((a, b) => b.score - a.score)
36
+ .slice(0, limit);
37
+ }
38
+ //# sourceMappingURL=fuzzy-match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fuzzy-match.js","sourceRoot":"","sources":["../../src/utils/fuzzy-match.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,IAAY;IACpD,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IACrC,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAE7B,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAEjB,gDAAgD;IAChD,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IAE9B,wCAAwC;IACxC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAEjC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnD,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QACpC,oBAAoB;QACpB,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC;IAClC,CAAC;IAED,kBAAkB;IAClB,OAAO,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC;AAC7B,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,WAAW,CACzB,KAAU,EACV,KAAa,EACb,OAA4B,EAC5B,KAAK,GAAG,EAAE;IAEV,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;SAClE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;SAC1B,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;SACjC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AACrB,CAAC"}
@@ -0,0 +1,2 @@
1
+ /** Normalize backslashes to forward slashes for mpv IPC */
2
+ export declare function toMpvPath(p: string): string;
@@ -0,0 +1,5 @@
1
+ /** Normalize backslashes to forward slashes for mpv IPC */
2
+ export function toMpvPath(p) {
3
+ return p.replace(/\\/g, "/");
4
+ }
5
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../../src/utils/path.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "mpv-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for controlling mpv media player — playback, playlists, YouTube streaming and downloading",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mpv-mcp-server": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "keywords": [
14
+ "mcp",
15
+ "mpv",
16
+ "music",
17
+ "media",
18
+ "youtube",
19
+ "claude",
20
+ "model-context-protocol"
21
+ ],
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/arijit-gogoi/mpv-mcp-server.git"
26
+ },
27
+ "homepage": "https://github.com/arijit-gogoi/mpv-mcp-server#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/arijit-gogoi/mpv-mcp-server/issues"
30
+ },
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "start": "node dist/index.js",
37
+ "dev": "tsc --watch"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.29.0",
41
+ "zod": "^3.24.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "typescript": "^5.7.0"
46
+ }
47
+ }