diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f6b438..3af513d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this ## [Unreleased] ### Fixed +- Use Shopify Ruby LSP for Ruby file launches, passing the command and file separately. - Startup failures now report the configured pre-launch task and exit code when available, preserve configuration and test-dispatch errors, and direct agents to launch/task diagnostics instead of assuming a missing language extension. Readiness waits are cancelled when startup fails. ## [2.3.4] - 2026-09-03 @@ -122,4 +123,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - Initial release - Core debugging capabilities via MCP protocol - VS Code Debug Adapter Protocol integration -- Automatic MCP server startup on extension activation \ No newline at end of file +- Automatic MCP server startup on extension activation diff --git a/README.md b/README.md index 15a5bd8..b208050 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ DebugMCP supports debugging for the following languages with their respective VS | **Go** | [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go) | `.go` | ✅ Fully Supported | | **Rust** | [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) | `.rs` | ✅ Fully Supported | | **PHP** | [PHP Debug](https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug) | `.php` | ✅ Fully Supported | -| **Ruby** | [Ruby](https://marketplace.visualstudio.com/items?itemName=rebornix.ruby) | `.rb` | ✅ Fully Supported | +| **Ruby** | [Ruby](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp) | `.rb` | ✅ Fully Supported | | **C#/.NET** | [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) | `.cs`, `.csproj` | ✅ Fully Supported | ## Configuration @@ -428,7 +428,7 @@ The extension handles debug configurations intelligently: - **Go**: [Go extension](vscode:extension/golang.go) - **Rust**: [rust-analyzer extension](vscode:extension/rust-lang.rust-analyzer) - **PHP**: [PHP Debug extension](vscode:extension/xdebug.php-debug) - - **Ruby**: [Ruby extension](vscode:extension/rebornix.ruby) with debug support + - **Ruby**: [Ruby extension](vscode:extension/Shopify.ruby-lsp) and the [`debug` gem](https://github.com/ruby/debug) - MCP-compatible AI assistant (Copilot, Cline, Cursor, Codex, Windsurf, Roo Code, etc.) ## Development @@ -499,4 +499,4 @@ If DebugMCP has helped you debug faster, please consider giving it a star on Git MIT License - See [LICENSE](LICENSE.txt) for details -This extension was created by **Oz Zafar**, **Ori Bar-Ilan** and **Karin Brisker**. \ No newline at end of file +This extension was created by **Oz Zafar**, **Ori Bar-Ilan** and **Karin Brisker**. diff --git a/docs/architecture/debugConfigurationManager.md b/docs/architecture/debugConfigurationManager.md index 81d8349..f67d873 100644 --- a/docs/architecture/debugConfigurationManager.md +++ b/docs/architecture/debugConfigurationManager.md @@ -17,6 +17,7 @@ Delegating to those mechanisms keeps this class small and ensures defaults stay - Return a launch.json configuration name when the caller provides one — VS Code looks it up itself. - Otherwise, return a minimal launch stub (`type`, `request`, `name`, `program`) for the file's language and let the language extension resolve the rest. +- For Ruby, select Shopify Ruby LSP's `ruby_lsp` adapter and pass `command: 'ruby'` and `file` separately so the adapter quotes the path. - For `.NET` (`coreclr`), locate the project's built DLL since `program` cannot be a `.cs` source file. - Detect the debugger `type` from a file extension. @@ -39,7 +40,7 @@ Maps file extensions to debugger `type` values: - `.go` → `go` - `.rs` → `lldb` - `.php` → `php` -- `.rb` → `ruby` +- `.rb` → `ruby_lsp` ### Test framework support @@ -49,7 +50,8 @@ Test launches are dispatched via `DebuggingExecutor.debugTestAtCursor`, not via 1. If `configurationName` is provided and is not the sentinel `Default Configuration`, return that name verbatim. 2. Otherwise, if the file is C# (`coreclr`), walk up to find the `.csproj`, locate its built DLL under `bin/{Debug,Release}//`, and return a coreclr config pointing at that assembly. -3. Otherwise, return `{ type, request: 'launch', name: 'DebugMCP Launch', program: fileFullPath }`. +3. For Ruby (`ruby_lsp`), return `{ type, request: 'launch', name: 'DebugMCP Launch', command: 'ruby', file: fileFullPath }`. +4. Otherwise, return `{ type, request: 'launch', name: 'DebugMCP Launch', program: fileFullPath }`. ## Key code locations diff --git a/skills/debug-live/README.md b/skills/debug-live/README.md index a4eef19..462a70e 100644 --- a/skills/debug-live/README.md +++ b/skills/debug-live/README.md @@ -28,7 +28,8 @@ skills/debugmcp/ ├── java.md ├── csharp.md ├── cpp.md - └── go.md + ├── go.md + └── ruby.md ``` ## Installation diff --git a/skills/debug-live/SKILL.md b/skills/debug-live/SKILL.md index 41d7ea0..cf66b61 100644 --- a/skills/debug-live/SKILL.md +++ b/skills/debug-live/SKILL.md @@ -16,6 +16,7 @@ allowed-tools: - step_out - continue_execution - pause_execution + - get_debug_status - list_variable_names - get_variables_values - evaluate_expression @@ -232,6 +233,7 @@ Load the relevant reference file for the language you're debugging: - **C#** → `references/troubleshooting/csharp.md` - **C++** → `references/troubleshooting/cpp.md` - **Go** → `references/troubleshooting/go.md` +- **Ruby** → `references/troubleshooting/ruby.md` Each reference covers prerequisites (which VS Code extension to install), framework-specific configuration (e.g. enabling `pytest` test discovery, building `.NET` projects before diff --git a/skills/debug-live/references/troubleshooting/ruby.md b/skills/debug-live/references/troubleshooting/ruby.md new file mode 100644 index 0000000..e064f59 --- /dev/null +++ b/skills/debug-live/references/troubleshooting/ruby.md @@ -0,0 +1,77 @@ +# Ruby Debugging Tips + +## Prerequisites + +- Install the [Shopify Ruby LSP](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp) extension. +- Add Ruby's official [`debug`](https://github.com/ruby/debug) gem to the project bundle. With Bundler, verify that the + selected Ruby can run it with `bundle exec rdbg -v`. +- Make sure Ruby LSP activates the same Ruby version and bundle as the application. If automatic detection selects the + wrong runtime, configure `rubyLsp.rubyVersionManager` and restart Ruby LSP. + +DebugMCP uses the `ruby_lsp` debug adapter for `.rb` files. Do not install or configure the deprecated +`rebornix.ruby` adapter for this workflow. + +## Ruby scripts + +For an ordinary Ruby script, set a breakpoint on an executable line and call `start_debugging` with the script path and +the project root. DebugMCP creates a minimal Ruby LSP launch configuration and passes the Ruby command and file +separately, so paths containing spaces or shell metacharacters remain valid. + +Use a named `launch.json` configuration only when the program needs a specific command, environment, or attach mode: + +```json +{ + "type": "ruby_lsp", + "request": "launch", + "name": "Debug Rails", + "program": "bin/rails server" +} +``` + +Pass its name as `configurationName` to `start_debugging`. + +## Rails and long-running processes + +For a Rails request, job, or callback, set the breakpoint before starting the debug session and make sure the request or +job is triggered in the process launched by the debugger. A breakpoint in a web process will not catch code executed by +a separate Sidekiq process, Puma worker, test process, container, or preloader. + +To attach to a Rails server instead of launching a new one, start it with the official debugger and use a named +`ruby_lsp` attach configuration: + +```text +bundle exec rdbg -O -n -c -- bin/rails server +``` + +```json +{ + "type": "ruby_lsp", + "request": "attach", + "name": "Attach to Ruby" +} +``` + +Then pass `configurationName: "Attach to Ruby"` to `start_debugging`. Keep the debugger endpoint local unless the +project has an explicit, secured remote-debugging setup. + +`start_debugging` returns as soon as the Ruby attach succeeds; it does not wait for a later request or job to hit the +breakpoint. Trigger that work after the attach result, then call `get_debug_status` with `waitForPauseSeconds` to wait +for the Ruby breakpoint without polling or interrupting the server with `pause_execution`. Inspect values only after +the returned status is `paused`. + +## Common failures + +### The debugger does not start + +- Run `bundle exec rdbg -v` from `workingDirectory`. +- Verify VS Code and Ruby LSP selected the project's intended Ruby and Gemfile. +- Open the workspace at the directory that owns the bundle; incorrect workspace roots commonly select another runtime. +- Restart Ruby LSP after changing gems, version-manager settings, or the bundle. + +### A breakpoint is not hit + +- Put it on an executable Ruby line, not a blank line, comment, method declaration terminator, or DSL line that ran + before the debugger attached. +- Confirm the debugged process actually executes that file and code path. +- Check for a different worker, preloader, container, generated copy, or mismatched local/remote source path. +- For a long-running server, trigger the request or job only after the debugger session has started. diff --git a/src/test/debugConfigurationManager.test.ts b/src/test/debugConfigurationManager.test.ts new file mode 100644 index 0000000..7392a07 --- /dev/null +++ b/src/test/debugConfigurationManager.test.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. + +import * as assert from 'assert'; +import { DebugConfigurationManager } from '../utils/debugConfigurationManager'; + +suite('DebugConfigurationManager', () => { + const manager = new DebugConfigurationManager(); + + test('uses the Ruby LSP debug adapter for Ruby files', () => { + assert.strictEqual(manager.detectLanguageFromFilePath('/repo/example.rb'), 'ruby_lsp'); + }); + + test('creates a Ruby LSP launch config with a separate command and file', async () => { + const fileFullPath = '/repo/with spaces/example.rb'; + + const config = await manager.getDebugConfig('/repo', fileFullPath); + + assert.deepStrictEqual(config, { + type: 'ruby_lsp', + request: 'launch', + name: 'DebugMCP Launch', + command: 'ruby', + file: fileFullPath + }); + }); + + test('preserves explicitly named launch configurations for Ruby', async () => { + const config = await manager.getDebugConfig('/repo', '/repo/example.rb', 'Debug Rails'); + + assert.strictEqual(config, 'Debug Rails'); + }); +}); diff --git a/src/test/debugSkillGuidance.test.ts b/src/test/debugSkillGuidance.test.ts index 4f363d9..a021bcb 100644 --- a/src/test/debugSkillGuidance.test.ts +++ b/src/test/debugSkillGuidance.test.ts @@ -7,8 +7,10 @@ import { suite, test } from 'mocha'; suite('debug-live skill guidance', () => { const skillPath = path.resolve(__dirname, '..', '..', 'skills', 'debug-live', 'SKILL.md'); + const rubyGuidancePath = path.resolve(__dirname, '..', '..', 'skills', 'debug-live', 'references', 'troubleshooting', 'ruby.md'); const serverPath = path.resolve(__dirname, '..', '..', 'src', 'debugMCPServer.ts'); const skill = fs.readFileSync(skillPath, 'utf8'); + const rubyGuidance = fs.readFileSync(rubyGuidancePath, 'utf8'); const serverSource = fs.readFileSync(serverPath, 'utf8'); const description = skill.match(/^description:\s*(.+)$/m)?.[1] ?? ''; @@ -42,4 +44,10 @@ suite('debug-live skill guidance', () => { test('start_debugging points agents to the skill', () => { assert.match(serverSource, /Invoke the "debug-live" skill first\./); }); + + test('Ruby attach guidance waits for a breakpoint without interrupting the server', () => { + assert.match(skill, /^ - get_debug_status$/m); + assert.match(rubyGuidance, /`get_debug_status` with `waitForPauseSeconds`/); + assert.match(rubyGuidance, /without polling or interrupting the server with `pause_execution`/); + }); }); diff --git a/src/test/startDebuggingMatrix.test.ts b/src/test/startDebuggingMatrix.test.ts index 4d162d0..0d4b155 100644 --- a/src/test/startDebuggingMatrix.test.ts +++ b/src/test/startDebuggingMatrix.test.ts @@ -119,7 +119,8 @@ const LANGUAGES: LangCase[] = [ { label: 'Java', file: '/repo/src/App.java', debuggerType: 'java' }, { label: 'C#', file: '/repo/src/AppTests.cs', debuggerType: 'coreclr' }, { label: 'C++', file: '/repo/src/app.cpp', debuggerType: 'cppdbg' }, - { label: 'Go', file: '/repo/src/main.go', debuggerType: 'go' } + { label: 'Go', file: '/repo/src/main.go', debuggerType: 'go' }, + { label: 'Ruby', file: '/repo/src/app.rb', debuggerType: 'ruby_lsp' } ]; suite('handleStartDebugging regression matrix', () => { diff --git a/src/utils/debugConfigurationManager.ts b/src/utils/debugConfigurationManager.ts index 096267d..54e4102 100644 --- a/src/utils/debugConfigurationManager.ts +++ b/src/utils/debugConfigurationManager.ts @@ -48,7 +48,7 @@ export class DebugConfigurationManager implements IDebugConfigurationManager { '.go': 'go', '.rs': 'lldb', '.php': 'php', - '.rb': 'ruby' + '.rb': 'ruby_lsp' }; /** @@ -75,6 +75,19 @@ export class DebugConfigurationManager implements IDebugConfigurationManager { return await this.createDotNetLaunchConfig(fileFullPath); } + // Ruby LSP accepts a command and file separately, then quotes the file + // when it builds the rdbg command. Keeping them separate avoids broken + // launches for paths containing spaces or shell metacharacters. + if (language === 'ruby_lsp') { + return { + type: language, + request: 'launch', + name: 'DebugMCP Launch', + command: 'ruby', + file: fileFullPath + }; + } + // Minimal stub. The language extension's resolveDebugConfiguration // fills in cwd, console, env, stopOnEntry, and other defaults. return {