#!/usr/bin/env python3
"""
DAW MIDI Generator MCP Server
Professional MIDI file generator for any DAW, powered by Claude AI
"""
import asyncio
import json
import os
from pathlib import Path
from typing import Any, Sequence
from datetime import datetime
try:
from mcp.server import Server
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
from mido import MidiFile, MidiTrack, Message, MetaMessage
except ImportError:
print("Please install: pip3 install mcp mido")
exit(1)
# Initialize MCP server
app = Server("daw-midi-generator")
# MIDI output directory
MIDI_OUTPUT_DIR = Path.home() / "Music" / "DAW" / "Claude_MIDI"
MIDI_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
print(f"π MIDI files will be saved to: {MIDI_OUTPUT_DIR}")
# ====================
# MIDI UTILITIES
# ====================
def note_to_midi(note_name: str) -> int:
"""Convert note name (e.g., C4) to MIDI number"""
notes = {
'C': 0, 'C#': 1, 'Db': 1, 'D': 2, 'D#': 3, 'Eb': 3,
'E': 4, 'F': 5, 'F#': 6, 'Gb': 6, 'G': 7,
'G#': 8, 'Ab': 8, 'A': 9, 'A#': 10, 'Bb': 10, 'B': 11
}
note = note_name[:-1]
octave = int(note_name[-1])
return notes.get(note, 0) + (octave + 1) * 12
def get_chord_notes(root: str, chord_type: str = "major") -> list:
"""Get MIDI notes for a chord"""
root_midi = note_to_midi(root + "4")
if chord_type == "major":
return [root_midi, root_midi + 4, root_midi + 7]
elif chord_type == "minor":
return [root_midi, root_midi + 3, root_midi + 7]
elif chord_type == "7":
return [root_midi, root_midi + 4, root_midi + 7, root_midi + 10]
elif chord_type == "m7":
return [root_midi, root_midi + 3, root_midi + 7, root_midi + 10]
else:
return [root_midi, root_midi + 4, root_midi + 7]
# ====================
# MIDI GENERATORS
# ====================
def create_chord_progression_midi(key: str, progression: list, tempo: int = 120, bars: int = 4) -> str:
"""Generate a MIDI file with chord progression"""
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
# Set tempo and time signature
track.append(MetaMessage('set_tempo', tempo=int(60000000 / tempo)))
track.append(MetaMessage('time_signature', numerator=4, denominator=4))
# Bar duration (in ticks, 480 ticks = 1 beat)
ticks_per_beat = 480
beats_per_bar = 4
bar_duration = ticks_per_beat * beats_per_bar
# Generate chords
for i in range(bars):
chord_name = progression[i % len(progression)]
# Determine chord type
if "m" in chord_name.lower() and "7" in chord_name:
root = chord_name.replace("m", "").replace("7", "")
chord_type = "m7"
elif "m" in chord_name.lower():
root = chord_name.replace("m", "")
chord_type = "minor"
elif "7" in chord_name:
root = chord_name.replace("7", "")
chord_type = "7"
else:
root = chord_name
chord_type = "major"
notes = get_chord_notes(root, chord_type)
# Note on
for j, note in enumerate(notes):
track.append(Message('note_on', note=note, velocity=80, time=0 if j > 0 else 0))
# Note off after one bar
for j, note in enumerate(notes):
track.append(Message('note_off', note=note, velocity=0, time=bar_duration if j == 0 else 0))
# Save file
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"chord_progression_{key}_{timestamp}.mid"
filepath = MIDI_OUTPUT_DIR / filename
mid.save(str(filepath))
return str(filepath)
def create_drum_pattern_midi(pattern_type: str = "house", tempo: int = 120, bars: int = 4) -> str:
"""Generate a drum pattern MIDI file"""
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
track.append(MetaMessage('set_tempo', tempo=int(60000000 / tempo)))
track.append(MetaMessage('time_signature', numerator=4, denominator=4))
# General MIDI drum notes
KICK = 36
SNARE = 38
CLOSED_HAT = 42
OPEN_HAT = 46
ticks_per_beat = 480
eighth_note = ticks_per_beat // 2
sixteenth_note = ticks_per_beat // 4
bar_duration = ticks_per_beat * 4
# Create MIDI events for each bar
events = []
for bar in range(bars):
bar_offset = bar * bar_duration
if pattern_type == "house":
# Kick on 1, 2, 3, 4
for beat in [0, 1, 2, 3]:
events.append(('note_on', bar_offset + beat * ticks_per_beat, KICK, 100))
events.append(('note_off', bar_offset + beat * ticks_per_beat + 10, KICK, 0))
# Snare on 2, 4
for beat in [1, 3]:
events.append(('note_on', bar_offset + beat * ticks_per_beat, SNARE, 90))
events.append(('note_off', bar_offset + beat * ticks_per_beat + 10, SNARE, 0))
# Hi-hat on eighths
for eighth in range(8):
events.append(('note_on', bar_offset + eighth * eighth_note, CLOSED_HAT, 70))
events.append(('note_off', bar_offset + eighth * eighth_note + 10, CLOSED_HAT, 0))
elif pattern_type == "techno":
# Kick on every beat
for beat in [0, 1, 2, 3]:
events.append(('note_on', bar_offset + beat * ticks_per_beat, KICK, 100))
events.append(('note_off', bar_offset + beat * ticks_per_beat + 10, KICK, 0))
# Hi-hat on eighths
for eighth in range(8):
events.append(('note_on', bar_offset + eighth * eighth_note, CLOSED_HAT, 60))
events.append(('note_off', bar_offset + eighth * eighth_note + 10, CLOSED_HAT, 0))
# Open hat on off-beats
events.append(('note_on', bar_offset + 3 * ticks_per_beat + eighth_note, OPEN_HAT, 80))
events.append(('note_off', bar_offset + 3 * ticks_per_beat + eighth_note + 10, OPEN_HAT, 0))
elif pattern_type == "trap":
# Kick pattern
for pos in [0, ticks_per_beat, ticks_per_beat + eighth_note, 3 * ticks_per_beat]:
events.append(('note_on', bar_offset + pos, KICK, 100))
events.append(('note_off', bar_offset + pos + 10, KICK, 0))
# Snare on 2 and 4
for beat in [1, 3]:
events.append(('note_on', bar_offset + beat * ticks_per_beat, SNARE, 90))
events.append(('note_off', bar_offset + beat * ticks_per_beat + 10, SNARE, 0))
# Hi-hat on sixteenths
for sixteenth in range(16):
events.append(('note_on', bar_offset + sixteenth * sixteenth_note, CLOSED_HAT, 50))
events.append(('note_off', bar_offset + sixteenth * sixteenth_note + 10, CLOSED_HAT, 0))
# Sort events by time
events.sort(key=lambda x: x[1])
# Convert to MIDI messages with delta time
last_time = 0
for event in events:
msg_type, abs_time, note, velocity = event
delta = abs_time - last_time
track.append(Message(msg_type, note=note, velocity=velocity, time=delta, channel=9))
last_time = abs_time
# Save file
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"drums_{pattern_type}_{timestamp}.mid"
filepath = MIDI_OUTPUT_DIR / filename
mid.save(str(filepath))
return str(filepath)
def create_bass_line_midi(key: str, pattern: str = "steady", tempo: int = 120, bars: int = 4) -> str:
"""Generate a bass line MIDI file"""
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
track.append(MetaMessage('set_tempo', tempo=int(60000000 / tempo)))
root_note = note_to_midi(key + "2")
ticks_per_beat = 480
patterns_dict = {
"steady": [0, 0, 0, 0],
"syncopated": [0, -1, 7, 0],
"walking": [0, 2, 4, 7],
}
intervals = patterns_dict.get(pattern, patterns_dict["steady"])
for bar in range(bars):
for i, interval in enumerate(intervals):
if interval == -1: # Rest
continue
note = root_note + interval
track.append(Message('note_on', note=note, velocity=90, time=0))
track.append(Message('note_off', note=note, velocity=0, time=ticks_per_beat))
# Save file
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"bass_{key}_{pattern}_{timestamp}.mid"
filepath = MIDI_OUTPUT_DIR / filename
mid.save(str(filepath))
return str(filepath)
def create_melody_midi(scale: str, key: str, tempo: int = 120, bars: int = 4) -> str:
"""Generate a simple melody MIDI file"""
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
track.append(MetaMessage('set_tempo', tempo=int(60000000 / tempo)))
scales = {
"major": [0, 2, 4, 5, 7, 9, 11, 12],
"minor": [0, 2, 3, 5, 7, 8, 10, 12],
"pentatonic": [0, 2, 4, 7, 9, 12],
}
root = note_to_midi(key + "4")
scale_notes = [root + interval for interval in scales.get(scale, scales["major"])]
ticks_per_beat = 480
import random
for bar in range(bars):
for beat in range(4):
note = random.choice(scale_notes)
duration = random.choice([ticks_per_beat // 2, ticks_per_beat])
track.append(Message('note_on', note=note, velocity=70, time=0))
track.append(Message('note_off', note=note, velocity=0, time=duration))
# Save file
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"melody_{scale}_{key}_{timestamp}.mid"
filepath = MIDI_OUTPUT_DIR / filename
mid.save(str(filepath))
return str(filepath)
# ====================
# MCP TOOLS
# ====================
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List all available tools"""
return [
Tool(
name="generate_chord_progression",
description="Generate a MIDI file with a chord progression ready for any DAW",
inputSchema={
"type": "object",
"properties": {
"key": {"type": "string", "description": "Key (e.g., 'C', 'Am', 'F#')", "default": "C"},
"progression": {"type": "array", "items": {"type": "string"}, "description": "Chord progression (e.g., ['C', 'Am', 'F', 'G'])", "default": ["C", "Am", "F", "G"]},
"tempo": {"type": "number", "description": "Tempo in BPM", "default": 120},
"bars": {"type": "number", "description": "Number of bars", "default": 4}
},
"required": []
}
),
Tool(
name="generate_drum_pattern",
description="Generate a drum pattern MIDI file for any DAW",
inputSchema={
"type": "object",
"properties": {
"pattern_type": {"type": "string", "enum": ["house", "techno", "trap"], "description": "Rhythm pattern type", "default": "house"},
"tempo": {"type": "number", "description": "Tempo in BPM", "default": 120},
"bars": {"type": "number", "description": "Number of bars", "default": 4}
},
"required": []
}
),
Tool(
name="generate_bass_line",
description="Generate a bass line MIDI file",
inputSchema={
"type": "object",
"properties": {
"key": {"type": "string", "description": "Key (e.g., 'C', 'A', 'F#')", "default": "C"},
"pattern": {"type": "string", "enum": ["steady", "syncopated", "walking"], "description": "Bass pattern type", "default": "steady"},
"tempo": {"type": "number", "description": "Tempo in BPM", "default": 120},
"bars": {"type": "number", "description": "Number of bars", "default": 4}
},
"required": []
}
),
Tool(
name="generate_melody",
description="Generate a melody MIDI file based on a scale",
inputSchema={
"type": "object",
"properties": {
"scale": {"type": "string", "enum": ["major", "minor", "pentatonic"], "description": "Scale type", "default": "major"},
"key": {"type": "string", "description": "Key (e.g., 'C', 'A', 'F#')", "default": "C"},
"tempo": {"type": "number", "description": "Tempo in BPM", "default": 120},
"bars": {"type": "number", "description": "Number of bars", "default": 4}
},
"required": []
}
),
Tool(
name="create_full_arrangement",
description="Generate a complete arrangement with chords, bass, and drums",
inputSchema={
"type": "object",
"properties": {
"key": {"type": "string", "description": "Key (e.g., 'C', 'Am')", "default": "C"},
"genre": {"type": "string", "enum": ["house", "techno", "trap"], "description": "Music genre", "default": "house"},
"tempo": {"type": "number", "description": "Tempo in BPM", "default": 120},
"bars": {"type": "number", "description": "Number of bars", "default": 8}
},
"required": []
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
"""Execute the requested tool"""
try:
if name == "generate_chord_progression":
key = arguments.get("key", "C")
progression = arguments.get("progression", ["C", "Am", "F", "G"])
tempo = arguments.get("tempo", 120)
bars = arguments.get("bars", 4)
filepath = create_chord_progression_midi(key, progression, tempo, bars)
return [TextContent(
type="text",
text=f"πΉ Chord progression created!\n\n"
f"π File: {filepath}\n"
f"π΅ Key: {key}\n"
f"π Chords: {' - '.join(progression)}\n"
f"β±οΈ Tempo: {tempo} BPM\n"
f"π Bars: {bars}\n\n"
f"π‘ Import this file into your DAW to use it!"
)]
elif name == "generate_drum_pattern":
pattern_type = arguments.get("pattern_type", "house")
tempo = arguments.get("tempo", 120)
bars = arguments.get("bars", 4)
filepath = create_drum_pattern_midi(pattern_type, tempo, bars)
return [TextContent(
type="text",
text=f"π₯ Drum pattern created!\n\n"
f"π File: {filepath}\n"
f"πΆ Style: {pattern_type}\n"
f"β±οΈ Tempo: {tempo} BPM\n"
f"π Bars: {bars}\n\n"
f"π‘ Import into your DAW and assign to a drum kit!"
)]
elif name == "generate_bass_line":
key = arguments.get("key", "C")
pattern = arguments.get("pattern", "steady")
tempo = arguments.get("tempo", 120)
bars = arguments.get("bars", 4)
filepath = create_bass_line_midi(key, pattern, tempo, bars)
return [TextContent(
type="text",
text=f"πΈ Bass line created!\n\n"
f"π File: {filepath}\n"
f"π΅ Key: {key}\n"
f"πΌ Pattern: {pattern}\n"
f"β±οΈ Tempo: {tempo} BPM\n"
f"π Bars: {bars}\n\n"
f"π‘ Perfect for synth bass or electric bass!"
)]
elif name == "generate_melody":
scale = arguments.get("scale", "major")
key = arguments.get("key", "C")
tempo = arguments.get("tempo", 120)
bars = arguments.get("bars", 4)
filepath = create_melody_midi(scale, key, tempo, bars)
return [TextContent(
type="text",
text=f"πΌ Melody created!\n\n"
f"π File: {filepath}\n"
f"πΉ Scale: {scale}\n"
f"π΅ Key: {key}\n"
f"β±οΈ Tempo: {tempo} BPM\n"
f"π Bars: {bars}\n\n"
f"π‘ Use as a starting point for your lead!"
)]
elif name == "create_full_arrangement":
key = arguments.get("key", "C")
genre = arguments.get("genre", "house")
tempo = arguments.get("tempo", 120)
bars = arguments.get("bars", 8)
progression = ["C", "Am", "F", "G"] if "m" not in key else ["Am", "F", "C", "G"]
chord_file = create_chord_progression_midi(key, progression, tempo, bars)
drum_file = create_drum_pattern_midi(genre, tempo, bars)
bass_file = create_bass_line_midi(key.replace("m", ""), "syncopated", tempo, bars)
return [TextContent(
type="text",
text=f"π΅ Complete arrangement created!\n\n"
f"π¦ 3 MIDI files generated:\n"
f"1. πΉ Chords: {os.path.basename(chord_file)}\n"
f"2. π₯ Drums: {os.path.basename(drum_file)}\n"
f"3. πΈ Bass: {os.path.basename(bass_file)}\n\n"
f"π All saved to: {MIDI_OUTPUT_DIR}\n\n"
f"π΅ Key: {key}\n"
f"πΆ Genre: {genre}\n"
f"β±οΈ Tempo: {tempo} BPM\n"
f"π Bars: {bars}\n\n"
f"π‘ Import all files into your DAW for a complete arrangement!"
)]
else:
return [TextContent(type="text", text=f"Tool '{name}' not recognized")]
except Exception as e:
return [TextContent(type="text", text=f"Error: {str(e)}")]
# ====================
# MAIN
# ====================
async def main():
"""Start the MCP server"""
from mcp.server.stdio import stdio_server
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
print("π΅ DAW MIDI Generator - MCP Server started!")
print(f"π MIDI files will be saved to: {MIDI_OUTPUT_DIR}")
asyncio.run(main())