Forge
csharp5e452b8e
1using System.Text;
2using System.Text.Json;
3using DotnetDebug.Core;
4
5namespace DotnetDebugMcp;
6
7internal static class DebugLaunchToolHandlers
8{
9 internal static async Task<string> HandleDebugLaunch(IReadOnlyDictionary<string, JsonElement> args)
10 {
11 if (!McpArgumentHelpers.TryGetString(args, "workspace_path", out var workspacePath) || string.IsNullOrWhiteSpace(workspacePath))
12 throw new ArgumentException("workspace_path is required.");
13 if (!McpArgumentHelpers.TryGetString(args, "target_path", out var targetPath) || string.IsNullOrWhiteSpace(targetPath))
14 throw new ArgumentException("target_path is required.");
15
16 var netcoredbgPath = Environment.GetEnvironmentVariable("NETCOREDBG_PATH")?.Trim();
17 if (McpArgumentHelpers.TryGetString(args, "netcoredbg_path", out var customPath) && !string.IsNullOrWhiteSpace(customPath))
18 netcoredbgPath = customPath;
19 if (string.IsNullOrWhiteSpace(netcoredbgPath))
20 netcoredbgPath = "netcoredbg";
21
22 var workspaceRoot = Path.GetFullPath(workspacePath!.Trim());
23 if (File.Exists(workspaceRoot))
24 workspaceRoot = Path.GetDirectoryName(workspaceRoot) ?? workspaceRoot;
25 var programPath = Path.IsPathRooted(targetPath!.Trim())
26 ? Path.GetFullPath(targetPath)
27 : Path.GetFullPath(Path.Combine(workspaceRoot, targetPath.Trim()));
28 if (!File.Exists(programPath))
29 throw new ArgumentException($"Target not found: {programPath}");
30
31 var breakpoints = BreakpointsStorage.GetBreakpoints(workspacePath!, targetPath!).ToList();
32 var byFile = breakpoints
33 .GroupBy(b => DapHelpers.ResolveBreakpointFilePath(workspaceRoot, b.File))
34 .ToDictionary(g => g.Key, g => g.Select(b => (b.Line, b.Condition)).ToList());
35
36 IReadOnlyList<string>? programArgs = null;
37 if (args.TryGetValue("program_args", out var argsEl) && argsEl.ValueKind == JsonValueKind.Array)
38 {
39 var list = new List<string>();
40 foreach (var e in argsEl.EnumerateArray())
41 if (e.ValueKind == JsonValueKind.String && e.GetString() is { } s)
42 list.Add(s);
43 if (list.Count > 0)
44 programArgs = list;
45 }
46
47 var client = await DapClient.StartAsync(netcoredbgPath, cancellationToken: default, clientId: "dotnet-debug-mcp", clientName: "DotnetDebugMcp").ConfigureAwait(false);
48 client.OnConnectionLost = () =>
49 {
50 if (DebugSession.CurrentClient == client)
51 {
52 DebugSession.CurrentClient = null;
53 DebugSession.LastStoppedThreadId = 0;
54 DebugSession.LastExceptionText = null;
55 }
56 };
57 DebugSession.PrepareStoppedWait();
58 var stoppedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
59 client.OnEvent = (eventName, body) =>
60 {
61 if (eventName == "stopped" && body.TryGetProperty("threadId", out var tid))
62 {
63 var reason = body.TryGetProperty("reason", out var r) ? r.GetString() : null;
64 var exceptionText = (reason == "exception" && body.TryGetProperty("text", out var txt)) ? txt.GetString() : null;
65 DebugSession.OnStopped(tid.GetInt32(), exceptionText);
66 stoppedTcs.TrySetResult();
67 }
68 else if (eventName == "continued")
69 DebugSession.OnContinued();
70 };
71 var exceptionBpsOk = false;
72 try
73 {
74 await client.LaunchAsync(programPath, Path.GetDirectoryName(programPath), programArgs).ConfigureAwait(false);
75 foreach (var (file, list) in byFile)
76 {
77 if (list.Count > 0)
78 await client.SetBreakpointsAsync(file, list).ConfigureAwait(false);
79 }
80 exceptionBpsOk = await DapHelpers.TrySetUnhandledExceptionBreakpointsAsync(client).ConfigureAwait(false);
81 await client.ConfigurationDoneAsync().ConfigureAwait(false);
82 await stoppedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
83 }
84 catch (TimeoutException)
85 {
86 stoppedTcs.TrySetResult();
87 }
88 catch
89 {
90 await client.DisposeAsync().ConfigureAwait(false);
91 throw;
92 }
93
94 DebugSession.CurrentClient = client;
95
96 var stopped = DebugSession.LastStoppedThreadId != 0;
97 if (!stopped)
98 {
99 try
100 {
101 var threadsBody = await client.ThreadsAsync().ConfigureAwait(false);
102 if (threadsBody != null && threadsBody.Value.TryGetProperty("threads", out var threadsArr))
103 {
104 foreach (var t in threadsArr.EnumerateArray())
105 {
106 if (t.TryGetProperty("id", out var idEl) && idEl.TryGetInt32(out var tid))
107 {
108 DebugSession.LastStoppedThreadId = tid;
109 stopped = true;
110 break;
111 }
112 }
113 }
114 }
115 catch { /* ignore */ }
116 }
117
118 var sb = new StringBuilder();
119 sb.AppendLine("# Debug session started");
120 sb.AppendLine($"# Program: {programPath}");
121 sb.AppendLine($"# Breakpoints: {breakpoints.Count} applied");
122 sb.AppendLine(exceptionBpsOk
123 ? "# Exception breakpoints: unhandled (stop on throw)"
124 : "# Exception breakpoints: skipped (adapter rejected setExceptionBreakpoints; some netcoredbg builds return 0x80070057)");
125 if (!stopped)
126 sb.AppendLine("# (Wait for breakpoint timed out — call debug_continue then use stack_trace/step_* after it stops, or check that the target hits a breakpoint. First thread id used as fallback if available.)");
127 else if (DebugSession.LastExceptionText is { } exMsg)
128 sb.AppendLine($"# Stopped on exception: {exMsg}");
129 sb.AppendLine("# Use debug_continue or debug_step_over to control execution.");
130 return sb.ToString();
131 }
132
133 internal static async Task<string> HandleDebugAttach(IReadOnlyDictionary<string, JsonElement> args)
134 {
135 if (!McpArgumentHelpers.TryGetString(args, "workspace_path", out var workspacePath) || string.IsNullOrWhiteSpace(workspacePath))
136 throw new ArgumentException("workspace_path is required.");
137 if (!args.TryGetValue("process_id", out var pidEl) || !pidEl.TryGetInt32(out var processId) || processId <= 0)
138 throw new ArgumentException("process_id (positive integer) is required.");
139
140 var netcoredbgPath = Environment.GetEnvironmentVariable("NETCOREDBG_PATH")?.Trim();
141 if (McpArgumentHelpers.TryGetString(args, "netcoredbg_path", out var customPath) && !string.IsNullOrWhiteSpace(customPath))
142 netcoredbgPath = customPath;
143 if (string.IsNullOrWhiteSpace(netcoredbgPath))
144 netcoredbgPath = "netcoredbg";
145
146 var workspaceRoot = Path.GetFullPath(workspacePath!.Trim());
147 if (File.Exists(workspaceRoot))
148 workspaceRoot = Path.GetDirectoryName(workspaceRoot) ?? workspaceRoot;
149
150 var breakpoints = new List<BreakpointsStorage.BreakpointEntry>();
151 var byFile = new Dictionary<string, List<(int Line, string? Condition)>>();
152 if (McpArgumentHelpers.TryGetString(args, "target_path", out var targetPath) && !string.IsNullOrWhiteSpace(targetPath))
153 {
154 breakpoints = BreakpointsStorage.GetBreakpoints(workspacePath, targetPath).ToList();
155 byFile = breakpoints
156 .GroupBy(b => DapHelpers.ResolveBreakpointFilePath(workspaceRoot, b.File))
157 .ToDictionary(g => g.Key, g => g.Select(b => (b.Line, b.Condition)).ToList());
158 }
159
160 var client = await DapClient.StartAsync(netcoredbgPath, cancellationToken: default, clientId: "dotnet-debug-mcp", clientName: "DotnetDebugMcp").ConfigureAwait(false);
161 client.OnConnectionLost = () =>
162 {
163 if (DebugSession.CurrentClient == client)
164 {
165 DebugSession.CurrentClient = null;
166 DebugSession.LastStoppedThreadId = 0;
167 DebugSession.LastExceptionText = null;
168 }
169 };
170 DebugSession.PrepareStoppedWait();
171 var stoppedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
172 client.OnEvent = (eventName, body) =>
173 {
174 if (eventName == "stopped" && body.TryGetProperty("threadId", out var tid))
175 {
176 var reason = body.TryGetProperty("reason", out var r) ? r.GetString() : null;
177 var exceptionText = (reason == "exception" && body.TryGetProperty("text", out var txt)) ? txt.GetString() : null;
178 DebugSession.OnStopped(tid.GetInt32(), exceptionText);
179 stoppedTcs.TrySetResult();
180 }
181 else if (eventName == "continued")
182 DebugSession.OnContinued();
183 };
184 var attachExceptionBpsOk = false;
185 try
186 {
187 await client.AttachAsync(processId).ConfigureAwait(false);
188 foreach (var (file, list) in byFile)
189 {
190 if (list.Count > 0)
191 await client.SetBreakpointsAsync(file, list).ConfigureAwait(false);
192 }
193 attachExceptionBpsOk = await DapHelpers.TrySetUnhandledExceptionBreakpointsAsync(client).ConfigureAwait(false);
194 await client.ConfigurationDoneAsync().ConfigureAwait(false);
195 await stoppedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
196 }
197 catch (TimeoutException)
198 {
199 stoppedTcs.TrySetResult();
200 }
201 catch
202 {
203 await client.DisposeAsync().ConfigureAwait(false);
204 throw;
205 }
206
207 DebugSession.CurrentClient = client;
208
209 var stopped = DebugSession.LastStoppedThreadId != 0;
210 if (!stopped)
211 {
212 try
213 {
214 var threadsBody = await client.ThreadsAsync().ConfigureAwait(false);
215 if (threadsBody != null && threadsBody.Value.TryGetProperty("threads", out var threadsArr))
216 {
217 foreach (var t in threadsArr.EnumerateArray())
218 {
219 if (t.TryGetProperty("id", out var idEl) && idEl.TryGetInt32(out var tid))
220 {
221 DebugSession.LastStoppedThreadId = tid;
222 stopped = true;
223 break;
224 }
225 }
226 }
227 }
228 catch { /* ignore */ }
229 }
230
231 var sb = new StringBuilder();
232 sb.AppendLine("# Debug session started (attach)");
233 sb.AppendLine($"# Process ID: {processId}");
234 sb.AppendLine($"# Breakpoints: {breakpoints.Count} applied");
235 sb.AppendLine(attachExceptionBpsOk
236 ? "# Exception breakpoints: unhandled (stop on throw)"
237 : "# Exception breakpoints: skipped (adapter rejected setExceptionBreakpoints; some netcoredbg builds return 0x80070057)");
238 if (!stopped)
239 sb.AppendLine("# (Wait for breakpoint timed out — call debug_continue then use stack_trace/step_* after it stops.)");
240 else if (DebugSession.LastExceptionText is { } exMsg)
241 sb.AppendLine($"# Stopped on exception: {exMsg}");
242 sb.AppendLine("# Use debug_continue or debug_step_over to control execution.");
243 return sb.ToString();
244 }
245}
246
View only · write via MCP/CIDE