diff --git a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs index 9bfd881..6ff0f39 100644 --- a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs +++ b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs @@ -153,6 +153,8 @@ public Task FindReferencesAsync(string path, int line, int col public Task DebugRemoveBreakpointAsync(string file, int line) => Proxy.DebugRemoveBreakpointAsync(file, line); public Task> DebugGetBreakpointsAsync() => Proxy.DebugGetBreakpointsAsync(); public Task> DebugGetLocalsAsync() => Proxy.DebugGetLocalsAsync(); + public Task DebugEvaluateExpressionAsync(string expression) => Proxy.DebugEvaluateExpressionAsync(expression); + public Task DebugSetVariableValueAsync(string variableName, string value) => Proxy.DebugSetVariableValueAsync(variableName, value); public Task> DebugGetCallStackAsync() => Proxy.DebugGetCallStackAsync(); public Task GetErrorListAsync(string? severity = null, int maxResults = 100) diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs index e938983..cc7ae9f 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs @@ -143,6 +143,27 @@ public async Task 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 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 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 DebugGetCallStackAsync() diff --git a/src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs b/src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs index 0f6dd82..3ee98b5 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs @@ -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; } diff --git a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs index 875af1a..6ffdaeb 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs @@ -58,6 +58,8 @@ public interface IVisualStudioRpc Task DebugRemoveBreakpointAsync(string file, int line); Task> DebugGetBreakpointsAsync(); Task> DebugGetLocalsAsync(); + Task DebugEvaluateExpressionAsync(string expression); + Task DebugSetVariableValueAsync(string variableName, string value); Task> DebugGetCallStackAsync(); // Diagnostics tools diff --git a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs index 8a4e81d..2a925c4 100644 --- a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs @@ -54,6 +54,8 @@ public interface IVisualStudioService Task DebugRemoveBreakpointAsync(string file, int line); Task> DebugGetBreakpointsAsync(); Task> DebugGetLocalsAsync(); + Task DebugEvaluateExpressionAsync(string expression); + Task DebugSetVariableValueAsync(string variableName, string value); Task> DebugGetCallStackAsync(); Task GetErrorListAsync(string? severity = null, int maxResults = 100); diff --git a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs index d8fb6e5..4ea4b5d 100644 --- a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs +++ b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs @@ -211,6 +211,8 @@ public Task FindReferencesAsync(string path, int line, int col public Task DebugRemoveBreakpointAsync(string file, int line) => _vsService.DebugRemoveBreakpointAsync(file, line); public Task> DebugGetBreakpointsAsync() => _vsService.DebugGetBreakpointsAsync(); public Task> DebugGetLocalsAsync() => _vsService.DebugGetLocalsAsync(); + public Task DebugEvaluateExpressionAsync(string expression) => _vsService.DebugEvaluateExpressionAsync(expression); + public Task DebugSetVariableValueAsync(string variableName, string value) => _vsService.DebugSetVariableValueAsync(variableName, value); public Task> DebugGetCallStackAsync() => _vsService.DebugGetCallStackAsync(); public Task GetErrorListAsync(string? severity = null, int maxResults = 100) diff --git a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs index ceaddf1..5304b18 100644 --- a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs @@ -1633,6 +1633,85 @@ public async Task> DebugGetLocalsAsync() return results; } + public async Task 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 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> DebugGetCallStackAsync() { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();