--[[
Scythe MCP - REAPER Command Poller
Polls a JSON file for commands from the MCP server.
No external dependencies required - uses REAPER's built-in Lua.
Installation:
1. Copy this file to REAPER Scripts folder
2. Actions -> Load ReaScript
3. Run the script (it will keep polling in background)
]]
local POLL_INTERVAL = 0.1 -- seconds
local TEMP_DIR = os.getenv("TEMP") or os.getenv("TMP") or "/tmp"
local COMMAND_FILE = TEMP_DIR .. "/scythe_mcp/command.json"
local RESPONSE_FILE = TEMP_DIR .. "/scythe_mcp/response.json"
-- Ensure directory exists
os.execute('mkdir "' .. TEMP_DIR .. '\\scythe_mcp" 2>nul')
-- Simple JSON parser (minimal, for our specific format)
local function parse_json(str)
local result = {}
-- Extract command
local cmd = str:match('"command"%s*:%s*"([^"]*)"')
if cmd then result.command = cmd end
-- Extract params
result.params = {}
-- Special handling for "code" field (may contain escaped quotes)
local code_start = str:find('"code"%s*:%s*"')
if code_start then
local quote_pos = str:find('"', code_start + 8)
if quote_pos then
-- Find the end quote (not escaped)
local i = quote_pos + 1
local code_chars = {}
while i <= #str do
local c = str:sub(i, i)
if c == '\\' and str:sub(i+1, i+1) == '"' then
table.insert(code_chars, '"')
i = i + 2
elseif c == '"' then
break
else
table.insert(code_chars, c)
i = i + 1
end
end
result.params.code = table.concat(code_chars)
end
end
-- Extract other simple params
for key, val in str:gmatch('"(%w+)"%s*:%s*([%d%.]+)') do
if key ~= "timestamp" then
result.params[key] = tonumber(val)
end
end
-- Extract string params (not code)
for key, val in str:gmatch('"(%w+)"%s*:%s*"([^"]*)"') do
if key ~= "command" and key ~= "code" and not result.params[key] then
result.params[key] = val
end
end
return result
end
local function write_response(data)
local f = io.open(RESPONSE_FILE, "w")
if f then
-- Simple JSON encoding
f:write('{"success": ' .. tostring(data.success))
if data.message then
f:write(', "message": "' .. data.message .. '"')
end
if data.error then
f:write(', "error": "' .. data.error .. '"')
end
f:write('}')
f:close()
end
end
local function log(msg)
reaper.ShowConsoleMsg("[Scythe] " .. tostring(msg) .. "\n")
end
-- Command handlers
local handlers = {}
function handlers.name_selected_track(params)
local track = reaper.GetSelectedTrack(0, 0)
if track then
reaper.GetSetMediaTrackInfo_String(track, "P_NAME", params.name or "New Track", true)
return {success = true, message = "Track renamed"}
end
return {success = false, error = "No track selected"}
end
function handlers.insert_midi_item(params)
local track = reaper.GetTrack(0, params.track_index or 0)
if not track then
return {success = false, error = "Track not found"}
end
local tempo = reaper.Master_GetTempo()
local pos_sec = (params.position or 0) * (60 / tempo)
local len_sec = (params.length or 4) * (60 / tempo)
local item = reaper.CreateNewMIDIItemInProj(track, pos_sec, pos_sec + len_sec, false)
if item then
return {success = true, message = "MIDI item created"}
end
return {success = false, error = "Failed to create item"}
end
function handlers.add_notes(params)
local track = reaper.GetTrack(0, params.track_index or 0)
if not track then
return {success = false, error = "Track not found"}
end
local item = reaper.GetTrackMediaItem(track, params.item_index or 0)
if not item then
return {success = false, error = "Item not found"}
end
local take = reaper.GetActiveTake(item)
if not take or not reaper.TakeIsMIDI(take) then
return {success = false, error = "Not a MIDI take"}
end
local ppq = 960
local notes = params.notes or {}
-- TODO: Implement full note parsing from JSON array
-- Currently, notes from add_notes MCP tool are not fully parsed
return {success = true, message = "Notes command received"}
end
function handlers.execute_lua(params)
local code = params.code
if not code or code == "" then
return {success = false, error = "No code provided"}
end
-- Compile and execute the Lua code
local func, err = load(code)
if not func then
return {success = false, error = "Syntax error: " .. tostring(err)}
end
-- Execute with pcall for safety
local ok, result = pcall(func)
if not ok then
return {success = false, error = "Runtime error: " .. tostring(result)}
end
-- Return result if any
if result ~= nil then
return {success = true, message = "Executed", result = tostring(result)}
end
return {success = true, message = "Lua code executed successfully"}
end
-- Last processed timestamp to avoid re-processing
local last_timestamp = 0
local function process_commands()
-- Check if command file exists
local f = io.open(COMMAND_FILE, "r")
if not f then
reaper.defer(process_commands)
return
end
local content = f:read("*all")
f:close()
if not content or content == "" then
reaper.defer(process_commands)
return
end
local cmd = parse_json(content)
-- Check timestamp to avoid reprocessing
local timestamp = tonumber(content:match('"timestamp"%s*:%s*([%d%.]+)') or 0)
if timestamp and timestamp > last_timestamp then
last_timestamp = timestamp
if cmd.command then
log("Received command: " .. cmd.command)
local handler = handlers[cmd.command]
if handler then
reaper.Undo_BeginBlock()
local result = handler(cmd.params or {})
reaper.Undo_EndBlock("Scythe: " .. cmd.command, -1)
write_response(result)
else
write_response({success = false, error = "Unknown command: " .. cmd.command})
end
end
end
reaper.defer(process_commands)
end
-- Start
log("Scythe MCP Command Poller started")
log("Watching: " .. COMMAND_FILE)
log("Make sure OSC is enabled in REAPER: Preferences -> Control/OSC/Web")
process_commands()