Forge
csharp551a3611
1using System.Text.Json;
2using RoslynMcp.ServiceLayer;
3
4namespace RoslynMcp.Mcp;
5
6/// <summary>Обработчики вызовов инструментов MCP. Один вход — имя + аргументы, выход — текст результата или исключение.</summary>
7public static class ToolHandlers
8{
9 public static Task<string> HandleAsync(
10 string name,
11 IReadOnlyDictionary<string, JsonElement> args,
12 CancellationToken cancellationToken)
13 {
14 return name switch
15 {
16 "roslyn_ping" => Task.FromResult(Ping()),
17 "roslyn_get_document_symbols" => Task.FromResult(GetDocumentSymbols(args, cancellationToken)),
18 "roslyn_get_symbol_at_position" => GetSymbolAtPositionAsync(args, cancellationToken),
19 "roslyn_find_usages" => FindUsagesAsync(args, cancellationToken),
20 "roslyn_go_to_definition" => GoToDefinitionAsync(args, cancellationToken),
21 "roslyn_rename" => RenameAsync(args, cancellationToken),
22 "roslyn_get_code_actions" => GetCodeActionsAsync(args, cancellationToken),
23 "roslyn_apply_code_action" => ApplyCodeActionAsync(args, cancellationToken),
24 "roslyn_get_diagnostics" => GetDiagnosticsAsync(args, cancellationToken),
25 "roslyn_get_solution_structure" => GetSolutionStructureAsync(args, cancellationToken),
26 "roslyn_get_workspace_navigation_context" => GetWorkspaceNavigationContextAsync(args, cancellationToken),
27 "roslyn_sync_namespaces" => SyncNamespacesAsync(args, cancellationToken),
28 "roslyn_sync_dependent_upon_partials" => SyncDependentUponPartialsAsync(args, cancellationToken),
29 "roslyn_resolve_breakpoint" => ResolveBreakpointAsync(args, cancellationToken),
30 "roslyn_generate_interface_from_class" => GenerateInterfaceFromClassAsync(args, cancellationToken),
31 "roslyn_generate_base_class_from_class" => GenerateBaseClassFromClassAsync(args, cancellationToken),
32 "roslyn_generate_overrides" => GenerateOverridesAsync(args, cancellationToken),
33 "roslyn_generate_constructor_from_members" => GenerateConstructorFromMembersAsync(args, cancellationToken),
34 "roslyn_generate_equals_gethashcode" => GenerateEqualsGetHashCodeAsync(args, cancellationToken),
35 "roslyn_move_members_to_partial_file" => MoveMembersToPartialFileAsync(args, cancellationToken),
36 _ => throw new ArgumentException($"Unknown tool: {name}.", nameof(name))
37 };
38 }
39
40 private static string Ping() =>
41 $"OK {DateTime.UtcNow:O} — RoslynMcp. Tools: roslyn_get_document_symbols, roslyn_get_symbol_at_position, …";
42
43 private static string GetDocumentSymbols(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
44 {
45 if (!TryGetString(args, "file_path", out var filePath))
46 throw new ArgumentException("file_path (string) is required.");
47 return DocumentSymbols.GetDocumentSymbols(filePath!, ct);
48 }
49
50 private static Task<string> GetSymbolAtPositionAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
51 {
52 if (!TryGetString(args, "file_path", out var filePath))
53 throw new ArgumentException("file_path (string) is required.");
54 if (!TryGetInt(args, "line", out var line) || line < 1)
55 throw new ArgumentException("line (integer >= 1) is required.");
56 if (!TryGetInt(args, "column", out var column) || column < 1)
57 throw new ArgumentException("column (integer >= 1) is required.");
58 var solutionPath = args.TryGetValue("solution_or_project_path", out var solEl) && solEl.ValueKind == JsonValueKind.String
59 ? solEl.GetString()
60 : null;
61 return SymbolAtPosition.GetSymbolAtPositionAsync(filePath!, line, column, solutionPath, ct);
62 }
63
64 private static async Task<string> FindUsagesAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
65 {
66 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
67 throw new ArgumentException("solution_or_project_path (string) is required.");
68 if (!TryGetString(args, "file_path", out var filePath))
69 throw new ArgumentException("file_path (string) is required.");
70 if (!TryGetInt(args, "line", out var line) || line < 1)
71 throw new ArgumentException("line (integer >= 1) is required.");
72 if (!TryGetInt(args, "column", out var column) || column < 1)
73 throw new ArgumentException("column (integer >= 1) is required.");
74 return await FindUsages.FindUsagesAsync(solutionPath!, filePath!, line, column, ct).ConfigureAwait(false);
75 }
76
77 private static Task<string> GoToDefinitionAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
78 {
79 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
80 throw new ArgumentException("solution_or_project_path (string) is required.");
81 if (!TryGetString(args, "file_path", out var filePath))
82 throw new ArgumentException("file_path (string) is required.");
83 if (!TryGetInt(args, "line", out var line) || line < 1)
84 throw new ArgumentException("line (integer >= 1) is required.");
85 if (!TryGetInt(args, "column", out var column) || column < 1)
86 throw new ArgumentException("column (integer >= 1) is required.");
87 return GoToDefinition.GoToDefinitionAsync(solutionPath!, filePath!, line, column, ct);
88 }
89
90 private static async Task<string> RenameAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
91 {
92 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
93 throw new ArgumentException("solution_or_project_path (string) is required.");
94 if (!TryGetString(args, "file_path", out var filePath))
95 throw new ArgumentException("file_path (string) is required.");
96 if (!TryGetInt(args, "line", out var line) || line < 1)
97 throw new ArgumentException("line (integer >= 1) is required.");
98 if (!TryGetInt(args, "column", out var column) || column < 1)
99 throw new ArgumentException("column (integer >= 1) is required.");
100 if (!TryGetString(args, "new_name", out var newName))
101 throw new ArgumentException("new_name (string) is required.");
102 var apply = args.TryGetValue("apply", out var applyEl) && applyEl.ValueKind == JsonValueKind.True;
103 var renameInComments = args.TryGetValue("rename_in_comments", out var ricEl) && ricEl.ValueKind == JsonValueKind.True;
104 var renameInStrings = args.TryGetValue("rename_in_strings", out var risEl) && risEl.ValueKind == JsonValueKind.True;
105 var renameOverloads = args.TryGetValue("rename_overloads", out var roEl) && roEl.ValueKind == JsonValueKind.True;
106 var renameFile = args.TryGetValue("rename_file", out var rfEl) && rfEl.ValueKind == JsonValueKind.True;
107 var renamePartialTypeFiles = args.TryGetValue("rename_partial_type_files", out var rptfEl) && rptfEl.ValueKind == JsonValueKind.True;
108 return await RenameSymbol.RenameAsync(solutionPath!, filePath!, line, column, newName!, apply, renameInComments, renameInStrings, renameOverloads, renameFile, renamePartialTypeFiles, ct).ConfigureAwait(false);
109 }
110
111 private static Task<string> GetCodeActionsAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
112 {
113 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
114 throw new ArgumentException("solution_or_project_path (string) is required.");
115 if (!TryGetString(args, "file_path", out var filePath))
116 throw new ArgumentException("file_path (string) is required.");
117 if (!TryGetInt(args, "line", out var line) || line < 1)
118 throw new ArgumentException("line (integer >= 1) is required.");
119 if (!TryGetInt(args, "column", out var column) || column < 1)
120 throw new ArgumentException("column (integer >= 1) is required.");
121 int? endLine = TryGetInt(args, "end_line", out var el) && el >= 1 ? el : null;
122 int? endColumn = TryGetInt(args, "end_column", out var ec) && ec >= 1 ? ec : null;
123 return CodeActions.GetCodeActionsAsync(solutionPath!, filePath!, line, column, endLine, endColumn, ct);
124 }
125
126 private static Task<string> ApplyCodeActionAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
127 {
128 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
129 throw new ArgumentException("solution_or_project_path (string) is required.");
130 if (!TryGetString(args, "file_path", out var filePath))
131 throw new ArgumentException("file_path (string) is required.");
132 if (!TryGetInt(args, "line", out var line) || line < 1)
133 throw new ArgumentException("line (integer >= 1) is required.");
134 if (!TryGetInt(args, "column", out var column) || column < 1)
135 throw new ArgumentException("column (integer >= 1) is required.");
136 if (!TryGetInt(args, "action_index", out var actionIndex) || actionIndex < 0)
137 throw new ArgumentException("action_index (integer >= 0) is required.");
138 int? endLineApply = TryGetInt(args, "end_line", out var ela) && ela >= 1 ? ela : null;
139 int? endColumnApply = TryGetInt(args, "end_column", out var eca) && eca >= 1 ? eca : null;
140 TryGetString(args, "fix_all_scope", out var fixAllScope);
141 TryGetString(args, "constant_name", out var constantName);
142 var actionOptions = TryGetActionOptions(args);
143 return CodeActions.ApplyCodeActionAsync(solutionPath!, filePath!, line, column, actionIndex, endLineApply, endColumnApply, fixAllScope, constantName, actionOptions, ct);
144 }
145
146 /// <summary>Парсит action_options из args: JSON object → Dictionary (string, int, bool, string[]).</summary>
147 private static Dictionary<string, object?>? TryGetActionOptions(IReadOnlyDictionary<string, JsonElement> args)
148 {
149 if (!args.TryGetValue("action_options", out var el) || el.ValueKind != JsonValueKind.Object)
150 return null;
151 var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
152 foreach (var prop in el.EnumerateObject())
153 {
154 object? val = prop.Value.ValueKind switch
155 {
156 JsonValueKind.String => prop.Value.GetString(),
157 JsonValueKind.Number => prop.Value.TryGetInt32(out var i32) ? i32 : prop.Value.TryGetInt64(out var i64) ? i64 : (object?)prop.Value.GetDouble(),
158 JsonValueKind.True => true,
159 JsonValueKind.False => false,
160 JsonValueKind.Array => GetStringArray(prop.Value),
161 _ => null
162 };
163 if (val != null || prop.Value.ValueKind == JsonValueKind.Null || prop.Value.ValueKind == JsonValueKind.String)
164 dict[prop.Name] = val;
165 }
166 return dict.Count == 0 ? null : dict;
167 }
168
169 private static string[]? GetStringArray(JsonElement arr)
170 {
171 var list = new List<string>();
172 foreach (var item in arr.EnumerateArray())
173 {
174 if (item.ValueKind == JsonValueKind.String && item.GetString() is { } s)
175 list.Add(s);
176 }
177 return list.Count == 0 ? null : list.ToArray();
178 }
179
180 private static Task<string> GetDiagnosticsAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
181 {
182 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
183 throw new ArgumentException("solution_or_project_path (string) is required.");
184 TryGetString(args, "file_path", out var filePath);
185 return GetDiagnostics.GetDiagnosticsAsync(solutionPath!, filePath, ct);
186 }
187
188 private static Task<string> GetSolutionStructureAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
189 {
190 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
191 throw new ArgumentException("solution_or_project_path (string) is required.");
192 return GetSolutionStructure.GetStructureAsync(solutionPath!, ct);
193 }
194
195 private static Task<string> GetWorkspaceNavigationContextAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
196 {
197 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
198 throw new ArgumentException("solution_or_project_path (string) is required.");
199 if (!TryGetString(args, "file_path", out var filePath))
200 throw new ArgumentException("file_path (string) is required.");
201 if (!TryGetString(args, "mode", out var mode) || string.IsNullOrWhiteSpace(mode))
202 throw new ArgumentException("mode (string: related | subgraph) is required.");
203 int? line = TryGetInt(args, "line", out var l) && l >= 1 ? l : null;
204 int? column = TryGetInt(args, "column", out var c) && c >= 1 ? c : null;
205 var maxRelated = TryGetInt(args, "max_related", out var mr) && mr >= 1 ? mr : GetWorkspaceNavigationContext.DefaultMaxRelated;
206 var maxNodes = TryGetInt(args, "max_nodes", out var mn) && mn >= 1 ? mn : GetWorkspaceNavigationContext.DefaultMaxNodes;
207 var maxEdges = TryGetInt(args, "max_edges", out var me) && me >= 1 ? me : GetWorkspaceNavigationContext.DefaultMaxEdges;
208 IReadOnlyList<string>? includeKinds = TryGetOptionalStringList(args, "include_kinds");
209 IReadOnlyList<string>? excludeKinds = TryGetOptionalStringList(args, "exclude_kinds");
210 TryGetString(args, "preset", out var preset);
211 return GetWorkspaceNavigationContext.GetAsync(
212 solutionPath!,
213 filePath!,
214 mode!,
215 line,
216 column,
217 maxRelated,
218 maxNodes,
219 maxEdges,
220 includeKinds,
221 excludeKinds,
222 string.IsNullOrWhiteSpace(preset) ? null : preset,
223 ct);
224 }
225
226 private static string[]? TryGetOptionalStringList(IReadOnlyDictionary<string, JsonElement> args, string key)
227 {
228 if (!args.TryGetValue(key, out var el) || el.ValueKind != JsonValueKind.Array)
229 return null;
230 if (GetStringArray(el) is not string[] arr || arr.Length == 0)
231 return null;
232 return arr;
233 }
234
235 private static Task<string> SyncNamespacesAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
236 {
237 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
238 throw new ArgumentException("solution_or_project_path (string) is required.");
239 TryGetString(args, "project_path", out var projectPath);
240 var dryRun = args.TryGetValue("dry_run", out var dryRunEl) && dryRunEl.ValueKind == JsonValueKind.True;
241 return SyncNamespaces.SyncAsync(solutionPath!, dryRun, projectPath, cancellationToken: ct);
242 }
243
244 private static Task<string> SyncDependentUponPartialsAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
245 {
246 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
247 throw new ArgumentException("solution_or_project_path (string) is required.");
248 TryGetString(args, "project_path", out var projectPath);
249 var dryRun = args.TryGetValue("dry_run", out var dryRunEl) && dryRunEl.ValueKind == JsonValueKind.True;
250 return SyncDependentUponPartials.SyncAsync(solutionPath!, projectPath, dryRun, cancellationToken: ct);
251 }
252
253 private static Task<string> ResolveBreakpointAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
254 {
255 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
256 throw new ArgumentException("solution_or_project_path (string) is required.");
257 if (!TryGetString(args, "file_path", out var filePath))
258 throw new ArgumentException("file_path (string) is required.");
259 if (!TryGetString(args, "symbol_name", out var symbolName))
260 throw new ArgumentException("symbol_name (string) is required.");
261 return ResolveBreakpoint.ResolveAsync(solutionPath!, filePath!, symbolName!, ct);
262 }
263
264 private static Task<string> GenerateInterfaceFromClassAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
265 {
266 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
267 throw new ArgumentException("solution_or_project_path (string) is required.");
268 if (!TryGetString(args, "file_path", out var filePath))
269 throw new ArgumentException("file_path (string) is required.");
270 if (!TryGetInt(args, "line", out var line) || line < 1)
271 throw new ArgumentException("line (integer >= 1) is required.");
272 if (!TryGetInt(args, "column", out var column) || column < 1)
273 throw new ArgumentException("column (integer >= 1) is required.");
274 TryGetString(args, "interface_name", out var interfaceName);
275 TryGetString(args, "output_file_path", out var outputFilePath);
276 IReadOnlyList<string>? memberNames = null;
277 if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0)
278 memberNames = arr;
279 return GenerateInterface.GenerateInterfaceFromClassAsync(solutionPath!, filePath!, line, column, interfaceName, outputFilePath, memberNames, ct);
280 }
281
282 private static Task<string> GenerateBaseClassFromClassAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
283 {
284 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
285 throw new ArgumentException("solution_or_project_path (string) is required.");
286 if (!TryGetString(args, "file_path", out var filePath))
287 throw new ArgumentException("file_path (string) is required.");
288 if (!TryGetInt(args, "line", out var line) || line < 1)
289 throw new ArgumentException("line (integer >= 1) is required.");
290 if (!TryGetInt(args, "column", out var column) || column < 1)
291 throw new ArgumentException("column (integer >= 1) is required.");
292 TryGetString(args, "base_class_name", out var baseClassName);
293 TryGetString(args, "output_file_path", out var outputFilePath);
294 IReadOnlyList<string>? memberNames = null;
295 if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0)
296 memberNames = arr;
297 return GenerateBaseClass.GenerateBaseClassFromClassAsync(solutionPath!, filePath!, line, column, baseClassName, outputFilePath, memberNames, ct);
298 }
299
300 private static Task<string> GenerateOverridesAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
301 {
302 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
303 throw new ArgumentException("solution_or_project_path (string) is required.");
304 if (!TryGetString(args, "file_path", out var filePath))
305 throw new ArgumentException("file_path (string) is required.");
306 if (!TryGetInt(args, "line", out var line) || line < 1)
307 throw new ArgumentException("line (integer >= 1) is required.");
308 if (!TryGetInt(args, "column", out var column) || column < 1)
309 throw new ArgumentException("column (integer >= 1) is required.");
310 IReadOnlyList<string>? memberNames = null;
311 if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0)
312 memberNames = arr;
313 var insertIntoFile = args.TryGetValue("insert_into_file", out var insEl) && insEl.ValueKind == JsonValueKind.True;
314 return GenerateOverrides.GenerateOverridesAsync(solutionPath!, filePath!, line, column, memberNames, insertIntoFile, ct);
315 }
316
317 private static Task<string> GenerateConstructorFromMembersAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
318 {
319 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
320 throw new ArgumentException("solution_or_project_path (string) is required.");
321 if (!TryGetString(args, "file_path", out var filePath))
322 throw new ArgumentException("file_path (string) is required.");
323 if (!TryGetInt(args, "line", out var line) || line < 1)
324 throw new ArgumentException("line (integer >= 1) is required.");
325 if (!TryGetInt(args, "column", out var column) || column < 1)
326 throw new ArgumentException("column (integer >= 1) is required.");
327 IReadOnlyList<string>? memberNames = null;
328 if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0)
329 memberNames = arr;
330 var insertIntoFile = args.TryGetValue("insert_into_file", out var insEl) && insEl.ValueKind == JsonValueKind.True;
331 return GenerateConstructor.GenerateConstructorFromMembersAsync(solutionPath!, filePath!, line, column, memberNames, insertIntoFile, ct);
332 }
333
334 private static Task<string> GenerateEqualsGetHashCodeAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
335 {
336 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
337 throw new ArgumentException("solution_or_project_path (string) is required.");
338 if (!TryGetString(args, "file_path", out var filePath))
339 throw new ArgumentException("file_path (string) is required.");
340 if (!TryGetInt(args, "line", out var line) || line < 1)
341 throw new ArgumentException("line (integer >= 1) is required.");
342 if (!TryGetInt(args, "column", out var column) || column < 1)
343 throw new ArgumentException("column (integer >= 1) is required.");
344 IReadOnlyList<string>? memberNames = null;
345 if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0)
346 memberNames = arr;
347 var insertIntoFile = args.TryGetValue("insert_into_file", out var insEl) && insEl.ValueKind == JsonValueKind.True;
348 return GenerateEqualsGetHashCode.GenerateEqualsGetHashCodeAsync(solutionPath!, filePath!, line, column, memberNames, insertIntoFile, ct);
349 }
350
351 private static Task<string> MoveMembersToPartialFileAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct)
352 {
353 if (!TryGetString(args, "solution_or_project_path", out var solutionPath))
354 throw new ArgumentException("solution_or_project_path (string) is required.");
355 if (!TryGetString(args, "file_path", out var filePath))
356 throw new ArgumentException("file_path (string) is required.");
357 if (!TryGetInt(args, "line", out var line) || line < 1)
358 throw new ArgumentException("line (integer >= 1) is required.");
359 if (!TryGetInt(args, "column", out var column) || column < 1)
360 throw new ArgumentException("column (integer >= 1) is required.");
361 if (!args.TryGetValue("member_names", out var mnEl) || mnEl.ValueKind != JsonValueKind.Array || GetStringArray(mnEl) is not string[] memberArr || memberArr.Length == 0)
362 throw new ArgumentException("member_names (non-empty array of strings) is required.");
363 if (!TryGetString(args, "output_file_path", out var outputFilePath))
364 throw new ArgumentException("output_file_path (string) is required.");
365 var apply = args.TryGetValue("apply", out var applyEl) && applyEl.ValueKind == JsonValueKind.True;
366 var addDependentUpon = !(args.TryGetValue("add_dependent_upon", out var adEl) && adEl.ValueKind == JsonValueKind.False);
367 return MoveMembersToPartialFile.MoveAsync(solutionPath!, filePath!, line, column, memberArr, outputFilePath!, apply, addDependentUpon, ct);
368 }
369
370 private static bool TryGetString(IReadOnlyDictionary<string, JsonElement> args, string key, out string? value)
371 {
372 value = null;
373 if (!args.TryGetValue(key, out var el) || el.ValueKind != JsonValueKind.String)
374 return false;
375 value = el.GetString();
376 return true;
377 }
378
379 private static bool TryGetInt(IReadOnlyDictionary<string, JsonElement> args, string key, out int value)
380 {
381 value = 0;
382 if (!args.TryGetValue(key, out var el))
383 return false;
384 if (el.ValueKind == JsonValueKind.Number && el.TryGetInt32(out var n))
385 {
386 value = n;
387 return true;
388 }
389 return false;
390 }
391}
392
View only · write via MCP/CIDE