mcp-djbooth 0.0.1

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 ADDED
@@ -0,0 +1,24 @@
1
+ # mcp-djbooth
2
+
3
+ DJ booth running-order and set-time planning over MCP.
4
+
5
+ Initial release by [OurMixtape](https://ourmixtape.org). Requires Node.js 20.19 or later.
6
+ All calculations run locally. This release does not play audio, analyze audio files,
7
+ control DJ hardware, or connect to streaming services.
8
+
9
+ ## Run as an MCP stdio server
10
+
11
+ ```sh
12
+ npx --yes mcp-djbooth@0.0.1
13
+ ```
14
+
15
+ This is a public package.
16
+ Configure your MCP client with command `npx` and args `["--yes", "mcp-djbooth@0.0.1"]`.
17
+
18
+ ## Tools
19
+
20
+ `booth_schedule`: provide `sets`, each with a nonempty `dj` and positive `durationMinutes`. Optional nonnegative `changeoverMinutes` defaults to 0. Returns relative start/end minutes and total duration, including changeovers between sets.
21
+
22
+ ## License
23
+
24
+ UNLICENSED. All rights reserved.
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "mcp-djbooth",
3
+ "version": "0.0.1",
4
+ "description": "DJ booth running-order and set-time planning over MCP.",
5
+ "type": "module",
6
+ "main": "./utilities.js",
7
+ "exports": "./utilities.js",
8
+ "bin": {
9
+ "mcp-djbooth": "server.js"
10
+ },
11
+ "files": [
12
+ "server.js",
13
+ "utilities.js",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20.19.0"
18
+ },
19
+ "license": "UNLICENSED",
20
+ "author": "OurMixtape",
21
+ "homepage": "https://ourmixtape.org",
22
+ "keywords": [
23
+ "mcp",
24
+ "music",
25
+ "dj",
26
+ "mixtape"
27
+ ],
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "1.30.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ }
35
+ }
package/server.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { tempoMatch, mixtapeTimeline, exportM3u, boothSchedule } from './utilities.js';
6
+
7
+ const number = { type: 'number', exclusiveMinimum: 0 };
8
+ const text = { type: 'string', minLength: 1 };
9
+ const object = (properties, required) => ({ type: 'object', properties, required, additionalProperties: false });
10
+ const array = (properties, required) => ({ type: 'array', minItems: 1, maxItems: 1000, items: object(properties, required) });
11
+ const definitions = {
12
+ tempo_match: { description: 'Calculate direct tempo-match playback rate, pitch percentage and beat duration.', inputSchema: object({ sourceBpm: number, targetBpm: number, beats: number }, ['sourceBpm', 'targetBpm']), run: tempoMatch },
13
+ mixtape_timeline: { description: 'Calculate relative track timings with a constant overlap in seconds.', inputSchema: object({ tracks: array({ title: text, durationSeconds: number }, ['title', 'durationSeconds']), overlapSeconds: { type: 'number', minimum: 0 } }, ['tracks']), run: mixtapeTimeline },
14
+ export_m3u: { description: 'Generate extended M3U playlist text from supplied track metadata without reading files or URLs.', inputSchema: object({ tracks: array({ title: text, durationSeconds: number, location: text }, ['title', 'durationSeconds', 'location']) }, ['tracks']), run: exportM3u },
15
+ booth_schedule: { description: 'Calculate a DJ running order in relative minutes, including changeovers.', inputSchema: object({ sets: array({ dj: text, durationMinutes: number }, ['dj', 'durationMinutes']), changeoverMinutes: { type: 'number', minimum: 0 } }, ['sets']), run: boothSchedule },
16
+ };
17
+ const selected = ["booth_schedule"];
18
+ const server = new Server({ name: 'mcp-djbooth', version: '0.0.1' }, { capabilities: { tools: {} } });
19
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: selected.map(name => {
20
+ const { run, ...definition } = definitions[name];
21
+ return { name, ...definition, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } };
22
+ }) }));
23
+ server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
24
+ try {
25
+ if (!selected.includes(params.name)) throw new Error(`Unknown tool: ${params.name}`);
26
+ const args = params.arguments ?? {};
27
+ const definition = definitions[params.name];
28
+ if (Object.keys(args).some(key => !(key in definition.inputSchema.properties))) throw new Error('Unexpected argument');
29
+ const value = definition.run(args);
30
+ return { content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) }] };
31
+ } catch (error) {
32
+ return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Invalid input' }] };
33
+ }
34
+ });
35
+ await server.connect(new StdioServerTransport());
package/utilities.js ADDED
@@ -0,0 +1,57 @@
1
+ const positive = (value, label) => {
2
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number`);
3
+ return value;
4
+ };
5
+ const nonnegative = (value, label) => {
6
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) throw new Error(`${label} must be a nonnegative finite number`);
7
+ return value;
8
+ };
9
+ const line = (value, label) => {
10
+ if (typeof value !== 'string' || !value.trim() || /[\r\n\x00]/.test(value)) throw new Error(`${label} must be nonempty single-line text`);
11
+ return value;
12
+ };
13
+ const items = (value, label) => {
14
+ if (!Array.isArray(value) || value.length < 1 || value.length > 1000) throw new Error(`${label} must contain 1 to 1000 items`);
15
+ for (const item of value) if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error(`${label} entries must be objects`);
16
+ return value;
17
+ };
18
+ const finiteResult = (value) => {
19
+ if (!Number.isFinite(value)) throw new Error('Calculation exceeds numeric range');
20
+ return value;
21
+ };
22
+ export function tempoMatch({ sourceBpm, targetBpm, beats = 4 }) {
23
+ positive(sourceBpm, 'sourceBpm'); positive(targetBpm, 'targetBpm'); positive(beats, 'beats');
24
+ const rate = finiteResult(targetBpm / sourceBpm);
25
+ return { playbackRate: rate, pitchPercent: finiteResult((rate - 1) * 100), beatDurationSeconds: finiteResult(60 / targetBpm), durationSeconds: finiteResult((60 / targetBpm) * beats) };
26
+ }
27
+ export function mixtapeTimeline({ tracks, overlapSeconds = 0 }) {
28
+ items(tracks, 'tracks'); nonnegative(overlapSeconds, 'overlapSeconds');
29
+ let end = 0;
30
+ const timeline = tracks.map((track, index) => {
31
+ line(track.title, 'title'); positive(track.durationSeconds, 'durationSeconds');
32
+ if (overlapSeconds >= track.durationSeconds) throw new Error('overlapSeconds must be smaller than every track duration');
33
+ const startSeconds = index === 0 ? 0 : end - overlapSeconds;
34
+ end = finiteResult(startSeconds + track.durationSeconds);
35
+ return { title: track.title, startSeconds, endSeconds: end };
36
+ });
37
+ return { tracks: timeline, totalDurationSeconds: end };
38
+ }
39
+ export function exportM3u({ tracks }) {
40
+ items(tracks, 'tracks');
41
+ return '#EXTM3U\n' + tracks.map(track => {
42
+ line(track.title, 'title'); positive(track.durationSeconds, 'durationSeconds'); line(track.location, 'location');
43
+ if (track.location.trimStart().startsWith('#')) throw new Error('location must not start with an M3U directive');
44
+ return `#EXTINF:${Math.round(track.durationSeconds)},${track.title}\n${track.location}\n`;
45
+ }).join('');
46
+ }
47
+ export function boothSchedule({ sets, changeoverMinutes = 0 }) {
48
+ items(sets, 'sets'); nonnegative(changeoverMinutes, 'changeoverMinutes');
49
+ let end = 0;
50
+ const schedule = sets.map((set, index) => {
51
+ line(set.dj, 'dj'); positive(set.durationMinutes, 'durationMinutes');
52
+ const startMinutes = finiteResult(end + (index ? changeoverMinutes : 0));
53
+ end = finiteResult(startMinutes + set.durationMinutes);
54
+ return { dj: set.dj, startMinutes, endMinutes: end };
55
+ });
56
+ return { sets: schedule, totalDurationMinutes: end };
57
+ }