@powerfm/libretime-mcp 0.1.5 → 0.1.8
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 +8 -4
- package/dist/http/admin.js +4 -2
- package/dist/http/client.js +4 -2
- package/dist/stdio/admin.js +0 -1
- package/dist/stdio/client.js +0 -1
- package/dist/tools/admin/get_users.js +2 -2
- package/dist/tools/admin/types.js +1 -1
- package/dist/tools/analytics/get_listener_counts.js +8 -3
- package/dist/tools/analytics/get_playout_history.js +4 -2
- package/dist/tools/analytics/index.js +0 -2
- package/dist/tools/shows/types.js +12 -7
- package/package.json +13 -6
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Tools are organised into subdirectories under `src/tools/` — one file per tool
|
|
|
16
16
|
- `get_stream_state` — current on-air state
|
|
17
17
|
|
|
18
18
|
**Analytics (admin)**
|
|
19
|
-
-
|
|
19
|
+
- ~~`get_listener_counts`~~ — disabled (API returns full history with no filtering, ~120k records)
|
|
20
20
|
- `get_playout_history` — recent playout history with track metadata
|
|
21
21
|
|
|
22
22
|
**Media library (admin)**
|
|
@@ -86,14 +86,17 @@ Clone the repo, create a `.env` file with your credentials (see [Development](#d
|
|
|
86
86
|
"mcpServers": {
|
|
87
87
|
"libretime": {
|
|
88
88
|
"command": "node",
|
|
89
|
-
"args": ["/absolute/path/to/libretime-mcp/dist/stdio/admin.js"]
|
|
89
|
+
"args": ["/absolute/path/to/libretime-mcp/dist/stdio/admin.js"],
|
|
90
|
+
"env": {
|
|
91
|
+
"LIBRETIME_URL": "https://your-instance.example.com",
|
|
92
|
+
"LIBRETIME_USER": "user",
|
|
93
|
+
"LIBRETIME_PASS": "pass"
|
|
94
|
+
}
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
No `env` block needed — credentials are loaded automatically from `.env`.
|
|
96
|
-
|
|
97
100
|
## Option 2 — Self-hosted HTTP server
|
|
98
101
|
|
|
99
102
|
Best for advanced setups — connect any MCP-compatible client over the network.
|
|
@@ -110,6 +113,7 @@ LIBRETIME_USER=your_api_username
|
|
|
110
113
|
LIBRETIME_PASS=your_api_password
|
|
111
114
|
MCP_API_KEY=your_secret_api_key # clients must send this as a Bearer token
|
|
112
115
|
MCP_PORT=3000 # optional, defaults to 3000 (admin) / 3001 (client)
|
|
116
|
+
CORS_ORIGIN=https://your-app.example.com # optional, lock CORS to a specific origin (default: reflect any)
|
|
113
117
|
```
|
|
114
118
|
|
|
115
119
|
Generate a random API key:
|
package/dist/http/admin.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// Exposes POST /mcp using MCP Streamable HTTP transport.
|
|
4
4
|
// Requires Authorization: Bearer <MCP_API_KEY> on every request.
|
|
5
5
|
// Intended for network clients (e.g. powerfm-agent). For Claude Desktop use stdio/admin.ts.
|
|
6
|
-
import '
|
|
6
|
+
import '../env.js';
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
10
10
|
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
11
|
+
import cors from 'cors';
|
|
11
12
|
const { version } = createRequire(import.meta.url)('../../package.json');
|
|
12
13
|
import { register as registerShows } from '../tools/shows/index.js';
|
|
13
14
|
import { register as registerAnalytics } from '../tools/analytics/index.js';
|
|
@@ -19,6 +20,7 @@ if (!API_KEY) {
|
|
|
19
20
|
process.exit(1);
|
|
20
21
|
}
|
|
21
22
|
const app = createMcpExpressApp({ host: '0.0.0.0' });
|
|
23
|
+
app.use(cors({ origin: process.env.CORS_ORIGIN ?? true, credentials: true }));
|
|
22
24
|
// Simple API key middleware — checks Authorization: Bearer <key>
|
|
23
25
|
app.use('/mcp', (req, res, next) => {
|
|
24
26
|
const auth = req.headers['authorization'];
|
|
@@ -30,7 +32,7 @@ app.use('/mcp', (req, res, next) => {
|
|
|
30
32
|
});
|
|
31
33
|
// Stateless transport — a fresh McpServer per request keeps things simple
|
|
32
34
|
// and avoids session management complexity for now
|
|
33
|
-
app.
|
|
35
|
+
app.all('/mcp', async (req, res) => {
|
|
34
36
|
const server = new McpServer({ name: 'libretime-mcp-admin', version });
|
|
35
37
|
registerShows(server);
|
|
36
38
|
registerAnalytics(server);
|
package/dist/http/client.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// Exposes POST /mcp using MCP Streamable HTTP transport.
|
|
4
4
|
// Requires Authorization: Bearer <MCP_API_KEY> on every request.
|
|
5
5
|
// Intended for network clients. For Claude Desktop use stdio/client.ts.
|
|
6
|
-
import '
|
|
6
|
+
import '../env.js';
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
10
10
|
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
11
|
+
import cors from 'cors';
|
|
11
12
|
import { register as registerShows } from '../tools/shows/index.js';
|
|
12
13
|
const { version } = createRequire(import.meta.url)('../../package.json');
|
|
13
14
|
const PORT = parseInt(process.env.MCP_PORT ?? '3001', 10);
|
|
@@ -17,6 +18,7 @@ if (!API_KEY) {
|
|
|
17
18
|
process.exit(1);
|
|
18
19
|
}
|
|
19
20
|
const app = createMcpExpressApp({ host: '0.0.0.0' });
|
|
21
|
+
app.use(cors({ origin: process.env.CORS_ORIGIN ?? true, credentials: true }));
|
|
20
22
|
// Simple API key middleware — checks Authorization: Bearer <key>
|
|
21
23
|
app.use('/mcp', (req, res, next) => {
|
|
22
24
|
const auth = req.headers['authorization'];
|
|
@@ -28,7 +30,7 @@ app.use('/mcp', (req, res, next) => {
|
|
|
28
30
|
});
|
|
29
31
|
// Stateless transport — a fresh McpServer per request keeps things simple
|
|
30
32
|
// and avoids session management complexity for now
|
|
31
|
-
app.
|
|
33
|
+
app.all('/mcp', async (req, res) => {
|
|
32
34
|
const server = new McpServer({ name: 'libretime-mcp-client', version });
|
|
33
35
|
registerShows(server);
|
|
34
36
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
package/dist/stdio/admin.js
CHANGED
package/dist/stdio/client.js
CHANGED
|
@@ -8,8 +8,8 @@ export function register(server) {
|
|
|
8
8
|
}, async () => {
|
|
9
9
|
const raw = await libreGet('/api/v2/users');
|
|
10
10
|
const users = z.array(UserSchema).parse(raw);
|
|
11
|
-
const trimmed = users.map(({ id, username, first_name, last_name, email,
|
|
12
|
-
id, username, first_name, last_name, email, role
|
|
11
|
+
const trimmed = users.map(({ id, username, first_name, last_name, email, role }) => ({
|
|
12
|
+
id, username, first_name, last_name, email, role,
|
|
13
13
|
}));
|
|
14
14
|
return toolText(trimmed);
|
|
15
15
|
});
|
|
@@ -4,8 +4,11 @@ import { toolText } from '../../tool-response.js';
|
|
|
4
4
|
import { ListenerCountSchema, MountNameSchema } from './types.js';
|
|
5
5
|
export function register(server) {
|
|
6
6
|
server.registerTool('get_listener_counts', {
|
|
7
|
-
description: 'Get listener count history for PowerFM streams. Returns
|
|
8
|
-
|
|
7
|
+
description: 'Get listener count history for PowerFM streams. Returns the most recent entries per mount point with timestamps. Useful for understanding peak listening times and audience size.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
limit: z.number().int().min(1).max(500).default(100).describe('Number of most recent entries to return (default 100, max 500)'),
|
|
10
|
+
},
|
|
11
|
+
}, async ({ limit }) => {
|
|
9
12
|
const [rawCounts, rawMounts] = await Promise.all([
|
|
10
13
|
libreGet('/api/v2/listener-counts'),
|
|
11
14
|
libreGet('/api/v2/mount-names'),
|
|
@@ -13,7 +16,9 @@ export function register(server) {
|
|
|
13
16
|
const counts = z.array(ListenerCountSchema).parse(rawCounts);
|
|
14
17
|
const mounts = z.array(MountNameSchema).parse(rawMounts);
|
|
15
18
|
const mountMap = new Map(mounts.map((m) => [m.id, m.mount_name]));
|
|
16
|
-
const enriched = counts
|
|
19
|
+
const enriched = counts
|
|
20
|
+
.slice(-limit)
|
|
21
|
+
.map(({ id, listener_count, timestamp, mount_name }) => ({
|
|
17
22
|
id,
|
|
18
23
|
listener_count,
|
|
19
24
|
timestamp,
|
|
@@ -19,8 +19,10 @@ export function register(server) {
|
|
|
19
19
|
const history = z.array(PlayoutHistorySchema).parse(rawHistory);
|
|
20
20
|
// Collect unique file IDs then fetch metadata in parallel
|
|
21
21
|
const fileIds = [...new Set(history.map((h) => h.file).filter((id) => id !== null))];
|
|
22
|
-
const rawFiles = await Promise.
|
|
23
|
-
const files = rawFiles
|
|
22
|
+
const rawFiles = await Promise.allSettled(fileIds.map((id) => libreGet(`/api/v2/files/${id}`)));
|
|
23
|
+
const files = rawFiles
|
|
24
|
+
.map((r) => r.status === 'fulfilled' ? FileMetadataSchema.safeParse(r.value) : null)
|
|
25
|
+
.flatMap((r) => (r?.success ? [r.data] : []));
|
|
24
26
|
const fileMap = new Map(files.map((f) => [f.id, f]));
|
|
25
27
|
const enriched = history.map(({ id, starts, ends, file, instance }) => {
|
|
26
28
|
const meta = file !== null ? fileMap.get(file) : undefined;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { register as registerGetListenerCounts } from './get_listener_counts.js';
|
|
2
1
|
import { register as registerGetPlayoutHistory } from './get_playout_history.js';
|
|
3
2
|
export function register(server) {
|
|
4
|
-
registerGetListenerCounts(server);
|
|
5
3
|
registerGetPlayoutHistory(server);
|
|
6
4
|
}
|
|
@@ -8,12 +8,17 @@ export const ShowSchema = z.object({
|
|
|
8
8
|
});
|
|
9
9
|
export const ScheduleItemSchema = z.object({
|
|
10
10
|
id: z.number(),
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
starts_at: z.string(),
|
|
12
|
+
ends_at: z.string(),
|
|
13
|
+
instance: z.number(),
|
|
14
|
+
file: z.number().nullable(),
|
|
15
15
|
broadcasted: z.number(),
|
|
16
|
-
|
|
17
|
-
export const StreamStateSchema = z.object({
|
|
18
|
-
source_enabled: z.boolean(),
|
|
16
|
+
played: z.boolean(),
|
|
19
17
|
}).passthrough();
|
|
18
|
+
export const StreamStateSchema = z.object({
|
|
19
|
+
input_main_connected: z.boolean(),
|
|
20
|
+
input_main_streaming: z.boolean(),
|
|
21
|
+
input_show_connected: z.boolean(),
|
|
22
|
+
input_show_streaming: z.boolean(),
|
|
23
|
+
schedule_streaming: z.boolean(),
|
|
24
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerfm/libretime-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"license": "GPL-3.0",
|
|
6
7
|
"description": "MCP server for LibreTime radio station — connect Claude to your broadcast schedule, media library, and analytics",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
@@ -20,13 +21,13 @@
|
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
|
-
"dev:client": "tsx --env-file=.env
|
|
24
|
-
"dev:admin": "tsx --env-file=.env
|
|
25
|
-
"dev:client-http": "tsx --env-file=.env
|
|
26
|
-
"dev:admin-http": "tsx --env-file=.env
|
|
24
|
+
"dev:client": "tsx watch --env-file=.env src/stdio/client.ts",
|
|
25
|
+
"dev:admin": "tsx watch --env-file=.env src/stdio/admin.ts",
|
|
26
|
+
"dev:client-http": "tsx watch --env-file=.env src/http/client.ts",
|
|
27
|
+
"dev:admin-http": "tsx watch --env-file=.env src/http/admin.ts",
|
|
27
28
|
"clean": "rm -rf dist",
|
|
28
29
|
"build": "npm run clean && tsc",
|
|
29
|
-
"prepublishOnly": "npm run build",
|
|
30
|
+
"prepublishOnly": "npm run build && npm test",
|
|
30
31
|
"publish:patch": "npm version patch && npm publish --access public",
|
|
31
32
|
"publish:minor": "npm version minor && npm publish --access public",
|
|
32
33
|
"start:client": "node --env-file=.env dist/stdio/client.js",
|
|
@@ -34,15 +35,21 @@
|
|
|
34
35
|
"start:client-http": "node --env-file=.env dist/http/client.js",
|
|
35
36
|
"start:admin-http": "node --env-file=.env dist/http/admin.js",
|
|
36
37
|
"generate:key": "node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"",
|
|
38
|
+
"inspect:client": "npx @modelcontextprotocol/inspector tsx --env-file=.env src/stdio/client.ts",
|
|
39
|
+
"inspect:admin": "npx @modelcontextprotocol/inspector tsx --env-file=.env src/stdio/admin.ts",
|
|
40
|
+
"inspect:admin-http": "npx @modelcontextprotocol/inspector --transport http --server-url http://localhost:3000/mcp --header \"Authorization: Bearer $MCP_API_KEY\"",
|
|
41
|
+
"inspect:client-http": "npx @modelcontextprotocol/inspector --transport http --server-url http://localhost:3001/mcp --header \"Authorization: Bearer $MCP_API_KEY\"",
|
|
37
42
|
"test": "vitest run",
|
|
38
43
|
"test:watch": "vitest"
|
|
39
44
|
},
|
|
40
45
|
"dependencies": {
|
|
41
46
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
47
|
+
"cors": "~2.8.6",
|
|
42
48
|
"express": "~5.2.1",
|
|
43
49
|
"zod": "^3.23.8"
|
|
44
50
|
},
|
|
45
51
|
"devDependencies": {
|
|
52
|
+
"@types/cors": "~2.8.19",
|
|
46
53
|
"@types/express": "~5.0.6",
|
|
47
54
|
"@types/node": "^22.0.0",
|
|
48
55
|
"tsx": "^4.19.0",
|