@powerfm/libretime-mcp 0.3.0 → 0.5.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 +37 -4
- package/dist/http/admin.js +4 -0
- package/dist/http/client.js +2 -0
- package/dist/prompts/index.js +66 -0
- package/dist/stdio/admin.js +4 -0
- package/dist/stdio/client.js +2 -0
- package/dist/tools/admin/get_hosts.js +1 -3
- package/dist/tools/admin/get_users.js +9 -4
- package/dist/tools/admin/index.js +4 -0
- package/dist/tools/admin/types.js +6 -6
- package/dist/tools/playlists/add_to_playlist.js +31 -0
- package/dist/tools/playlists/create_playlist.js +17 -0
- package/dist/tools/playlists/get_playlist_contents.js +13 -0
- package/dist/tools/playlists/get_playlists.js +10 -0
- package/dist/tools/playlists/index.js +10 -0
- package/dist/tools/playlists/types.js +24 -0
- package/dist/tools/shows/create_show.js +27 -0
- package/dist/tools/shows/get_show.js +11 -0
- package/dist/tools/shows/get_show_instances.js +22 -0
- package/dist/tools/shows/get_station_info.js +13 -0
- package/dist/tools/shows/index.js +6 -0
- package/dist/tools/shows/schedule_file.js +56 -0
- package/dist/tools/shows/types.js +22 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,11 @@ Tools are organised into subdirectories under `src/tools/` — one file per tool
|
|
|
12
12
|
|
|
13
13
|
**Read-only (client & admin)**
|
|
14
14
|
- `get_shows` — list all shows
|
|
15
|
+
- `get_show` — get a single show by ID
|
|
16
|
+
- `get_show_instances` — list scheduled show slots (filterable by show, date range)
|
|
15
17
|
- `get_schedule` — broadcast schedule
|
|
16
18
|
- `get_stream_state` — current on-air state
|
|
19
|
+
- `get_station_info` — station name, timezone, and configuration
|
|
17
20
|
|
|
18
21
|
**Analytics (admin)**
|
|
19
22
|
- ~~`get_listener_counts`~~ — disabled (API returns full history with no filtering, ~120k records)
|
|
@@ -21,13 +24,32 @@ Tools are organised into subdirectories under `src/tools/` — one file per tool
|
|
|
21
24
|
|
|
22
25
|
**Media library (admin)**
|
|
23
26
|
- `search_files` — search your media library
|
|
24
|
-
- `upload_file` — upload an audio file
|
|
27
|
+
- `upload_file` — upload an audio file via drag-and-drop UI (works in both stdio and HTTP modes — see [File Upload](#file-upload))
|
|
25
28
|
- `update_file_metadata` — edit track metadata
|
|
26
29
|
- `delete_file` — remove a file
|
|
27
30
|
|
|
31
|
+
**Shows & scheduling (admin)**
|
|
32
|
+
- `create_show` — create a new show
|
|
33
|
+
- `schedule_file` — schedule an uploaded file into a show instance
|
|
34
|
+
|
|
35
|
+
**Playlists (admin)**
|
|
36
|
+
- `get_playlists` — list all playlists
|
|
37
|
+
- `create_playlist` — create a new playlist
|
|
38
|
+
- `get_playlist_contents` — list items in a playlist
|
|
39
|
+
- `add_to_playlist` — add a file or stream to a playlist
|
|
40
|
+
|
|
28
41
|
**Users (admin)**
|
|
29
|
-
- `get_users` — list station users
|
|
30
|
-
- `get_hosts` — list show hosts
|
|
42
|
+
- `get_users` — list station users (pass `include_email: true` to include email addresses, omitted by default)
|
|
43
|
+
- `get_hosts` — list show hosts with their show assignments
|
|
44
|
+
|
|
45
|
+
## File Upload
|
|
46
|
+
|
|
47
|
+
`upload_file` renders a drag-and-drop UI in the Claude chat window. It works the same way in both transport modes:
|
|
48
|
+
|
|
49
|
+
- **HTTP mode** — the UI posts directly to the MCP server's `/upload` endpoint
|
|
50
|
+
- **stdio mode** — the admin server spins up a lightweight sidecar HTTP server (default port `4000`) that the UI posts to. The MCP protocol itself continues over stdio; the upload is a side-channel.
|
|
51
|
+
|
|
52
|
+
Both modes use the same upload token generated at startup, so there's no separate configuration needed. To change the sidecar port in stdio mode set `UPLOAD_PORT` in your environment.
|
|
31
53
|
|
|
32
54
|
## Option 1 — Claude Desktop (stdio)
|
|
33
55
|
|
|
@@ -50,13 +72,16 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
50
72
|
"env": {
|
|
51
73
|
"LIBRETIME_URL": "https://your-instance.example.com",
|
|
52
74
|
"LIBRETIME_USER": "user",
|
|
53
|
-
"LIBRETIME_PASS": "pass"
|
|
75
|
+
"LIBRETIME_PASS": "pass",
|
|
76
|
+
"LIBRETIME_API_KEY": "your_libretime_api_key"
|
|
54
77
|
}
|
|
55
78
|
}
|
|
56
79
|
}
|
|
57
80
|
}
|
|
58
81
|
```
|
|
59
82
|
|
|
83
|
+
`LIBRETIME_API_KEY` is the `general.api_key` value from your LibreTime `config.yml`. It is required for file uploads — omit it if you are using the read-only client.
|
|
84
|
+
|
|
60
85
|
Use `libretime-mcp-client` instead of `libretime-mcp` for read-only access.
|
|
61
86
|
|
|
62
87
|
### npx (no install)
|
|
@@ -156,8 +181,16 @@ npm run build
|
|
|
156
181
|
|
|
157
182
|
# Tests
|
|
158
183
|
npm test
|
|
184
|
+
|
|
185
|
+
# MCP Inspector — browse and call tools interactively
|
|
186
|
+
npm run inspect:admin # stdio admin server
|
|
187
|
+
npm run inspect:client # stdio read-only server
|
|
188
|
+
npm run inspect:admin-http # HTTP admin (start the server first, then run this)
|
|
189
|
+
npm run inspect:client-http # HTTP read-only (start the server first, then run this)
|
|
159
190
|
```
|
|
160
191
|
|
|
192
|
+
The inspector opens a browser UI at `http://localhost:5173` where you can list tools, see their schemas, and call them with custom inputs.
|
|
193
|
+
|
|
161
194
|
## Servers
|
|
162
195
|
|
|
163
196
|
| Command | Transport | Port | Access |
|
package/dist/http/admin.js
CHANGED
|
@@ -9,6 +9,8 @@ import { register as registerShows } from '../tools/shows/index.js';
|
|
|
9
9
|
import { register as registerAnalytics } from '../tools/analytics/index.js';
|
|
10
10
|
import { register as registerAdmin } from '../tools/admin/index.js';
|
|
11
11
|
import { register as registerFiles } from '../tools/files/index.js';
|
|
12
|
+
import { register as registerPlaylists } from '../tools/playlists/index.js';
|
|
13
|
+
import { register as registerPrompts } from '../prompts/index.js';
|
|
12
14
|
import { registerUploadEndpoint } from './upload.js';
|
|
13
15
|
import { createHttpServer } from './server.js';
|
|
14
16
|
const PORT = parseInt(process.env.MCP_PORT ?? '3000', 10);
|
|
@@ -30,5 +32,7 @@ createHttpServer({
|
|
|
30
32
|
registerAnalytics(server);
|
|
31
33
|
registerAdmin(server);
|
|
32
34
|
registerFiles(server, uploadUrl, UPLOAD_TOKEN);
|
|
35
|
+
registerPlaylists(server);
|
|
36
|
+
registerPrompts(server);
|
|
33
37
|
},
|
|
34
38
|
});
|
package/dist/http/client.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
// Intended for network clients. For Claude Desktop use stdio/client.ts.
|
|
6
6
|
import '../env.js';
|
|
7
7
|
import { register as registerShows } from '../tools/shows/index.js';
|
|
8
|
+
import { register as registerPrompts } from '../prompts/index.js';
|
|
8
9
|
import { createHttpServer } from './server.js';
|
|
9
10
|
createHttpServer({
|
|
10
11
|
name: 'libretime-mcp-client',
|
|
11
12
|
defaultPort: 3001,
|
|
12
13
|
register: (server) => {
|
|
13
14
|
registerShows(server);
|
|
15
|
+
registerPrompts(server);
|
|
14
16
|
},
|
|
15
17
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function register(server) {
|
|
3
|
+
server.registerPrompt('on_air_now', {
|
|
4
|
+
description: 'Check what is currently on air and what is coming up next',
|
|
5
|
+
}, () => ({
|
|
6
|
+
messages: [{
|
|
7
|
+
role: 'user',
|
|
8
|
+
content: {
|
|
9
|
+
type: 'text',
|
|
10
|
+
text: 'What is currently on air right now, and what shows are coming up next? Use get_stream_state for the live status and get_schedule to check today\'s schedule.',
|
|
11
|
+
},
|
|
12
|
+
}],
|
|
13
|
+
}));
|
|
14
|
+
server.registerPrompt('station_status', {
|
|
15
|
+
description: 'Full station overview — stream state, current schedule, and configuration',
|
|
16
|
+
}, () => ({
|
|
17
|
+
messages: [{
|
|
18
|
+
role: 'user',
|
|
19
|
+
content: {
|
|
20
|
+
type: 'text',
|
|
21
|
+
text: 'Give me a full status overview of the station. Check get_stream_state for what is on air, get_schedule for the upcoming schedule, and get_station_info for the station configuration.',
|
|
22
|
+
},
|
|
23
|
+
}],
|
|
24
|
+
}));
|
|
25
|
+
server.registerPrompt('show_overview', {
|
|
26
|
+
description: 'Get a full overview of a specific show including upcoming instances',
|
|
27
|
+
argsSchema: {
|
|
28
|
+
show_name: z.string().describe('Name of the show'),
|
|
29
|
+
},
|
|
30
|
+
}, ({ show_name }) => ({
|
|
31
|
+
messages: [{
|
|
32
|
+
role: 'user',
|
|
33
|
+
content: {
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: `Give me a full overview of the show called "${show_name}". Use get_shows to find it by name, then get_show_instances to list its upcoming scheduled slots.`,
|
|
36
|
+
},
|
|
37
|
+
}],
|
|
38
|
+
}));
|
|
39
|
+
server.registerPrompt('upload_and_schedule', {
|
|
40
|
+
description: 'Upload an audio file and schedule it into a show',
|
|
41
|
+
argsSchema: {
|
|
42
|
+
show_name: z.string().describe('Name of the show to schedule the file into'),
|
|
43
|
+
},
|
|
44
|
+
}, ({ show_name }) => ({
|
|
45
|
+
messages: [{
|
|
46
|
+
role: 'user',
|
|
47
|
+
content: {
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: `I want to upload an audio file and schedule it into the show called "${show_name}". Start by using upload_file to get the file from me. Once it is uploaded, use get_shows to find the show, get_show_instances to find the next available instance, then schedule_file to add it to the schedule.`,
|
|
50
|
+
},
|
|
51
|
+
}],
|
|
52
|
+
}));
|
|
53
|
+
server.registerPrompt('manage_playlist', {
|
|
54
|
+
description: 'View or manage playlists — list all playlists or inspect a specific one',
|
|
55
|
+
argsSchema: {
|
|
56
|
+
playlist_name: z.string().optional().describe('Name of a specific playlist to inspect. Leave blank to list all playlists.'),
|
|
57
|
+
},
|
|
58
|
+
}, ({ playlist_name }) => {
|
|
59
|
+
const text = playlist_name
|
|
60
|
+
? `Show me the contents of the playlist called "${playlist_name}". Use get_playlists to find it by name, then get_playlist_contents to list its tracks.`
|
|
61
|
+
: 'List all playlists in the media library using get_playlists, and show the contents of each one using get_playlist_contents.';
|
|
62
|
+
return {
|
|
63
|
+
messages: [{ role: 'user', content: { type: 'text', text } }],
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
package/dist/stdio/admin.js
CHANGED
|
@@ -8,6 +8,8 @@ import { register as registerShows } from '../tools/shows/index.js';
|
|
|
8
8
|
import { register as registerAnalytics } from '../tools/analytics/index.js';
|
|
9
9
|
import { register as registerAdmin } from '../tools/admin/index.js';
|
|
10
10
|
import { register as registerFiles } from '../tools/files/index.js';
|
|
11
|
+
import { register as registerPlaylists } from '../tools/playlists/index.js';
|
|
12
|
+
import { register as registerPrompts } from '../prompts/index.js';
|
|
11
13
|
import { registerUploadEndpoint } from '../http/upload.js';
|
|
12
14
|
const { version } = createRequire(import.meta.url)('../../package.json');
|
|
13
15
|
// Spin up a local HTTP server solely for the file upload endpoint.
|
|
@@ -29,6 +31,8 @@ registerShows(server);
|
|
|
29
31
|
registerAnalytics(server);
|
|
30
32
|
registerAdmin(server);
|
|
31
33
|
registerFiles(server, uploadUrl, UPLOAD_TOKEN);
|
|
34
|
+
registerPlaylists(server);
|
|
35
|
+
registerPrompts(server);
|
|
32
36
|
const transport = new StdioServerTransport();
|
|
33
37
|
await server.connect(transport);
|
|
34
38
|
console.error('LibreTime MCP admin server running (shows + analytics + admin)');
|
package/dist/stdio/client.js
CHANGED
|
@@ -3,12 +3,14 @@ import { createRequire } from 'module';
|
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { register as registerShows } from '../tools/shows/index.js';
|
|
6
|
+
import { register as registerPrompts } from '../prompts/index.js';
|
|
6
7
|
const { version } = createRequire(import.meta.url)('../../package.json');
|
|
7
8
|
const server = new McpServer({
|
|
8
9
|
name: 'libretime-mcp-client',
|
|
9
10
|
version,
|
|
10
11
|
});
|
|
11
12
|
registerShows(server);
|
|
13
|
+
registerPrompts(server);
|
|
12
14
|
const transport = new StdioServerTransport();
|
|
13
15
|
await server.connect(transport);
|
|
14
16
|
console.error('LibreTime MCP client server running (read-only: shows)');
|
|
@@ -3,9 +3,7 @@ import { libreGet } from '../../libretime.js';
|
|
|
3
3
|
import { toolText } from '../../tool-response.js';
|
|
4
4
|
import { ShowHostSchema, UserSchema } from './types.js';
|
|
5
5
|
export function register(server) {
|
|
6
|
-
server.
|
|
7
|
-
description: 'Get all show-to-host assignments. Enriches each entry with user details so you can see which presenter hosts which show by name.',
|
|
8
|
-
}, async () => {
|
|
6
|
+
server.tool('get_hosts', 'Get all show-to-host assignments. Enriches each entry with user details so you can see which presenter hosts which show by name.', {}, async () => {
|
|
9
7
|
const [rawHosts, rawUsers] = await Promise.all([
|
|
10
8
|
libreGet('/api/v2/show-hosts'),
|
|
11
9
|
libreGet('/api/v2/users'),
|
|
@@ -3,13 +3,18 @@ import { libreGet } from '../../libretime.js';
|
|
|
3
3
|
import { toolText } from '../../tool-response.js';
|
|
4
4
|
import { UserSchema } from './types.js';
|
|
5
5
|
export function register(server) {
|
|
6
|
-
server.
|
|
7
|
-
|
|
8
|
-
}, async () => {
|
|
6
|
+
server.tool('get_users', 'List all LibreTime users (presenters and admins). Returns id, username, name, and role. Roles: G = Guest, H = Host, P = Manager, A = Admin.', {
|
|
7
|
+
include_email: z.boolean().optional().describe('Include email addresses in the response (default: false)'),
|
|
8
|
+
}, async ({ include_email = false }) => {
|
|
9
9
|
const raw = await libreGet('/api/v2/users');
|
|
10
10
|
const users = z.array(UserSchema).parse(raw);
|
|
11
11
|
const trimmed = users.map(({ id, username, first_name, last_name, email, role }) => ({
|
|
12
|
-
id,
|
|
12
|
+
id,
|
|
13
|
+
username,
|
|
14
|
+
first_name,
|
|
15
|
+
last_name,
|
|
16
|
+
...(include_email ? { email } : {}),
|
|
17
|
+
role,
|
|
13
18
|
}));
|
|
14
19
|
return toolText(trimmed);
|
|
15
20
|
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { register as registerGetUsers } from './get_users.js';
|
|
2
2
|
import { register as registerGetHosts } from './get_hosts.js';
|
|
3
|
+
import { register as registerCreateShow } from '../shows/create_show.js';
|
|
4
|
+
import { register as registerScheduleFile } from '../shows/schedule_file.js';
|
|
3
5
|
export function register(server) {
|
|
4
6
|
registerGetUsers(server);
|
|
5
7
|
registerGetHosts(server);
|
|
8
|
+
registerCreateShow(server);
|
|
9
|
+
registerScheduleFile(server);
|
|
6
10
|
}
|
|
@@ -2,13 +2,13 @@ import { z } from 'zod';
|
|
|
2
2
|
export const UserSchema = z.object({
|
|
3
3
|
id: z.number(),
|
|
4
4
|
username: z.string(),
|
|
5
|
-
first_name: z.string(),
|
|
6
|
-
last_name: z.string(),
|
|
7
|
-
email: z.string(),
|
|
8
|
-
role: z.string(),
|
|
9
|
-
});
|
|
5
|
+
first_name: z.string().nullable(),
|
|
6
|
+
last_name: z.string().nullable(),
|
|
7
|
+
email: z.string().nullable(),
|
|
8
|
+
role: z.string().nullable(),
|
|
9
|
+
}).passthrough();
|
|
10
10
|
export const ShowHostSchema = z.object({
|
|
11
11
|
id: z.number(),
|
|
12
12
|
show: z.number(),
|
|
13
13
|
user: z.number(),
|
|
14
|
-
});
|
|
14
|
+
}).passthrough();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { librePost } from '../../libretime.js';
|
|
3
|
+
import { toolText } from '../../tool-response.js';
|
|
4
|
+
import { PlaylistContentSchema } from './types.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.tool('add_to_playlist', 'Add a file or stream to a playlist. Use get_playlist_contents to check the current contents and determine the next position.', {
|
|
7
|
+
playlist_id: z.number().describe('Playlist ID to add to'),
|
|
8
|
+
file_id: z.number().optional().describe('File ID to add (use for audio files)'),
|
|
9
|
+
stream_id: z.number().optional().describe('Stream ID to add (use for webstreams)'),
|
|
10
|
+
position: z.number().optional().describe('Position in the playlist (0-based). Appends to end if omitted.'),
|
|
11
|
+
}, async ({ playlist_id, file_id, stream_id, position }) => {
|
|
12
|
+
if (file_id === undefined && stream_id === undefined) {
|
|
13
|
+
return toolText({ status: 'error', reason: 'Either file_id or stream_id must be provided.' });
|
|
14
|
+
}
|
|
15
|
+
const kind = stream_id !== undefined ? 1 : 0;
|
|
16
|
+
const raw = await librePost('/api/v2/playlist-contents', {
|
|
17
|
+
playlist: playlist_id,
|
|
18
|
+
file: file_id ?? null,
|
|
19
|
+
stream: stream_id ?? null,
|
|
20
|
+
kind,
|
|
21
|
+
position: position ?? null,
|
|
22
|
+
offset: 0,
|
|
23
|
+
cue_in: '0:00:00.000000',
|
|
24
|
+
cue_out: null,
|
|
25
|
+
fade_in: null,
|
|
26
|
+
fade_out: null,
|
|
27
|
+
});
|
|
28
|
+
const content = PlaylistContentSchema.parse(raw);
|
|
29
|
+
return toolText({ status: 'added', content });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { librePost } from '../../libretime.js';
|
|
3
|
+
import { toolText } from '../../tool-response.js';
|
|
4
|
+
import { PlaylistSchema } from './types.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.tool('create_playlist', 'Create a new playlist in LibreTime. Returns the playlist ID for use with add_to_playlist.', {
|
|
7
|
+
name: z.string().describe('Playlist name'),
|
|
8
|
+
description: z.string().optional().describe('Playlist description'),
|
|
9
|
+
}, async ({ name, description }) => {
|
|
10
|
+
const raw = await librePost('/api/v2/playlists', {
|
|
11
|
+
name,
|
|
12
|
+
description: description ?? null,
|
|
13
|
+
});
|
|
14
|
+
const playlist = PlaylistSchema.parse(raw);
|
|
15
|
+
return toolText(playlist);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { libreGet } from '../../libretime.js';
|
|
3
|
+
import { toolText } from '../../tool-response.js';
|
|
4
|
+
import { PlaylistContentSchema } from './types.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.tool('get_playlist_contents', 'List the contents of a playlist.', {
|
|
7
|
+
playlist_id: z.number().describe('Playlist ID'),
|
|
8
|
+
}, async ({ playlist_id }) => {
|
|
9
|
+
const raw = await libreGet('/api/v2/playlist-contents', { playlist: String(playlist_id) });
|
|
10
|
+
const contents = PlaylistContentSchema.array().parse(raw);
|
|
11
|
+
return toolText(contents);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { libreGet } from '../../libretime.js';
|
|
2
|
+
import { toolText } from '../../tool-response.js';
|
|
3
|
+
import { PlaylistSchema } from './types.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.tool('get_playlists', 'List all playlists in the LibreTime media library.', {}, async () => {
|
|
6
|
+
const raw = await libreGet('/api/v2/playlists');
|
|
7
|
+
const playlists = PlaylistSchema.array().parse(raw);
|
|
8
|
+
return toolText(playlists);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { register as registerGetPlaylists } from './get_playlists.js';
|
|
2
|
+
import { register as registerCreatePlaylist } from './create_playlist.js';
|
|
3
|
+
import { register as registerGetPlaylistContents } from './get_playlist_contents.js';
|
|
4
|
+
import { register as registerAddToPlaylist } from './add_to_playlist.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
registerGetPlaylists(server);
|
|
7
|
+
registerCreatePlaylist(server);
|
|
8
|
+
registerGetPlaylistContents(server);
|
|
9
|
+
registerAddToPlaylist(server);
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const PlaylistSchema = z.object({
|
|
3
|
+
id: z.number(),
|
|
4
|
+
name: z.string(),
|
|
5
|
+
description: z.string().nullable(),
|
|
6
|
+
length: z.string().nullable(),
|
|
7
|
+
owner: z.number().nullable(),
|
|
8
|
+
created_at: z.string().nullable(),
|
|
9
|
+
updated_at: z.string().nullable(),
|
|
10
|
+
}).passthrough();
|
|
11
|
+
export const PlaylistContentSchema = z.object({
|
|
12
|
+
id: z.number(),
|
|
13
|
+
kind: z.number().describe('0=File, 1=Stream, 2=Block'),
|
|
14
|
+
position: z.number().nullable(),
|
|
15
|
+
offset: z.number(),
|
|
16
|
+
length: z.string().nullable(),
|
|
17
|
+
cue_in: z.string().nullable(),
|
|
18
|
+
cue_out: z.string().nullable(),
|
|
19
|
+
fade_in: z.string().nullable(),
|
|
20
|
+
fade_out: z.string().nullable(),
|
|
21
|
+
playlist: z.number().nullable(),
|
|
22
|
+
file: z.number().nullable(),
|
|
23
|
+
stream: z.number().nullable(),
|
|
24
|
+
}).passthrough();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { librePost } from '../../libretime.js';
|
|
3
|
+
import { toolText } from '../../tool-response.js';
|
|
4
|
+
import { ShowSchema } from './types.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.tool('create_show', 'Create a new show in LibreTime. Returns the created show with its ID, which can be used to schedule instances.', {
|
|
7
|
+
name: z.string().describe('Show name'),
|
|
8
|
+
description: z.string().optional().describe('Show description'),
|
|
9
|
+
genre: z.string().optional().describe('Genre'),
|
|
10
|
+
url: z.string().optional().describe('Show website URL'),
|
|
11
|
+
}, async ({ name, description, genre, url }) => {
|
|
12
|
+
const raw = await librePost('/api/v2/shows', {
|
|
13
|
+
name,
|
|
14
|
+
description: description ?? null,
|
|
15
|
+
genre: genre ?? null,
|
|
16
|
+
url: url ?? null,
|
|
17
|
+
linked: false,
|
|
18
|
+
linkable: false,
|
|
19
|
+
auto_playlist_enabled: false,
|
|
20
|
+
auto_playlist_repeat: false,
|
|
21
|
+
override_intro_playlist: false,
|
|
22
|
+
override_outro_playlist: false,
|
|
23
|
+
});
|
|
24
|
+
const show = ShowSchema.parse(raw);
|
|
25
|
+
return toolText(show);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { libreGet } from '../../libretime.js';
|
|
3
|
+
import { toolText } from '../../tool-response.js';
|
|
4
|
+
import { ShowSchema } from './types.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.tool('get_show', 'Get details of a single LibreTime show by ID.', { id: z.number().describe('Show ID') }, async ({ id }) => {
|
|
7
|
+
const raw = await libreGet(`/api/v2/shows/${id}`);
|
|
8
|
+
const show = ShowSchema.parse(raw);
|
|
9
|
+
return toolText(show);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { libreGet } from '../../libretime.js';
|
|
3
|
+
import { toolText } from '../../tool-response.js';
|
|
4
|
+
import { ShowInstanceSchema } from './types.js';
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.tool('get_show_instances', 'List scheduled instances of a show. Optionally filter by show ID or date range.', {
|
|
7
|
+
show_id: z.number().optional().describe('Filter by show ID'),
|
|
8
|
+
starts_after: z.string().optional().describe('ISO 8601 datetime — only return instances starting after this time'),
|
|
9
|
+
starts_before: z.string().optional().describe('ISO 8601 datetime — only return instances starting before this time'),
|
|
10
|
+
}, async ({ show_id, starts_after, starts_before }) => {
|
|
11
|
+
const params = {};
|
|
12
|
+
if (show_id !== undefined)
|
|
13
|
+
params.show = String(show_id);
|
|
14
|
+
if (starts_after)
|
|
15
|
+
params.starts_after = starts_after;
|
|
16
|
+
if (starts_before)
|
|
17
|
+
params.starts_before = starts_before;
|
|
18
|
+
const raw = await libreGet('/api/v2/show-instances', params);
|
|
19
|
+
const instances = ShowInstanceSchema.array().parse(raw);
|
|
20
|
+
return toolText(instances);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { libreGet } from '../../libretime.js';
|
|
3
|
+
import { toolText } from '../../tool-response.js';
|
|
4
|
+
const InfoSchema = z.object({
|
|
5
|
+
station_name: z.string(),
|
|
6
|
+
}).passthrough();
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.tool('get_station_info', 'Get basic station information such as the station name.', {}, async () => {
|
|
9
|
+
const raw = await libreGet('/api/v2/info');
|
|
10
|
+
const info = InfoSchema.parse(raw);
|
|
11
|
+
return toolText(info);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { register as registerGetShows } from './get_shows.js';
|
|
2
|
+
import { register as registerGetShow } from './get_show.js';
|
|
3
|
+
import { register as registerGetShowInstances } from './get_show_instances.js';
|
|
2
4
|
import { register as registerGetSchedule } from './get_schedule.js';
|
|
3
5
|
import { register as registerGetStreamState } from './get_stream_state.js';
|
|
6
|
+
import { register as registerGetStationInfo } from './get_station_info.js';
|
|
4
7
|
export function register(server) {
|
|
5
8
|
registerGetShows(server);
|
|
9
|
+
registerGetShow(server);
|
|
10
|
+
registerGetShowInstances(server);
|
|
6
11
|
registerGetSchedule(server);
|
|
7
12
|
registerGetStreamState(server);
|
|
13
|
+
registerGetStationInfo(server);
|
|
8
14
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { libreGet, librePost } from '../../libretime.js';
|
|
3
|
+
import { toolText } from '../../tool-response.js';
|
|
4
|
+
import { ScheduleItemSchema } from './types.js';
|
|
5
|
+
import { LibreFileSchema } from '../files/types.js';
|
|
6
|
+
// LibreTime duration format: "H:MM:SS.ffffff" → milliseconds
|
|
7
|
+
function parseDurationMs(length) {
|
|
8
|
+
if (!length)
|
|
9
|
+
return null;
|
|
10
|
+
const match = length.match(/^(\d+):(\d{2}):(\d{2})\.(\d+)$/);
|
|
11
|
+
if (!match)
|
|
12
|
+
return null;
|
|
13
|
+
const [, h, m, s, frac] = match;
|
|
14
|
+
const ms = Math.round(parseInt(frac.padEnd(3, '0').slice(0, 3)));
|
|
15
|
+
return (parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s)) * 1000 + ms;
|
|
16
|
+
}
|
|
17
|
+
export function register(server) {
|
|
18
|
+
server.tool('schedule_file', 'Schedule an uploaded file into a show instance at a specific time. ' +
|
|
19
|
+
'Fetches the file duration automatically to calculate the end time. ' +
|
|
20
|
+
'Use get_show_instances to find the instance ID for the show slot you want to fill.', {
|
|
21
|
+
instance_id: z.number().describe('Show instance ID to schedule into'),
|
|
22
|
+
file_id: z.number().describe('ID of the uploaded file to schedule'),
|
|
23
|
+
starts_at: z.string().describe('ISO 8601 datetime for when the file should start playing'),
|
|
24
|
+
position: z.number().optional().describe('Position in the show queue (defaults to 0)'),
|
|
25
|
+
}, async ({ instance_id, file_id, starts_at, position = 0 }) => {
|
|
26
|
+
// Fetch file to get duration for ends_at / cue_out calculation
|
|
27
|
+
const fileRaw = await libreGet(`/api/v2/files/${file_id}`);
|
|
28
|
+
const file = LibreFileSchema.passthrough().parse(fileRaw);
|
|
29
|
+
const length = file.length;
|
|
30
|
+
const durationMs = parseDurationMs(length);
|
|
31
|
+
if (!durationMs) {
|
|
32
|
+
return toolText({
|
|
33
|
+
status: 'error',
|
|
34
|
+
reason: `Could not determine file duration (length: ${length ?? 'null'}). ` +
|
|
35
|
+
'The file may still be processing — check import_status and try again.',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const startsDate = new Date(starts_at);
|
|
39
|
+
const endsDate = new Date(startsDate.getTime() + durationMs);
|
|
40
|
+
const ends_at = endsDate.toISOString();
|
|
41
|
+
const cue_out = length;
|
|
42
|
+
const raw = await librePost('/api/v2/schedule', {
|
|
43
|
+
instance: instance_id,
|
|
44
|
+
file: file_id,
|
|
45
|
+
starts_at,
|
|
46
|
+
ends_at,
|
|
47
|
+
cue_in: '0:00:00.000000',
|
|
48
|
+
cue_out,
|
|
49
|
+
position,
|
|
50
|
+
position_status: 0,
|
|
51
|
+
broadcasted: 0,
|
|
52
|
+
});
|
|
53
|
+
const item = ScheduleItemSchema.parse(raw);
|
|
54
|
+
return toolText({ status: 'scheduled', item });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -2,10 +2,27 @@ import { z } from 'zod';
|
|
|
2
2
|
export const ShowSchema = z.object({
|
|
3
3
|
id: z.number(),
|
|
4
4
|
name: z.string(),
|
|
5
|
-
description: z.string(),
|
|
6
|
-
genre: z.string(),
|
|
7
|
-
url: z.string(),
|
|
8
|
-
|
|
5
|
+
description: z.string().nullable(),
|
|
6
|
+
genre: z.string().nullable(),
|
|
7
|
+
url: z.string().nullable(),
|
|
8
|
+
linked: z.boolean().optional(),
|
|
9
|
+
linkable: z.boolean().optional(),
|
|
10
|
+
auto_playlist_enabled: z.boolean().optional(),
|
|
11
|
+
auto_playlist_repeat: z.boolean().optional(),
|
|
12
|
+
override_intro_playlist: z.boolean().optional(),
|
|
13
|
+
override_outro_playlist: z.boolean().optional(),
|
|
14
|
+
}).passthrough();
|
|
15
|
+
export const ShowInstanceSchema = z.object({
|
|
16
|
+
id: z.number(),
|
|
17
|
+
starts_at: z.string(),
|
|
18
|
+
ends_at: z.string(),
|
|
19
|
+
filled_time: z.string().nullable(),
|
|
20
|
+
description: z.string().nullable(),
|
|
21
|
+
modified: z.boolean(),
|
|
22
|
+
auto_playlist_built: z.boolean(),
|
|
23
|
+
show: z.number(),
|
|
24
|
+
instance: z.number().nullable(),
|
|
25
|
+
}).passthrough();
|
|
9
26
|
export const ScheduleItemSchema = z.object({
|
|
10
27
|
id: z.number(),
|
|
11
28
|
starts_at: z.string(),
|
|
@@ -13,7 +30,7 @@ export const ScheduleItemSchema = z.object({
|
|
|
13
30
|
instance: z.number(),
|
|
14
31
|
file: z.number().nullable(),
|
|
15
32
|
broadcasted: z.number(),
|
|
16
|
-
played: z.boolean(),
|
|
33
|
+
played: z.boolean().nullable(),
|
|
17
34
|
}).passthrough();
|
|
18
35
|
export const StreamStateSchema = z.object({
|
|
19
36
|
input_main_connected: z.boolean(),
|