Skip to content

Commit 6e84867

Browse files
authored
feat(debugger): add breakpoint, locals, and callstack inspection tools (#31)
Implement 5 new MCP tools for enhanced debugger functionality: - debugger_add_breakpoint: Set breakpoint at file:line (works in any mode) - debugger_remove_breakpoint: Remove breakpoint by file:line (destructive) - debugger_list_breakpoints: List all breakpoints with metadata - debugger_get_locals: Inspect local variables in Break mode - debugger_get_callstack: Inspect call stack frames with file/line info Also improve document_read pagination: - Add offset (1-based line start) and limit (max lines) parameters - Reduce default limit from 2000 to 500 to prevent token overflow - Return numbered lines with continuation hints for large files - Enables safe reading of files > 100K characters
1 parent 62e6d18 commit 6e84867

9 files changed

Lines changed: 713 additions & 5 deletions

File tree

src/CodingWithCalvin.MCPServer.Server/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ static async Task RunServerAsync(string pipeName, string host, int port, string
9797
.WithTools<SolutionTools>()
9898
.WithTools<DocumentTools>()
9999
.WithTools<BuildTools>()
100-
.WithTools<NavigationTools>();
100+
.WithTools<NavigationTools>()
101+
.WithTools<DebuggerTools>();
101102

102103
var app = builder.Build();
103104

src/CodingWithCalvin.MCPServer.Server/RpcClient.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public Task<List<ToolInfo>> GetAvailableToolsAsync()
6565
}
6666

6767
var tools = new List<ToolInfo>();
68-
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools) };
68+
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools), typeof(Tools.DebuggerTools) };
6969

7070
foreach (var toolType in toolTypes)
7171
{
@@ -132,4 +132,20 @@ public Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int col
132132
=> Proxy.GoToDefinitionAsync(path, line, column);
133133
public Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100)
134134
=> Proxy.FindReferencesAsync(path, line, column, maxResults);
135+
136+
public Task<DebuggerStatus> GetDebuggerStatusAsync() => Proxy.GetDebuggerStatusAsync();
137+
public Task<bool> DebugLaunchAsync() => Proxy.DebugLaunchAsync();
138+
public Task<bool> DebugLaunchWithoutDebuggingAsync() => Proxy.DebugLaunchWithoutDebuggingAsync();
139+
public Task<bool> DebugContinueAsync() => Proxy.DebugContinueAsync();
140+
public Task<bool> DebugBreakAsync() => Proxy.DebugBreakAsync();
141+
public Task<bool> DebugStopAsync() => Proxy.DebugStopAsync();
142+
public Task<bool> DebugStepOverAsync() => Proxy.DebugStepOverAsync();
143+
public Task<bool> DebugStepIntoAsync() => Proxy.DebugStepIntoAsync();
144+
public Task<bool> DebugStepOutAsync() => Proxy.DebugStepOutAsync();
145+
146+
public Task<bool> DebugAddBreakpointAsync(string file, int line) => Proxy.DebugAddBreakpointAsync(file, line);
147+
public Task<bool> DebugRemoveBreakpointAsync(string file, int line) => Proxy.DebugRemoveBreakpointAsync(file, line);
148+
public Task<List<BreakpointInfo>> DebugGetBreakpointsAsync() => Proxy.DebugGetBreakpointsAsync();
149+
public Task<List<Shared.Models.LocalVariableInfo>> DebugGetLocalsAsync() => Proxy.DebugGetLocalsAsync();
150+
public Task<List<CallStackFrameInfo>> DebugGetCallStackAsync() => Proxy.DebugGetCallStackAsync();
135151
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System.ComponentModel;
2+
using System.Text.Json;
3+
using System.Threading.Tasks;
4+
using ModelContextProtocol.Server;
5+
6+
namespace CodingWithCalvin.MCPServer.Server.Tools;
7+
8+
[McpServerToolType]
9+
public class DebuggerTools
10+
{
11+
private readonly RpcClient _rpcClient;
12+
private readonly JsonSerializerOptions _jsonOptions;
13+
14+
public DebuggerTools(RpcClient rpcClient)
15+
{
16+
_rpcClient = rpcClient;
17+
_jsonOptions = new JsonSerializerOptions { WriteIndented = true };
18+
}
19+
20+
[McpServerTool(Name = "debugger_status", ReadOnly = true)]
21+
[Description("Get the current debugger state. Returns the mode (Design = not debugging, Run = executing, Break = paused at breakpoint/step), break reason, current source location (file, line, function), and debugged process name. Always succeeds regardless of debugger state.")]
22+
public async Task<string> GetDebuggerStatusAsync()
23+
{
24+
var status = await _rpcClient.GetDebuggerStatusAsync();
25+
return JsonSerializer.Serialize(status, _jsonOptions);
26+
}
27+
28+
[McpServerTool(Name = "debugger_launch", Destructive = false)]
29+
[Description("Start debugging the current startup project (equivalent to F5). A solution must be open with a valid startup project configured. Use debugger_status to check the resulting state.")]
30+
public async Task<string> DebugLaunchAsync()
31+
{
32+
var success = await _rpcClient.DebugLaunchAsync();
33+
return success ? "Debugging started" : "Failed to start debugging (is a solution open with a startup project configured?)";
34+
}
35+
36+
[McpServerTool(Name = "debugger_launch_without_debugging", Destructive = false)]
37+
[Description("Start the current startup project without the debugger attached (equivalent to Ctrl+F5). The application runs normally without breakpoints or stepping. A solution must be open with a valid startup project configured.")]
38+
public async Task<string> DebugLaunchWithoutDebuggingAsync()
39+
{
40+
var success = await _rpcClient.DebugLaunchWithoutDebuggingAsync();
41+
return success ? "Started without debugging" : "Failed to start without debugging (is a solution open with a startup project configured?)";
42+
}
43+
44+
[McpServerTool(Name = "debugger_continue", Destructive = false)]
45+
[Description("Continue execution after a break (equivalent to F5 while paused). Only works when the debugger is in Break mode (paused at a breakpoint or after stepping). Use debugger_status to verify the debugger is in Break mode first.")]
46+
public async Task<string> DebugContinueAsync()
47+
{
48+
var success = await _rpcClient.DebugContinueAsync();
49+
return success ? "Execution continued" : "Cannot continue (debugger is not in Break mode)";
50+
}
51+
52+
[McpServerTool(Name = "debugger_break", Destructive = false)]
53+
[Description("Pause execution of the running program (equivalent to Ctrl+Alt+Break). Only works when the debugger is in Run mode. Use debugger_status to verify the debugger is in Run mode first.")]
54+
public async Task<string> DebugBreakAsync()
55+
{
56+
var success = await _rpcClient.DebugBreakAsync();
57+
return success ? "Execution paused" : "Cannot break (debugger is not in Run mode)";
58+
}
59+
60+
[McpServerTool(Name = "debugger_stop", Destructive = true)]
61+
[Description("Stop the current debugging session (equivalent to Shift+F5). Terminates the debugged process. Only works when a debugging session is active (Run or Break mode).")]
62+
public async Task<string> DebugStopAsync()
63+
{
64+
var success = await _rpcClient.DebugStopAsync();
65+
return success ? "Debugging stopped" : "Cannot stop (no active debugging session)";
66+
}
67+
68+
[McpServerTool(Name = "debugger_step_over", Destructive = false)]
69+
[Description("Step over the current statement (equivalent to F10). Executes the current line and stops at the next line in the same function. Only works when the debugger is in Break mode.")]
70+
public async Task<string> DebugStepOverAsync()
71+
{
72+
var success = await _rpcClient.DebugStepOverAsync();
73+
return success ? "Stepped over" : "Cannot step over (debugger is not in Break mode)";
74+
}
75+
76+
[McpServerTool(Name = "debugger_step_into", Destructive = false)]
77+
[Description("Step into the current statement (equivalent to F11). If the current line contains a function call, steps into that function. Only works when the debugger is in Break mode.")]
78+
public async Task<string> DebugStepIntoAsync()
79+
{
80+
var success = await _rpcClient.DebugStepIntoAsync();
81+
return success ? "Stepped into" : "Cannot step into (debugger is not in Break mode)";
82+
}
83+
84+
[McpServerTool(Name = "debugger_step_out", Destructive = false)]
85+
[Description("Step out of the current function (equivalent to Shift+F11). Continues execution until the current function returns, then breaks at the caller. Only works when the debugger is in Break mode.")]
86+
public async Task<string> DebugStepOutAsync()
87+
{
88+
var success = await _rpcClient.DebugStepOutAsync();
89+
return success ? "Stepped out" : "Cannot step out (debugger is not in Break mode)";
90+
}
91+
92+
[McpServerTool(Name = "debugger_add_breakpoint", Destructive = false)]
93+
[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.")]
94+
public async Task<string> DebugAddBreakpointAsync(string path, int line)
95+
{
96+
var success = await _rpcClient.DebugAddBreakpointAsync(path, line);
97+
return success ? $"Breakpoint added at {path}:{line}" : $"Failed to add breakpoint at {path}:{line}";
98+
}
99+
100+
[McpServerTool(Name = "debugger_remove_breakpoint", Destructive = true)]
101+
[Description("Remove a breakpoint at a specific file and line number. Returns whether a breakpoint was found and removed.")]
102+
public async Task<string> DebugRemoveBreakpointAsync(string path, int line)
103+
{
104+
var success = await _rpcClient.DebugRemoveBreakpointAsync(path, line);
105+
return success ? $"Breakpoint removed from {path}:{line}" : $"No breakpoint found at {path}:{line}";
106+
}
107+
108+
[McpServerTool(Name = "debugger_list_breakpoints", ReadOnly = true)]
109+
[Description("List all breakpoints in the current solution. Returns file, line, column, function name, condition, enabled state, and hit count for each breakpoint.")]
110+
public async Task<string> DebugListBreakpointsAsync()
111+
{
112+
var breakpoints = await _rpcClient.DebugGetBreakpointsAsync();
113+
return JsonSerializer.Serialize(breakpoints, _jsonOptions);
114+
}
115+
116+
[McpServerTool(Name = "debugger_get_locals", ReadOnly = true)]
117+
[Description("Get local variables in the current stack frame. Only works when the debugger is in Break mode. Returns name, value, type, and validity for each local variable.")]
118+
public async Task<string> DebugGetLocalsAsync()
119+
{
120+
var locals = await _rpcClient.DebugGetLocalsAsync();
121+
return JsonSerializer.Serialize(locals, _jsonOptions);
122+
}
123+
124+
[McpServerTool(Name = "debugger_get_callstack", ReadOnly = true)]
125+
[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.")]
126+
public async Task<string> DebugGetCallStackAsync()
127+
{
128+
var callStack = await _rpcClient.DebugGetCallStackAsync();
129+
return JsonSerializer.Serialize(callStack, _jsonOptions);
130+
}
131+
}

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.ComponentModel;
23
using System.Text.Json;
34
using System.Threading.Tasks;
@@ -65,10 +66,42 @@ public async Task<string> CloseDocumentAsync(
6566
[McpServerTool(Name = "document_read", ReadOnly = true)]
6667
[Description("Read the contents of a document. If the document is open in VS, reads the current editor buffer (including unsaved changes); otherwise reads from disk.")]
6768
public async Task<string> ReadDocumentAsync(
68-
[Description("The full absolute path to the document. Supports forward slashes (/) or backslashes (\\).")] string path)
69+
[Description("The full absolute path to the document. Supports forward slashes (/) or backslashes (\\).")] string path,
70+
[Description("The line number to start reading from (1-based). Defaults to 1.")] int offset = 1,
71+
[Description("Maximum number of lines to read. Defaults to 500. Use smaller values for large files.")] int limit = 500)
6972
{
7073
var content = await _rpcClient.ReadDocumentAsync(path);
71-
return content ?? $"Could not read document: {path}";
74+
if (content == null)
75+
{
76+
return $"Could not read document: {path}";
77+
}
78+
79+
var lines = content.Split('\n');
80+
var totalLines = lines.Length;
81+
var startIndex = Math.Max(0, offset - 1);
82+
var count = Math.Min(limit, totalLines - startIndex);
83+
84+
if (startIndex >= totalLines)
85+
{
86+
return $"Offset {offset} is beyond end of file ({totalLines} lines)";
87+
}
88+
89+
var selectedLines = new string[count];
90+
Array.Copy(lines, startIndex, selectedLines, 0, count);
91+
92+
var result = new System.Text.StringBuilder();
93+
for (int i = 0; i < selectedLines.Length; i++)
94+
{
95+
result.AppendLine($"{startIndex + i + 1}\t{selectedLines[i].TrimEnd('\r')}");
96+
}
97+
98+
var header = $"Lines {startIndex + 1}-{startIndex + count} of {totalLines}";
99+
if (startIndex + count < totalLines)
100+
{
101+
header += $" (use offset={startIndex + count + 1} to read more)";
102+
}
103+
104+
return $"{header}\n{result}";
72105
}
73106

74107
[McpServerTool(Name = "document_write", Destructive = true)]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace CodingWithCalvin.MCPServer.Shared.Models;
2+
3+
public class DebuggerStatus
4+
{
5+
public string Mode { get; set; } = string.Empty;
6+
public bool IsDebugging { get; set; }
7+
public string LastBreakReason { get; set; } = string.Empty;
8+
public string CurrentProcessName { get; set; } = string.Empty;
9+
public string CurrentFile { get; set; } = string.Empty;
10+
public int CurrentLine { get; set; }
11+
public string CurrentFunction { get; set; } = string.Empty;
12+
}
13+
14+
public class BreakpointInfo
15+
{
16+
public string File { get; set; } = string.Empty;
17+
public int Line { get; set; }
18+
public int Column { get; set; }
19+
public string FunctionName { get; set; } = string.Empty;
20+
public string Condition { get; set; } = string.Empty;
21+
public bool Enabled { get; set; }
22+
public int CurrentHits { get; set; }
23+
}
24+
25+
public class LocalVariableInfo
26+
{
27+
public string Name { get; set; } = string.Empty;
28+
public string Value { get; set; } = string.Empty;
29+
public string Type { get; set; } = string.Empty;
30+
public bool IsValidValue { get; set; }
31+
}
32+
33+
public class CallStackFrameInfo
34+
{
35+
public int Depth { get; set; }
36+
public string FunctionName { get; set; } = string.Empty;
37+
public string FileName { get; set; } = string.Empty;
38+
public int LineNumber { get; set; }
39+
public string Module { get; set; } = string.Empty;
40+
public string Language { get; set; } = string.Empty;
41+
public string ReturnType { get; set; } = string.Empty;
42+
}

src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ public interface IVisualStudioRpc
3939
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
4040
Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column);
4141
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);
42+
43+
Task<DebuggerStatus> GetDebuggerStatusAsync();
44+
Task<bool> DebugLaunchAsync();
45+
Task<bool> DebugLaunchWithoutDebuggingAsync();
46+
Task<bool> DebugContinueAsync();
47+
Task<bool> DebugBreakAsync();
48+
Task<bool> DebugStopAsync();
49+
Task<bool> DebugStepOverAsync();
50+
Task<bool> DebugStepIntoAsync();
51+
Task<bool> DebugStepOutAsync();
52+
53+
Task<bool> DebugAddBreakpointAsync(string file, int line);
54+
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
55+
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
56+
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
57+
Task<List<CallStackFrameInfo>> DebugGetCallStackAsync();
4258
}
4359

4460
/// <summary>

src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,20 @@ public interface IVisualStudioService
3535
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
3636
Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column);
3737
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);
38+
39+
Task<DebuggerStatus> GetDebuggerStatusAsync();
40+
Task<bool> DebugLaunchAsync();
41+
Task<bool> DebugLaunchWithoutDebuggingAsync();
42+
Task<bool> DebugContinueAsync();
43+
Task<bool> DebugBreakAsync();
44+
Task<bool> DebugStopAsync();
45+
Task<bool> DebugStepOverAsync();
46+
Task<bool> DebugStepIntoAsync();
47+
Task<bool> DebugStepOutAsync();
48+
49+
Task<bool> DebugAddBreakpointAsync(string file, int line);
50+
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
51+
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
52+
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
53+
Task<List<CallStackFrameInfo>> DebugGetCallStackAsync();
3854
}

src/CodingWithCalvin.MCPServer/Services/RpcServer.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,20 @@ public Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int col
192192
=> _vsService.GoToDefinitionAsync(path, line, column);
193193
public Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100)
194194
=> _vsService.FindReferencesAsync(path, line, column, maxResults);
195+
196+
public Task<DebuggerStatus> GetDebuggerStatusAsync() => _vsService.GetDebuggerStatusAsync();
197+
public Task<bool> DebugLaunchAsync() => _vsService.DebugLaunchAsync();
198+
public Task<bool> DebugLaunchWithoutDebuggingAsync() => _vsService.DebugLaunchWithoutDebuggingAsync();
199+
public Task<bool> DebugContinueAsync() => _vsService.DebugContinueAsync();
200+
public Task<bool> DebugBreakAsync() => _vsService.DebugBreakAsync();
201+
public Task<bool> DebugStopAsync() => _vsService.DebugStopAsync();
202+
public Task<bool> DebugStepOverAsync() => _vsService.DebugStepOverAsync();
203+
public Task<bool> DebugStepIntoAsync() => _vsService.DebugStepIntoAsync();
204+
public Task<bool> DebugStepOutAsync() => _vsService.DebugStepOutAsync();
205+
206+
public Task<bool> DebugAddBreakpointAsync(string file, int line) => _vsService.DebugAddBreakpointAsync(file, line);
207+
public Task<bool> DebugRemoveBreakpointAsync(string file, int line) => _vsService.DebugRemoveBreakpointAsync(file, line);
208+
public Task<List<BreakpointInfo>> DebugGetBreakpointsAsync() => _vsService.DebugGetBreakpointsAsync();
209+
public Task<List<LocalVariableInfo>> DebugGetLocalsAsync() => _vsService.DebugGetLocalsAsync();
210+
public Task<List<CallStackFrameInfo>> DebugGetCallStackAsync() => _vsService.DebugGetCallStackAsync();
195211
}

0 commit comments

Comments
 (0)