Forge
csharpdeeb25a2
1using System.Text.Json;
2using System.Linq;
3using ModelContextProtocol.Protocol;
4
5namespace CascadeIDE.Services;
6
7/// <summary>
8/// Single source of truth for MCP tool definitions (name/description/schema).
9/// Tool execution is handled separately by <see cref="IdeMcpServer"/> and ultimately <see cref="IIdeMcpActions.ExecuteCommandAsync"/>.
10/// </summary>
11internal static class IdeMcpToolCatalog
12{
13 private static JsonElement Schema(object schema) => JsonSerializer.SerializeToElement(schema);
14
15 public static List<Tool> BuildTools(bool includeDebugTools)
16 {
17 // Full list of "rich" MCP tools (typed schemas).
18 // Generated proxies are added below for all IdeCommands not covered explicitly.
19 var toolsList = IdeMcpToolCatalogFull.BuildRichTools(includeDebugTools);
20
21 if (includeDebugTools)
22 {
23 // Debug-only tools are already included in BuildRichTools when includeDebugTools = true.
24 }
25
26 // Auto-generated proxy tools for all IdeCommands (except those already defined above).
27 var existingNames = new HashSet<string>(toolsList.Select(t => t.Name), StringComparer.Ordinal);
28 toolsList.AddRange(BuildGeneratedProxyTools(existingNames));
29
30 // Make descriptions self-sufficient (agent-friendly): always include args list from schema.
31 foreach (var t in toolsList)
32 {
33 EnsurePurposePrefix(t);
34 EnsureArgsBlockInDescription(t);
35 EnsureReturnsAndExampleBlocks(t);
36 }
37
38 return toolsList;
39 }
40
41 public static IEnumerable<Tool> BuildGeneratedProxyTools(ISet<string> existingToolNames)
42 {
43 var type = typeof(IdeCommands);
44 foreach (var f in type.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
45 {
46 if (f.FieldType != typeof(string))
47 continue;
48 var commandId = (string?)f.GetValue(null);
49 if (string.IsNullOrWhiteSpace(commandId))
50 continue;
51
52 if (!IdeMcpToolNaming.IsSupportedAutoProxyToolName(commandId))
53 continue;
54
55 var toolName = IdeMcpToolNaming.ToToolName(commandId);
56 if (existingToolNames.Contains(toolName))
57 continue;
58
59 yield return new Tool
60 {
61 Name = toolName,
62 Description = BuildProxyToolDescription(commandId),
63 InputSchema = BuildProxyToolInputSchema(commandId)
64 };
65 }
66 }
67
68 private static JsonElement BuildProxyToolInputSchema(string commandId)
69 {
70 if (!IdeCommandsArgs.TryGetArgs(commandId, out var args) || args.Length == 0)
71 {
72 // Keep loose schema for unknown/undocumented args.
73 return Schema(new
74 {
75 type = "object",
76 properties = new { },
77 additionalProperties = true,
78 required = Array.Empty<string>()
79 });
80 }
81
82 // Build JSON schema with known properties but still allow extra keys (future-proof).
83 var props = new Dictionary<string, object>(StringComparer.Ordinal);
84 var required = new List<string>();
85
86 foreach (var a in args)
87 {
88 object schema;
89 if (a.IsArray)
90 {
91 schema = new
92 {
93 type = "array",
94 items = new { type = a.ItemJsonType ?? "string" }
95 };
96 }
97 else
98 {
99 schema = new { type = a.JsonType };
100 }
101
102 props[a.Name] = schema;
103 if (a.Required)
104 required.Add(a.Name);
105 }
106
107 return Schema(new
108 {
109 type = "object",
110 properties = props,
111 additionalProperties = true,
112 required = required.ToArray()
113 });
114 }
115
116 private static string BuildProxyToolDescription(string commandId)
117 {
118 var summaryLine = IdeCommandsDoc.TryGetSummary(commandId, out var s) && !string.IsNullOrWhiteSpace(s)
119 ? $"Назначение: {s}\n\n"
120 : "";
121
122 var argsBlock = BuildProxyToolArgsBlock(commandId);
123
124 return
125 summaryLine +
126 "Прокси-обёртка над ide_execute_command.\n\n" +
127 "Эквивалент вызову:\n" +
128 $" ide_execute_command {{ command_id: \"{commandId}\", args: <твои поля> }}\n\n" +
129 argsBlock +
130 "Если не уверен(а) в полях — см. docs/MCP-PROTOCOL.md или используй ide_execute_command.";
131 }
132
133 private static string BuildProxyToolArgsBlock(string commandId)
134 {
135 if (!IdeCommandsArgs.TryGetArgs(commandId, out var args) || args.Length == 0)
136 {
137 return
138 $"Аргументы: нет (или не задокументированы). Передавай плоский JSON object; поля будут прокинуты в args команды \"{commandId}\".\n\n";
139 }
140
141 static string FormatType(IdeCommandsArgs.Arg a)
142 {
143 if (a.IsArray)
144 return $"{a.ItemJsonType ?? "string"}[]";
145 return a.JsonType;
146 }
147
148 var lines = new List<string>(capacity: 4 + args.Length)
149 {
150 "Аргументы (плоский JSON object):"
151 };
152
153 foreach (var a in args)
154 {
155 var req = a.Required ? "required" : "optional";
156 lines.Add($" - {a.Name}: {FormatType(a)} ({req})");
157 }
158
159 lines.Add("");
160 return string.Join("\n", lines) + "\n";
161 }
162
163 private static void EnsureArgsBlockInDescription(Tool tool)
164 {
165 var d = tool.Description ?? "";
166 if (d.Contains("\nАргументы", StringComparison.Ordinal) || d.Contains("Аргументы (", StringComparison.Ordinal))
167 return;
168
169 if (!TryBuildArgsBlockFromSchema(tool.InputSchema, out var argsBlock))
170 return;
171
172 tool.Description = (d.TrimEnd() + "\n\n" + argsBlock).TrimEnd();
173 }
174
175 private static void EnsurePurposePrefix(Tool tool)
176 {
177 var d = (tool.Description ?? "").Trim();
178 if (d.Length == 0)
179 return;
180
181 // Keep existing structured descriptions intact.
182 if (d.StartsWith("Назначение:", StringComparison.Ordinal))
183 return;
184
185 tool.Description = $"Назначение: {d}";
186 }
187
188 private static bool TryBuildArgsBlockFromSchema(JsonElement inputSchema, out string argsBlock)
189 {
190 argsBlock = "";
191
192 if (inputSchema.ValueKind != JsonValueKind.Object)
193 return false;
194
195 if (!inputSchema.TryGetProperty("properties", out var props) || props.ValueKind != JsonValueKind.Object)
196 return false;
197
198 var required = new HashSet<string>(StringComparer.Ordinal);
199 if (inputSchema.TryGetProperty("required", out var req) && req.ValueKind == JsonValueKind.Array)
200 {
201 foreach (var it in req.EnumerateArray())
202 {
203 if (it.ValueKind == JsonValueKind.String)
204 required.Add(it.GetString()!);
205 }
206 }
207
208 var propNames = props.EnumerateObject().Select(p => p.Name).OrderBy(n => n, StringComparer.Ordinal).ToList();
209
210 if (propNames.Count == 0)
211 {
212 argsBlock = "Аргументы: нет.\n";
213 return true;
214 }
215
216 static string GetJsonType(JsonElement schema)
217 {
218 if (schema.ValueKind != JsonValueKind.Object)
219 return "object";
220
221 if (schema.TryGetProperty("type", out var t) && t.ValueKind == JsonValueKind.String)
222 {
223 var type = t.GetString()!;
224 if (string.Equals(type, "array", StringComparison.Ordinal) &&
225 schema.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Object &&
226 items.TryGetProperty("type", out var it) && it.ValueKind == JsonValueKind.String)
227 {
228 return it.GetString()! + "[]";
229 }
230
231 return type;
232 }
233
234 return "object";
235 }
236
237 var lines = new List<string>(capacity: 4 + propNames.Count)
238 {
239 "Аргументы (плоский JSON object):"
240 };
241
242 foreach (var name in propNames)
243 {
244 var schema = props.GetProperty(name);
245 var type = GetJsonType(schema);
246 var reqLabel = required.Contains(name) ? "required" : "optional";
247 lines.Add($" - {name}: {type} ({reqLabel})");
248 }
249
250 argsBlock = string.Join("\n", lines) + "\n";
251 return true;
252 }
253
254 private static void EnsureReturnsAndExampleBlocks(Tool tool)
255 {
256 var d = tool.Description ?? "";
257
258 if (!IdeMcpToolNaming.TryToCommandId(tool.Name, out var commandId))
259 return;
260
261 if (!d.Contains("\nВозвращает:", StringComparison.Ordinal) && IdeCommandsContract.TryGetReturns(commandId, out var kind))
262 {
263 var ret = kind switch
264 {
265 IdeReturnKind.Json => "json",
266 IdeReturnKind.Text => "text",
267 IdeReturnKind.None => "none",
268 _ => "unspecified"
269 };
270 d = d.TrimEnd() + $"\n\nВозвращает: {ret}.";
271 }
272
273 if (!d.Contains("\nExample:", StringComparison.Ordinal) && IdeCommandsContract.TryGetExample(commandId, out var ex) && !string.IsNullOrWhiteSpace(ex))
274 {
275 d = d.TrimEnd() + "\n\nExample:\n" + ex.Trim();
276 }
277
278 tool.Description = d;
279 }
280}
281
282
View only · write via MCP/CIDE