Skip to content

Commit 263e158

Browse files
authored
feat(debugger): add expression evaluation and variable editing tools (#51)
Add debugger_evaluate tool for evaluating expressions in the current debug context via Debugger2.GetExpression. Add debugger_set_variable tool for modifying local variable values via Expression.Value setter. Both require the debugger to be in Break mode.
1 parent 8b54779 commit 263e158

7 files changed

Lines changed: 116 additions & 0 deletions

File tree

src/CodingWithCalvin.MCPServer.Server/RpcClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
153153
public Task<bool> DebugRemoveBreakpointAsync(string file, int line) => Proxy.DebugRemoveBreakpointAsync(file, line);
154154
public Task<List<BreakpointInfo>> DebugGetBreakpointsAsync() => Proxy.DebugGetBreakpointsAsync();
155155
public Task<List<Shared.Models.LocalVariableInfo>> DebugGetLocalsAsync() => Proxy.DebugGetLocalsAsync();
156+
public Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression) => Proxy.DebugEvaluateExpressionAsync(expression);
157+
public Task<bool> DebugSetVariableValueAsync(string variableName, string value) => Proxy.DebugSetVariableValueAsync(variableName, value);
156158
public Task<List<CallStackFrameInfo>> DebugGetCallStackAsync() => Proxy.DebugGetCallStackAsync();
157159

158160
public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxResults = 100)

src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,27 @@ public async Task<string> DebugGetLocalsAsync()
143143
return JsonSerializer.Serialize(locals, _jsonOptions);
144144
}
145145

146+
[McpServerTool(Name = "debugger_evaluate", ReadOnly = true)]
147+
[Description("Evaluate an expression in the current debug context (like the Immediate Window). Only works when the debugger is in Break mode. Returns the expression, its value, type, and whether the value is valid.")]
148+
public async Task<string> DebugEvaluateExpressionAsync(
149+
[Description("The expression to evaluate (e.g., 'myVariable', 'list.Count', 'x + y', 'myObject.ToString()')")] string expression)
150+
{
151+
var result = await _rpcClient.DebugEvaluateExpressionAsync(expression);
152+
return JsonSerializer.Serialize(result, _jsonOptions);
153+
}
154+
155+
[McpServerTool(Name = "debugger_set_variable", Destructive = true)]
156+
[Description("Set the value of a local variable in the current stack frame. Only works when the debugger is in Break mode. The variable must exist in the current scope.")]
157+
public async Task<string> DebugSetVariableValueAsync(
158+
[Description("The name of the local variable to modify (e.g., 'count', 'name'). Use debugger_get_locals to see available variables.")] string variableName,
159+
[Description("The new value to assign (e.g., '42', '\"hello\"', 'true'). Must be a valid expression for the variable's type.")] string value)
160+
{
161+
var success = await _rpcClient.DebugSetVariableValueAsync(variableName, value);
162+
return success
163+
? $"Variable '{variableName}' set to: {value}"
164+
: $"Failed to set variable '{variableName}'. Ensure the debugger is in Break mode, the variable exists in the current scope, and the value is valid for its type.";
165+
}
166+
146167
[McpServerTool(Name = "debugger_get_callstack", ReadOnly = true)]
147168
[Description("Get the call stack of the current thread. Only works when the debugger is in Break mode. Returns depth, function name, file name, line number, module, language, and return type for each frame.")]
148169
public async Task<string> DebugGetCallStackAsync()

src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ public class LocalVariableInfo
3030
public bool IsValidValue { get; set; }
3131
}
3232

33+
public class ExpressionResult
34+
{
35+
public string Expression { get; set; } = string.Empty;
36+
public string Value { get; set; } = string.Empty;
37+
public string Type { get; set; } = string.Empty;
38+
public bool IsValidValue { get; set; }
39+
}
40+
3341
public class CallStackFrameInfo
3442
{
3543
public int Depth { get; set; }

src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public interface IVisualStudioRpc
5858
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
5959
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
6060
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
61+
Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression);
62+
Task<bool> DebugSetVariableValueAsync(string variableName, string value);
6163
Task<List<CallStackFrameInfo>> DebugGetCallStackAsync();
6264

6365
// Diagnostics tools

src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public interface IVisualStudioService
5454
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
5555
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
5656
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
57+
Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression);
58+
Task<bool> DebugSetVariableValueAsync(string variableName, string value);
5759
Task<List<CallStackFrameInfo>> DebugGetCallStackAsync();
5860

5961
Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxResults = 100);

src/CodingWithCalvin.MCPServer/Services/RpcServer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
211211
public Task<bool> DebugRemoveBreakpointAsync(string file, int line) => _vsService.DebugRemoveBreakpointAsync(file, line);
212212
public Task<List<BreakpointInfo>> DebugGetBreakpointsAsync() => _vsService.DebugGetBreakpointsAsync();
213213
public Task<List<LocalVariableInfo>> DebugGetLocalsAsync() => _vsService.DebugGetLocalsAsync();
214+
public Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression) => _vsService.DebugEvaluateExpressionAsync(expression);
215+
public Task<bool> DebugSetVariableValueAsync(string variableName, string value) => _vsService.DebugSetVariableValueAsync(variableName, value);
214216
public Task<List<CallStackFrameInfo>> DebugGetCallStackAsync() => _vsService.DebugGetCallStackAsync();
215217

216218
public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxResults = 100)

src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,6 +1633,85 @@ public async Task<List<LocalVariableInfo>> DebugGetLocalsAsync()
16331633
return results;
16341634
}
16351635

1636+
public async Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression)
1637+
{
1638+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
1639+
var dte = await GetDteAsync();
1640+
var debugger = (Debugger2)dte.Debugger;
1641+
1642+
if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode)
1643+
{
1644+
return new ExpressionResult
1645+
{
1646+
Expression = expression,
1647+
IsValidValue = false,
1648+
Value = "Debugger is not in Break mode"
1649+
};
1650+
}
1651+
1652+
try
1653+
{
1654+
var expr = debugger.GetExpression(expression, false, 1000);
1655+
return new ExpressionResult
1656+
{
1657+
Expression = expression,
1658+
Value = expr.Value ?? string.Empty,
1659+
Type = expr.Type ?? string.Empty,
1660+
IsValidValue = expr.IsValidValue
1661+
};
1662+
}
1663+
catch (Exception ex)
1664+
{
1665+
VsixTelemetry.TrackException(ex);
1666+
return new ExpressionResult
1667+
{
1668+
Expression = expression,
1669+
IsValidValue = false,
1670+
Value = ex.Message
1671+
};
1672+
}
1673+
}
1674+
1675+
public async Task<bool> DebugSetVariableValueAsync(string variableName, string value)
1676+
{
1677+
using var activity = VsixTelemetry.Tracer.StartActivity("DebugSetVariableValue");
1678+
1679+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
1680+
var dte = await GetDteAsync();
1681+
var debugger = (Debugger2)dte.Debugger;
1682+
1683+
if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode)
1684+
{
1685+
return false;
1686+
}
1687+
1688+
try
1689+
{
1690+
var frame = debugger.CurrentStackFrame;
1691+
if (frame == null)
1692+
{
1693+
return false;
1694+
}
1695+
1696+
foreach (Expression local in frame.Locals)
1697+
{
1698+
if (local.Name == variableName)
1699+
{
1700+
local.Value = value;
1701+
return true;
1702+
}
1703+
}
1704+
1705+
return false;
1706+
}
1707+
catch (Exception ex)
1708+
{
1709+
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
1710+
activity?.RecordException(ex);
1711+
return false;
1712+
}
1713+
}
1714+
16361715
public async Task<List<CallStackFrameInfo>> DebugGetCallStackAsync()
16371716
{
16381717
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

0 commit comments

Comments
 (0)