Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
- Automatic MCP server startup on extension activation
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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**.
This extension was created by **Oz Zafar**, **Ori Bar-Ilan** and **Karin Brisker**.
6 changes: 4 additions & 2 deletions docs/architecture/debugConfigurationManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand All @@ -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}/<tfm>/`, 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

Expand Down
3 changes: 2 additions & 1 deletion skills/debug-live/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ skills/debugmcp/
├── java.md
├── csharp.md
├── cpp.md
└── go.md
├── go.md
└── ruby.md
```

## Installation
Expand Down
2 changes: 2 additions & 0 deletions skills/debug-live/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ allowed-tools:
- step_out
- continue_execution
- pause_execution
- get_debug_status
- list_variable_names
- get_variables_values
- evaluate_expression
Expand Down Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions skills/debug-live/references/troubleshooting/ruby.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions src/test/debugConfigurationManager.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
8 changes: 8 additions & 0 deletions src/test/debugSkillGuidance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] ?? '';

Expand Down Expand Up @@ -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`/);
});
});
3 changes: 2 additions & 1 deletion src/test/startDebuggingMatrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
15 changes: 14 additions & 1 deletion src/utils/debugConfigurationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class DebugConfigurationManager implements IDebugConfigurationManager {
'.go': 'go',
'.rs': 'lldb',
'.php': 'php',
'.rb': 'ruby'
'.rb': 'ruby_lsp'
};

/**
Expand All @@ -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 {
Expand Down
Loading