diff --git a/README.md b/README.md index 66267c1..7569af6 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,15 @@ | `output_read` | Read content from an Output window pane | | `output_write` | Write a message to an Output window pane | +### 🪟 Window Tools + +| Tool | Description | +|------|-------------| +| `toolwindow_hide` | Hide (close) a tool window by caption | +| `toolwindow_show` | Show a tool window by name (SolutionExplorer, ErrorList, Output, Terminal, etc.) | +| `window_activate` | Activate (focus) a window by caption | +| `window_list` | List all open windows with caption, kind, visibility, and GUID | + ## 🛠️ Installation ### Visual Studio Marketplace diff --git a/src/CodingWithCalvin.MCPServer.Server/Program.cs b/src/CodingWithCalvin.MCPServer.Server/Program.cs index ae9f87c..fd48782 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Program.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Program.cs @@ -100,7 +100,8 @@ static async Task RunServerAsync(string pipeName, string host, int port, string .WithTools() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); var app = builder.Build(); diff --git a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs index 6ff0f39..a7602ff 100644 --- a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs +++ b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs @@ -65,7 +65,7 @@ public Task> GetAvailableToolsAsync() } var tools = new List(); - var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools), typeof(Tools.DebuggerTools), typeof(Tools.DiagnosticsTools) }; + var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools), typeof(Tools.DebuggerTools), typeof(Tools.DiagnosticsTools), typeof(Tools.WindowTools) }; foreach (var toolType in toolTypes) { @@ -163,4 +163,9 @@ public Task GetErrorListAsync(string? severity = null, int maxR public Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false) => Proxy.WriteOutputPaneAsync(paneIdentifier, message, activate); public Task> GetOutputPanesAsync() => Proxy.GetOutputPanesAsync(); + + public Task> GetWindowsAsync() => Proxy.GetWindowsAsync(); + public Task ActivateWindowAsync(string caption) => Proxy.ActivateWindowAsync(caption); + public Task ShowToolWindowAsync(string name) => Proxy.ShowToolWindowAsync(name); + public Task HideToolWindowAsync(string caption) => Proxy.HideToolWindowAsync(caption); } diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/WindowTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/WindowTools.cs new file mode 100644 index 0000000..fe39eac --- /dev/null +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/WindowTools.cs @@ -0,0 +1,88 @@ +using System.ComponentModel; +using System.Text.Json; +using System.Threading.Tasks; +using ModelContextProtocol.Server; + +namespace CodingWithCalvin.MCPServer.Server.Tools; + +[McpServerToolType] +public class WindowTools +{ + private static readonly string[] SupportedToolWindows = + [ + "SolutionExplorer", + "ErrorList", + "Output", + "TeamExplorer", + "Terminal", + "TaskList", + "Properties", + "Toolbox", + "FindResults", + "Bookmarks", + ]; + + private readonly RpcClient _rpcClient; + private readonly JsonSerializerOptions _jsonOptions; + + public WindowTools(RpcClient rpcClient) + { + _rpcClient = rpcClient; + _jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + } + + [McpServerTool(Name = "window_list", ReadOnly = true)] + [Description("List all open windows in Visual Studio. Returns each window's caption, kind (Document or Tool), visibility, and GUID.")] + public async Task GetWindowsAsync() + { + var windows = await _rpcClient.GetWindowsAsync(); + + if (windows.Count == 0) + { + return "No windows found"; + } + + return JsonSerializer.Serialize(windows, _jsonOptions); + } + + [McpServerTool(Name = "window_activate", Destructive = false, Idempotent = true)] + [Description("Activate (bring to front and focus) a specific window by its caption. Use window_list to find available window captions.")] + public async Task ActivateWindowAsync( + [Description("The caption/title of the window to activate. Case-insensitive.")] + string caption) + { + var success = await _rpcClient.ActivateWindowAsync(caption); + return success + ? $"Activated window: {caption}" + : $"Window not found: {caption}"; + } + + [McpServerTool(Name = "toolwindow_show", Destructive = false, Idempotent = true)] + [Description("Show a tool window by well-known name. Supported names: SolutionExplorer, ErrorList, Output, TeamExplorer, Terminal, TaskList, Properties, Toolbox, FindResults, Bookmarks.")] + public async Task ShowToolWindowAsync( + [Description("Well-known tool window name (e.g., \"SolutionExplorer\", \"ErrorList\", \"Output\"). Case-insensitive.")] + string name) + { + var success = await _rpcClient.ShowToolWindowAsync(name); + + if (success) + { + return $"Shown tool window: {name}"; + } + + var supported = string.Join(", ", SupportedToolWindows); + return $"Unknown tool window: {name}. Supported names: {supported}"; + } + + [McpServerTool(Name = "toolwindow_hide", Destructive = false, Idempotent = true)] + [Description("Hide (close) a tool window by its caption. Use window_list to find available window captions.")] + public async Task HideToolWindowAsync( + [Description("The caption/title of the tool window to hide. Case-insensitive.")] + string caption) + { + var success = await _rpcClient.HideToolWindowAsync(caption); + return success + ? $"Hidden tool window: {caption}" + : $"Tool window not found: {caption}"; + } +} diff --git a/src/CodingWithCalvin.MCPServer.Shared/Models/WindowModels.cs b/src/CodingWithCalvin.MCPServer.Shared/Models/WindowModels.cs new file mode 100644 index 0000000..f797e65 --- /dev/null +++ b/src/CodingWithCalvin.MCPServer.Shared/Models/WindowModels.cs @@ -0,0 +1,9 @@ +namespace CodingWithCalvin.MCPServer.Shared.Models; + +public class WindowInfo +{ + public string Caption { get; set; } = string.Empty; + public string Kind { get; set; } = string.Empty; // "Document" or "Tool" + public bool IsVisible { get; set; } + public string ObjectKind { get; set; } = string.Empty; // Window GUID +} diff --git a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs index 6ffdaeb..317a3b4 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs @@ -67,6 +67,12 @@ public interface IVisualStudioRpc Task ReadOutputPaneAsync(string paneIdentifier); Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false); Task> GetOutputPanesAsync(); + + // Window management tools + Task> GetWindowsAsync(); + Task ActivateWindowAsync(string caption); + Task ShowToolWindowAsync(string name); + Task HideToolWindowAsync(string caption); } /// diff --git a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs index 2a925c4..548f4dd 100644 --- a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs @@ -62,4 +62,9 @@ public interface IVisualStudioService Task ReadOutputPaneAsync(string paneIdentifier); Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false); Task> GetOutputPanesAsync(); + + Task> GetWindowsAsync(); + Task ActivateWindowAsync(string caption); + Task ShowToolWindowAsync(string name); + Task HideToolWindowAsync(string caption); } diff --git a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs index 4ea4b5d..14d20be 100644 --- a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs +++ b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs @@ -221,4 +221,9 @@ public Task GetErrorListAsync(string? severity = null, int maxR public Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false) => _vsService.WriteOutputPaneAsync(paneIdentifier, message, activate); public Task> GetOutputPanesAsync() => _vsService.GetOutputPanesAsync(); + + public Task> GetWindowsAsync() => _vsService.GetWindowsAsync(); + public Task ActivateWindowAsync(string caption) => _vsService.ActivateWindowAsync(caption); + public Task ShowToolWindowAsync(string name) => _vsService.ShowToolWindowAsync(name); + public Task HideToolWindowAsync(string caption) => _vsService.HideToolWindowAsync(caption); } diff --git a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs index 5304b18..002a2ed 100644 --- a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs @@ -2166,4 +2166,153 @@ private bool IsWellKnownPane(Guid paneGuid) paneGuid == VSConstants.OutputWindowPaneGuid.DebugPane_guid || paneGuid == VSConstants.OutputWindowPaneGuid.GeneralPane_guid; } + + private static readonly Dictionary ToolWindowCommands = new(StringComparer.OrdinalIgnoreCase) + { + ["SolutionExplorer"] = "View.SolutionExplorer", + ["ErrorList"] = "View.ErrorList", + ["Output"] = "View.Output", + ["TeamExplorer"] = "View.TeamExplorer", + ["Terminal"] = "View.Terminal", + ["TaskList"] = "View.TaskList", + ["Properties"] = "View.PropertiesWindow", + ["Toolbox"] = "View.Toolbox", + ["FindResults"] = "View.FindResults1", + ["Bookmarks"] = "View.BookmarkWindow", + }; + + public async Task> GetWindowsAsync() + { + using var activity = VsixTelemetry.Tracer.StartActivity("GetWindows"); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + try + { + var windows = new List(); + + foreach (Window window in dte.Windows) + { + try + { + windows.Add(new Shared.Models.WindowInfo + { + Caption = window.Caption, + Kind = window.Document != null ? "Document" : "Tool", + IsVisible = window.Visible, + ObjectKind = window.ObjectKind ?? string.Empty, + }); + } + catch (Exception) + { + // Some windows may not be accessible + } + } + + return windows; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return new List(); + } + } + + public async Task ActivateWindowAsync(string caption) + { + using var activity = VsixTelemetry.Tracer.StartActivity("ActivateWindow"); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + try + { + foreach (Window window in dte.Windows) + { + try + { + if (string.Equals(window.Caption, caption, StringComparison.OrdinalIgnoreCase)) + { + window.Activate(); + return true; + } + } + catch (Exception) + { + // Some windows may not be accessible + } + } + + return false; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return false; + } + } + + public async Task ShowToolWindowAsync(string name) + { + using var activity = VsixTelemetry.Tracer.StartActivity("ShowToolWindow"); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + try + { + if (!ToolWindowCommands.TryGetValue(name, out var command)) + { + return false; + } + + dte.ExecuteCommand(command); + return true; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return false; + } + } + + public async Task HideToolWindowAsync(string caption) + { + using var activity = VsixTelemetry.Tracer.StartActivity("HideToolWindow"); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + try + { + foreach (Window window in dte.Windows) + { + try + { + if (string.Equals(window.Caption, caption, StringComparison.OrdinalIgnoreCase)) + { + window.Close(); + return true; + } + } + catch (Exception) + { + // Some windows may not be accessible + } + } + + return false; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return false; + } + } + + public static IReadOnlyCollection GetSupportedToolWindowNames() + { + return ToolWindowCommands.Keys; + } }