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
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,16 @@ dotnet build src/CodingWithCalvin.VSMCP/CodingWithCalvin.VSMCP.csproj
- `symbol_workspace` - Search for symbols across the solution

### Debugger Tools
- `debugger_add_breakpoint` - Add a breakpoint at a file and line
- `breakpoint_enable` - Enable or disable a breakpoint at a file and line
- `debugger_add_breakpoint` - Add a breakpoint, optionally with a condition and hit-count rule
- `debugger_break` - Pause execution (Ctrl+Alt+Break)
- `debugger_continue` - Continue execution (F5)
- `debugger_evaluate` - Evaluate an expression in the current debug context
- `debugger_get_callstack` - Get the call stack
- `debugger_get_locals` - Get local variables in current frame
- `debugger_launch` - Start debugging (F5), optionally for a specific project
- `debugger_launch_without_debugging` - Start without debugger (Ctrl+F5), optionally for a specific project
- `debugger_list_breakpoints` - List all breakpoints
- `debugger_list_breakpoints` - List all breakpoints, including conditions and hit-count rules
- `debugger_remove_breakpoint` - Remove a breakpoint
- `debugger_set_variable` - Set the value of a local variable
- `debugger_status` - Get current debugger state
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,16 @@

| Tool | Description |
|------|-------------|
| `debugger_add_breakpoint` | Add a breakpoint at a file and line |
| `breakpoint_enable` | Enable or disable a breakpoint at a file and line |
| `debugger_add_breakpoint` | Add a breakpoint, optionally with a condition and hit-count rule |
| `debugger_break` | Pause execution (Ctrl+Alt+Break) |
| `debugger_continue` | Continue execution (F5) |
| `debugger_evaluate` | Evaluate an expression in the current debug context |
| `debugger_get_callstack` | Get the call stack |
| `debugger_get_locals` | Get local variables in current frame |
| `debugger_launch` | Start debugging (F5), optionally for a specific project |
| `debugger_launch_without_debugging` | Start without debugger (Ctrl+F5), optionally for a specific project |
| `debugger_list_breakpoints` | List all breakpoints |
| `debugger_list_breakpoints` | List all breakpoints, including conditions and hit-count rules |
| `debugger_remove_breakpoint` | Remove a breakpoint |
| `debugger_set_variable` | Set the value of a local variable |
| `debugger_status` | Get current debugger state |
Expand Down
11 changes: 10 additions & 1 deletion src/CodingWithCalvin.MCPServer.Server/RpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,16 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
public Task<bool> DebugStepIntoAsync() => Proxy.DebugStepIntoAsync();
public Task<bool> DebugStepOutAsync() => Proxy.DebugStepOutAsync();

public Task<bool> DebugAddBreakpointAsync(string file, int line) => Proxy.DebugAddBreakpointAsync(file, line);
public Task<bool> DebugAddBreakpointAsync(
string file,
int line,
string? condition = null,
BreakpointConditionType conditionType = BreakpointConditionType.WhenTrue,
int? hitCount = null,
BreakpointHitCountType hitCountType = BreakpointHitCountType.None)
=> Proxy.DebugAddBreakpointAsync(file, line, condition, conditionType, hitCount, hitCountType);
public Task<bool> DebugSetBreakpointEnabledAsync(string file, int line, bool enabled)
=> Proxy.DebugSetBreakpointEnabledAsync(file, line, enabled);
public Task<bool> DebugRemoveBreakpointAsync(string file, int line) => Proxy.DebugRemoveBreakpointAsync(file, line);
public Task<List<BreakpointInfo>> DebugGetBreakpointsAsync() => Proxy.DebugGetBreakpointsAsync();
public Task<List<Shared.Models.LocalVariableInfo>> DebugGetLocalsAsync() => Proxy.DebugGetLocalsAsync();
Expand Down
88 changes: 83 additions & 5 deletions src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks;
using CodingWithCalvin.MCPServer.Shared.Models;
using ModelContextProtocol.Server;

namespace CodingWithCalvin.MCPServer.Server.Tools;
Expand Down Expand Up @@ -112,13 +114,77 @@ public async Task<string> DebugStepOutAsync()
}

[McpServerTool(Name = "debugger_add_breakpoint", Destructive = false)]
[Description("Add a breakpoint at a specific file and line number. Works in any debugger mode (Design, Run, or Break). The file path must be absolute.")]
public async Task<string> DebugAddBreakpointAsync(string path, int line)
{
var success = await _rpcClient.DebugAddBreakpointAsync(path, line);
[Description("Add a breakpoint at a specific file and line number, optionally with a conditional expression and/or hit-count rule. Works in any debugger mode (Design, Run, or Break). The file path must be absolute.")]
public async Task<string> DebugAddBreakpointAsync(
[Description("Absolute path to the source file.")] string path,
[Description("One-based source line number.")] int line,
[Description("Optional expression that controls whether execution breaks.")] string? condition = null,
[Description("How to evaluate condition: whenTrue or whenChanged. Defaults to whenTrue.")] string conditionType = "whenTrue",
[Description("Optional positive hit-count target. When supplied without hitCountType, the equal rule is used.")] int? hitCount = null,
[Description("How to apply hitCount: none, equal, greaterOrEqual, or multiple. Defaults to none.")] string hitCountType = "none")
{
if (line < 1)
{
return "Invalid line. The line number must be greater than zero.";
}

if (!TryParseConditionType(conditionType, out var parsedConditionType))
{
return $"Invalid conditionType '{conditionType}'. Valid values are: whenTrue, whenChanged.";
}

if (!TryParseHitCountType(hitCountType, out var parsedHitCountType))
{
return $"Invalid hitCountType '{hitCountType}'. Valid values are: none, equal, greaterOrEqual, multiple.";
}

if (string.IsNullOrWhiteSpace(condition) && parsedConditionType == BreakpointConditionType.WhenChanged)
{
return "A condition expression is required when conditionType is whenChanged.";
}

if (hitCount.HasValue && hitCount.Value < 1)
{
return "Invalid hitCount. The hit-count target must be greater than zero.";
}

if (parsedHitCountType != BreakpointHitCountType.None && !hitCount.HasValue)
{
return $"A hitCount value is required when hitCountType is {hitCountType}.";
}

if (hitCount.HasValue && parsedHitCountType == BreakpointHitCountType.None)
{
parsedHitCountType = BreakpointHitCountType.Equal;
}

var success = await _rpcClient.DebugAddBreakpointAsync(
path,
line,
condition,
parsedConditionType,
hitCount,
parsedHitCountType);
return success ? $"Breakpoint added at {path}:{line}" : $"Failed to add breakpoint at {path}:{line}";
}

[McpServerTool(Name = "breakpoint_enable", Destructive = false)]
[Description("Enable or disable a breakpoint at a specific file and line number. Returns whether a matching breakpoint was found and updated.")]
public async Task<string> DebugSetBreakpointEnabledAsync(
[Description("Absolute path to the source file.")] string path,
[Description("One-based source line number.")] int line,
[Description("True to enable the breakpoint; false to disable it.")] bool enabled)
{
if (line < 1)
{
return "Invalid line. The line number must be greater than zero.";
}

var success = await _rpcClient.DebugSetBreakpointEnabledAsync(path, line, enabled);
var action = enabled ? "enabled" : "disabled";
return success ? $"Breakpoint {action} at {path}:{line}" : $"No breakpoint found at {path}:{line}";
}

[McpServerTool(Name = "debugger_remove_breakpoint", Destructive = true)]
[Description("Remove a breakpoint at a specific file and line number. Returns whether a breakpoint was found and removed.")]
public async Task<string> DebugRemoveBreakpointAsync(string path, int line)
Expand All @@ -128,7 +194,7 @@ public async Task<string> DebugRemoveBreakpointAsync(string path, int line)
}

[McpServerTool(Name = "debugger_list_breakpoints", ReadOnly = true)]
[Description("List all breakpoints in the current solution. Returns file, line, column, function name, condition, enabled state, and hit count for each breakpoint.")]
[Description("List all breakpoints in the current solution. Returns file, line, column, function name, condition and condition type, enabled state, current hits, hit-count target, and hit-count rule for each breakpoint.")]
public async Task<string> DebugListBreakpointsAsync()
{
var breakpoints = await _rpcClient.DebugGetBreakpointsAsync();
Expand Down Expand Up @@ -171,4 +237,16 @@ public async Task<string> DebugGetCallStackAsync()
var callStack = await _rpcClient.DebugGetCallStackAsync();
return JsonSerializer.Serialize(callStack, _jsonOptions);
}

private static bool TryParseConditionType(string value, out BreakpointConditionType conditionType)
{
return Enum.TryParse(value, ignoreCase: true, out conditionType)
&& Enum.IsDefined(typeof(BreakpointConditionType), conditionType);
}

private static bool TryParseHitCountType(string value, out BreakpointHitCountType hitCountType)
{
return Enum.TryParse(value, ignoreCase: true, out hitCountType)
&& Enum.IsDefined(typeof(BreakpointHitCountType), hitCountType);
}
}
17 changes: 17 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,32 @@ public class DebuggerStatus
public string CurrentFunction { get; set; } = string.Empty;
}

public enum BreakpointConditionType
{
WhenTrue,
WhenChanged
}

public enum BreakpointHitCountType
{
None,
Equal,
GreaterOrEqual,
Multiple
}

public class BreakpointInfo
{
public string File { get; set; } = string.Empty;
public int Line { get; set; }
public int Column { get; set; }
public string FunctionName { get; set; } = string.Empty;
public string Condition { get; set; } = string.Empty;
public string ConditionType { get; set; } = string.Empty;
public bool Enabled { get; set; }
public int CurrentHits { get; set; }
public int HitCountTarget { get; set; }
public string HitCountType { get; set; } = string.Empty;
}

public class LocalVariableInfo
Expand Down
9 changes: 8 additions & 1 deletion src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,14 @@ public interface IVisualStudioRpc
Task<bool> DebugStepIntoAsync();
Task<bool> DebugStepOutAsync();

Task<bool> DebugAddBreakpointAsync(string file, int line);
Task<bool> DebugAddBreakpointAsync(
string file,
int line,
string? condition = null,
BreakpointConditionType conditionType = BreakpointConditionType.WhenTrue,
int? hitCount = null,
BreakpointHitCountType hitCountType = BreakpointHitCountType.None);
Task<bool> DebugSetBreakpointEnabledAsync(string file, int line, bool enabled);
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
Expand Down
60 changes: 60 additions & 0 deletions src/CodingWithCalvin.MCPServer.Tests/DebuggerBreakpointTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using CodingWithCalvin.MCPServer.Services;
using CodingWithCalvin.MCPServer.Shared.Models;
using EnvDTE;
using Xunit;

namespace CodingWithCalvin.MCPServer.Tests;

public class DebuggerBreakpointTests
{
[Fact]
public void ConditionTypes_MapToDteAndApiNames()
{
Assert.Equal(
dbgBreakpointConditionType.dbgBreakpointConditionTypeWhenTrue,
VisualStudioService.ToDteConditionType(BreakpointConditionType.WhenTrue));
Assert.Equal(
dbgBreakpointConditionType.dbgBreakpointConditionTypeWhenChanged,
VisualStudioService.ToDteConditionType(BreakpointConditionType.WhenChanged));
Assert.Equal(
"whenTrue",
VisualStudioService.GetConditionTypeName(dbgBreakpointConditionType.dbgBreakpointConditionTypeWhenTrue));
Assert.Equal(
"whenChanged",
VisualStudioService.GetConditionTypeName(dbgBreakpointConditionType.dbgBreakpointConditionTypeWhenChanged));
}

[Fact]
public void HitCountTypes_MapToDteAndApiNames()
{
Assert.Equal(dbgHitCountType.dbgHitCountTypeNone, VisualStudioService.ToDteHitCountType(BreakpointHitCountType.None));
Assert.Equal(dbgHitCountType.dbgHitCountTypeEqual, VisualStudioService.ToDteHitCountType(BreakpointHitCountType.Equal));
Assert.Equal(dbgHitCountType.dbgHitCountTypeGreaterOrEqual, VisualStudioService.ToDteHitCountType(BreakpointHitCountType.GreaterOrEqual));
Assert.Equal(dbgHitCountType.dbgHitCountTypeMultiple, VisualStudioService.ToDteHitCountType(BreakpointHitCountType.Multiple));

Assert.Equal("none", VisualStudioService.GetHitCountTypeName(dbgHitCountType.dbgHitCountTypeNone));
Assert.Equal("equal", VisualStudioService.GetHitCountTypeName(dbgHitCountType.dbgHitCountTypeEqual));
Assert.Equal("greaterOrEqual", VisualStudioService.GetHitCountTypeName(dbgHitCountType.dbgHitCountTypeGreaterOrEqual));
Assert.Equal("multiple", VisualStudioService.GetHitCountTypeName(dbgHitCountType.dbgHitCountTypeMultiple));
}

[Theory]
[InlineData(1, null, BreakpointHitCountType.None, true)]
[InlineData(0, null, BreakpointHitCountType.None, false)]
[InlineData(1, 1, BreakpointHitCountType.Equal, true)]
[InlineData(1, 5, BreakpointHitCountType.GreaterOrEqual, true)]
[InlineData(1, 2, BreakpointHitCountType.Multiple, true)]
[InlineData(1, null, BreakpointHitCountType.Equal, false)]
[InlineData(1, 0, BreakpointHitCountType.Equal, false)]
[InlineData(1, 1, BreakpointHitCountType.None, false)]
public void BreakpointConfiguration_ValidatesLineAndHitCount(
int line,
int? hitCount,
BreakpointHitCountType hitCountType,
bool expected)
{
Assert.Equal(
expected,
VisualStudioService.IsValidBreakpointConfiguration(line, hitCount, hitCountType));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ public interface IVisualStudioService
Task<bool> DebugStepIntoAsync();
Task<bool> DebugStepOutAsync();

Task<bool> DebugAddBreakpointAsync(string file, int line);
Task<bool> DebugAddBreakpointAsync(
string file,
int line,
string? condition = null,
BreakpointConditionType conditionType = BreakpointConditionType.WhenTrue,
int? hitCount = null,
BreakpointHitCountType hitCountType = BreakpointHitCountType.None);
Task<bool> DebugSetBreakpointEnabledAsync(string file, int line, bool enabled);
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
Expand Down
11 changes: 10 additions & 1 deletion src/CodingWithCalvin.MCPServer/Services/RpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,16 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
public Task<bool> DebugStepIntoAsync() => _vsService.DebugStepIntoAsync();
public Task<bool> DebugStepOutAsync() => _vsService.DebugStepOutAsync();

public Task<bool> DebugAddBreakpointAsync(string file, int line) => _vsService.DebugAddBreakpointAsync(file, line);
public Task<bool> DebugAddBreakpointAsync(
string file,
int line,
string? condition = null,
BreakpointConditionType conditionType = BreakpointConditionType.WhenTrue,
int? hitCount = null,
BreakpointHitCountType hitCountType = BreakpointHitCountType.None)
=> _vsService.DebugAddBreakpointAsync(file, line, condition, conditionType, hitCount, hitCountType);
public Task<bool> DebugSetBreakpointEnabledAsync(string file, int line, bool enabled)
=> _vsService.DebugSetBreakpointEnabledAsync(file, line, enabled);
public Task<bool> DebugRemoveBreakpointAsync(string file, int line) => _vsService.DebugRemoveBreakpointAsync(file, line);
public Task<List<BreakpointInfo>> DebugGetBreakpointsAsync() => _vsService.DebugGetBreakpointsAsync();
public Task<List<LocalVariableInfo>> DebugGetLocalsAsync() => _vsService.DebugGetLocalsAsync();
Expand Down
Loading
Loading