@williamzujkowski/live-coding-music-mcp 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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1735 -0
  3. package/dist/AudioAnalyzer.js +678 -0
  4. package/dist/PatternStore.js +169 -0
  5. package/dist/StrudelController.js +676 -0
  6. package/dist/__tests__/MusicTheory.test.js +245 -0
  7. package/dist/__tests__/PatternGenerator.test.js +337 -0
  8. package/dist/__tests__/PatternStore.test.js +153 -0
  9. package/dist/__tests__/utils/MockPlaywright.js +130 -0
  10. package/dist/index.js +21 -0
  11. package/dist/server/EnhancedMCPServer.js +654 -0
  12. package/dist/server/EnhancedMCPServerFixed.js +2506 -0
  13. package/dist/server/server.js +580 -0
  14. package/dist/server/tools/ai.js +530 -0
  15. package/dist/server/tools/analysis.js +132 -0
  16. package/dist/server/tools/capture.js +184 -0
  17. package/dist/server/tools/diagnostics.js +96 -0
  18. package/dist/server/tools/editor.js +128 -0
  19. package/dist/server/tools/generate.js +218 -0
  20. package/dist/server/tools/history.js +196 -0
  21. package/dist/server/tools/playback.js +41 -0
  22. package/dist/server/tools/session.js +115 -0
  23. package/dist/server/tools/storage.js +77 -0
  24. package/dist/server/tools/transform.js +399 -0
  25. package/dist/server/tools/types.js +9 -0
  26. package/dist/services/AudioCaptureService.js +279 -0
  27. package/dist/services/GeminiService.js +790 -0
  28. package/dist/services/MIDIExportService.js +412 -0
  29. package/dist/services/MusicTheory.js +186 -0
  30. package/dist/services/PatternGenerator.js +647 -0
  31. package/dist/services/SessionManager.js +316 -0
  32. package/dist/services/StrudelEngine.js +374 -0
  33. package/dist/services/__mocks__/StrudelEngine.js +157 -0
  34. package/dist/types/AudioAnalysis.js +4 -0
  35. package/dist/types/index.js +1 -0
  36. package/dist/utils/ErrorRecovery.js +248 -0
  37. package/dist/utils/InputValidator.js +281 -0
  38. package/dist/utils/Logger.js +19 -0
  39. package/dist/utils/PatternValidator.js +241 -0
  40. package/dist/utils/PerformanceMonitor.js +128 -0
  41. package/package.json +86 -0
@@ -0,0 +1,19 @@
1
+ export class Logger {
2
+ getTimestamp() {
3
+ return new Date().toISOString();
4
+ }
5
+ info(message, data) {
6
+ console.error(`[${this.getTimestamp()}] INFO: ${message}`, data ? JSON.stringify(data) : '');
7
+ }
8
+ error(message, error) {
9
+ console.error(`[${this.getTimestamp()}] ERROR: ${message}`, error?.message || error);
10
+ }
11
+ warn(message, data) {
12
+ console.error(`[${this.getTimestamp()}] WARN: ${message}`, data ? JSON.stringify(data) : '');
13
+ }
14
+ debug(message, data) {
15
+ if (process.env.DEBUG) {
16
+ console.error(`[${this.getTimestamp()}] DEBUG: ${message}`, data ? JSON.stringify(data) : '');
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * PatternValidator - Validates Strudel patterns before execution
3
+ * Provides syntax checking and error recovery for generated patterns
4
+ */
5
+ export class PatternValidator {
6
+ /**
7
+ * Validates a Strudel pattern for syntax errors
8
+ * @param pattern - The pattern code to validate
9
+ * @returns ValidationResult with errors and suggestions
10
+ */
11
+ validate(pattern) {
12
+ const errors = [];
13
+ const warnings = [];
14
+ const suggestions = [];
15
+ // Check for empty pattern
16
+ if (!pattern || pattern.trim().length === 0) {
17
+ errors.push('Pattern is empty');
18
+ suggestions.push('Add a simple pattern like: s("bd*4")');
19
+ return { valid: false, errors, warnings, suggestions };
20
+ }
21
+ // Check for balanced parentheses
22
+ const parenCheck = this.checkBalancedParentheses(pattern);
23
+ if (!parenCheck.valid) {
24
+ errors.push(`Unbalanced parentheses: ${parenCheck.message}`);
25
+ suggestions.push('Check that all ( ) [ ] { } are properly matched');
26
+ }
27
+ // Check for balanced quotes
28
+ const quoteCheck = this.checkBalancedQuotes(pattern);
29
+ if (!quoteCheck.valid) {
30
+ errors.push(`Unbalanced quotes: ${quoteCheck.message}`);
31
+ suggestions.push('Ensure all strings are properly quoted');
32
+ }
33
+ // Check for common Strudel syntax patterns
34
+ const syntaxCheck = this.checkStrudelSyntax(pattern);
35
+ warnings.push(...syntaxCheck.warnings);
36
+ suggestions.push(...syntaxCheck.suggestions);
37
+ // Check for dangerous operations
38
+ const safetyCheck = this.checkSafety(pattern);
39
+ if (!safetyCheck.safe) {
40
+ errors.push(...safetyCheck.errors);
41
+ }
42
+ warnings.push(...safetyCheck.warnings);
43
+ return {
44
+ valid: errors.length === 0,
45
+ errors,
46
+ warnings,
47
+ suggestions
48
+ };
49
+ }
50
+ /**
51
+ * Checks if parentheses, brackets, and braces are balanced
52
+ */
53
+ checkBalancedParentheses(pattern) {
54
+ const stack = [];
55
+ const pairs = { '(': ')', '[': ']', '{': '}' };
56
+ const opening = Object.keys(pairs);
57
+ const closing = Object.values(pairs);
58
+ for (let i = 0; i < pattern.length; i++) {
59
+ const char = pattern[i];
60
+ if (opening.includes(char)) {
61
+ stack.push(char);
62
+ }
63
+ else if (closing.includes(char)) {
64
+ if (stack.length === 0) {
65
+ return { valid: false, message: `Unexpected closing '${char}' at position ${i}` };
66
+ }
67
+ const last = stack.pop();
68
+ if (pairs[last] !== char) {
69
+ return { valid: false, message: `Mismatched '${last}' and '${char}' at position ${i}` };
70
+ }
71
+ }
72
+ }
73
+ if (stack.length > 0) {
74
+ return { valid: false, message: `Unclosed '${stack[stack.length - 1]}'` };
75
+ }
76
+ return { valid: true, message: '' };
77
+ }
78
+ /**
79
+ * Checks if quotes are balanced
80
+ */
81
+ checkBalancedQuotes(pattern) {
82
+ let inSingleQuote = false;
83
+ let inDoubleQuote = false;
84
+ let inBacktick = false;
85
+ for (let i = 0; i < pattern.length; i++) {
86
+ const char = pattern[i];
87
+ const prevChar = i > 0 ? pattern[i - 1] : '';
88
+ // Skip escaped quotes
89
+ if (prevChar === '\\') {
90
+ continue;
91
+ }
92
+ if (char === "'" && !inDoubleQuote && !inBacktick) {
93
+ inSingleQuote = !inSingleQuote;
94
+ }
95
+ else if (char === '"' && !inSingleQuote && !inBacktick) {
96
+ inDoubleQuote = !inDoubleQuote;
97
+ }
98
+ else if (char === '`' && !inSingleQuote && !inDoubleQuote) {
99
+ inBacktick = !inBacktick;
100
+ }
101
+ }
102
+ if (inSingleQuote) {
103
+ return { valid: false, message: 'Unclosed single quote' };
104
+ }
105
+ if (inDoubleQuote) {
106
+ return { valid: false, message: 'Unclosed double quote' };
107
+ }
108
+ if (inBacktick) {
109
+ return { valid: false, message: 'Unclosed backtick' };
110
+ }
111
+ return { valid: true, message: '' };
112
+ }
113
+ /**
114
+ * Checks for common Strudel syntax patterns and potential issues
115
+ */
116
+ checkStrudelSyntax(pattern) {
117
+ const warnings = [];
118
+ const suggestions = [];
119
+ // Check for common Strudel functions
120
+ const hasSound = /s\(/.test(pattern);
121
+ const hasNote = /note\(/.test(pattern);
122
+ const hasStack = /stack\(/.test(pattern);
123
+ if (!hasSound && !hasNote && !hasStack) {
124
+ warnings.push('Pattern may not produce sound - no s(), note(), or stack() found');
125
+ suggestions.push('Try adding s("bd*4") for a basic kick drum pattern');
126
+ }
127
+ // Check for common mistakes
128
+ if (/s\([^"']/.test(pattern)) {
129
+ warnings.push('Sound patterns should be in quotes: s("bd") not s(bd)');
130
+ }
131
+ // Check for undefined variables
132
+ const functionCalls = pattern.match(/\b([a-z_][a-z0-9_]*)\s*\(/gi);
133
+ if (functionCalls) {
134
+ const knownFunctions = [
135
+ 's', 'note', 'stack', 'setcpm', 'sound', 'n', 'room', 'delay',
136
+ 'reverb', 'fast', 'slow', 'rev', 'iter', 'jux', 'every', 'sometimes',
137
+ 'rarely', 'often', 'gain', 'pan', 'cutoff', 'resonance', 'attack',
138
+ 'release', 'sustain', 'decay', 'lpf', 'hpf', 'bpf', 'struct', 'euclid',
139
+ 'euclidLegacy', 'choose', 'chooseWith', 'range', 'sine', 'saw', 'square',
140
+ 'tri', 'rand', 'perlin', 'add', 'sub', 'mul', 'div', 'mod', 'pow',
141
+ 'min', 'max', 'floor', 'ceil', 'round'
142
+ ];
143
+ functionCalls.forEach(call => {
144
+ const funcName = call.replace(/\s*\($/, '').toLowerCase();
145
+ if (!knownFunctions.includes(funcName)) {
146
+ warnings.push(`Unknown function: ${funcName}`);
147
+ }
148
+ });
149
+ }
150
+ // Check for tempo setting
151
+ if (!pattern.includes('setcpm') && !pattern.includes('cpm')) {
152
+ suggestions.push('Consider adding setcpm(120) to set tempo');
153
+ }
154
+ return { warnings, suggestions };
155
+ }
156
+ /**
157
+ * Checks for potentially dangerous operations
158
+ */
159
+ checkSafety(pattern) {
160
+ const errors = [];
161
+ const warnings = [];
162
+ // Check for excessive gain that could damage speakers
163
+ const gainMatches = pattern.match(/\.gain\(([0-9.]+)\)/g);
164
+ if (gainMatches) {
165
+ gainMatches.forEach(match => {
166
+ const gainValue = parseFloat(match.match(/([0-9.]+)/)?.[0] || '0');
167
+ if (gainValue > 2) {
168
+ warnings.push(`High gain value detected: ${gainValue} - may be too loud`);
169
+ }
170
+ if (gainValue > 5) {
171
+ errors.push(`Dangerous gain value: ${gainValue} - reduced to 2.0 for safety`);
172
+ }
173
+ });
174
+ }
175
+ // Check for potentially infinite loops
176
+ if (/while\s*\(true\)/.test(pattern) || /for\s*\(.*;;.*\)/.test(pattern)) {
177
+ errors.push('Potential infinite loop detected');
178
+ }
179
+ // Check for eval or similar dangerous functions
180
+ if (/\beval\(/.test(pattern) || /Function\(/.test(pattern)) {
181
+ errors.push('Use of eval() or Function() is not allowed');
182
+ }
183
+ return {
184
+ safe: errors.length === 0,
185
+ errors,
186
+ warnings
187
+ };
188
+ }
189
+ /**
190
+ * Attempts to auto-fix common pattern errors
191
+ * @param pattern - The pattern to fix
192
+ * @returns Fixed pattern with applied corrections
193
+ */
194
+ autoFix(pattern) {
195
+ const fixes = [];
196
+ let fixed = pattern;
197
+ // Fix excessive gain values
198
+ fixed = fixed.replace(/\.gain\(([0-9.]+)\)/g, (match, value) => {
199
+ const gainValue = parseFloat(value);
200
+ if (gainValue > 2) {
201
+ fixes.push(`Reduced gain from ${gainValue} to 2.0`);
202
+ return '.gain(2.0)';
203
+ }
204
+ return match;
205
+ });
206
+ // Add missing quotes around sound patterns
207
+ fixed = fixed.replace(/s\(([a-z0-9*]+)\)/gi, (match, sound) => {
208
+ if (!sound.includes('"') && !sound.includes("'")) {
209
+ fixes.push(`Added quotes around sound: ${sound}`);
210
+ return `s("${sound}")`;
211
+ }
212
+ return match;
213
+ });
214
+ return { pattern: fixed, fixes };
215
+ }
216
+ /**
217
+ * Provides helpful suggestions based on pattern analysis
218
+ * @param pattern - The pattern to analyze
219
+ * @returns Array of suggestions for improvement
220
+ */
221
+ suggest(pattern) {
222
+ const suggestions = [];
223
+ // Suggest adding effects if pattern is plain
224
+ if (!pattern.includes('room') && !pattern.includes('delay') && !pattern.includes('reverb')) {
225
+ suggestions.push('Consider adding spatial effects: .room(0.5) or .delay(0.25)');
226
+ }
227
+ // Suggest adding variation
228
+ if (!pattern.includes('sometimes') && !pattern.includes('every')) {
229
+ suggestions.push('Add variation with .sometimes() or .every()');
230
+ }
231
+ // Suggest structure if pattern is very simple
232
+ if (pattern.length < 30 && pattern.includes('s(')) {
233
+ suggestions.push('Try stacking multiple patterns with stack()');
234
+ }
235
+ // Suggest tempo control
236
+ if (!pattern.includes('setcpm') && !pattern.includes('cpm')) {
237
+ suggestions.push('Set tempo with setcpm(120) at the beginning');
238
+ }
239
+ return suggestions;
240
+ }
241
+ }
@@ -0,0 +1,128 @@
1
+ export class PerformanceMonitor {
2
+ metrics = new Map();
3
+ startTimes = new Map();
4
+ getTime;
5
+ constructor(timeProvider) {
6
+ this.getTime = timeProvider || (() => performance.now());
7
+ }
8
+ startOperation(operationName) {
9
+ this.startTimes.set(operationName, this.getTime());
10
+ }
11
+ endOperation(operationName, success = true) {
12
+ const startTime = this.startTimes.get(operationName);
13
+ if (startTime === undefined)
14
+ return 0;
15
+ const duration = this.getTime() - startTime;
16
+ this.startTimes.delete(operationName);
17
+ const metric = this.metrics.get(operationName) || {
18
+ count: 0,
19
+ totalTime: 0,
20
+ minTime: Infinity,
21
+ maxTime: 0,
22
+ errors: 0
23
+ };
24
+ metric.count++;
25
+ metric.totalTime += duration;
26
+ metric.minTime = Math.min(metric.minTime, duration);
27
+ metric.maxTime = Math.max(metric.maxTime, duration);
28
+ if (!success)
29
+ metric.errors++;
30
+ this.metrics.set(operationName, metric);
31
+ return duration;
32
+ }
33
+ async measureAsync(operationName, operation) {
34
+ this.startOperation(operationName);
35
+ try {
36
+ const result = await operation();
37
+ this.endOperation(operationName, true);
38
+ return result;
39
+ }
40
+ catch (error) {
41
+ this.endOperation(operationName, false);
42
+ throw error;
43
+ }
44
+ }
45
+ getMetrics(operationName) {
46
+ if (operationName !== undefined) {
47
+ const metric = this.metrics.get(operationName);
48
+ if (!metric)
49
+ return null;
50
+ return {
51
+ operation: operationName,
52
+ calls: metric.count,
53
+ averageTime: metric.totalTime / metric.count,
54
+ minTime: metric.minTime,
55
+ maxTime: metric.maxTime,
56
+ totalTime: metric.totalTime,
57
+ errorRate: (metric.errors / metric.count) * 100
58
+ };
59
+ }
60
+ const allMetrics = [];
61
+ this.metrics.forEach((metric, name) => {
62
+ allMetrics.push({
63
+ operation: name,
64
+ calls: metric.count,
65
+ averageTime: metric.totalTime / metric.count,
66
+ minTime: metric.minTime,
67
+ maxTime: metric.maxTime,
68
+ totalTime: metric.totalTime,
69
+ errorRate: (metric.errors / metric.count) * 100
70
+ });
71
+ });
72
+ return allMetrics.sort((a, b) => b.totalTime - a.totalTime);
73
+ }
74
+ getReport() {
75
+ const metrics = this.getMetrics();
76
+ if (!metrics || metrics.length === 0) {
77
+ return 'No performance metrics collected';
78
+ }
79
+ let report = '\n=== PERFORMANCE REPORT ===\n\n';
80
+ report += 'Operation'.padEnd(30) + 'Calls'.padEnd(10) + 'Avg(ms)'.padEnd(12) + 'Min(ms)'.padEnd(12) + 'Max(ms)'.padEnd(12) + 'Errors\n';
81
+ report += '-'.repeat(86) + '\n';
82
+ metrics.forEach(m => {
83
+ report += m.operation.padEnd(30) +
84
+ m.calls.toString().padEnd(10) +
85
+ m.averageTime.toFixed(2).padEnd(12) +
86
+ m.minTime.toFixed(2).padEnd(12) +
87
+ m.maxTime.toFixed(2).padEnd(12) +
88
+ `${m.errorRate.toFixed(1)}%\n`;
89
+ });
90
+ const totalCalls = metrics.reduce((sum, m) => sum + m.calls, 0);
91
+ const totalTime = metrics.reduce((sum, m) => sum + m.totalTime, 0);
92
+ report += '\n';
93
+ report += `Total Operations: ${totalCalls}\n`;
94
+ report += `Total Time: ${totalTime.toFixed(2)}ms\n`;
95
+ report += `Average per Operation: ${(totalTime / totalCalls).toFixed(2)}ms\n`;
96
+ return report;
97
+ }
98
+ reset() {
99
+ this.metrics.clear();
100
+ this.startTimes.clear();
101
+ }
102
+ // Memory usage tracking
103
+ getMemoryUsage() {
104
+ if (typeof process !== 'undefined' && process.memoryUsage) {
105
+ const usage = process.memoryUsage();
106
+ return {
107
+ heapUsed: (usage.heapUsed / 1024 / 1024).toFixed(2) + ' MB',
108
+ heapTotal: (usage.heapTotal / 1024 / 1024).toFixed(2) + ' MB',
109
+ rss: (usage.rss / 1024 / 1024).toFixed(2) + ' MB',
110
+ external: (usage.external / 1024 / 1024).toFixed(2) + ' MB'
111
+ };
112
+ }
113
+ return null;
114
+ }
115
+ // Get top slowest operations
116
+ getBottlenecks(limit = 5) {
117
+ const metrics = this.getMetrics();
118
+ return metrics
119
+ .sort((a, b) => b.averageTime - a.averageTime)
120
+ .slice(0, limit)
121
+ .map(m => ({
122
+ operation: m.operation,
123
+ averageTime: m.averageTime.toFixed(2) + 'ms',
124
+ maxTime: m.maxTime.toFixed(2) + 'ms',
125
+ calls: m.calls
126
+ }));
127
+ }
128
+ }
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@williamzujkowski/live-coding-music-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server (unofficial, fan project) for AI-assisted live-coding music via strudel.cc. Lets Claude drive pattern generation, audio analysis, and MIDI export through the Strudel browser REPL.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "live-coding-music-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "README.md",
13
+ "LICENSE",
14
+ "package.json"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc && npx tsx scripts/generate-tool-docs.ts",
18
+ "dev": "tsx watch src/index.ts",
19
+ "start": "node dist/index.js",
20
+ "test": "jest --coverage",
21
+ "test:nocov": "jest",
22
+ "test:watch": "jest --watch",
23
+ "test:integration": "node tests/strudel-integration.js",
24
+ "test:browser": "HEADLESS=true jest --testPathPattern=browser",
25
+ "lint": "eslint src --ext .ts",
26
+ "format": "prettier --write \"src/**/*.ts\"",
27
+ "docker:build": "docker build -t live-coding-music-mcp .",
28
+ "docker:run": "docker-compose up",
29
+ "prepublishOnly": "npm run build && npm run test:nocov -- --testPathIgnorePatterns=browser",
30
+ "prepare": "npm run build",
31
+ "validate": "echo '{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}' | node dist/index.js",
32
+ "benchmark": "tsx src/__tests__/benchmarks/latency.benchmark.ts",
33
+ "benchmark:gate": "tsx src/__tests__/benchmarks/latency.benchmark.ts --gate",
34
+ "clean": "rm -rf dist coverage"
35
+ },
36
+ "dependencies": {
37
+ "@google/generative-ai": "^0.24.1",
38
+ "@modelcontextprotocol/sdk": "^1.24.3",
39
+ "@strudel/core": "1.2.5",
40
+ "@strudel/mini": "1.2.5",
41
+ "@strudel/tonal": "1.2.5",
42
+ "@strudel/transpiler": "1.2.5",
43
+ "@tonejs/midi": "^2.0.28",
44
+ "google-auth-library": "^10.5.0",
45
+ "playwright": "^1.52.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/jest": "^29.5.0",
49
+ "@types/node": "^22",
50
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
51
+ "@typescript-eslint/parser": "^7.0.0",
52
+ "eslint": "^8.57.0",
53
+ "jest": "^29.7.0",
54
+ "prettier": "^3.2.0",
55
+ "ts-jest": "^29.1.0",
56
+ "tsx": "^4.0.0",
57
+ "typescript": "^5.0.0"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/williamzujkowski/live-coding-music-mcp.git"
62
+ },
63
+ "keywords": [
64
+ "mcp",
65
+ "model-context-protocol",
66
+ "strudel",
67
+ "tidalcycles",
68
+ "music",
69
+ "ai",
70
+ "claude",
71
+ "live-coding",
72
+ "algorithmic-composition",
73
+ "web-audio",
74
+ "music-generation"
75
+ ],
76
+ "author": "William Zujkowski <william.zujkowski@gmail.com>",
77
+ "license": "MIT",
78
+ "bugs": {
79
+ "url": "https://github.com/williamzujkowski/live-coding-music-mcp/issues"
80
+ },
81
+ "homepage": "https://github.com/williamzujkowski/live-coding-music-mcp#readme",
82
+ "engines": {
83
+ "node": ">=22.0.0",
84
+ "npm": ">=10.0.0"
85
+ }
86
+ }