@williamzujkowski/live-coding-music-mcp 2.0.0 → 3.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 +222 -231
- package/dist/AudioAnalyzer.js +0 -1
- package/dist/PatternStore.js +0 -1
- package/dist/index.js +0 -0
- package/dist/server/resources.js +175 -0
- package/dist/server/server.js +226 -348
- package/dist/server/tools/ai.js +78 -22
- package/dist/server/tools/analysis.js +266 -73
- package/dist/server/tools/capture.js +67 -20
- package/dist/server/tools/compose.js +159 -0
- package/dist/server/tools/diagnostics.js +112 -60
- package/dist/server/tools/editor.js +130 -66
- package/dist/server/tools/generate.js +190 -74
- package/dist/server/tools/history.js +149 -88
- package/dist/server/tools/playback.js +49 -18
- package/dist/server/tools/session.js +108 -70
- package/dist/server/tools/storage.js +86 -30
- package/dist/server/tools/transform.js +250 -116
- package/dist/server/tools/types.js +66 -1
- package/dist/services/AudioCaptureService.js +4 -3
- package/dist/services/GeminiService.js +1 -1
- package/dist/services/MIDIExportService.js +2 -4
- package/dist/services/SessionManager.js +4 -2
- package/dist/services/StrudelEngine.js +24 -126
- package/dist/services/StrudelEngineHelpers.js +151 -0
- package/package.json +1 -1
- package/dist/__tests__/MusicTheory.test.js +0 -245
- package/dist/__tests__/PatternGenerator.test.js +0 -337
- package/dist/__tests__/PatternStore.test.js +0 -153
- package/dist/__tests__/utils/MockPlaywright.js +0 -130
- package/dist/server/EnhancedMCPServer.js +0 -654
- package/dist/server/EnhancedMCPServerFixed.js +0 -2506
|
@@ -1,126 +1,190 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* editor domain — pattern editing in the CodeMirror editor.
|
|
3
3
|
*
|
|
4
|
-
* Owns
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Owns one consolidated mutation tool (`edit_pattern(mode)`) plus
|
|
5
|
+
* `get_pattern` (kept separate, hot read path) and five deprecated
|
|
6
|
+
* aliases (write/append/insert/replace/clear) per #120 / #148.
|
|
7
|
+
*
|
|
8
|
+
* Aliases forward to `edit_pattern` and will be removed in a future
|
|
9
|
+
* release per the deprecation policy.
|
|
8
10
|
*/
|
|
9
11
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
12
|
+
const SESSION_ID_PROP = {
|
|
13
|
+
session_id: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Optional session ID (#108). Omit to use default session.',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
10
18
|
export const tools = [
|
|
19
|
+
{
|
|
20
|
+
name: 'edit_pattern',
|
|
21
|
+
description: 'Mutate the current session pattern. ' +
|
|
22
|
+
'mode=write replaces the editor contents (default; mirrors the old write tool exactly, including optional pattern validation and auto_play). ' +
|
|
23
|
+
'mode=append concatenates `code` after the current pattern with a newline. ' +
|
|
24
|
+
'mode=insert places `code` at the given line `position`. ' +
|
|
25
|
+
'mode=replace runs a single string-replace from `search` to `replace`. ' +
|
|
26
|
+
'mode=clear empties the editor. ' +
|
|
27
|
+
'Example: edit_pattern({ mode: "write", pattern: "s(\\"bd\\")", auto_play: true }). ' +
|
|
28
|
+
'For reading the editor without mutating it use get_pattern; for the on-disk pattern catalog use pattern_store.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
mode: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
enum: ['write', 'append', 'insert', 'replace', 'clear'],
|
|
35
|
+
description: 'Which edit operation to perform (default: write)',
|
|
36
|
+
},
|
|
37
|
+
pattern: { type: 'string', description: 'Pattern code (mode=write)' },
|
|
38
|
+
code: { type: 'string', description: 'Code to append/insert (mode=append/insert)' },
|
|
39
|
+
position: { type: 'number', description: 'Line number (mode=insert)' },
|
|
40
|
+
search: { type: 'string', description: 'Text to replace (mode=replace)' },
|
|
41
|
+
replace: { type: 'string', description: 'Replacement text (mode=replace)' },
|
|
42
|
+
auto_play: { type: 'boolean', description: 'Start playback after write (mode=write only, default: false)' },
|
|
43
|
+
validate: { type: 'boolean', description: 'Validate pattern before write (mode=write only, default: true)' },
|
|
44
|
+
...SESSION_ID_PROP,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'get_pattern',
|
|
50
|
+
description: 'Get current pattern code',
|
|
51
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
52
|
+
},
|
|
11
53
|
{
|
|
12
54
|
name: 'write',
|
|
13
|
-
description: 'Write pattern to editor with optional auto-play and validation',
|
|
55
|
+
description: '[DEPRECATED — use edit_pattern({ mode: "write" }) instead] Write pattern to editor with optional auto-play and validation',
|
|
14
56
|
inputSchema: {
|
|
15
57
|
type: 'object',
|
|
16
58
|
properties: {
|
|
17
59
|
pattern: { type: 'string', description: 'Pattern code' },
|
|
18
60
|
auto_play: { type: 'boolean', description: 'Start playback immediately after writing (default: false)' },
|
|
19
61
|
validate: { type: 'boolean', description: 'Validate pattern before writing (default: true)' },
|
|
62
|
+
...SESSION_ID_PROP,
|
|
20
63
|
},
|
|
21
64
|
required: ['pattern'],
|
|
22
65
|
},
|
|
23
66
|
},
|
|
24
67
|
{
|
|
25
68
|
name: 'append',
|
|
26
|
-
description: 'Append code to current pattern',
|
|
69
|
+
description: '[DEPRECATED — use edit_pattern({ mode: "append" }) instead] Append code to current pattern',
|
|
27
70
|
inputSchema: {
|
|
28
71
|
type: 'object',
|
|
29
|
-
properties: { code: { type: 'string', description: 'Code to append' } },
|
|
72
|
+
properties: { code: { type: 'string', description: 'Code to append' }, ...SESSION_ID_PROP },
|
|
30
73
|
required: ['code'],
|
|
31
74
|
},
|
|
32
75
|
},
|
|
33
76
|
{
|
|
34
77
|
name: 'insert',
|
|
35
|
-
description: 'Insert code at specific line',
|
|
78
|
+
description: '[DEPRECATED — use edit_pattern({ mode: "insert" }) instead] Insert code at specific line',
|
|
36
79
|
inputSchema: {
|
|
37
80
|
type: 'object',
|
|
38
81
|
properties: {
|
|
39
82
|
position: { type: 'number', description: 'Line number' },
|
|
40
83
|
code: { type: 'string', description: 'Code to insert' },
|
|
84
|
+
...SESSION_ID_PROP,
|
|
41
85
|
},
|
|
42
86
|
required: ['position', 'code'],
|
|
43
87
|
},
|
|
44
88
|
},
|
|
45
89
|
{
|
|
46
90
|
name: 'replace',
|
|
47
|
-
description: 'Replace pattern section',
|
|
91
|
+
description: '[DEPRECATED — use edit_pattern({ mode: "replace" }) instead] Replace pattern section',
|
|
48
92
|
inputSchema: {
|
|
49
93
|
type: 'object',
|
|
50
94
|
properties: {
|
|
51
95
|
search: { type: 'string', description: 'Text to replace' },
|
|
52
96
|
replace: { type: 'string', description: 'Replacement text' },
|
|
97
|
+
...SESSION_ID_PROP,
|
|
53
98
|
},
|
|
54
99
|
required: ['search', 'replace'],
|
|
55
100
|
},
|
|
56
101
|
},
|
|
57
102
|
{
|
|
58
103
|
name: 'clear',
|
|
59
|
-
description: 'Clear the editor',
|
|
60
|
-
inputSchema: { type: 'object', properties: {} },
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'get_pattern',
|
|
64
|
-
description: 'Get current pattern code',
|
|
65
|
-
inputSchema: { type: 'object', properties: {} },
|
|
104
|
+
description: '[DEPRECATED — use edit_pattern({ mode: "clear" }) instead] Clear the editor',
|
|
105
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
66
106
|
},
|
|
67
107
|
];
|
|
68
108
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
ctx.logger.warn('Pattern validation threw error, continuing with write');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const writeResult = await ctx.writePatternSafe(args.pattern);
|
|
92
|
-
// Auto-play if requested — issue #38
|
|
93
|
-
if (args.auto_play && ctx.isInitialized()) {
|
|
94
|
-
await ctx.controller.play();
|
|
95
|
-
return `${writeResult}. Playing.`;
|
|
109
|
+
async function doWrite(args, ctx, sid) {
|
|
110
|
+
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, true);
|
|
111
|
+
const controller = ctx.getController(sid);
|
|
112
|
+
// Validate pattern if requested (default: true) — issue #40
|
|
113
|
+
if (args.validate !== false && typeof controller.validatePattern === 'function') {
|
|
114
|
+
try {
|
|
115
|
+
const validation = await controller.validatePattern(args.pattern);
|
|
116
|
+
if (validation && !validation.valid) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
errors: validation.errors,
|
|
120
|
+
warnings: validation.warnings,
|
|
121
|
+
suggestions: validation.suggestions,
|
|
122
|
+
message: `Pattern validation failed: ${validation.errors.join('; ')}`,
|
|
123
|
+
};
|
|
96
124
|
}
|
|
97
|
-
return writeResult;
|
|
98
125
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const current = await ctx.getCurrentPatternSafe();
|
|
102
|
-
return await ctx.writePatternSafe(current + '\n' + args.code);
|
|
126
|
+
catch {
|
|
127
|
+
ctx.logger.warn('Pattern validation threw error, continuing with write');
|
|
103
128
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
}
|
|
130
|
+
const writeResult = await ctx.writePatternSafe(args.pattern, sid);
|
|
131
|
+
// Auto-play if requested — issue #38
|
|
132
|
+
if (args.auto_play) {
|
|
133
|
+
await controller.play();
|
|
134
|
+
return `${writeResult}. Playing.`;
|
|
135
|
+
}
|
|
136
|
+
return writeResult;
|
|
137
|
+
}
|
|
138
|
+
async function doAppend(args, ctx, sid) {
|
|
139
|
+
InputValidator.validateStringLength(args.code, 'code', 10000, true);
|
|
140
|
+
const current = await ctx.getCurrentPatternSafe(sid);
|
|
141
|
+
return await ctx.writePatternSafe(current + '\n' + args.code, sid);
|
|
142
|
+
}
|
|
143
|
+
async function doInsert(args, ctx, sid) {
|
|
144
|
+
InputValidator.validatePositiveInteger(args.position, 'position');
|
|
145
|
+
InputValidator.validateStringLength(args.code, 'code', 10000, true);
|
|
146
|
+
const lines = (await ctx.getCurrentPatternSafe(sid)).split('\n');
|
|
147
|
+
lines.splice(args.position, 0, args.code);
|
|
148
|
+
return await ctx.writePatternSafe(lines.join('\n'), sid);
|
|
149
|
+
}
|
|
150
|
+
async function doReplace(args, ctx, sid) {
|
|
151
|
+
InputValidator.validateStringLength(args.search, 'search', 1000, true);
|
|
152
|
+
InputValidator.validateStringLength(args.replace, 'replace', 10000, true);
|
|
153
|
+
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
154
|
+
// Escape $ in replacement to prevent special sequence injection ($&, $1, $', etc.)
|
|
155
|
+
const safeReplacement = args.replace.replace(/\$/g, '$$$$');
|
|
156
|
+
const replaced = pattern.replace(args.search, safeReplacement);
|
|
157
|
+
return await ctx.writePatternSafe(replaced, sid);
|
|
158
|
+
}
|
|
159
|
+
async function doClear(ctx, sid) {
|
|
160
|
+
return await ctx.writePatternSafe('', sid);
|
|
161
|
+
}
|
|
162
|
+
export async function execute(name, args, ctx) {
|
|
163
|
+
const sid = args?.session_id;
|
|
164
|
+
if (!sid && !ctx.isInitialized()) {
|
|
165
|
+
return 'Browser not initialized. Run init first.';
|
|
166
|
+
}
|
|
167
|
+
switch (name) {
|
|
168
|
+
case 'edit_pattern': {
|
|
169
|
+
const mode = args?.mode ?? 'write';
|
|
170
|
+
switch (mode) {
|
|
171
|
+
case 'write': return await doWrite(args, ctx, sid);
|
|
172
|
+
case 'append': return await doAppend(args, ctx, sid);
|
|
173
|
+
case 'insert': return await doInsert(args, ctx, sid);
|
|
174
|
+
case 'replace': return await doReplace(args, ctx, sid);
|
|
175
|
+
case 'clear': return await doClear(ctx, sid);
|
|
176
|
+
default:
|
|
177
|
+
throw new Error(`Invalid mode: ${mode}. Must be one of: write, append, insert, replace, clear`);
|
|
178
|
+
}
|
|
119
179
|
}
|
|
120
|
-
|
|
121
|
-
|
|
180
|
+
// Deprecated aliases — forward to consolidated handler.
|
|
181
|
+
case 'write': return await doWrite(args, ctx, sid);
|
|
182
|
+
case 'append': return await doAppend(args, ctx, sid);
|
|
183
|
+
case 'insert': return await doInsert(args, ctx, sid);
|
|
184
|
+
case 'replace': return await doReplace(args, ctx, sid);
|
|
185
|
+
case 'clear': return await doClear(ctx, sid);
|
|
122
186
|
case 'get_pattern':
|
|
123
|
-
return await ctx.getCurrentPatternSafe();
|
|
187
|
+
return await ctx.getCurrentPatternSafe(sid);
|
|
124
188
|
default:
|
|
125
189
|
throw new Error(`editor module does not handle tool: ${name}`);
|
|
126
190
|
}
|
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
* `generate_part(role)`, while scale+chord_prog become `music_theory`.
|
|
14
14
|
*/
|
|
15
15
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
16
|
+
const SESSION_ID_PROP = {
|
|
17
|
+
session_id: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Optional session ID (#108). Omit to use default session.',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
16
22
|
export const tools = [
|
|
17
23
|
{
|
|
18
24
|
name: 'generate_pattern',
|
|
19
|
-
description: 'Generate complete pattern from style with optional auto-play',
|
|
25
|
+
description: '[DEPRECATED — use compose instead, which auto-inits and returns richer metadata] Generate complete pattern from style with optional auto-play.',
|
|
20
26
|
inputSchema: {
|
|
21
27
|
type: 'object',
|
|
22
28
|
properties: {
|
|
@@ -24,50 +30,99 @@ export const tools = [
|
|
|
24
30
|
key: { type: 'string', description: 'Musical key' },
|
|
25
31
|
bpm: { type: 'number', description: 'Tempo in BPM' },
|
|
26
32
|
auto_play: { type: 'boolean', description: 'Start playback immediately (default: false)' },
|
|
33
|
+
...SESSION_ID_PROP,
|
|
27
34
|
},
|
|
28
35
|
required: ['style'],
|
|
29
36
|
},
|
|
30
37
|
},
|
|
38
|
+
{
|
|
39
|
+
name: 'generate_part',
|
|
40
|
+
description: 'Generate a single instrumental layer and append it to the current session pattern. ' +
|
|
41
|
+
'role=drums takes `style` (e.g. "techno"/"house") and optional `complexity` 0-1. ' +
|
|
42
|
+
'role=bass takes `key` (e.g. "C") + `style`. ' +
|
|
43
|
+
'role=melody takes `root`/`scale` (e.g. C/minor) and optional `length` (notes). ' +
|
|
44
|
+
'role=fill takes `style` and optional `bars`. ' +
|
|
45
|
+
'Example: generate_part({ role: "drums", style: "techno", complexity: 0.7 }). ' +
|
|
46
|
+
'For full compositions use compose; for rhythmic patterns use generate_rhythm; for music-theory queries use music_theory.',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
role: { type: 'string', enum: ['drums', 'bass', 'melody', 'fill'], description: 'Which part to generate' },
|
|
51
|
+
style: { type: 'string', description: 'role=drums/bass/fill: musical style' },
|
|
52
|
+
complexity: { type: 'number', description: 'role=drums: complexity 0-1 (default 0.5)' },
|
|
53
|
+
key: { type: 'string', description: 'role=bass: musical key' },
|
|
54
|
+
root: { type: 'string', description: 'role=melody: root note' },
|
|
55
|
+
scale: { type: 'string', description: 'role=melody: scale name' },
|
|
56
|
+
length: { type: 'number', description: 'role=melody: number of notes (default 8)' },
|
|
57
|
+
bars: { type: 'number', description: 'role=fill: number of bars (default 1)' },
|
|
58
|
+
...SESSION_ID_PROP,
|
|
59
|
+
},
|
|
60
|
+
required: ['role'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
31
63
|
{
|
|
32
64
|
name: 'generate_drums',
|
|
33
|
-
description: 'Generate drum pattern',
|
|
65
|
+
description: '[DEPRECATED — use generate_part({ role: "drums" }) instead] Generate drum pattern',
|
|
34
66
|
inputSchema: {
|
|
35
67
|
type: 'object',
|
|
36
68
|
properties: {
|
|
37
69
|
style: { type: 'string', description: 'Drum style' },
|
|
38
70
|
complexity: { type: 'number', description: 'Complexity (0-1)' },
|
|
71
|
+
...SESSION_ID_PROP,
|
|
39
72
|
},
|
|
40
73
|
required: ['style'],
|
|
41
74
|
},
|
|
42
75
|
},
|
|
43
76
|
{
|
|
44
77
|
name: 'generate_bassline',
|
|
45
|
-
description: 'Generate bassline',
|
|
78
|
+
description: '[DEPRECATED — use generate_part({ role: "bass" }) instead] Generate bassline',
|
|
46
79
|
inputSchema: {
|
|
47
80
|
type: 'object',
|
|
48
81
|
properties: {
|
|
49
82
|
key: { type: 'string', description: 'Musical key' },
|
|
50
83
|
style: { type: 'string', description: 'Bass style' },
|
|
84
|
+
...SESSION_ID_PROP,
|
|
51
85
|
},
|
|
52
86
|
required: ['key', 'style'],
|
|
53
87
|
},
|
|
54
88
|
},
|
|
55
89
|
{
|
|
56
90
|
name: 'generate_melody',
|
|
57
|
-
description: 'Generate melody from scale',
|
|
91
|
+
description: '[DEPRECATED — use generate_part({ role: "melody" }) instead] Generate melody from scale',
|
|
58
92
|
inputSchema: {
|
|
59
93
|
type: 'object',
|
|
60
94
|
properties: {
|
|
61
95
|
scale: { type: 'string', description: 'Scale name' },
|
|
62
96
|
root: { type: 'string', description: 'Root note' },
|
|
63
97
|
length: { type: 'number', description: 'Number of notes' },
|
|
98
|
+
...SESSION_ID_PROP,
|
|
64
99
|
},
|
|
65
100
|
required: ['scale', 'root'],
|
|
66
101
|
},
|
|
67
102
|
},
|
|
103
|
+
{
|
|
104
|
+
name: 'music_theory',
|
|
105
|
+
description: 'Music-theory queries. ' +
|
|
106
|
+
'query=scale returns the notes of a scale (e.g. "C major scale: C, D, E, F, G, A, B"). ' +
|
|
107
|
+
'query=chord_progression returns a chord progression for the key/style AND writes the resulting chord pattern into the current session (matches the pre-consolidation behaviour of generate_chord_progression). ' +
|
|
108
|
+
'Example: music_theory({ query: "scale", root: "C", scale: "major" }). ' +
|
|
109
|
+
'For pattern generation (drums/bass/melody) use generate_part; for rhythmic patterns use generate_rhythm.',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
query: { type: 'string', enum: ['scale', 'chord_progression'], description: 'Which theory query' },
|
|
114
|
+
root: { type: 'string', description: 'Root note (query=scale; also used as key for chord_progression)' },
|
|
115
|
+
scale: { type: 'string', description: 'Scale type (query=scale)' },
|
|
116
|
+
key: { type: 'string', description: 'Key (query=chord_progression)' },
|
|
117
|
+
style: { type: 'string', description: 'Style (query=chord_progression: pop/jazz/blues/etc)' },
|
|
118
|
+
...SESSION_ID_PROP,
|
|
119
|
+
},
|
|
120
|
+
required: ['query'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
68
123
|
{
|
|
69
124
|
name: 'generate_scale',
|
|
70
|
-
description: 'Generate scale notes',
|
|
125
|
+
description: '[DEPRECATED — use music_theory({ query: "scale" }) instead] Generate scale notes',
|
|
71
126
|
inputSchema: {
|
|
72
127
|
type: 'object',
|
|
73
128
|
properties: {
|
|
@@ -79,61 +134,149 @@ export const tools = [
|
|
|
79
134
|
},
|
|
80
135
|
{
|
|
81
136
|
name: 'generate_chord_progression',
|
|
82
|
-
description: 'Generate chord progression',
|
|
137
|
+
description: '[DEPRECATED — use music_theory({ query: "chord_progression" }) instead] Generate chord progression',
|
|
83
138
|
inputSchema: {
|
|
84
139
|
type: 'object',
|
|
85
140
|
properties: {
|
|
86
141
|
key: { type: 'string', description: 'Key' },
|
|
87
142
|
style: { type: 'string', description: 'Style (pop/jazz/blues/etc)' },
|
|
143
|
+
...SESSION_ID_PROP,
|
|
88
144
|
},
|
|
89
145
|
required: ['key', 'style'],
|
|
90
146
|
},
|
|
91
147
|
},
|
|
148
|
+
{
|
|
149
|
+
name: 'generate_rhythm',
|
|
150
|
+
description: 'Generate a rhythmic pattern and append it to the current session. ' +
|
|
151
|
+
'type=euclidean produces a Euclidean rhythm with `hits` evenly distributed across `steps` (optional `sound` param, default "bd"). ' +
|
|
152
|
+
'type=polyrhythm overlays multiple sound layers with given pattern numbers. ' +
|
|
153
|
+
'Example: generate_rhythm({ type: "euclidean", hits: 3, steps: 8, sound: "hh" }). ' +
|
|
154
|
+
'For complete patterns (drums/bass/melody) use generate_part; for whole compositions use compose.',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
type: { type: 'string', enum: ['euclidean', 'polyrhythm'], description: 'Rhythm type' },
|
|
159
|
+
hits: { type: 'number', description: 'type=euclidean: hits count' },
|
|
160
|
+
steps: { type: 'number', description: 'type=euclidean: total steps' },
|
|
161
|
+
sound: { type: 'string', description: 'type=euclidean: sound to use (default bd)' },
|
|
162
|
+
sounds: { type: 'array', items: { type: 'string' }, description: 'type=polyrhythm: sounds per layer' },
|
|
163
|
+
patterns: { type: 'array', items: { type: 'number' }, description: 'type=polyrhythm: pattern numbers per layer' },
|
|
164
|
+
...SESSION_ID_PROP,
|
|
165
|
+
},
|
|
166
|
+
required: ['type'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
92
169
|
{
|
|
93
170
|
name: 'generate_euclidean',
|
|
94
|
-
description: 'Generate Euclidean rhythm',
|
|
171
|
+
description: '[DEPRECATED — use generate_rhythm({ type: "euclidean" }) instead] Generate Euclidean rhythm',
|
|
95
172
|
inputSchema: {
|
|
96
173
|
type: 'object',
|
|
97
174
|
properties: {
|
|
98
175
|
hits: { type: 'number', description: 'Number of hits' },
|
|
99
176
|
steps: { type: 'number', description: 'Total steps' },
|
|
100
177
|
sound: { type: 'string', description: 'Sound to use' },
|
|
178
|
+
...SESSION_ID_PROP,
|
|
101
179
|
},
|
|
102
180
|
required: ['hits', 'steps'],
|
|
103
181
|
},
|
|
104
182
|
},
|
|
105
183
|
{
|
|
106
184
|
name: 'generate_polyrhythm',
|
|
107
|
-
description: 'Generate polyrhythm',
|
|
185
|
+
description: '[DEPRECATED — use generate_rhythm({ type: "polyrhythm" }) instead] Generate polyrhythm',
|
|
108
186
|
inputSchema: {
|
|
109
187
|
type: 'object',
|
|
110
188
|
properties: {
|
|
111
189
|
sounds: { type: 'array', items: { type: 'string' }, description: 'Sounds to use' },
|
|
112
190
|
patterns: { type: 'array', items: { type: 'number' }, description: 'Pattern numbers' },
|
|
191
|
+
...SESSION_ID_PROP,
|
|
113
192
|
},
|
|
114
193
|
required: ['sounds', 'patterns'],
|
|
115
194
|
},
|
|
116
195
|
},
|
|
117
196
|
{
|
|
118
197
|
name: 'generate_fill',
|
|
119
|
-
description: 'Generate drum fill',
|
|
198
|
+
description: '[DEPRECATED — use generate_part({ role: "fill" }) instead] Generate drum fill',
|
|
120
199
|
inputSchema: {
|
|
121
200
|
type: 'object',
|
|
122
201
|
properties: {
|
|
123
202
|
style: { type: 'string', description: 'Fill style' },
|
|
124
203
|
bars: { type: 'number', description: 'Number of bars' },
|
|
204
|
+
...SESSION_ID_PROP,
|
|
125
205
|
},
|
|
126
206
|
required: ['style'],
|
|
127
207
|
},
|
|
128
208
|
},
|
|
129
209
|
];
|
|
130
210
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
131
|
-
async function appendOrSet(generated, ctx) {
|
|
132
|
-
const current = await ctx.getCurrentPatternSafe();
|
|
211
|
+
async function appendOrSet(generated, ctx, sessionId) {
|
|
212
|
+
const current = await ctx.getCurrentPatternSafe(sessionId);
|
|
133
213
|
const combined = current ? current + '\n' + generated : generated;
|
|
134
|
-
await ctx.writePatternSafe(combined);
|
|
214
|
+
await ctx.writePatternSafe(combined, sessionId);
|
|
215
|
+
}
|
|
216
|
+
function doScale(args, ctx) {
|
|
217
|
+
InputValidator.validateRootNote(args.root);
|
|
218
|
+
InputValidator.validateScaleName(args.scale);
|
|
219
|
+
const notes = ctx.theory.generateScale(args.root, args.scale);
|
|
220
|
+
return `${args.root} ${args.scale} scale: ${notes.join(', ')}`;
|
|
221
|
+
}
|
|
222
|
+
async function doChordProgression(args, ctx, sid) {
|
|
223
|
+
InputValidator.validateRootNote(args.key);
|
|
224
|
+
InputValidator.validateChordStyle(args.style);
|
|
225
|
+
const progression = ctx.theory.generateChordProgression(args.key, args.style);
|
|
226
|
+
const chordPattern = ctx.generator.generateChords(progression);
|
|
227
|
+
await appendOrSet(chordPattern, ctx, sid);
|
|
228
|
+
return `Generated ${args.style} progression in ${args.key}: ${progression}`;
|
|
229
|
+
}
|
|
230
|
+
async function doEuclidean(args, ctx, sid) {
|
|
231
|
+
InputValidator.validateEuclidean(args.hits, args.steps);
|
|
232
|
+
if (args.sound)
|
|
233
|
+
InputValidator.validateStringLength(args.sound, 'sound', 100, false);
|
|
234
|
+
const euclidean = ctx.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
|
|
235
|
+
await appendOrSet(euclidean, ctx, sid);
|
|
236
|
+
return `Generated Euclidean rhythm (${args.hits}/${args.steps})`;
|
|
237
|
+
}
|
|
238
|
+
async function doPolyrhythm(args, ctx, sid) {
|
|
239
|
+
args.sounds.forEach((s) => InputValidator.validateStringLength(s, 'sound', 100, false));
|
|
240
|
+
args.patterns.forEach((p) => InputValidator.validatePositiveInteger(p, 'pattern'));
|
|
241
|
+
const poly = ctx.generator.generatePolyrhythm(args.sounds, args.patterns);
|
|
242
|
+
await appendOrSet(poly, ctx, sid);
|
|
243
|
+
return 'Generated polyrhythm';
|
|
244
|
+
}
|
|
245
|
+
async function doDrums(args, ctx, sid) {
|
|
246
|
+
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
247
|
+
if (args.complexity !== undefined)
|
|
248
|
+
InputValidator.validateNormalizedValue(args.complexity, 'complexity');
|
|
249
|
+
const drums = ctx.generator.generateDrumPattern(args.style, args.complexity || 0.5);
|
|
250
|
+
await appendOrSet(drums, ctx, sid);
|
|
251
|
+
return `Generated ${args.style} drums`;
|
|
252
|
+
}
|
|
253
|
+
async function doBassline(args, ctx, sid) {
|
|
254
|
+
InputValidator.validateRootNote(args.key);
|
|
255
|
+
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
256
|
+
const bass = ctx.generator.generateBassline(args.key, args.style);
|
|
257
|
+
await appendOrSet(bass, ctx, sid);
|
|
258
|
+
return `Generated ${args.style} bassline in ${args.key}`;
|
|
259
|
+
}
|
|
260
|
+
async function doMelody(args, ctx, sid) {
|
|
261
|
+
InputValidator.validateRootNote(args.root);
|
|
262
|
+
InputValidator.validateScaleName(args.scale);
|
|
263
|
+
if (args.length !== undefined)
|
|
264
|
+
InputValidator.validatePositiveInteger(args.length, 'length');
|
|
265
|
+
const scale = ctx.theory.generateScale(args.root, args.scale);
|
|
266
|
+
const melody = ctx.generator.generateMelody(scale, args.length || 8);
|
|
267
|
+
await appendOrSet(melody, ctx, sid);
|
|
268
|
+
return `Generated melody in ${args.root} ${args.scale}`;
|
|
269
|
+
}
|
|
270
|
+
async function doFill(args, ctx, sid) {
|
|
271
|
+
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
272
|
+
if (args.bars !== undefined)
|
|
273
|
+
InputValidator.validatePositiveInteger(args.bars, 'bars');
|
|
274
|
+
const fill = ctx.generator.generateFill(args.style, args.bars || 1);
|
|
275
|
+
await appendOrSet(fill, ctx, sid);
|
|
276
|
+
return `Generated ${args.bars || 1} bar fill`;
|
|
135
277
|
}
|
|
136
278
|
export async function execute(name, args, ctx) {
|
|
279
|
+
const sid = args?.session_id;
|
|
137
280
|
switch (name) {
|
|
138
281
|
case 'generate_pattern': {
|
|
139
282
|
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
@@ -142,75 +285,48 @@ export async function execute(name, args, ctx) {
|
|
|
142
285
|
if (args.bpm !== undefined)
|
|
143
286
|
InputValidator.validateBPM(args.bpm);
|
|
144
287
|
const generated = ctx.generator.generateCompletePattern(args.style, args.key || 'C', args.bpm || 120);
|
|
145
|
-
await ctx.writePatternSafe(generated);
|
|
146
|
-
if (args.auto_play && ctx.isInitialized()) {
|
|
147
|
-
await ctx.
|
|
288
|
+
await ctx.writePatternSafe(generated, sid);
|
|
289
|
+
if (args.auto_play && (sid || ctx.isInitialized())) {
|
|
290
|
+
await ctx.getController(sid).play();
|
|
148
291
|
return `Generated ${args.style} pattern. Playing.`;
|
|
149
292
|
}
|
|
150
293
|
return `Generated ${args.style} pattern`;
|
|
151
294
|
}
|
|
152
|
-
case '
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
163
|
-
const bass = ctx.generator.generateBassline(args.key, args.style);
|
|
164
|
-
await appendOrSet(bass, ctx);
|
|
165
|
-
return `Generated ${args.style} bassline in ${args.key}`;
|
|
166
|
-
}
|
|
167
|
-
case 'generate_melody': {
|
|
168
|
-
InputValidator.validateRootNote(args.root);
|
|
169
|
-
InputValidator.validateScaleName(args.scale);
|
|
170
|
-
if (args.length !== undefined)
|
|
171
|
-
InputValidator.validatePositiveInteger(args.length, 'length');
|
|
172
|
-
const scale = ctx.theory.generateScale(args.root, args.scale);
|
|
173
|
-
const melody = ctx.generator.generateMelody(scale, args.length || 8);
|
|
174
|
-
await appendOrSet(melody, ctx);
|
|
175
|
-
return `Generated melody in ${args.root} ${args.scale}`;
|
|
176
|
-
}
|
|
177
|
-
case 'generate_scale': {
|
|
178
|
-
InputValidator.validateRootNote(args.root);
|
|
179
|
-
InputValidator.validateScaleName(args.scale);
|
|
180
|
-
const notes = ctx.theory.generateScale(args.root, args.scale);
|
|
181
|
-
return `${args.root} ${args.scale} scale: ${notes.join(', ')}`;
|
|
182
|
-
}
|
|
183
|
-
case 'generate_chord_progression': {
|
|
184
|
-
InputValidator.validateRootNote(args.key);
|
|
185
|
-
InputValidator.validateChordStyle(args.style);
|
|
186
|
-
const progression = ctx.theory.generateChordProgression(args.key, args.style);
|
|
187
|
-
const chordPattern = ctx.generator.generateChords(progression);
|
|
188
|
-
await appendOrSet(chordPattern, ctx);
|
|
189
|
-
return `Generated ${args.style} progression in ${args.key}: ${progression}`;
|
|
190
|
-
}
|
|
191
|
-
case 'generate_euclidean': {
|
|
192
|
-
InputValidator.validateEuclidean(args.hits, args.steps);
|
|
193
|
-
if (args.sound)
|
|
194
|
-
InputValidator.validateStringLength(args.sound, 'sound', 100, false);
|
|
195
|
-
const euclidean = ctx.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
|
|
196
|
-
await appendOrSet(euclidean, ctx);
|
|
197
|
-
return `Generated Euclidean rhythm (${args.hits}/${args.steps})`;
|
|
295
|
+
case 'generate_part': {
|
|
296
|
+
const role = args?.role;
|
|
297
|
+
switch (role) {
|
|
298
|
+
case 'drums': return await doDrums(args, ctx, sid);
|
|
299
|
+
case 'bass': return await doBassline(args, ctx, sid);
|
|
300
|
+
case 'melody': return await doMelody(args, ctx, sid);
|
|
301
|
+
case 'fill': return await doFill(args, ctx, sid);
|
|
302
|
+
default:
|
|
303
|
+
throw new Error(`Invalid role: ${role}. Must be one of: drums, bass, melody, fill`);
|
|
304
|
+
}
|
|
198
305
|
}
|
|
199
|
-
case '
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
306
|
+
case 'generate_drums': return await doDrums(args, ctx, sid);
|
|
307
|
+
case 'generate_bassline': return await doBassline(args, ctx, sid);
|
|
308
|
+
case 'generate_melody': return await doMelody(args, ctx, sid);
|
|
309
|
+
case 'music_theory': {
|
|
310
|
+
const q = args?.query;
|
|
311
|
+
if (q !== 'scale' && q !== 'chord_progression') {
|
|
312
|
+
throw new Error(`Invalid query: ${q}. Must be one of: scale, chord_progression`);
|
|
313
|
+
}
|
|
314
|
+
return q === 'scale' ? doScale(args, ctx) : await doChordProgression(args, ctx, sid);
|
|
205
315
|
}
|
|
206
|
-
case '
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
316
|
+
case 'generate_scale': return doScale(args, ctx);
|
|
317
|
+
case 'generate_chord_progression': return await doChordProgression(args, ctx, sid);
|
|
318
|
+
case 'generate_rhythm': {
|
|
319
|
+
const t = args?.type;
|
|
320
|
+
if (t !== 'euclidean' && t !== 'polyrhythm') {
|
|
321
|
+
throw new Error(`Invalid type: ${t}. Must be one of: euclidean, polyrhythm`);
|
|
322
|
+
}
|
|
323
|
+
return t === 'euclidean'
|
|
324
|
+
? await doEuclidean(args, ctx, sid)
|
|
325
|
+
: await doPolyrhythm(args, ctx, sid);
|
|
213
326
|
}
|
|
327
|
+
case 'generate_euclidean': return await doEuclidean(args, ctx, sid);
|
|
328
|
+
case 'generate_polyrhythm': return await doPolyrhythm(args, ctx, sid);
|
|
329
|
+
case 'generate_fill': return await doFill(args, ctx, sid);
|
|
214
330
|
default:
|
|
215
331
|
throw new Error(`generate module does not handle tool: ${name}`);
|
|
216
332
|
}
|