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
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # mpv-mcp-server
2
+
3
+ MCP server for controlling [mpv](https://mpv.io) media player. Browse your music library, control playback, stream from YouTube, and download tracks — all from inside an MCP client like Claude Code.
4
+
5
+ ## Prerequisites
6
+
7
+ - **[mpv](https://mpv.io/installation/)** — media player (must be on your PATH, or set `MPV_PATH`)
8
+ - **[Node.js](https://nodejs.org/) 22+**
9
+ - **[yt-dlp](https://github.com/yt-dlp/yt-dlp)** *(optional)* — required for YouTube streaming and downloading
10
+ - **[ffmpeg](https://ffmpeg.org/)** *(optional)* — required for audio extraction when downloading from YouTube
11
+
12
+ ## Quick Start
13
+
14
+ ### Claude Code
15
+
16
+ Add to your project's `.mcp.json`:
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "mpv": {
22
+ "command": "npx",
23
+ "args": ["-y", "mpv-mcp-server"]
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ Or add at user scope (available in all projects):
30
+
31
+ ```bash
32
+ claude mcp add mpv --scope user -- npx -y mpv-mcp-server
33
+ ```
34
+
35
+ ### Claude Desktop
36
+
37
+ Add to your Claude Desktop config:
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "mpv": {
43
+ "command": "npx",
44
+ "args": ["-y", "mpv-mcp-server"]
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ### With environment overrides
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "mpv": {
56
+ "command": "npx",
57
+ "args": ["-y", "mpv-mcp-server"],
58
+ "env": {
59
+ "MPV_PATH": "/usr/local/bin/mpv",
60
+ "MPV_MEDIA_DIRS": "/home/user/Music,/home/user/Podcasts",
61
+ "MPV_DOWNLOAD_DIR": "/home/user/Music"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ All configuration is via environment variables. Everything has sensible defaults.
71
+
72
+ | Variable | Default | Description |
73
+ |---|---|---|
74
+ | `MPV_PATH` | `mpv` | Path to mpv executable |
75
+ | `MPV_IPC_PATH` | `\\.\pipe\mpvpipe` (Windows) or `/tmp/mpv-ipc.sock` (Unix) | IPC socket path |
76
+ | `MPV_MEDIA_DIRS` | `~/Music,~/Videos` | Comma-separated media directories to scan |
77
+ | `MPV_DOWNLOAD_DIR` | `~/Downloads` | Where downloaded files are saved |
78
+
79
+ ## Tools
80
+
81
+ ### Playback
82
+
83
+ | Tool | Description |
84
+ |---|---|
85
+ | `mpv_play` | Play a file by path or search term |
86
+ | `mpv_pause` | Pause playback |
87
+ | `mpv_resume` | Resume playback |
88
+ | `mpv_stop` | Stop playback |
89
+ | `mpv_status` | Get current playback status |
90
+ | `mpv_seek` | Seek to position (`"90"`, `"1:30"`, `"+10"`, `"-30"`) |
91
+ | `mpv_volume` | Get or set volume (0-150) |
92
+
93
+ ### Library
94
+
95
+ | Tool | Description |
96
+ |---|---|
97
+ | `mpv_browse` | List and search available media files |
98
+ | `mpv_playlist` | Show current playlist |
99
+ | `mpv_add` | Add a track to the playlist |
100
+ | `mpv_load_playlist` | Load a playlist file (.m3u, .pls, .txt) |
101
+ | `mpv_next` | Skip to next track |
102
+ | `mpv_prev` | Go to previous track |
103
+
104
+ ### YouTube
105
+
106
+ | Tool | Description |
107
+ |---|---|
108
+ | `mpv_youtube` | Search YouTube and stream through mpv |
109
+ | `mpv_download` | Download from YouTube (audio or video) |
110
+
111
+ YouTube tools require [yt-dlp](https://github.com/yt-dlp/yt-dlp) on your PATH. Audio downloads also require [ffmpeg](https://ffmpeg.org/).
112
+
113
+ ## How It Works
114
+
115
+ The server communicates with mpv via its [JSON IPC protocol](https://mpv.io/manual/master/#json-ipc). On Windows this uses a named pipe, on macOS/Linux a Unix domain socket. If mpv isn't running, the server spawns it automatically in idle mode. The mpv process is detached, so it keeps playing even if the MCP server exits.
116
+
117
+ ## Platform Support
118
+
119
+ Developed and tested on **Windows**. macOS/Linux support is implemented but untested — issues and PRs welcome!
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,12 @@
1
+ export declare const IS_WINDOWS: boolean;
2
+ export declare const MPV_PATH: string;
3
+ export declare const PIPE_PATH: string;
4
+ export declare const MEDIA_DIRS: string[];
5
+ export declare const DOWNLOAD_DIR: string;
6
+ export declare const MEDIA_EXTENSIONS: Set<string>;
7
+ export declare const PLAYLIST_EXTENSIONS: Set<string>;
8
+ export declare const MPV_SPAWN_ARGS: string[];
9
+ export declare const CACHE_TTL_MS = 60000;
10
+ export declare const IPC_TIMEOUT_MS = 5000;
11
+ export declare const MPV_STARTUP_TIMEOUT_MS = 3000;
12
+ export declare const MPV_STARTUP_POLL_MS = 200;
package/dist/config.js ADDED
@@ -0,0 +1,30 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ export const IS_WINDOWS = process.platform === "win32";
4
+ export const MPV_PATH = process.env.MPV_PATH ?? "mpv";
5
+ function defaultIpcPath() {
6
+ return IS_WINDOWS ? "\\\\.\\pipe\\mpvpipe" : "/tmp/mpv-ipc.sock";
7
+ }
8
+ export const PIPE_PATH = process.env.MPV_IPC_PATH ?? defaultIpcPath();
9
+ export const MEDIA_DIRS = process.env.MPV_MEDIA_DIRS
10
+ ? process.env.MPV_MEDIA_DIRS.split(",").map((d) => d.trim())
11
+ : [join(homedir(), "Music"), join(homedir(), "Videos")];
12
+ export const DOWNLOAD_DIR = process.env.MPV_DOWNLOAD_DIR ?? join(homedir(), "Downloads");
13
+ export const MEDIA_EXTENSIONS = new Set([
14
+ ".mp3", ".mp4", ".m4a", ".flac", ".wav", ".ogg",
15
+ ".mkv", ".webm", ".avi", ".wma", ".aac", ".opus",
16
+ ]);
17
+ export const PLAYLIST_EXTENSIONS = new Set([
18
+ ".m3u", ".m3u8", ".txt", ".pls",
19
+ ]);
20
+ export const MPV_SPAWN_ARGS = [
21
+ "--idle=yes",
22
+ "--no-terminal",
23
+ `--input-ipc-server=${PIPE_PATH}`,
24
+ "--force-window=no",
25
+ ];
26
+ export const CACHE_TTL_MS = 60_000;
27
+ export const IPC_TIMEOUT_MS = 5_000;
28
+ export const MPV_STARTUP_TIMEOUT_MS = 3_000;
29
+ export const MPV_STARTUP_POLL_MS = 200;
30
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;AAEvD,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,KAAK,CAAC;AAEtD,SAAS,cAAc;IACrB,OAAO,UAAU,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,mBAAmB,CAAC;AACnE,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,cAAc,EAAE,CAAC;AAEtE,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc;IAClD,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,YAAY,GACvB,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;AAE/D,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IACtC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM;IAC/C,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;CACjD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IACzC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM;CAChC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,YAAY;IACZ,eAAe;IACf,sBAAsB,SAAS,EAAE;IACjC,mBAAmB;CACpB,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAC;AACpC,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAC;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { MpvProcessManager } from "./services/mpv-process.js";
5
+ import { MpvIPC } from "./services/mpv-ipc.js";
6
+ import { MediaScanner } from "./services/media-scanner.js";
7
+ import { registerStatusTool } from "./tools/status.js";
8
+ import { registerPlayTool } from "./tools/play.js";
9
+ import { registerPlaybackTools } from "./tools/playback.js";
10
+ import { registerNavigationTools } from "./tools/navigation.js";
11
+ import { registerSeekTool } from "./tools/seek.js";
12
+ import { registerVolumeTool } from "./tools/volume.js";
13
+ import { registerPlaylistTools } from "./tools/playlist.js";
14
+ import { registerBrowseTool } from "./tools/browse.js";
15
+ import { registerYoutubeTools } from "./tools/youtube.js";
16
+ // Services
17
+ const processManager = new MpvProcessManager();
18
+ const ipc = new MpvIPC(processManager);
19
+ const scanner = new MediaScanner();
20
+ // Server
21
+ const server = new McpServer({
22
+ name: "mpv-mcp-server",
23
+ version: "1.0.0",
24
+ });
25
+ // Register all tools
26
+ registerStatusTool(server, ipc);
27
+ registerPlayTool(server, ipc, scanner);
28
+ registerPlaybackTools(server, ipc);
29
+ registerNavigationTools(server, ipc);
30
+ registerSeekTool(server, ipc);
31
+ registerVolumeTool(server, ipc);
32
+ registerPlaylistTools(server, ipc, scanner);
33
+ registerBrowseTool(server, scanner);
34
+ registerYoutubeTools(server, ipc, scanner);
35
+ // Cleanup on exit
36
+ function cleanup() {
37
+ ipc.disconnect();
38
+ }
39
+ process.on("exit", cleanup);
40
+ process.on("SIGINT", () => {
41
+ cleanup();
42
+ process.exit(0);
43
+ });
44
+ process.on("SIGTERM", () => {
45
+ cleanup();
46
+ process.exit(0);
47
+ });
48
+ // Start
49
+ const transport = new StdioServerTransport();
50
+ await server.connect(transport);
51
+ console.error("mpv MCP server running on stdio");
52
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,WAAW;AACX,MAAM,cAAc,GAAG,IAAI,iBAAiB,EAAE,CAAC;AAC/C,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC,cAAc,CAAC,CAAC;AACvC,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAC;AAEnC,SAAS;AACT,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,gBAAgB;IACtB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,qBAAqB;AACrB,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAChC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AACvC,qBAAqB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACnC,uBAAuB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACrC,gBAAgB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAC9B,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAChC,qBAAqB,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AAC5C,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACpC,oBAAoB,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AAE3C,kBAAkB;AAClB,SAAS,OAAO;IACd,GAAG,CAAC,UAAU,EAAE,CAAC;AACnB,CAAC;AAED,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC5B,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;IACxB,OAAO,EAAE,CAAC;IACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;IACzB,OAAO,EAAE,CAAC;IACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,QAAQ;AACR,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAChC,OAAO,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ export interface MediaFile {
2
+ path: string;
3
+ name: string;
4
+ ext: string;
5
+ dir: string;
6
+ isPlaylist: boolean;
7
+ }
8
+ export declare class MediaScanner {
9
+ private cache;
10
+ private lastScanTime;
11
+ /** Scan configured directories for media files, with caching */
12
+ scan(): Promise<MediaFile[]>;
13
+ /** Search media files by fuzzy matching against filename */
14
+ search(query: string, limit?: number): Promise<MediaFile[]>;
15
+ /** List available playlist files */
16
+ findPlaylists(): Promise<MediaFile[]>;
17
+ /** Force re-scan on next access */
18
+ invalidateCache(): void;
19
+ }
@@ -0,0 +1,61 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join, extname, basename, dirname } from "node:path";
3
+ import { MEDIA_DIRS, MEDIA_EXTENSIONS, PLAYLIST_EXTENSIONS, CACHE_TTL_MS } from "../config.js";
4
+ import { fuzzySearch } from "../utils/fuzzy-match.js";
5
+ export class MediaScanner {
6
+ cache = null;
7
+ lastScanTime = 0;
8
+ /** Scan configured directories for media files, with caching */
9
+ async scan() {
10
+ if (this.cache && Date.now() - this.lastScanTime < CACHE_TTL_MS) {
11
+ return this.cache;
12
+ }
13
+ const allFiles = [];
14
+ for (const dir of MEDIA_DIRS) {
15
+ try {
16
+ const entries = await readdir(dir, { recursive: true, withFileTypes: true });
17
+ for (const entry of entries) {
18
+ if (!entry.isFile())
19
+ continue;
20
+ const ext = extname(entry.name).toLowerCase();
21
+ const isMedia = MEDIA_EXTENSIONS.has(ext);
22
+ const isPlaylist = PLAYLIST_EXTENSIONS.has(ext);
23
+ if (!isMedia && !isPlaylist)
24
+ continue;
25
+ const parentPath = entry.parentPath ?? entry.path;
26
+ const fullPath = join(parentPath, entry.name);
27
+ allFiles.push({
28
+ path: fullPath,
29
+ name: basename(entry.name, ext),
30
+ ext,
31
+ dir: dirname(fullPath),
32
+ isPlaylist,
33
+ });
34
+ }
35
+ }
36
+ catch {
37
+ // Directory doesn't exist or isn't readable — skip
38
+ }
39
+ }
40
+ this.cache = allFiles;
41
+ this.lastScanTime = Date.now();
42
+ return allFiles;
43
+ }
44
+ /** Search media files by fuzzy matching against filename */
45
+ async search(query, limit = 10) {
46
+ const files = await this.scan();
47
+ const media = files.filter((f) => !f.isPlaylist);
48
+ const results = fuzzySearch(media, query, (f) => f.name, limit);
49
+ return results.map((r) => r.item);
50
+ }
51
+ /** List available playlist files */
52
+ async findPlaylists() {
53
+ const files = await this.scan();
54
+ return files.filter((f) => f.isPlaylist);
55
+ }
56
+ /** Force re-scan on next access */
57
+ invalidateCache() {
58
+ this.cache = null;
59
+ }
60
+ }
61
+ //# sourceMappingURL=media-scanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-scanner.js","sourceRoot":"","sources":["../../src/services/media-scanner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { MpvProcessManager } from "./mpv-process.js";
2
+ export declare class MpvIPC {
3
+ private processManager;
4
+ private socket;
5
+ private requestId;
6
+ private pendingRequests;
7
+ private buffer;
8
+ constructor(processManager: MpvProcessManager);
9
+ /** Connect to mpv's named pipe. Ensures mpv is running first. */
10
+ connect(): Promise<void>;
11
+ /** Disconnect from the pipe */
12
+ disconnect(): void;
13
+ /** Send a command to mpv and return the result */
14
+ command(args: (string | number | boolean)[]): Promise<unknown>;
15
+ /** Shorthand: get a property value */
16
+ getProperty(name: string): Promise<unknown>;
17
+ /** Shorthand: set a property */
18
+ setProperty(name: string, value: string | number | boolean): Promise<void>;
19
+ /** Process buffered data from the pipe, handling complete JSON lines */
20
+ private processBuffer;
21
+ private handleMessage;
22
+ private rejectAll;
23
+ }
@@ -0,0 +1,112 @@
1
+ import net from "node:net";
2
+ import { PIPE_PATH, IPC_TIMEOUT_MS } from "../config.js";
3
+ export class MpvIPC {
4
+ processManager;
5
+ socket = null;
6
+ requestId = 0;
7
+ pendingRequests = new Map();
8
+ buffer = "";
9
+ constructor(processManager) {
10
+ this.processManager = processManager;
11
+ }
12
+ /** Connect to mpv's named pipe. Ensures mpv is running first. */
13
+ async connect() {
14
+ if (this.socket)
15
+ return;
16
+ await this.processManager.ensureRunning();
17
+ return new Promise((resolve, reject) => {
18
+ const sock = net.connect(PIPE_PATH);
19
+ sock.on("connect", () => {
20
+ this.socket = sock;
21
+ this.buffer = "";
22
+ resolve();
23
+ });
24
+ sock.on("data", (chunk) => {
25
+ this.buffer += chunk.toString();
26
+ this.processBuffer();
27
+ });
28
+ sock.on("error", (err) => {
29
+ this.rejectAll(err.message);
30
+ this.socket = null;
31
+ reject(err);
32
+ });
33
+ sock.on("close", () => {
34
+ this.rejectAll("pipe closed");
35
+ this.socket = null;
36
+ });
37
+ });
38
+ }
39
+ /** Disconnect from the pipe */
40
+ disconnect() {
41
+ if (this.socket) {
42
+ this.socket.destroy();
43
+ this.socket = null;
44
+ }
45
+ this.rejectAll("disconnected");
46
+ }
47
+ /** Send a command to mpv and return the result */
48
+ async command(args) {
49
+ if (!this.socket) {
50
+ await this.connect();
51
+ }
52
+ const id = ++this.requestId;
53
+ const msg = JSON.stringify({ command: args, request_id: id }) + "\n";
54
+ return new Promise((resolve, reject) => {
55
+ const timer = setTimeout(() => {
56
+ this.pendingRequests.delete(id);
57
+ reject(new Error(`mpv command timed out: ${args[0]}`));
58
+ }, IPC_TIMEOUT_MS);
59
+ this.pendingRequests.set(id, { resolve, reject, timer });
60
+ this.socket.write(msg);
61
+ });
62
+ }
63
+ /** Shorthand: get a property value */
64
+ async getProperty(name) {
65
+ return this.command(["get_property", name]);
66
+ }
67
+ /** Shorthand: set a property */
68
+ async setProperty(name, value) {
69
+ await this.command(["set_property", name, value]);
70
+ }
71
+ /** Process buffered data from the pipe, handling complete JSON lines */
72
+ processBuffer() {
73
+ const lines = this.buffer.split("\n");
74
+ // Keep the last (possibly incomplete) chunk in the buffer
75
+ this.buffer = lines.pop() ?? "";
76
+ for (const line of lines) {
77
+ if (!line.trim())
78
+ continue;
79
+ try {
80
+ const msg = JSON.parse(line);
81
+ this.handleMessage(msg);
82
+ }
83
+ catch {
84
+ // Malformed JSON — skip
85
+ }
86
+ }
87
+ }
88
+ handleMessage(msg) {
89
+ // Unsolicited events have no request_id — ignore them
90
+ if (msg.request_id == null)
91
+ return;
92
+ const pending = this.pendingRequests.get(msg.request_id);
93
+ if (!pending)
94
+ return;
95
+ clearTimeout(pending.timer);
96
+ this.pendingRequests.delete(msg.request_id);
97
+ if (msg.error !== "success") {
98
+ pending.reject(new Error(`mpv error: ${msg.error}`));
99
+ }
100
+ else {
101
+ pending.resolve(msg.data);
102
+ }
103
+ }
104
+ rejectAll(reason) {
105
+ for (const [id, pending] of this.pendingRequests) {
106
+ clearTimeout(pending.timer);
107
+ pending.reject(new Error(reason));
108
+ }
109
+ this.pendingRequests.clear();
110
+ }
111
+ }
112
+ //# sourceMappingURL=mpv-ipc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mpv-ipc.js","sourceRoot":"","sources":["../../src/services/mpv-ipc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export declare class MpvProcessManager {
2
+ private childProcess;
3
+ /** Test whether the named pipe is connectable */
4
+ isRunning(): Promise<boolean>;
5
+ /** Ensure mpv is running with IPC enabled. Spawns if needed. */
6
+ ensureRunning(): Promise<void>;
7
+ /** Shut down the mpv process we spawned (if any) */
8
+ shutdown(): void;
9
+ }
@@ -0,0 +1,73 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, unlinkSync } from "node:fs";
3
+ import net from "node:net";
4
+ import { MPV_PATH, MPV_SPAWN_ARGS, PIPE_PATH, MPV_STARTUP_TIMEOUT_MS, MPV_STARTUP_POLL_MS, IS_WINDOWS } from "../config.js";
5
+ export class MpvProcessManager {
6
+ childProcess = null;
7
+ /** Test whether the named pipe is connectable */
8
+ async isRunning() {
9
+ return new Promise((resolve) => {
10
+ const sock = net.connect(PIPE_PATH);
11
+ sock.on("connect", () => {
12
+ sock.destroy();
13
+ resolve(true);
14
+ });
15
+ sock.on("error", () => {
16
+ resolve(false);
17
+ });
18
+ });
19
+ }
20
+ /** Ensure mpv is running with IPC enabled. Spawns if needed. */
21
+ async ensureRunning() {
22
+ if (await this.isRunning())
23
+ return;
24
+ // On Unix, clean up stale socket from a previous crashed mpv
25
+ if (!IS_WINDOWS && existsSync(PIPE_PATH)) {
26
+ try {
27
+ unlinkSync(PIPE_PATH);
28
+ }
29
+ catch { }
30
+ }
31
+ // Spawn mpv in idle mode with IPC
32
+ try {
33
+ this.childProcess = spawn(MPV_PATH, MPV_SPAWN_ARGS, {
34
+ detached: true,
35
+ stdio: "ignore",
36
+ windowsHide: true,
37
+ });
38
+ // Unref so the MCP server can exit without waiting for mpv
39
+ this.childProcess.unref();
40
+ this.childProcess.on("error", (err) => {
41
+ console.error(`mpv process error: ${err.message}`);
42
+ this.childProcess = null;
43
+ });
44
+ this.childProcess.on("exit", () => {
45
+ this.childProcess = null;
46
+ });
47
+ }
48
+ catch (err) {
49
+ throw new Error(`Failed to start mpv at ${MPV_PATH}. Is mpv installed and on your PATH?\n${err instanceof Error ? err.message : err}`);
50
+ }
51
+ // Poll until pipe becomes available
52
+ const deadline = Date.now() + MPV_STARTUP_TIMEOUT_MS;
53
+ while (Date.now() < deadline) {
54
+ if (await this.isRunning())
55
+ return;
56
+ await new Promise((r) => setTimeout(r, MPV_STARTUP_POLL_MS));
57
+ }
58
+ throw new Error(`mpv started but IPC pipe ${PIPE_PATH} not available after ${MPV_STARTUP_TIMEOUT_MS}ms`);
59
+ }
60
+ /** Shut down the mpv process we spawned (if any) */
61
+ shutdown() {
62
+ if (this.childProcess) {
63
+ try {
64
+ this.childProcess.kill();
65
+ }
66
+ catch {
67
+ // already gone
68
+ }
69
+ this.childProcess = null;
70
+ }
71
+ }
72
+ }
73
+ //# sourceMappingURL=mpv-process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mpv-process.js","sourceRoot":"","sources":["../../src/services/mpv-process.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { MediaScanner } from "../services/media-scanner.js";
3
+ export declare function registerBrowseTool(server: McpServer, scanner: MediaScanner): void;
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ import { MEDIA_DIRS } from "../config.js";
3
+ export function registerBrowseTool(server, scanner) {
4
+ server.registerTool("mpv_browse", {
5
+ title: "Browse Music Library",
6
+ description: "List available music and video files. Optionally filter by search term or directory.",
7
+ inputSchema: {
8
+ search: z
9
+ .string()
10
+ .optional()
11
+ .describe("Filter files by search term"),
12
+ directory: z
13
+ .enum(["music", "videos", "all"])
14
+ .default("all")
15
+ .optional()
16
+ .describe("Which directory to browse"),
17
+ limit: z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .max(100)
22
+ .default(25)
23
+ .optional()
24
+ .describe("Maximum number of results (default 25)"),
25
+ },
26
+ annotations: {
27
+ readOnlyHint: true,
28
+ destructiveHint: false,
29
+ idempotentHint: true,
30
+ openWorldHint: false,
31
+ },
32
+ }, async ({ search, directory, limit }) => {
33
+ try {
34
+ const maxResults = limit ?? 25;
35
+ const dir = directory ?? "all";
36
+ let files;
37
+ if (search) {
38
+ files = await scanner.search(search, maxResults);
39
+ }
40
+ else {
41
+ const all = await scanner.scan();
42
+ files = all.filter((f) => !f.isPlaylist);
43
+ }
44
+ // Filter by directory
45
+ if (dir !== "all") {
46
+ const targetDir = dir === "music" ? MEDIA_DIRS[0] : MEDIA_DIRS[1];
47
+ files = files.filter((f) => f.path.toLowerCase().startsWith(targetDir.toLowerCase()));
48
+ }
49
+ const total = files.length;
50
+ const display = files.slice(0, maxResults);
51
+ if (display.length === 0) {
52
+ return {
53
+ content: [
54
+ {
55
+ type: "text",
56
+ text: search
57
+ ? `No files matching "${search}" found.`
58
+ : "No media files found.",
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ const lines = [`Music Library (${total} files):`];
64
+ for (let i = 0; i < display.length; i++) {
65
+ const f = display[i];
66
+ lines.push(` ${i + 1}. ${f.name}${f.ext}`);
67
+ }
68
+ if (total > maxResults) {
69
+ lines.push(` [showing ${maxResults} of ${total}]`);
70
+ }
71
+ return { content: [{ type: "text", text: lines.join("\n") }] };
72
+ }
73
+ catch (err) {
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: `Error scanning library: ${err instanceof Error ? err.message : err}`,
79
+ },
80
+ ],
81
+ isError: true,
82
+ };
83
+ }
84
+ });
85
+ }
86
+ //# sourceMappingURL=browse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browse.js","sourceRoot":"","sources":["../../src/tools/browse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { MpvIPC } from "../services/mpv-ipc.js";
3
+ export declare function registerNavigationTools(server: McpServer, ipc: MpvIPC): void;