Skip to main content
Glama
project.py16.7 kB
""" Project Management Tools for REAPER MCP This module contains tools for managing REAPER projects. """ from typing import Optional from ..bridge import bridge # ============================================================================ # Basic Project Management (6 tools) # ============================================================================ async def get_project_name(project_index: int = 0) -> str: """Get the current project name""" result = await bridge.call_lua("GetProjectName", [project_index, "", 512]) if result.get("ok"): ret = result.get("ret", []) if isinstance(ret, list) and len(ret) > 0: project_name = ret[0] if len(ret) > 0 else "Untitled" return f"Project name: {project_name}" else: return "Project name: Untitled" else: raise Exception(f"Failed to get project name: {result.get('error', 'Unknown error')}") async def get_project_path(project_index: int = 0) -> str: """Get the current project path""" result = await bridge.call_lua("GetProjectPath", ["", 2048]) if result.get("ok"): path = result.get("ret", "") return f"Project path: {path}" else: raise Exception(f"Failed to get project path: {result.get('error', 'Unknown error')}") async def save_project(project_index: int = 0, force_save_as: bool = False) -> str: """Save the current project""" result = await bridge.call_lua("Main_SaveProject", [project_index, force_save_as]) if result.get("ok"): return "Project saved successfully" else: raise Exception(f"Failed to save project: {result.get('error', 'Unknown error')}") async def open_project(filename: str, in_new_tab: bool = False) -> str: """Open a REAPER project file""" # 0 = current tab, 1 = new tab mode = 1 if in_new_tab else 0 result = await bridge.call_lua("Main_openProject", [filename]) if result.get("ok"): return f"Opened project: {filename}" else: raise Exception(f"Failed to open project: {result.get('error', 'Unknown error')}") async def close_project(save_prompt: bool = True) -> str: """Close the current project""" # Use the close project action result = await bridge.call_lua("Main_OnCommand", [40860, 0]) # File: Close project if result.get("ok"): return "Project closed" else: raise Exception(f"Failed to close project: {result.get('error', 'Unknown error')}") async def mark_project_dirty(project_index: int = 0) -> str: """Mark the project as having unsaved changes""" result = await bridge.call_lua("MarkProjectDirty", [project_index]) if result.get("ok"): return "Project marked as dirty (unsaved changes)" else: raise Exception(f"Failed to mark project dirty: {result.get('error', 'Unknown error')}") # ============================================================================ # Project Instance Management (3 tools) # ============================================================================ async def enum_projects() -> str: """Get information about all open projects""" result = await bridge.call_lua("EnumProjects", []) if result.get("ok"): projects = [] index = 0 # Get all open projects while True: proj_result = await bridge.call_lua("EnumProjects", [index]) if not proj_result.get("ok") or not proj_result.get("ret"): break # Get project name name_result = await bridge.call_lua("GetProjectName", [index, "", 512]) if name_result.get("ok"): ret = name_result.get("ret", []) if isinstance(ret, list) and len(ret) > 0: project_name = ret[0] else: project_name = "Untitled" else: project_name = "Unknown" projects.append(f"{index}: {project_name}") index += 1 if projects: return f"Open projects:\n" + "\n".join(projects) else: return "No projects open" else: raise Exception(f"Failed to enumerate projects: {result.get('error', 'Unknown error')}") async def select_project_instance(project_index: int) -> str: """Switch to a specific project by index""" result = await bridge.call_lua("SelectProjectInstance", [project_index]) if result.get("ok"): return f"Switched to project {project_index}" else: raise Exception(f"Failed to switch project: {result.get('error', 'Unknown error')}") async def get_current_project_index() -> str: """Get the index of the currently active project""" result = await bridge.call_lua("GetCurrentProjectIndex", []) if result.get("ok"): index = result.get("ret", -1) return f"Current project index: {index}" else: raise Exception(f"Failed to get current project index: {result.get('error', 'Unknown error')}") # ============================================================================ # Project Properties (8 tools) # ============================================================================ async def get_project_length(project_index: int = 0) -> str: """Get the project length in seconds""" result = await bridge.call_lua("GetProjectLength", [project_index]) if result.get("ok"): length = result.get("ret", 0.0) return f"Project length: {length:.3f} seconds" else: raise Exception(f"Failed to get project length: {result.get('error', 'Unknown error')}") async def set_project_length(length: float, project_index: int = 0) -> str: """Set the project length in seconds""" # Get current cursor position cursor_result = await bridge.call_lua("GetCursorPosition", []) if not cursor_result.get("ok"): raise Exception("Failed to get cursor position") original_pos = cursor_result.get("ret", 0.0) # Move cursor to the desired length await bridge.call_lua("SetEditCurPos", [length, False, False]) # Use action to set project end to cursor result = await bridge.call_lua("Main_OnCommand", [40043, 0]) # Transport: Go to end of project # Restore cursor position await bridge.call_lua("SetEditCurPos", [original_pos, False, False]) if result.get("ok"): return f"Set project length to {length:.3f} seconds" else: raise Exception(f"Failed to set project length: {result.get('error', 'Unknown error')}") async def get_project_tempo() -> str: """Get the current project tempo in BPM""" # Get master tempo result = await bridge.call_lua("Master_GetTempo", []) if result.get("ok"): tempo = result.get("ret", 120.0) return f"Project tempo: {tempo:.2f} BPM" else: raise Exception(f"Failed to get project tempo: {result.get('error', 'Unknown error')}") async def set_project_tempo(tempo: float, position: Optional[float] = None) -> str: """Set the project tempo in BPM""" # If position is not specified, use current cursor position if position is None: pos_result = await bridge.call_lua("GetCursorPosition", []) position = pos_result.get("ret", 0.0) if pos_result.get("ok") else 0.0 result = await bridge.call_lua("SetTempoTimeSigMarker", [0, -1, position, -1, -1, tempo, 0, 0, True]) if result.get("ok"): return f"Set project tempo to {tempo:.2f} BPM" else: raise Exception(f"Failed to set project tempo: {result.get('error', 'Unknown error')}") async def get_project_time_signature() -> str: """Get the current project time signature""" # Get time signature at edit cursor cursor_result = await bridge.call_lua("GetCursorPosition", []) if not cursor_result.get("ok"): raise Exception("Failed to get cursor position") position = cursor_result.get("ret", 0.0) result = await bridge.call_lua("TimeMap_GetTimeSigAtTime", [0, position]) if result.get("ok"): # Result should contain numerator and denominator tsig_num = result.get("tsig_num", 4) tsig_denom = result.get("tsig_denom", 4) return f"Time signature: {tsig_num}/{tsig_denom}" else: raise Exception(f"Failed to get time signature: {result.get('error', 'Unknown error')}") async def set_project_time_signature(numerator: int, denominator: int, position: Optional[float] = None) -> str: """Set the project time signature""" # If position is not specified, use current cursor position if position is None: pos_result = await bridge.call_lua("GetCursorPosition", []) position = pos_result.get("ret", 0.0) if pos_result.get("ok") else 0.0 result = await bridge.call_lua("SetTempoTimeSigMarker", [0, -1, position, -1, -1, -1, numerator, denominator, False]) if result.get("ok"): return f"Set time signature to {numerator}/{denominator}" else: raise Exception(f"Failed to set time signature: {result.get('error', 'Unknown error')}") async def get_project_sample_rate() -> str: """Get the project sample rate""" result = await bridge.call_lua("GetSetProjectInfo_String", [0, "RENDER_SRATE", "", False]) if result.get("ok"): # Parse sample rate from string srate_str = result.get("ret", "48000") try: # Extract numeric part srate = float(srate_str.split()[0]) if srate_str else 48000 # If it's a whole number, format without decimals if srate.is_integer(): return f"Project sample rate: {int(srate)} Hz" else: return f"Project sample rate: {srate} Hz" except: return f"Project sample rate: {srate_str}" else: raise Exception(f"Failed to get sample rate: {result.get('error', 'Unknown error')}") async def set_project_sample_rate(sample_rate: int) -> str: """Set the project sample rate""" result = await bridge.call_lua("GetSetProjectInfo_String", [0, "RENDER_SRATE", str(sample_rate), True]) if result.get("ok"): return f"Set project sample rate to {sample_rate} Hz" else: raise Exception(f"Failed to set sample rate: {result.get('error', 'Unknown error')}") # ============================================================================ # Project Grid & Display (2 tools) # ============================================================================ async def get_project_grid_division() -> str: """Get the project grid division""" result = await bridge.call_lua("GetSetProjectGrid", [0, False, 0, 0, 0]) if result.get("ok"): # Result contains division and swing info division = result.get("division", 0.25) swing = result.get("swing", 0.0) swing_amt = result.get("swing_amt", 1.0) # Convert division to musical notation divisions = { 4.0: "1 bar", 2.0: "1/2", 1.0: "1/4", 0.5: "1/8", 0.25: "1/16", 0.125: "1/32", 0.0625: "1/64" } div_text = divisions.get(division, f"{division}") swing_text = f", swing: {swing:.0%} ({swing_amt:.2f})" if swing != 0 else "" return f"Grid division: {div_text}{swing_text}" else: raise Exception(f"Failed to get grid division: {result.get('error', 'Unknown error')}") async def set_project_grid_division(division: float, swing: float = 0.0, swing_amt: float = 1.0) -> str: """Set the project grid division""" result = await bridge.call_lua("GetSetProjectGrid", [0, True, division, swing, swing_amt]) if result.get("ok"): # Convert division to musical notation divisions = { 4.0: "1 bar", 2.0: "1/2", 1.0: "1/4", 0.5: "1/8", 0.25: "1/16", 0.125: "1/32", 0.0625: "1/64" } div_text = divisions.get(division, f"{division}") swing_text = f" with swing: {swing:.0%}" if swing != 0 else "" return f"Set grid division to {div_text}{swing_text}" else: raise Exception(f"Failed to set grid division: {result.get('error', 'Unknown error')}") # ============================================================================ # Project Notes & Metadata (2 tools) # ============================================================================ async def get_project_notes() -> str: """Get the project notes""" result = await bridge.call_lua("GetSetProjectNotes", [0, False, ""]) if result.get("ok"): notes = result.get("ret", "") if notes: # Limit length for display if len(notes) > 500: notes = notes[:497] + "..." return f"Project notes:\n{notes}" else: return "Project has no notes" else: raise Exception(f"Failed to get project notes: {result.get('error', 'Unknown error')}") async def set_project_notes(notes: str) -> str: """Set the project notes""" result = await bridge.call_lua("GetSetProjectNotes", [0, True, notes]) if result.get("ok"): return "Project notes updated" else: raise Exception(f"Failed to set project notes: {result.get('error', 'Unknown error')}") # ============================================================================ # Rendering & Export (2 tools) # ============================================================================ async def get_project_render_bounds() -> str: """Get the project render bounds mode""" result = await bridge.call_lua("GetSetProjectInfo", [0, "RENDER_SETTINGS", 0, False]) if result.get("ok"): settings = int(result.get("ret", 0)) # Extract render bounds from settings (bits 0-3) bounds = settings & 0xF bounds_names = { 0: "Custom time range", 1: "Entire project", 2: "Time selection", 3: "Project regions", 4: "Selected media items", 5: "Selected regions" } bounds_name = bounds_names.get(bounds, f"Unknown ({bounds})") return f"Render bounds: {bounds_name}" else: raise Exception(f"Failed to get render bounds: {result.get('error', 'Unknown error')}") # ============================================================================ # Registration Function # ============================================================================ def register_project_tools(mcp) -> int: """Register all project management tools with the MCP instance""" tools = [ # Basic Project Management (get_project_name, "Get the current project name"), (get_project_path, "Get the current project path"), (save_project, "Save the current project"), (open_project, "Open a REAPER project file"), (close_project, "Close the current project"), (mark_project_dirty, "Mark the project as having unsaved changes"), # Project Instance Management (enum_projects, "Get information about all open projects"), (select_project_instance, "Switch to a specific project by index"), (get_current_project_index, "Get the index of the currently active project"), # Project Properties (get_project_length, "Get the project length in seconds"), (set_project_length, "Set the project length in seconds"), (get_project_tempo, "Get the current project tempo in BPM"), (set_project_tempo, "Set the project tempo in BPM"), (get_project_time_signature, "Get the current project time signature"), (set_project_time_signature, "Set the project time signature"), (get_project_sample_rate, "Get the project sample rate"), (set_project_sample_rate, "Set the project sample rate"), # Project Grid & Display (get_project_grid_division, "Get the project grid division"), (set_project_grid_division, "Set the project grid division"), # Project Notes & Metadata (get_project_notes, "Get the project notes"), (set_project_notes, "Set the project notes"), # Rendering & Export (get_project_render_bounds, "Get the project render bounds mode"), ] # Register each tool for func, desc in tools: decorated = mcp.tool()(func) return len(tools)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shiehn/total-reaper-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server