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
2 changes: 2 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/RpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
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();
public Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression) => Proxy.DebugEvaluateExpressionAsync(expression);
public Task<bool> DebugSetVariableValueAsync(string variableName, string value) => Proxy.DebugSetVariableValueAsync(variableName, value);
public Task<List<CallStackFrameInfo>> DebugGetCallStackAsync() => Proxy.DebugGetCallStackAsync();

public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxResults = 100)
Expand Down
21 changes: 21 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,27 @@ public async Task<string> DebugGetLocalsAsync()
return JsonSerializer.Serialize(locals, _jsonOptions);
}

[McpServerTool(Name = "debugger_evaluate", ReadOnly = true)]
[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.")]
public async Task<string> DebugEvaluateExpressionAsync(
[Description("The expression to evaluate (e.g., 'myVariable', 'list.Count', 'x + y', 'myObject.ToString()')")] string expression)
{
var result = await _rpcClient.DebugEvaluateExpressionAsync(expression);
return JsonSerializer.Serialize(result, _jsonOptions);
}

[McpServerTool(Name = "debugger_set_variable", Destructive = true)]
[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.")]
public async Task<string> DebugSetVariableValueAsync(
[Description("The name of the local variable to modify (e.g., 'count', 'name'). Use debugger_get_locals to see available variables.")] string variableName,
[Description("The new value to assign (e.g., '42', '\"hello\"', 'true'). Must be a valid expression for the variable's type.")] string value)
{
var success = await _rpcClient.DebugSetVariableValueAsync(variableName, value);
return success
? $"Variable '{variableName}' set to: {value}"
: $"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.";
}

[McpServerTool(Name = "debugger_get_callstack", ReadOnly = true)]
[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.")]
public async Task<string> DebugGetCallStackAsync()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public class LocalVariableInfo
public bool IsValidValue { get; set; }
}

public class ExpressionResult
{
public string Expression { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public bool IsValidValue { get; set; }
}

public class CallStackFrameInfo
{
public int Depth { get; set; }
Expand Down
2 changes: 2 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public interface IVisualStudioRpc
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression);
Task<bool> DebugSetVariableValueAsync(string variableName, string value);
Task<List<CallStackFrameInfo>> DebugGetCallStackAsync();

// Diagnostics tools
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public interface IVisualStudioService
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression);
Task<bool> DebugSetVariableValueAsync(string variableName, string value);
Task<List<CallStackFrameInfo>> DebugGetCallStackAsync();

Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxResults = 100);
Expand Down
2 changes: 2 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/RpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ public Task<ReferencesResult> FindReferencesAsync(string path, int line, int col
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();
public Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression) => _vsService.DebugEvaluateExpressionAsync(expression);
public Task<bool> DebugSetVariableValueAsync(string variableName, string value) => _vsService.DebugSetVariableValueAsync(variableName, value);
public Task<List<CallStackFrameInfo>> DebugGetCallStackAsync() => _vsService.DebugGetCallStackAsync();

public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxResults = 100)
Expand Down
79 changes: 79 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1633,6 +1633,85 @@ public async Task<List<LocalVariableInfo>> DebugGetLocalsAsync()
return results;
}

public async Task<ExpressionResult> DebugEvaluateExpressionAsync(string expression)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var dte = await GetDteAsync();
var debugger = (Debugger2)dte.Debugger;

if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode)
{
return new ExpressionResult
{
Expression = expression,
IsValidValue = false,
Value = "Debugger is not in Break mode"
};
}

try
{
var expr = debugger.GetExpression(expression, false, 1000);
return new ExpressionResult
{
Expression = expression,
Value = expr.Value ?? string.Empty,
Type = expr.Type ?? string.Empty,
IsValidValue = expr.IsValidValue
};
}
catch (Exception ex)
{
VsixTelemetry.TrackException(ex);
return new ExpressionResult
{
Expression = expression,
IsValidValue = false,
Value = ex.Message
};
}
}

public async Task<bool> DebugSetVariableValueAsync(string variableName, string value)
{
using var activity = VsixTelemetry.Tracer.StartActivity("DebugSetVariableValue");

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var dte = await GetDteAsync();
var debugger = (Debugger2)dte.Debugger;

if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode)
{
return false;
}

try
{
var frame = debugger.CurrentStackFrame;
if (frame == null)
{
return false;
}

foreach (Expression local in frame.Locals)
{
if (local.Name == variableName)
{
local.Value = value;
return true;
}
}

return false;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
return false;
}
}

public async Task<List<CallStackFrameInfo>> DebugGetCallStackAsync()
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Expand Down
Loading