Forge
csharpdeeb25a2
1using System.Text.Json;
2using ModelContextProtocol.Protocol;
3using ModelContextProtocol.Server;
4
5namespace CascadeIDE.Services;
6
7public static class IdeMcpServer
8{
9 public static McpServerOptions BuildOptions(IIdeMcpActions actions)
10 {
11 bool includeDebugTools = false;
12#if DEBUG
13 includeDebugTools = true;
14#endif
15 var toolsList = IdeMcpToolCatalog.BuildTools(includeDebugTools);
16
17 return new McpServerOptions
18 {
19 ServerInfo = new Implementation { Name = "CascadeIDE", Version = "0.1.0" },
20 ProtocolVersion = "2024-11-05",
21 Capabilities = new ServerCapabilities { Tools = new ToolsCapability { ListChanged = false } },
22 Handlers = new McpServerHandlers
23 {
24 ListToolsHandler = (_, _) => ValueTask.FromResult(new ListToolsResult { Tools = toolsList }),
25 CallToolHandler = async (request, cancellationToken) =>
26 {
27 var name = request.Params?.Name ?? "";
28 var args = request.Params?.Arguments is IReadOnlyDictionary<string, JsonElement> a ? a : null;
29 try
30 {
31 var text = await CallToolByConventionAsync(actions, name, args, cancellationToken).ConfigureAwait(false);
32 // Тулы-действия при ошибке возвращают текст (не "OK") — помечаем как IsError, чтобы агент видел сбой.
33 bool isError;
34 if (name == "ide_execute_command")
35 isError = text.StartsWith("Missing", StringComparison.Ordinal) || text.StartsWith("Unknown command", StringComparison.Ordinal) || text.StartsWith("Error", StringComparison.Ordinal);
36 else if (string.Equals(name, "ide_create_blank_solution", StringComparison.Ordinal))
37 {
38 // Успех: «OK:» + абсолютный путь к .sln; иначе — текст ошибки (в т.ч. на русском).
39 isError = !text.StartsWith("OK:", StringComparison.Ordinal);
40 }
41 else
42 {
43 var isActionTool = name is "ide_open_file" or "ide_load_solution" or "ide_select" or "ide_set_breakpoint" or "ide_remove_breakpoint"
44 or "ide_show_preview" or "ide_show_editor_preview" or "ide_apply_edit" or "ide_go_to_position" or "ide_reveal_editor_range" or "ide_intercom_reveal_attachment" or "ide_focus_editor"
45 or "ide_set_ui_theme" or "ide_set_control_layout" or "ide_set_control_text" or "ide_click_control"
46 or "ide_send_keys" or "ide_set_focus" or "ide_highlight_control" or "ide_set_panel_size" or "ide_add_control"
47 or "ide_write_agent_notes"
48 or "ide_run_code_cleanup" or "ide_git_commit" or "ide_git_push"
49 or "ide_git_log" or "ide_git_fetch" or "ide_git_pull" or "ide_git_branch" or "ide_git_show" or "ide_git_submodule";
50 isError = isActionTool && text != "OK";
51 }
52 return new CallToolResult { Content = [new TextContentBlock { Text = text }], IsError = isError };
53 }
54 catch (Exception ex)
55 {
56 if (ex.Message.Contains("invalid thread", StringComparison.OrdinalIgnoreCase))
57 {
58 try
59 {
60 var dir = AppContext.BaseDirectory;
61 var logPath = Path.Combine(dir, "invalid-thread-log.txt");
62 File.WriteAllText(logPath, ex.ToString());
63 }
64 catch { /* ignore */ }
65#if DEBUG
66 var stack = ex.StackTrace ?? "";
67 return new CallToolResult { Content = [new TextContentBlock { Text = "Error: " + ex.Message + " [caught in IdeMcpServer]\n\n" + stack }], IsError = true };
68#endif
69 }
70 return new CallToolResult { Content = [new TextContentBlock { Text = "Error: " + ex.Message }], IsError = true };
71 }
72 }
73 }
74 };
75 }
76
77 private static async Task<string> CallToolByConventionAsync(
78 IIdeMcpActions actions,
79 string toolName,
80 IReadOnlyDictionary<string, JsonElement>? args,
81 CancellationToken cancellationToken)
82 {
83 // Special case: dispatcher tool supports arbitrary command ids and nested args object.
84 if (toolName == "ide_execute_command")
85 return await CallExecuteCommand(actions, args, cancellationToken).ConfigureAwait(false);
86
87 if (!toolName.StartsWith("ide_", StringComparison.Ordinal))
88 return $"Unknown tool: {toolName}";
89
90 // Most proxy tools map 1:1:
91 // tool ide_open_file -> command_id open_file
92 // tool ide_get_ui_theme -> command_id get_ui_theme
93 // and so on.
94 if (!IdeMcpToolNaming.TryToCommandId(toolName, out var commandId))
95 return $"Unknown tool: {toolName}";
96
97 // Compatibility overrides (tool name stays stable; backend command id may evolve).
98 if (string.Equals(commandId, IdeCommands.Build, StringComparison.Ordinal))
99 commandId = IdeCommands.BuildStructured;
100
101 return await actions.ExecuteCommandAsync(commandId, args, cancellationToken).ConfigureAwait(false);
102 }
103
104 private static async Task<string> CallExecuteCommand(IIdeMcpActions actions, IReadOnlyDictionary<string, JsonElement>? args, CancellationToken cancellationToken)
105 {
106 var merged = IdeExecuteCommandArgs.MergeNestedArgs(args);
107 var commandId = merged is not null && merged.TryGetValue("command_id", out var cid) ? cid.GetString() : null;
108 if (string.IsNullOrEmpty(commandId))
109 return "Missing command_id";
110 return await actions.ExecuteCommandAsync(commandId, merged, cancellationToken);
111 }
112
113}
114
View only · write via MCP/CIDE