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.
- package/README.md +123 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/services/media-scanner.d.ts +19 -0
- package/dist/services/media-scanner.js +61 -0
- package/dist/services/media-scanner.js.map +1 -0
- package/dist/services/mpv-ipc.d.ts +23 -0
- package/dist/services/mpv-ipc.js +112 -0
- package/dist/services/mpv-ipc.js.map +1 -0
- package/dist/services/mpv-process.d.ts +9 -0
- package/dist/services/mpv-process.js +73 -0
- package/dist/services/mpv-process.js.map +1 -0
- package/dist/tools/browse.d.ts +3 -0
- package/dist/tools/browse.js +86 -0
- package/dist/tools/browse.js.map +1 -0
- package/dist/tools/navigation.d.ts +3 -0
- package/dist/tools/navigation.js +62 -0
- package/dist/tools/navigation.js.map +1 -0
- package/dist/tools/play.d.ts +4 -0
- package/dist/tools/play.js +82 -0
- package/dist/tools/play.js.map +1 -0
- package/dist/tools/playback.d.ts +3 -0
- package/dist/tools/playback.js +69 -0
- package/dist/tools/playback.js.map +1 -0
- package/dist/tools/playlist.d.ts +4 -0
- package/dist/tools/playlist.js +174 -0
- package/dist/tools/playlist.js.map +1 -0
- package/dist/tools/seek.d.ts +3 -0
- package/dist/tools/seek.js +57 -0
- package/dist/tools/seek.js.map +1 -0
- package/dist/tools/status.d.ts +3 -0
- package/dist/tools/status.js +58 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/volume.d.ts +3 -0
- package/dist/tools/volume.js +43 -0
- package/dist/tools/volume.js.map +1 -0
- package/dist/tools/youtube.d.ts +4 -0
- package/dist/tools/youtube.js +148 -0
- package/dist/tools/youtube.js.map +1 -0
- package/dist/utils/format.d.ts +7 -0
- package/dist/utils/format.js +34 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/fuzzy-match.d.ts +16 -0
- package/dist/utils/fuzzy-match.js +38 -0
- package/dist/utils/fuzzy-match.js.map +1 -0
- package/dist/utils/path.d.ts +2 -0
- package/dist/utils/path.js +5 -0
- package/dist/utils/path.js.map +1 -0
- 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
|
package/dist/config.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
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,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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"}
|