Skip to content

Add ROADMAP.md#80

Open
Jeff909Dev wants to merge 4 commits intoahujasid:mainfrom
Jeff909Dev:worktree-iridescent-whistling-puddle
Open

Add ROADMAP.md#80
Jeff909Dev wants to merge 4 commits intoahujasid:mainfrom
Jeff909Dev:worktree-iridescent-whistling-puddle

Conversation

@Jeff909Dev
Copy link
Copy Markdown

@Jeff909Dev Jeff909Dev commented Mar 17, 2026

Summary

Adds a researched roadmap based on 4 parallel investigation agents that analyzed:

  • Ableton Live Python API (automation, audio clips, arrangement, groove pool, export)
  • Context-aware AI music generation (key detection, variations, templates, mixing)
  • Mock testing strategies
  • Developer experience improvements (plugins, logging, protocol)

Each item has a feasibility verdict and effort estimate.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added AI-powered music generation tools including scale, chord, progression, rhythm, bassline, and melody generators with intelligent harmonization suggestions.
    • Introduced response caching for improved performance.
    • Enhanced clip management with quantization, looping, and note removal capabilities.
    • Added device parameter control and scene management tools.
    • Improved transport controls including metronome and loop management.
  • Documentation

    • Added comprehensive project guide and roadmap.
    • Updated README with clearer setup instructions and examples.
  • Chores

    • Introduced automated installation script for simplified setup.
    • Refactored architecture into modular tool system.

Jeff909Dev and others added 4 commits March 17, 2026 15:42
…architecture

Speed improvements:
- Async TCP connection (asyncio) replacing blocking sockets
- Remove all time.sleep() calls from command pipeline
- Response caching with TTL (2s session, 60s browser)
- get_full_session_state: complete session snapshot in 1 call vs N+1
- Batch command support (send array of commands in single TCP transmission)

New tools (15 → 67):
- Track management: create/delete/duplicate tracks, volume, pan, mute, solo, arm, sends
- Clip operations: get/remove notes, delete, duplicate, loop settings, quantize
- Scene management: create/delete/duplicate/fire scenes, stop all clips
- Device control: get/set parameters by index or name, toggle, delete devices
- Transport extensions: undo/redo, metronome, loop, capture MIDI, tap tempo, record
- Browser improvements: text search, aggressive caching
- AI music theory: 23 chord types, 15 scales, 10 rhythm styles, chord progressions,
  basslines, melodies, harmony analysis

Architecture:
- Modular tool system: 8 independent modules under MCP_Server/tools/
- Async connection layer with auto-reconnection (MCP_Server/connection.py)
- TTL-based response cache (MCP_Server/cache.py)
- Command registry pattern in Remote Script (replaces if/elif chain)
- Remote Script: 61 command handlers with batch support

Also adds:
- install.sh for automated installation
- CLAUDE.md project guide
- Updated README with full documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…vitation to contribute

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…uddle

Major overhaul: 67 tools, async, AI music theory
Based on investigation of:
- Ableton Live Python API (LOM) capabilities and limitations
- Context-aware AI music generation feasibility
- Mock testing approaches
- Developer experience improvements

Key findings:
- Automation envelopes: viable for session clips only (arrangement returns None)
- Audio clip warping/pitch/gain: fully viable
- Arrangement view: read + create viable, but clip position is read-only
- Export/render: not possible via API
- All AI features (key detection, variations, templates, mixing): pure Python, no ML needed
- Testing: architecture already supports dependency injection for mocking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

The project undergoes a major architectural refactoring: replacing a monolithic server with modular, async-based infrastructure. New async TCP connection abstraction, TTL-based response caching, and eight specialized tool modules are introduced, delegating MCP endpoint registrations from server.py to organized tool submodules while updating documentation and installation mechanisms.

Changes

Cohort / File(s) Summary
Documentation & Project Guides
CLAUDE.md, README.md, ROADMAP.md, install.sh
Adds comprehensive project documentation (architecture, tool pattern, constraints), revamps README with AI-assisted narrative and installation paths, introduces roadmap with version-specific features, and provides automated installation script with multi-target Ableton directory support.
Core Infrastructure
MCP_Server/__init__.py, MCP_Server/cache.py, MCP_Server/connection.py, MCP_Server/server.py
Introduces async TCP connection abstraction with automatic reconnection, batch support, and thread-safe locking; adds TTL-based in-memory response cache; refactors server.py from 628 embedded tool definitions to modular tool imports, eliminating socket I/O and introducing lifecycle via connection/cache abstractions.
Tool Modules
MCP_Server/tools/__init__.py, MCP_Server/tools/session_tools.py, MCP_Server/tools/track_tools.py, MCP_Server/tools/clip_tools.py, MCP_Server/tools/scene_tools.py, MCP_Server/tools/device_tools.py, MCP_Server/tools/transport_tools.py, MCP_Server/tools/browser_tools.py, MCP_Server/tools/ai_tools.py
Introduces eight specialized tool modules (~1500 lines) providing session/track/clip/scene/device/transport/browser management and AI-powered music theory utilities (chords, scales, patterns, basslines, melodies); each module follows register(mcp, get_connection, cache) pattern with shared error handling, JSON responses, and TTL-based result caching.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 The warren rejoices!
From monoliths we hop to modular bliss,
Eight tool burrows, each with its own twist,
Async hops through TCP with cached clarity,
A refactored feast of organized tools—hooray!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Add ROADMAP.md' is factually accurate and directly corresponds to the main addition of a ROADMAP.md file documented in the raw summary. However, the actual changeset is significantly larger, involving substantial refactoring of the MCP server architecture (async connection model, response caching, modular tool reorganization), removal of 17 endpoint implementations from server.py, addition of 8 new tool modules with 67+ tools, cache layer, documentation, and an installer script. The title captures only one minor file addition while omitting the major architectural changes that dominate the changeset. Revise the title to reflect the primary changes, such as 'Refactor MCP server architecture with async connection, caching, and modular tools' or 'Restructure server into modular tool system with async TCP and response caching'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 98.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Major architectural refactor to modular async design with 67+ tools and comprehensive documentation

✨ Enhancement 📝 Documentation

Grey Divider

Walkthroughs

Description
• **Major architectural refactor**: Transformed monolithic server.py (660 lines) into modular
  architecture with 8 specialized tool modules (session_tools, track_tools, clip_tools,
  scene_tools, device_tools, transport_tools, browser_tools, ai_tools)
• **Async connection layer**: Replaced blocking socket implementation with non-blocking
  asyncio-based AbletonConnection class supporting automatic reconnection and batch commands
• **Response caching system**: Implemented TTL-based ResponseCache for efficient repeated queries,
  reducing redundant socket round-trips
• **Comprehensive tool coverage**: Added 67+ tools across 8 categories including track management,
  clip operations, scene control, device parameters, transport/playback, browser navigation, session
  queries, and AI-powered music theory generation
• **AI music theory module**: New 1128-line ai_tools.py providing 9 tools for note conversion,
  chord/scale generation, progressions, rhythm patterns, basslines, melodies, and chord suggestions
• **Remote Script enhancements**: Refactored command registry with 60+ handler implementations,
  batch command support, and improved error handling in AbletonMCP_Remote_Script/__init__.py
• **Installation automation**: Added install.sh script for automated setup of both MCP Server and
  Ableton Remote Script with multi-version detection
• **Documentation overhaul**: Completely restructured README for user-centric focus, added
  comprehensive ROADMAP.md (v2.0-v2.3 features), and created CLAUDE.md developer guide
• **Version bump**: Updated to v1.0.0 reflecting production-ready status
Diagram
flowchart LR
  A["Monolithic<br/>server.py<br/>660 lines"] -->|"Refactor to<br/>modular architecture"| B["MCP Server<br/>77 lines"]
  B -->|"Registers"| C["8 Tool Modules"]
  C -->|"session_tools"| D["Session Queries"]
  C -->|"track_tools"| E["Track Management"]
  C -->|"clip_tools"| F["Clip Operations"]
  C -->|"ai_tools"| G["Music Theory<br/>Generation"]
  B -->|"Uses"| H["Async Connection<br/>asyncio streams"]
  B -->|"Caches with"| I["ResponseCache<br/>TTL-based"]
  H -->|"Communicates with"| J["Ableton Remote<br/>Script"]
  J -->|"Handles 60+<br/>commands"| K["Ableton Live"]
Loading

Grey Divider

File Changes

1. AbletonMCP_Remote_Script/__init__.py ✨ Enhancement +1667/-489

Comprehensive refactor of command registry and handler architecture

• Refactored command handling into a centralized registry with handler wrappers for cleaner dispatch
 logic
• Added comprehensive command handler implementations for 60+ commands covering tracks, clips,
 scenes, devices, transport, and browser operations
• Implemented batch command support allowing multiple commands in a single JSON payload
• Added new query methods: _get_all_tracks_info(), _get_full_session_state(),
 _get_playing_position(), _get_scene_info(), _get_all_scenes(), and _search_browser()
• Improved code organization with section comments separating lifecycle, socket server, command
 dispatch, handlers, and implementations
• Enhanced error handling with explicit Exception catches instead of bare except clauses

AbletonMCP_Remote_Script/init.py


2. MCP_Server/tools/clip_tools.py ✨ Enhancement +273/-0

New clip operation tools module for MCP server

• New file implementing 10 clip operation tools for the MCP server
• Tools include: create_clip(), add_notes_to_clip(), set_clip_name(), delete_clip(),
 duplicate_clip_to_slot(), get_clip_notes(), remove_notes_from_clip(), set_clip_loop(), and
 quantize_clip()
• Each tool wraps socket communication with proper error handling and cache invalidation
• Includes caching for read-only get_clip_notes() operation

MCP_Server/tools/clip_tools.py


3. MCP_Server/tools/transport_tools.py ✨ Enhancement +237/-0

New transport and session control tools module

• New file implementing 13 transport and global session control tools
• Tools include: playback control (start_playback(), stop_playback()), clip firing, tempo/time
 signature management, undo/redo, metronome, looping, MIDI capture, and arrangement position
• Implements caching for read-only get_playing_position() query
• All tools include proper error logging and cache invalidation on state changes

MCP_Server/tools/transport_tools.py


View more (15)
4. MCP_Server/cache.py ✨ Enhancement +41/-0

New TTL-based response caching system

• New file implementing a TTL-based response cache for read-only Ableton queries
• ResponseCache class with methods: get(), set(), invalidate(), and invalidate_all()
• Automatically expires cached entries based on configurable TTL
• Reduces redundant socket round-trips for frequently queried data

MCP_Server/cache.py


5. MCP_Server/__init__.py ⚙️ Configuration changes +1/-4

Version bump and module cleanup

• Updated version from 0.1.0 to 1.0.0
• Removed explicit imports of AbletonConnection and get_ableton_connection for cleaner module
 interface

MCP_Server/init.py


6. MCP_Server/tools/__init__.py 📝 Documentation +5/-0

Tools module documentation and interface definition

• New file documenting the tools module structure
• Specifies that each tool module exports a register(mcp, get_connection, cache) function for
 FastMCP integration

MCP_Server/tools/init.py


7. MCP_Server/tools/ai_tools.py ✨ Enhancement +1128/-0

AI music theory and pattern generation tools module

• New 1128-line module providing AI-powered music theory and pattern generation tools
• Implements 9 tools: note conversion, chord/scale generation, progressions, rhythm patterns,
 basslines, melodies, and chord suggestions
• Includes comprehensive music theory constants (scales, chords, drum mappings) and helper functions
 for MIDI/note parsing
• All tools perform pure computation without requiring Ableton connection, generating MIDI note data
 for use with add_notes_to_clip

MCP_Server/tools/ai_tools.py


8. MCP_Server/server.py ✨ Enhancement +45/-628

Refactor to modular architecture with async connection

• Refactored from 660 lines of monolithic code to 77 lines of modular architecture
• Removed inline connection management and tool definitions; now imports from separate tool modules
• Replaced blocking socket connection with async connection from MCP_Server.connection
• Registers 8 tool modules: session_tools, track_tools, clip_tools, scene_tools,
 device_tools, transport_tools, browser_tools, ai_tools
• Simplified server initialization with async lifespan management and global response cache

MCP_Server/server.py


9. MCP_Server/connection.py ✨ Enhancement +215/-0

Async connection module for non-blocking Ableton communication

• New async TCP connection module replacing blocking socket implementation
• Implements AbletonConnection class with non-blocking I/O using asyncio streams
• Provides automatic reconnection, connection locking for thread safety, and batch command support
• Global singleton get_connection() function for managing persistent connection state
• Includes cleanup_connection() for graceful shutdown

MCP_Server/connection.py


10. MCP_Server/tools/track_tools.py ✨ Enhancement +144/-0

Track management tools module

• New module with 10 track management tools extracted from main server
• Implements track creation, deletion, duplication, naming, and parameter control (volume, pan,
 mute, solo, arm, sends)
• All tools use async connection and response caching for read operations
• Invalidates cache on state-modifying operations

MCP_Server/tools/track_tools.py


11. MCP_Server/tools/scene_tools.py ✨ Enhancement +149/-0

Scene management tools module

• New module with 8 scene management tools
• Implements scene querying, creation, deletion, duplication, firing, and naming
• Includes stop_all_clips() utility function
• Uses async connection with cache support for read-only operations

MCP_Server/tools/scene_tools.py


12. MCP_Server/tools/session_tools.py ✨ Enhancement +103/-0

Session information and state querying tools

• New module with 5 session information tools
• Provides get_session_info(), get_track_info(), get_full_session_state(), and
 get_all_tracks_info()
• Implements response caching with 2-3 second TTL for efficient repeated queries
• Uses async connection for non-blocking I/O

MCP_Server/tools/session_tools.py


13. MCP_Server/tools/device_tools.py ✨ Enhancement +180/-0

Device parameter control tools module

• New module with 6 device parameter control tools
• Implements device parameter querying, setting by index or name, toggling, info retrieval, and
 deletion
• Provides both indexed and name-based parameter access for user convenience
• Uses async connection with cache support for read operations

MCP_Server/tools/device_tools.py


14. MCP_Server/tools/browser_tools.py ✨ Enhancement +208/-0

Browser navigation and instrument loading tools

• New module with 5 browser navigation and instrument loading tools
• Implements tree browsing, path-based item lookup, search functionality, and instrument/drum kit
 loading
• Includes helper functions for formatting browser tree output into readable strings
• Uses async connection with cache support for expensive read operations

MCP_Server/tools/browser_tools.py


15. install.sh ⚙️ Configuration changes +92/-0

Installation script for MCP Server and Remote Script

• New installation script for setting up both MCP Server and Ableton Remote Script
• Detects Ableton installations in standard macOS locations
• Supports installation via uv or pip3 package managers
• Provides interactive selection when multiple Ableton versions are found
• Includes post-installation instructions for configuring Ableton and MCP clients

install.sh


16. README.md 📝 Documentation +114/-125

Comprehensive README restructure for user-focused documentation

• Completely restructured README with new title emphasizing AI integration and natural language
 control
• Reorganized content from technical focus to user-centric examples and feature showcase (67 tools
 in 8 categories)
• Simplified installation instructions with quick install script and manual steps for
 macOS/Linux/Windows
• Added architecture diagram showing async TCP communication between AI assistant, MCP server, and
 Ableton Remote Script
• Replaced old troubleshooting and technical details with performance improvements, project
 structure, and contribution guidelines
• Updated credits to acknowledge original author Siddharth Ahuja and clarified this is a fork with
 open contribution policy

README.md


17. ROADMAP.md 📝 Documentation +103/-0

Detailed roadmap with v2.0-v2.3 features and priorities

• New file establishing a comprehensive roadmap with 3 major versions (v2.0, v2.1, v2.2, v2.3)
• v2.0 focuses on pure Python AI improvements: key detection, genre templates, clip variations,
 mixing suggestions, rhythm analysis, genre classification, and Markov-based variations
• v2.1 details new Ableton API commands for automation, audio clips, arrangement view, return
 tracks, and groove pool with viability assessments
• v2.2 covers developer experience improvements: structured logging, protocol framing, and plugin
 system architecture
• v2.3 outlines testing strategy with mock connections and pytest fixtures
• Includes priority matrix and effort estimates for each feature

ROADMAP.md


18. CLAUDE.md 📝 Documentation +55/-0

Developer guide for architecture and tool implementation

• New project guide documenting two-component architecture (MCP Server and Ableton Remote Script)
• Outlines key constraints: Python 2.7 compatibility for Remote Script, single-file limitation,
 thread-safety requirements using schedule_message
• Provides pattern for adding new tools with command handler registration and MCP tool
 implementation
• Documents tool module pattern using register(mcp, get_connection, cache) convention
• Includes performance notes on response caching TTLs and optimization strategies
• Covers testing approach distinguishing between AI tools (pure computation) and full integration
 tests

CLAUDE.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 17, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Pitch span off-by-one 🐞 Bug ✓ Correctness
Description
remove_notes_from_clip defaults pitch_span=127, but Ableton’s API treats pitch_span as a
count, so the default call fails to remove MIDI pitch 127 notes. This causes silent partial note
deletion when callers rely on defaults.
Code

MCP_Server/tools/clip_tools.py[184]

+        pitch_span: int = 127,
Evidence
The MCP tool sends a default pitch_span of 127, while the Remote Script’s handler defaults to 128
and passes the value directly into clip.remove_notes(..., pitch_span), meaning the correct “full
MIDI range” span is 128 (0–127 inclusive).

MCP_Server/tools/clip_tools.py[177-205]
AbletonMCP_Remote_Script/init.py[579-586]
AbletonMCP_Remote_Script/init.py[1279-1293]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MCP_Server/tools/clip_tools.py::remove_notes_from_clip` defaults `pitch_span` to 127, but the Remote Script/Ableton API uses `pitch_span` as a count, so the default call won’t cover pitch 127.

## Issue Context
Remote script handler defaults `pitch_span` to 128 and forwards it directly to `clip.remove_notes(from_time, from_pitch, time_span, pitch_span)`.

## Fix Focus Areas
- MCP_Server/tools/clip_tools.py[177-205]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Loop toggle resets range 🐞 Bug ✓ Correctness
Description
set_loop(enabled=False) always sends start=0.0 and length=4.0, so disabling looping
unintentionally overwrites the current loop range. This destroys the user’s existing
loop_start/loop_length even when they only intended to toggle looping off.
Code

MCP_Server/tools/transport_tools.py[R127-138]

+    async def set_loop(enabled: bool, start: float = 0.0, length: float = 4.0) -> str:
+        """Set loop on/off and configure the loop range in beats.
+
+        Args:
+            enabled: True to enable looping, False to disable it.
+            start: Loop start position in beats. Defaults to 0.0.
+            length: Loop length in beats. Defaults to 4.0.
+        """
+        try:
+            conn = await get_connection()
+            result = await conn.send_command("set_loop", {"enabled": enabled, "start": start, "length": length})
+            cache.invalidate_all()
Evidence
The MCP tool always includes start and length in the payload due to non-None defaults; the
Remote Script applies loop_start/loop_length whenever those values are not None, regardless of
whether looping is being enabled or disabled.

MCP_Server/tools/transport_tools.py[126-139]
AbletonMCP_Remote_Script/init.py[1709-1716]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`set_loop` always sends `start` and `length` due to default values, which causes unintended mutation of Ableton’s loop range when the user only wants to enable/disable looping.

## Issue Context
Remote script only mutates `loop_start`/`loop_length` when received values are not None.

## Fix Focus Areas
- MCP_Server/tools/transport_tools.py[126-139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Loads non-loadable kit 🐞 Bug ✓ Correctness
Description
load_drum_kit selects the first item that has a uri even when is_loadable is false, so it can
try to load folders/non-loadable items. This can load the wrong content or fail unpredictably
depending on browser structure ordering.
Code

MCP_Server/tools/browser_tools.py[R179-183]

+            items = kit_items if isinstance(kit_items, list) else kit_items.get("items", [])
+            for item in items:
+                if item.get("is_loadable") or item.get("uri"):
+                    loadable_uri = item.get("uri")
+                    break
Evidence
Remote Script returns items where uri may be present even when is_loadable is false; the MCP
tool’s or condition treats any uri as acceptable and can select a non-loadable item.

MCP_Server/tools/browser_tools.py[177-183]
AbletonMCP_Remote_Script/init.py[2195-2201]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`load_drum_kit` currently picks the first item with a URI even if it’s not loadable, which can attempt to load folders or other non-loadable browser nodes.

## Issue Context
`get_browser_items_at_path` returns both `is_loadable` and `uri`; `uri` alone is not a safe proxy for loadability.

## Fix Focus Areas
- MCP_Server/tools/browser_tools.py[177-183]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Client threads hang shutdown 🐞 Bug ⛯ Reliability
Description
Remote Script client threads can block forever in recv() because the socket is put into fully
blocking mode, so disconnect() setting self.running=False may not terminate them. Since
disconnect() also doesn’t close connected client sockets, threads can leak across unload/shutdown.
Code

AbletonMCP_Remote_Script/init.py[R289-296]

+        client.settimeout(None)
+        buffer = ''
+
        try:
            while self.running:
                try:
-                    # Receive data
                    data = client.recv(8192)
-                    
+
Evidence
_handle_client uses blocking recv() with no timeout, which prevents the loop condition from
being re-checked after disconnect() flips self.running to false; disconnect() only closes the
listening socket and does not close client sockets.

AbletonMCP_Remote_Script/init.py[286-299]
AbletonMCP_Remote_Script/init.py[210-227]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Client handler threads can block indefinitely on `recv()` and never exit during `disconnect()`, leaking threads and sockets.

## Issue Context
`disconnect()` currently only closes the listening socket; client sockets are not closed, and client sockets are configured for blocking reads.

## Fix Focus Areas
- AbletonMCP_Remote_Script/__init__.py[210-227]
- AbletonMCP_Remote_Script/__init__.py[286-299]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

from_time: float = 0.0,
time_span: float = 999.0,
from_pitch: int = 0,
pitch_span: int = 127,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Pitch span off-by-one 🐞 Bug ✓ Correctness

remove_notes_from_clip defaults pitch_span=127, but Ableton’s API treats pitch_span as a
count, so the default call fails to remove MIDI pitch 127 notes. This causes silent partial note
deletion when callers rely on defaults.
Agent Prompt
## Issue description
`MCP_Server/tools/clip_tools.py::remove_notes_from_clip` defaults `pitch_span` to 127, but the Remote Script/Ableton API uses `pitch_span` as a count, so the default call won’t cover pitch 127.

## Issue Context
Remote script handler defaults `pitch_span` to 128 and forwards it directly to `clip.remove_notes(from_time, from_pitch, time_span, pitch_span)`.

## Fix Focus Areas
- MCP_Server/tools/clip_tools.py[177-205]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +127 to +138
async def set_loop(enabled: bool, start: float = 0.0, length: float = 4.0) -> str:
"""Set loop on/off and configure the loop range in beats.

Args:
enabled: True to enable looping, False to disable it.
start: Loop start position in beats. Defaults to 0.0.
length: Loop length in beats. Defaults to 4.0.
"""
try:
conn = await get_connection()
result = await conn.send_command("set_loop", {"enabled": enabled, "start": start, "length": length})
cache.invalidate_all()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Loop toggle resets range 🐞 Bug ✓ Correctness

set_loop(enabled=False) always sends start=0.0 and length=4.0, so disabling looping
unintentionally overwrites the current loop range. This destroys the user’s existing
loop_start/loop_length even when they only intended to toggle looping off.
Agent Prompt
## Issue description
`set_loop` always sends `start` and `length` due to default values, which causes unintended mutation of Ableton’s loop range when the user only wants to enable/disable looping.

## Issue Context
Remote script only mutates `loop_start`/`loop_length` when received values are not None.

## Fix Focus Areas
- MCP_Server/tools/transport_tools.py[126-139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +179 to +183
items = kit_items if isinstance(kit_items, list) else kit_items.get("items", [])
for item in items:
if item.get("is_loadable") or item.get("uri"):
loadable_uri = item.get("uri")
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Loads non-loadable kit 🐞 Bug ✓ Correctness

load_drum_kit selects the first item that has a uri even when is_loadable is false, so it can
try to load folders/non-loadable items. This can load the wrong content or fail unpredictably
depending on browser structure ordering.
Agent Prompt
## Issue description
`load_drum_kit` currently picks the first item with a URI even if it’s not loadable, which can attempt to load folders or other non-loadable browser nodes.

## Issue Context
`get_browser_items_at_path` returns both `is_loadable` and `uri`; `uri` alone is not a safe proxy for loadability.

## Fix Focus Areas
- MCP_Server/tools/browser_tools.py[177-183]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +289 to +296
client.settimeout(None)
buffer = ''

try:
while self.running:
try:
# Receive data
data = client.recv(8192)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Client threads hang shutdown 🐞 Bug ⛯ Reliability

Remote Script client threads can block forever in recv() because the socket is put into fully
blocking mode, so disconnect() setting self.running=False may not terminate them. Since
disconnect() also doesn’t close connected client sockets, threads can leak across unload/shutdown.
Agent Prompt
## Issue description
Client handler threads can block indefinitely on `recv()` and never exit during `disconnect()`, leaking threads and sockets.

## Issue Context
`disconnect()` currently only closes the listening socket; client sockets are not closed, and client sockets are configured for blocking reads.

## Fix Focus Areas
- AbletonMCP_Remote_Script/__init__.py[210-227]
- AbletonMCP_Remote_Script/__init__.py[286-299]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (1)
MCP_Server/tools/transport_tools.py (1)

71-79: Add fail-fast validation for documented parameter bounds.

These tools document constraints (e.g., tempo/time-signature/loop fields) but currently pass values through unchecked. Lightweight validation here will reduce avoidable remote command failures.

Also applies to: 127-137, 162-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCP_Server/tools/transport_tools.py` around lines 71 - 79, The functions
(e.g., async def set_tempo) currently accept parameters that are documented to
be bounded but do not validate them before calling get_connection/send_command;
add lightweight fail-fast checks in set_tempo to validate tempo is a float and
within 20.0–999.0 and raise a ValueError (or return an error string) if out of
range, and apply the same pattern to the other tools that document bounds (e.g.,
set_time_signature and set_loop): validate input types and documented ranges up
front, return/raise a clear error immediately, and only call conn.send_command
when inputs pass validation so remote commands aren’t invoked with invalid
parameters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@install.sh`:
- Around line 59-74: Validate the user selection before indexing DIRS: check
that choice is a positive integer (e.g., match against ^[0-9]+$) and that it
falls between 1 and ${`#DIRS`[@]}; if the check fails, print a clear error and
re-prompt or exit with a non-zero status instead of computing idx/CHOSEN. Only
compute idx=$((choice - 1)) and set CHOSEN="${DIRS[$idx]}" after the validation
succeeds so TARGET="$CHOSEN/AbletonMCP" cannot become "/AbletonMCP" due to
invalid input; keep existing symbols (choice, DIRS, idx, CHOSEN, TARGET,
REMOTE_SCRIPT_SRC) so the rest of the flow is unchanged.
- Around line 36-38: The find invocation inside the while loop that populates
DIRS uses -maxdepth 2 which is too shallow for Ableton app bundles; update the
find command (the line feeding the while loop that populates DIRS) to use a
greater maxdepth (e.g., -maxdepth 5) so paths like /Applications/Ableton Live
<version>.app/Contents/App-Resources/MIDI Remote Scripts are discovered
correctly; keep the same -type d -name "MIDI Remote Scripts" -path "*/Ableton*"
and preserve the null-separated output and stderr redirection.

In `@MCP_Server/cache.py`:
- Around line 26-28: The set method currently treats ttl with truthiness so an
explicit ttl=0 is ignored; update MCP_Server.cache.Cache.set (the set method
that writes to self._cache using self.default_ttl) to check "if ttl is None" (or
use a conditional expression testing is None) and only fall back to
self.default_ttl when ttl is None, ensuring ttl=0 is preserved as a valid
expiry; keep the same tuple storage (value, expiry_time) and TTL arithmetic
using time.time() + ttl.

In `@MCP_Server/connection.py`:
- Around line 59-83: ensure_connected currently performs connect() and the
subsequent validation without holding self._lock, allowing two coroutines (via
get_connection) to race and overwrite shared self.reader/self.writer; fix by
serializing reconnects: acquire self._lock at the start of ensure_connected (or
wrap the connect(), _send_and_receive("get_session_info") validation, and
disconnect() calls inside a with await self._lock) so only one coroutine can
perform connect/validation/disconnect at a time. Apply the same protection to
the other reconnect path referenced around the get_connection usage (the logic
at the other occurrence noted in the comment) so both code paths use self._lock
when touching reader/writer or calling connect/disconnect.
- Around line 110-129: send_batch() currently treats a non-list server response
as a single successful result and returns [result], which drops all other
commands; fix by detecting the non-list (batch-unsupported) case, decoding the
original payload (the JSON list of commands) and replaying each command
sequentially: for each command in the decoded payload write it to self.writer,
await drain, call self._read_json_response() to parse that single response, and
append either {"error": message} or the result to the results list; return the
full results list so callers receive per-command results/errors. Ensure you
reference send_batch, payload, self.writer, and self._read_json_response in your
change.

In `@MCP_Server/server.py`:
- Around line 22-23: The session caches use exact keys ("session_info",
"full_session_state", "all_tracks_info") while browser_tools.py only calls
cache.invalidate("browser") and cache.invalidate("track"), so those mutations
don't clear session entries; update browser_tools.py to call
cache.invalidate_all() (or at least include session-prefixed keys) after
mutation points such as load_instrument_or_effect and load_drum_kit so the
global ResponseCache (cache) is fully cleared for affected data; specifically
replace or augment calls to
cache.invalidate("browser")/cache.invalidate("track") with
cache.invalidate_all() where devices are added/changed.

In `@MCP_Server/tools/ai_tools.py`:
- Around line 603-621: The mode inference in generate_bassline() incorrectly
treats many scale types as major because it only checks a small minor whitelist;
replace this fragile heuristic by mapping scale_type to mode explicitly (e.g.,
add a SCALE_TO_MODE dict and use mode = SCALE_TO_MODE.get(scale_type, "major")
or accept an explicit mode parameter) and update any logic that uses mode (the
existing mode variable and downstream Roman-numeral resolution) so scales like
"blues" or "locrian" resolve to the correct "minor" or "dorian"/"locrian" modes
rather than defaulting to major; ensure SCALE_INTERVALS lookups remain unchanged
but use the new mapping to determine major/minor behavior.
- Around line 433-459: In generate_chord_progression, validate the mode
parameter before any parsing: ensure mode is exactly "major" or "minor" (or
explicitly accept agreed synonyms) and raise a clear ValueError if it's invalid
so _parse_roman_numeral isn't fed a wrong mode; update the top of the function
(before resolving presets and calling _parse_roman_numeral) to check the mode
string and normalize it if needed, referencing the function name
generate_chord_progression and the parameter mode.
- Around line 523-571: The bug is inconsistent time-unit math: compute a beat
length in quarter-note units using beat_unit (e.g., beat_length_quarters = 4.0 /
beat_unit) and then use that to derive step_duration, bar_offset, and
total_beats so all timings use the same unit; specifically update step_duration,
bar_offset (inside the bars loop) and the returned total_beats to use
beat_length_quarters (with steps_per_beat = 16 / beat_unit if needed) so the
grid from patterns returned by _get_drum_pattern and the notes'
start_time/duration are correct for non-*/4 meters.

In `@MCP_Server/tools/browser_tools.py`:
- Around line 143-148: The load_browser_item handler that calls get_connection()
and conn.send_command("load_browser_item", ...) currently only invalidates
"browser" and "track"; update this to also invalidate session/device-related
caches so device chain changes aren't missed—specifically call cache.invalidate
for "full_session_state", "all_tracks_info", and any per-device keys (e.g.,
"device_{device_id}" or pattern "device_*") after the send_command; apply the
same additional invalidations to the other similar block around the 166-205
range that performs load_browser_item operations.

In `@MCP_Server/tools/clip_tools.py`:
- Around line 178-205: The default pitch span in remove_notes_from_clip is off
by one: change the default parameter pitch_span from 127 to 128 so the function
uses the full MIDI range when callers rely on the default; update the function
signature for remove_notes_from_clip to pitch_span: int = 128 and ensure any
docstring wording that mentions default pitch span remains accurate.

In `@MCP_Server/tools/track_tools.py`:
- Around line 20-23: The current exception handling in functions like the MIDI
track handler (the except Exception as e block that calls logger.error and
returns a plain string) is inconsistent with success responses that use
json.dumps; create a small helper (e.g., wrap_tool_call or handle_tool_error)
that executes the tool logic, catches exceptions, logs the full exception via
logger.exception or logger.error(..., exc_info=True), and returns a consistent
JSON error payload using json.dumps({"success": False, "error": str(e)}) (or
your project's standard error schema); replace the repeated except blocks (the
shown logger.error/return lines and the ~10 other occurrences) to call this
helper so all handlers return structured JSON errors and logging includes
exception details.

In `@MCP_Server/tools/transport_tools.py`:
- Around line 20-23: The current bare except in transport_tools.py (in the
playback startup block, e.g., the except in the function that starts playback)
should be replaced with structured JSON error responses and full traceback
logging: change logger.error(...) to logger.exception(...) to capture the stack
trace and return json.dumps({"error": str(e)}) instead of a plain string; apply
the same pattern to other handlers in transport_tools (any functions that
currently use bare "except Exception as e" and return plain strings) to match
ai_tools' error format and ensure consistent parseable client-side responses.

In `@README.md`:
- Around line 104-108: Update the two plain fenced code blocks in README.md to
include a language tag of "text" to satisfy markdownlint MD040; specifically
modify the blocks containing the ASCII diagram ("AI Assistant  <──MCP──>  MCP
Server...") and the project tree block (starting with "MCP_Server/") so their
opening fences are ```text instead of ```; this will silence the lint warnings
without changing the block contents.

In `@ROADMAP.md`:
- Around line 3-5: Corrige la ortografía y acentuación en las líneas de resumen
clave: reemplaza "conexion" por "conexión", "cache" por "caché", "basica" por
"básica" en la línea que empieza "Estado actual: **67 herramientas**, conexion
async, cache, AI music theory basica." y cambia "item" por "ítem",
"investigacion" por "investigación" y "tecnica" por "técnica" en la frase que
empieza "Cada item tiene un veredicto..."; además, usa "reguetón" en lugar de
"regueton" si aparece en la lista de géneros y revisa otras ocurrencias
similares en el documento para aplicar las mismas correcciones.

---

Nitpick comments:
In `@MCP_Server/tools/transport_tools.py`:
- Around line 71-79: The functions (e.g., async def set_tempo) currently accept
parameters that are documented to be bounded but do not validate them before
calling get_connection/send_command; add lightweight fail-fast checks in
set_tempo to validate tempo is a float and within 20.0–999.0 and raise a
ValueError (or return an error string) if out of range, and apply the same
pattern to the other tools that document bounds (e.g., set_time_signature and
set_loop): validate input types and documented ranges up front, return/raise a
clear error immediately, and only call conn.send_command when inputs pass
validation so remote commands aren’t invoked with invalid parameters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e683390-eb2d-4e40-9afc-869e66e944d1

📥 Commits

Reviewing files that changed from the base of the PR and between e008328 and ee5889a.

📒 Files selected for processing (18)
  • AbletonMCP_Remote_Script/__init__.py
  • CLAUDE.md
  • MCP_Server/__init__.py
  • MCP_Server/cache.py
  • MCP_Server/connection.py
  • MCP_Server/server.py
  • MCP_Server/tools/__init__.py
  • MCP_Server/tools/ai_tools.py
  • MCP_Server/tools/browser_tools.py
  • MCP_Server/tools/clip_tools.py
  • MCP_Server/tools/device_tools.py
  • MCP_Server/tools/scene_tools.py
  • MCP_Server/tools/session_tools.py
  • MCP_Server/tools/track_tools.py
  • MCP_Server/tools/transport_tools.py
  • README.md
  • ROADMAP.md
  • install.sh

Comment on lines +36 to +38
while IFS= read -r -d '' dir; do
DIRS+=("$dir")
done < <(find /Applications -maxdepth 2 -type d -name "MIDI Remote Scripts" -path "*/Ableton*" -print0 2>/dev/null)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the install.sh file around the mentioned lines
fd install.sh -type f

Repository: ahujasid/ableton-mcp

Length of output: 234


🏁 Script executed:

# Correct fd syntax
fd install.sh -t f

Repository: ahujasid/ableton-mcp

Length of output: 74


🏁 Script executed:

# Read install.sh with line numbers
cat -n install.sh

Repository: ahujasid/ableton-mcp

Length of output: 3521


🌐 Web query:

Ableton Live app bundle MIDI Remote Scripts directory path macOS

💡 Result:

On macOS, the built‑in (app-bundle) MIDI Remote Scripts folder is inside the Live app package:

  • /Applications/Ableton Live <version>.app/Contents/App-Resources/MIDI Remote Scripts/  (you can also Finder → right‑click Ableton Live .appShow Package ContentsContents/App-Resources/MIDI Remote Scripts) [1][2]

If you meant the user locations Ableton documents:

  • Remote Scripts (User Library): ~/Music/Ableton/User Library/Remote Scripts/ [3]
  • User Remote Scripts: ~/Library/Preferences/Ableton/Live <version>/User Remote Scripts/ [4]

Sources: [1] Remotify knowledge base, [2] third‑party install guides/GitHub instructions reflecting Live’s macOS bundle layout, [3] Ableton “Installing third-party remote scripts”, [4] Ableton “Creating your own Control Surface script.


Fix Ableton app-bundle discovery depth.

Line 38 uses -maxdepth 2, which is too shallow for standard Ableton .app bundle paths. The MIDI Remote Scripts directory is located at /Applications/Ableton Live <version>.app/Contents/App-Resources/MIDI Remote Scripts/, which is 4 levels deep. This causes false "No Ableton installation found" failures.

Proposed fix
-done < <(find /Applications -maxdepth 2 -type d -name "MIDI Remote Scripts" -path "*/Ableton*" -print0 2>/dev/null)
+done < <(find /Applications -maxdepth 4 -type d -name "MIDI Remote Scripts" -path "*/Ableton*" -print0 2>/dev/null)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while IFS= read -r -d '' dir; do
DIRS+=("$dir")
done < <(find /Applications -maxdepth 2 -type d -name "MIDI Remote Scripts" -path "*/Ableton*" -print0 2>/dev/null)
while IFS= read -r -d '' dir; do
DIRS+=("$dir")
done < <(find /Applications -maxdepth 4 -type d -name "MIDI Remote Scripts" -path "*/Ableton*" -print0 2>/dev/null)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 36 - 38, The find invocation inside the while loop
that populates DIRS uses -maxdepth 2 which is too shallow for Ableton app
bundles; update the find command (the line feeding the while loop that populates
DIRS) to use a greater maxdepth (e.g., -maxdepth 5) so paths like
/Applications/Ableton Live <version>.app/Contents/App-Resources/MIDI Remote
Scripts are discovered correctly; keep the same -type d -name "MIDI Remote
Scripts" -path "*/Ableton*" and preserve the null-separated output and stderr
redirection.

Comment on lines +59 to +74
read -rp " Which one? [1-${#DIRS[@]}, or 'a' for all]: " choice
if [ "$choice" = "a" ]; then
for dir in "${DIRS[@]}"; do
TARGET="$dir/AbletonMCP"
mkdir -p "$TARGET"
cp "$REMOTE_SCRIPT_SRC" "$TARGET/__init__.py"
echo " Installed to: $TARGET"
done
echo ""
echo "=== Done! ==="
echo "Restart Ableton and select 'AbletonMCP' in Settings > Link, Tempo & MIDI > Control Surface."
exit 0
else
idx=$((choice - 1))
CHOSEN="${DIRS[$idx]}"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the install.sh file
find . -name "install.sh" -type f | head -20

Repository: ahujasid/ableton-mcp

Length of output: 76


🏁 Script executed:

# Get the full context of install.sh around lines 59-74
if git ls-files | grep -q "install.sh"; then
  git ls-files | grep "install.sh" | head -5
fi

Repository: ahujasid/ableton-mcp

Length of output: 74


🏁 Script executed:

# Try to find install.sh in the repository
fd install.sh

Repository: ahujasid/ableton-mcp

Length of output: 74


🏁 Script executed:

cat -n install.sh

Repository: ahujasid/ableton-mcp

Length of output: 3521


🏁 Script executed:

# Test bash arithmetic with non-numeric input
bash <<'EOF'
# Simulate the problematic code
DIRS=("dir1" "dir2")
choice="abc"
idx=$((choice - 1))
echo "choice='abc' → idx=$idx"

choice="0"
idx=$((choice - 1))
echo "choice='0' → idx=$idx"

choice="5"
idx=$((choice - 1))
echo "choice='5' → idx=$idx, array length=${`#DIRS`[@]}"
CHOSEN="${DIRS[$idx]}"
echo "CHOSEN='$CHOSEN' (empty if out of range)"

# Show what happens with empty CHOSEN
TARGET="$CHOSEN/AbletonMCP"
echo "TARGET='$TARGET' (dangerous if empty!)"
EOF

Repository: ahujasid/ableton-mcp

Length of output: 217


Validate selection input before indexing DIRS.

Lines 72–74 accept arbitrary input and index directly without validation. Non-numeric input (e.g., "abc") or out-of-range values leave CHOSEN empty, causing TARGET to become /AbletonMCP—attempting installation at the filesystem root instead of the intended directory.

Proposed fix
    else
-        idx=$((choice - 1))
-        CHOSEN="${DIRS[$idx]}"
+        if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
+            echo "  ERROR: Invalid selection: $choice"
+            exit 1
+        fi
+        idx=$((choice - 1))
+        if [ "$idx" -lt 0 ] || [ "$idx" -ge "${`#DIRS`[@]}" ]; then
+            echo "  ERROR: Selection out of range: $choice"
+            exit 1
+        fi
+        CHOSEN="${DIRS[$idx]}"
    fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
read -rp " Which one? [1-${#DIRS[@]}, or 'a' for all]: " choice
if [ "$choice" = "a" ]; then
for dir in "${DIRS[@]}"; do
TARGET="$dir/AbletonMCP"
mkdir -p "$TARGET"
cp "$REMOTE_SCRIPT_SRC" "$TARGET/__init__.py"
echo " Installed to: $TARGET"
done
echo ""
echo "=== Done! ==="
echo "Restart Ableton and select 'AbletonMCP' in Settings > Link, Tempo & MIDI > Control Surface."
exit 0
else
idx=$((choice - 1))
CHOSEN="${DIRS[$idx]}"
fi
read -rp " Which one? [1-${`#DIRS`[@]}, or 'a' for all]: " choice
if [ "$choice" = "a" ]; then
for dir in "${DIRS[@]}"; do
TARGET="$dir/AbletonMCP"
mkdir -p "$TARGET"
cp "$REMOTE_SCRIPT_SRC" "$TARGET/__init__.py"
echo " Installed to: $TARGET"
done
echo ""
echo "=== Done! ==="
echo "Restart Ableton and select 'AbletonMCP' in Settings > Link, Tempo & MIDI > Control Surface."
exit 0
else
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
echo " ERROR: Invalid selection: $choice"
exit 1
fi
idx=$((choice - 1))
if [ "$idx" -lt 0 ] || [ "$idx" -ge "${`#DIRS`[@]}" ]; then
echo " ERROR: Selection out of range: $choice"
exit 1
fi
CHOSEN="${DIRS[$idx]}"
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 59 - 74, Validate the user selection before indexing
DIRS: check that choice is a positive integer (e.g., match against ^[0-9]+$) and
that it falls between 1 and ${`#DIRS`[@]}; if the check fails, print a clear error
and re-prompt or exit with a non-zero status instead of computing idx/CHOSEN.
Only compute idx=$((choice - 1)) and set CHOSEN="${DIRS[$idx]}" after the
validation succeeds so TARGET="$CHOSEN/AbletonMCP" cannot become "/AbletonMCP"
due to invalid input; keep existing symbols (choice, DIRS, idx, CHOSEN, TARGET,
REMOTE_SCRIPT_SRC) so the rest of the flow is unchanged.

Comment on lines +26 to +28
def set(self, key: str, value: Any, ttl: Optional[float] = None):
"""Cache a value with optional custom TTL"""
self._cache[key] = (value, time.time() + (ttl or self.default_ttl))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve explicit ttl=0 semantics.

Line 28 uses truthiness (ttl or ...), so an explicit ttl=0 is ignored. Use is None to distinguish omitted TTL from zero.

Proposed fix
-        self._cache[key] = (value, time.time() + (ttl or self.default_ttl))
+        ttl_value = self.default_ttl if ttl is None else ttl
+        self._cache[key] = (value, time.time() + ttl_value)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCP_Server/cache.py` around lines 26 - 28, The set method currently treats
ttl with truthiness so an explicit ttl=0 is ignored; update
MCP_Server.cache.Cache.set (the set method that writes to self._cache using
self.default_ttl) to check "if ttl is None" (or use a conditional expression
testing is None) and only fall back to self.default_ttl when ttl is None,
ensuring ttl=0 is preserved as a valid expiry; keep the same tuple storage
(value, expiry_time) and TTL arithmetic using time.time() + ttl.

Comment on lines +59 to +83
async def ensure_connected(self):
"""Ensure we have an active connection, reconnecting if needed"""
if self._connected and self.writer and not self.writer.is_closing():
return

self._connected = False
max_attempts = 3
for attempt in range(1, max_attempts + 1):
logger.info(f"Connecting to Ableton (attempt {attempt}/{max_attempts})...")
if await self.connect():
# Validate with a quick command
try:
await self._send_and_receive("get_session_info")
logger.info("Connection validated successfully")
return
except Exception as e:
logger.warning(f"Connection validation failed: {e}")
await self.disconnect()

if attempt < max_attempts:
await asyncio.sleep(0.5)

raise ConnectionError(
"Could not connect to Ableton. Make sure the Remote Script is running."
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Serialize reconnects around ensure_connected().

get_connection() awaits ensure_connected() outside self._lock, so two concurrent tool calls during a reconnect can run connect() and the validation request in parallel against the same shared streams. One coroutine can overwrite self.reader/self.writer while the other is reading, which is a real race.

Also applies to: 201-206

🧰 Tools
🪛 Ruff (0.15.6)

[warning] 74-74: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCP_Server/connection.py` around lines 59 - 83, ensure_connected currently
performs connect() and the subsequent validation without holding self._lock,
allowing two coroutines (via get_connection) to race and overwrite shared
self.reader/self.writer; fix by serializing reconnects: acquire self._lock at
the start of ensure_connected (or wrap the connect(),
_send_and_receive("get_session_info") validation, and disconnect() calls inside
a with await self._lock) so only one coroutine can perform
connect/validation/disconnect at a time. Apply the same protection to the other
reconnect path referenced around the get_connection usage (the logic at the
other occurrence noted in the comment) so both code paths use self._lock when
touching reader/writer or calling connect/disconnect.

Comment on lines +110 to +129
try:
self.writer.write(payload)
await self.writer.drain()

response_data = await self._read_json_response()
responses = json.loads(response_data.decode("utf-8"))

if isinstance(responses, list):
results = []
for resp in responses:
if resp.get("status") == "error":
results.append(
{"error": resp.get("message", "Unknown error")}
)
else:
results.append(resp.get("result", {}))
return results
else:
# Server returned single response — batch not supported
return [responses.get("result", {})]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

send_batch() drops commands when batch mode is unsupported.

The non-list branch returns a one-element result list instead of replaying the original commands sequentially, so callers lose both the error and every command after the first.

📦 Suggested fix
                 if isinstance(responses, list):
                     results = []
                     for resp in responses:
                         if resp.get("status") == "error":
                             results.append(
                                 {"error": resp.get("message", "Unknown error")}
                             )
                         else:
                             results.append(resp.get("result", {}))
                     return results
-                else:
-                    # Server returned single response — batch not supported
-                    return [responses.get("result", {})]
+
+                # Server returned a single response — batch not supported.
+                results = []
+                for cmd in commands:
+                    try:
+                        results.append(
+                            await self._send_and_receive(
+                                cmd["type"],
+                                cmd.get("params", {}),
+                            )
+                        )
+                    except Exception as e:
+                        results.append({"error": str(e)})
+                return results
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCP_Server/connection.py` around lines 110 - 129, send_batch() currently
treats a non-list server response as a single successful result and returns
[result], which drops all other commands; fix by detecting the non-list
(batch-unsupported) case, decoding the original payload (the JSON list of
commands) and replaying each command sequentially: for each command in the
decoded payload write it to self.writer, await drain, call
self._read_json_response() to parse that single response, and append either
{"error": message} or the result to the results list; return the full results
list so callers receive per-command results/errors. Ensure you reference
send_batch, payload, self.writer, and self._read_json_response in your change.

Comment on lines +178 to +205
async def remove_notes_from_clip(
track_index: int,
clip_index: int,
from_time: float = 0.0,
time_span: float = 999.0,
from_pitch: int = 0,
pitch_span: int = 127,
) -> str:
"""Remove notes from a clip within the specified range.

Args:
track_index: The zero-based index of the track.
clip_index: The zero-based index of the clip slot.
from_time: Start of the time range in beats (default 0.0).
time_span: Length of the time range in beats (default 999.0).
from_pitch: Lowest MIDI pitch to remove (default 0).
pitch_span: Number of pitches above from_pitch to include (default 127).
"""
try:
conn = await get_connection()
result = await conn.send_command("remove_notes_from_clip", {
"track_index": track_index,
"clip_index": clip_index,
"from_time": from_time,
"time_span": time_span,
"from_pitch": from_pitch,
"pitch_span": pitch_span,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the full MIDI pitch span for the default note-removal range.

The Remote Script handler defaults pitch_span to 128. With 127 here, a default “remove everything” call leaves note 127 behind if the span is interpreted as a count, which is how the server-side API is defined.

🎹 Suggested fix
     async def remove_notes_from_clip(
         track_index: int,
         clip_index: int,
         from_time: float = 0.0,
         time_span: float = 999.0,
         from_pitch: int = 0,
-        pitch_span: int = 127,
+        pitch_span: int = 128,
     ) -> str:
@@
-            pitch_span: Number of pitches above from_pitch to include (default 127).
+            pitch_span: Number of pitches above from_pitch to include (default 128).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCP_Server/tools/clip_tools.py` around lines 178 - 205, The default pitch
span in remove_notes_from_clip is off by one: change the default parameter
pitch_span from 127 to 128 so the function uses the full MIDI range when callers
rely on the default; update the function signature for remove_notes_from_clip to
pitch_span: int = 128 and ensure any docstring wording that mentions default
pitch span remains accurate.

Comment on lines +20 to +23
except Exception as e:
logger.error(f"Error creating MIDI track: {e}")
return f"Error creating MIDI track: {e}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read the track_tools.py file
find . -name "track_tools.py" -type f

Repository: ahujasid/ableton-mcp

Length of output: 97


🏁 Script executed:

# Read the specific file around lines 20-23
cat -n MCP_Server/tools/track_tools.py | head -50

Repository: ahujasid/ableton-mcp

Length of output: 2316


🏁 Script executed:

# Count all "except Exception" patterns in the file
rg "except Exception" MCP_Server/tools/track_tools.py -A 2 -B 2

Repository: ahujasid/ableton-mcp

Length of output: 2564


Consolidate error handling in tool handlers to return structured error responses.

This pattern (repeated 11 times throughout the file) returns inconsistent error formats: success responses use json.dumps(), but errors return plain strings. This breaks the error contract for downstream handling. Replace with a helper wrapper that returns structured JSON errors consistently.

Refactor direction
+async def _run_write_command(get_connection, cache, command: str, payload: dict, err_ctx: str) -> str:
+    try:
+        conn = await get_connection()
+        result = await conn.send_command(command, payload)
+        cache.invalidate_all()
+        return json.dumps(result, indent=2)
+    except Exception:
+        logger.exception(err_ctx)
+        return json.dumps({"status": "error", "message": err_ctx}, indent=2)
-        try:
-            conn = await get_connection()
-            result = await conn.send_command("set_track_name", {"track_index": track_index, "name": name})
-            cache.invalidate_all()
-            return json.dumps(result, indent=2)
-        except Exception as e:
-            logger.error(f"Error setting track name: {e}")
-            return f"Error setting track name: {e}"
+        return await _run_write_command(
+            get_connection,
+            cache,
+            "set_track_name",
+            {"track_index": track_index, "name": name},
+            "Error setting track name",
+        )
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 20-20: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCP_Server/tools/track_tools.py` around lines 20 - 23, The current exception
handling in functions like the MIDI track handler (the except Exception as e
block that calls logger.error and returns a plain string) is inconsistent with
success responses that use json.dumps; create a small helper (e.g.,
wrap_tool_call or handle_tool_error) that executes the tool logic, catches
exceptions, logs the full exception via logger.exception or logger.error(...,
exc_info=True), and returns a consistent JSON error payload using
json.dumps({"success": False, "error": str(e)}) (or your project's standard
error schema); replace the repeated except blocks (the shown logger.error/return
lines and the ~10 other occurrences) to call this helper so all handlers return
structured JSON errors and logging includes exception details.

Comment on lines +20 to +23
except Exception as e:
logger.error(f"Error starting playback: {e}")
return f"Error starting playback: {e}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the transport_tools.py file around lines 20-23
fd -t f transport_tools.py

Repository: ahujasid/ableton-mcp

Length of output: 99


🏁 Script executed:

# Get the file structure to find transport_tools.py
find . -name "transport_tools.py" -type f 2>/dev/null | head -20

Repository: ahujasid/ableton-mcp

Length of output: 101


🏁 Script executed:

# List files in MCP_Server/tools directory
ls -la MCP_Server/tools/ 2>/dev/null || echo "Directory not found, searching for it..."
find . -path "*/MCP_Server/tools/*" -type f 2>/dev/null | head -20

Repository: ahujasid/ableton-mcp

Length of output: 1073


🏁 Script executed:

cat -n MCP_Server/tools/transport_tools.py | head -40

Repository: ahujasid/ableton-mcp

Length of output: 1651


🏁 Script executed:

# Check the full structure to understand error handling patterns
wc -l MCP_Server/tools/transport_tools.py
cat -n MCP_Server/tools/transport_tools.py

Repository: ahujasid/ableton-mcp

Length of output: 10683


🏁 Script executed:

# Search for similar exception handling patterns across all tool modules
rg "except Exception as e:" MCP_Server/tools/ -A 2

Repository: ahujasid/ableton-mcp

Length of output: 18021


🏁 Script executed:

# Search for MCP error handling or exception patterns
rg -i "mcp.*error|exception.*mcp" --type py

Repository: ahujasid/ableton-mcp

Length of output: 179


🏁 Script executed:

# Check imports and MCP usage to understand expected error patterns
rg "from mcp|import mcp" MCP_Server/ -A 3

Repository: ahujasid/ableton-mcp

Length of output: 2060


🏁 Script executed:

# Look for any error handling utilities or conventions
fd -t f "error|exception" MCP_Server/ --type py | head -10
rg "class.*Error|def.*error" MCP_Server/ --type py | head -20

Repository: ahujasid/ableton-mcp

Length of output: 233


🏁 Script executed:

# Check ai_tools.py more closely to understand why it differs
cat -n MCP_Server/tools/ai_tools.py | grep -A 2 -B 2 "except Exception"

Repository: ahujasid/ableton-mcp

Length of output: 2341


🏁 Script executed:

# Check if logger.exception is used anywhere in the codebase
rg "logger\.exception" MCP_Server/ --type py

Repository: ahujasid/ableton-mcp

Length of output: 46


🏁 Script executed:

# Look for any MCP error handling patterns or examples
rg "raise|McpError|exception" MCP_Server/server.py -A 2 -B 2

Repository: ahujasid/ableton-mcp

Length of output: 46


🏁 Script executed:

# Check FastMCP documentation or error handling patterns in imports
head -100 MCP_Server/server.py

Repository: ahujasid/ableton-mcp

Length of output: 2481


Replace broad exception handling with structured error responses to match the pattern already used in ai_tools.

This error handling pattern (bare except Exception returning plain strings) is inconsistent across the tool modules. ai_tools.py already demonstrates the better approach by returning structured JSON errors with json.dumps({"error": str(e)}). Adopt this pattern throughout transport_tools and other handlers for consistent, parseable client-side error handling. Additionally, consider using logger.exception() instead of logger.error() to capture full stack traces for debugging.

🧰 Tools
🪛 Ruff (0.15.6)

[warning] 20-20: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCP_Server/tools/transport_tools.py` around lines 20 - 23, The current bare
except in transport_tools.py (in the playback startup block, e.g., the except in
the function that starts playback) should be replaced with structured JSON error
responses and full traceback logging: change logger.error(...) to
logger.exception(...) to capture the stack trace and return json.dumps({"error":
str(e)}) instead of a plain string; apply the same pattern to other handlers in
transport_tools (any functions that currently use bare "except Exception as e"
and return plain strings) to match ai_tools' error format and ensure consistent
parseable client-side responses.

Comment on lines +104 to +108
```
AI Assistant <──MCP──> MCP Server (async Python) <──TCP:9877──> Ableton Remote Script
67 tools, response cache 61 command handlers
AI music theory engine thread-safe execution
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language tags to the plain-text fences.

markdownlint is already flagging both of these blocks (MD040), so they will keep the docs lint noisy or failing if the rule is enforced. text/plaintext is enough here.

📝 Suggested fix
-```
+```text
 AI Assistant  <──MCP──>  MCP Server (async Python)  <──TCP:9877──>  Ableton Remote Script
                          67 tools, response cache                    61 command handlers
                          AI music theory engine                      thread-safe execution

@@
- +text
MCP_Server/
server.py # Entry point
connection.py # Async TCP connection
cache.py # Response cache
tools/
session_tools.py # Session info
track_tools.py # Track management
clip_tools.py # Clip operations
scene_tools.py # Scene management
device_tools.py # Device parameters
transport_tools.py # Transport controls
browser_tools.py # Browser navigation
ai_tools.py # Music theory

AbletonMCP_Remote_Script/
init.py # Runs inside Ableton

Also applies to: 123-140

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 104 - 108, Update the two plain fenced code blocks in
README.md to include a language tag of "text" to satisfy markdownlint MD040;
specifically modify the blocks containing the ASCII diagram ("AI Assistant 
<──MCP──>  MCP Server...") and the project tree block (starting with
"MCP_Server/") so their opening fences are ```text instead of ```; this will
silence the lint warnings without changing the block contents.

Comment on lines +3 to +5
Estado actual: **67 herramientas**, conexion async, cache, AI music theory basica.

Cada item tiene un veredicto de viabilidad basado en investigacion de la API de Ableton, complejidad tecnica, y valor para el usuario. Cuando se implemente algo, se tacha de la lista.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Polish Spanish orthography in key summary lines.

There are minor language quality issues (accents/style) in the high-visibility intro and genre list (e.g., “conexión”, “caché”, “básica”, “ítem”, “investigación”, “técnica”, and preferred “reguetón”).

Also applies to: 14-14

🧰 Tools
🪛 LanguageTool

[grammar] ~3-~3: Oración con errores
Context: ... herramientas**, conexion async, cache, AI music theory basica. Cada item tiene u...

(QB_NEW_ES_OTHER_ERROR_IDS_REPLACEMENT_MULTITOKEN)


[grammar] ~3-~3: Oración con errores
Context: ...rramientas**, conexion async, cache, AI music theory basica. Cada item tiene un vere...

(QB_NEW_ES_OTHER_ERROR_IDS_REPLACEMENT_MULTITOKEN)


[grammar] ~3-~3: Oración con errores
Context: ...ntas**, conexion async, cache, AI music theory basica. Cada item tiene un veredicto d...

(QB_NEW_ES_OTHER_ERROR_IDS_REPLACEMENT_MULTITOKEN)


[grammar] ~3-~3: Oración con errores
Context: ... conexion async, cache, AI music theory basica. Cada item tiene un veredicto de viabi...

(QB_NEW_ES_OTHER_ERROR_IDS_REPLACEMENT_MULTITOKEN)


[grammar] ~5-~5: Cambia la palabra o signo.
Context: ...acion de la API de Ableton, complejidad tecnica, y valor para el usuario. Cuando se impl...

(QB_NEW_ES_OTHER_ERROR_IDS_REPLACEMENT_OTHER)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ROADMAP.md` around lines 3 - 5, Corrige la ortografía y acentuación en las
líneas de resumen clave: reemplaza "conexion" por "conexión", "cache" por
"caché", "basica" por "básica" en la línea que empieza "Estado actual: **67
herramientas**, conexion async, cache, AI music theory basica." y cambia "item"
por "ítem", "investigacion" por "investigación" y "tecnica" por "técnica" en la
frase que empieza "Cada item tiene un veredicto..."; además, usa "reguetón" en
lugar de "regueton" si aparece en la lista de géneros y revisa otras ocurrencias
similares en el documento para aplicar las mismas correcciones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant