Skip to content

Commit 779a5f6

Browse files
authored
feat(tools): add window management tools (#64)
* feat(tools): add window management tools Add window_list, window_activate, toolwindow_show, and toolwindow_hide MCP tools for interacting with Visual Studio windows. The toolwindow_show tool uses well-known friendly names mapped to DTE View commands instead of requiring GUIDs. * docs(readme): add window tools section
1 parent e3b2c35 commit 779a5f6

9 files changed

Lines changed: 279 additions & 2 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@
124124
| `output_read` | Read content from an Output window pane |
125125
| `output_write` | Write a message to an Output window pane |
126126

127+
### 🪟 Window Tools
128+
129+
| Tool | Description |
130+
|------|-------------|
131+
| `toolwindow_hide` | Hide (close) a tool window by caption |
132+
| `toolwindow_show` | Show a tool window by name (SolutionExplorer, ErrorList, Output, Terminal, etc.) |
133+
| `window_activate` | Activate (focus) a window by caption |
134+
| `window_list` | List all open windows with caption, kind, visibility, and GUID |
135+
127136
## 🛠️ Installation
128137

129138
### Visual Studio Marketplace

src/CodingWithCalvin.MCPServer.Server/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ static async Task RunServerAsync(string pipeName, string host, int port, string
100100
.WithTools<BuildTools>()
101101
.WithTools<NavigationTools>()
102102
.WithTools<DebuggerTools>()
103-
.WithTools<DiagnosticsTools>();
103+
.WithTools<DiagnosticsTools>()
104+
.WithTools<WindowTools>();
104105

105106
var app = builder.Build();
106107

src/CodingWithCalvin.MCPServer.Server/RpcClient.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public Task<List<ToolInfo>> GetAvailableToolsAsync()
6565
}
6666

6767
var tools = new List<ToolInfo>();
68-
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools), typeof(Tools.DebuggerTools), typeof(Tools.DiagnosticsTools) };
68+
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools), typeof(Tools.DebuggerTools), typeof(Tools.DiagnosticsTools), typeof(Tools.WindowTools) };
6969

7070
foreach (var toolType in toolTypes)
7171
{
@@ -163,4 +163,9 @@ public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxR
163163
public Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false)
164164
=> Proxy.WriteOutputPaneAsync(paneIdentifier, message, activate);
165165
public Task<List<OutputPaneInfo>> GetOutputPanesAsync() => Proxy.GetOutputPanesAsync();
166+
167+
public Task<List<WindowInfo>> GetWindowsAsync() => Proxy.GetWindowsAsync();
168+
public Task<bool> ActivateWindowAsync(string caption) => Proxy.ActivateWindowAsync(caption);
169+
public Task<bool> ShowToolWindowAsync(string name) => Proxy.ShowToolWindowAsync(name);
170+
public Task<bool> HideToolWindowAsync(string caption) => Proxy.HideToolWindowAsync(caption);
166171
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System.ComponentModel;
2+
using System.Text.Json;
3+
using System.Threading.Tasks;
4+
using ModelContextProtocol.Server;
5+
6+
namespace CodingWithCalvin.MCPServer.Server.Tools;
7+
8+
[McpServerToolType]
9+
public class WindowTools
10+
{
11+
private static readonly string[] SupportedToolWindows =
12+
[
13+
"SolutionExplorer",
14+
"ErrorList",
15+
"Output",
16+
"TeamExplorer",
17+
"Terminal",
18+
"TaskList",
19+
"Properties",
20+
"Toolbox",
21+
"FindResults",
22+
"Bookmarks",
23+
];
24+
25+
private readonly RpcClient _rpcClient;
26+
private readonly JsonSerializerOptions _jsonOptions;
27+
28+
public WindowTools(RpcClient rpcClient)
29+
{
30+
_rpcClient = rpcClient;
31+
_jsonOptions = new JsonSerializerOptions { WriteIndented = true };
32+
}
33+
34+
[McpServerTool(Name = "window_list", ReadOnly = true)]
35+
[Description("List all open windows in Visual Studio. Returns each window's caption, kind (Document or Tool), visibility, and GUID.")]
36+
public async Task<string> GetWindowsAsync()
37+
{
38+
var windows = await _rpcClient.GetWindowsAsync();
39+
40+
if (windows.Count == 0)
41+
{
42+
return "No windows found";
43+
}
44+
45+
return JsonSerializer.Serialize(windows, _jsonOptions);
46+
}
47+
48+
[McpServerTool(Name = "window_activate", Destructive = false, Idempotent = true)]
49+
[Description("Activate (bring to front and focus) a specific window by its caption. Use window_list to find available window captions.")]
50+
public async Task<string> ActivateWindowAsync(
51+
[Description("The caption/title of the window to activate. Case-insensitive.")]
52+
string caption)
53+
{
54+
var success = await _rpcClient.ActivateWindowAsync(caption);
55+
return success
56+
? $"Activated window: {caption}"
57+
: $"Window not found: {caption}";
58+
}
59+
60+
[McpServerTool(Name = "toolwindow_show", Destructive = false, Idempotent = true)]
61+
[Description("Show a tool window by well-known name. Supported names: SolutionExplorer, ErrorList, Output, TeamExplorer, Terminal, TaskList, Properties, Toolbox, FindResults, Bookmarks.")]
62+
public async Task<string> ShowToolWindowAsync(
63+
[Description("Well-known tool window name (e.g., \"SolutionExplorer\", \"ErrorList\", \"Output\"). Case-insensitive.")]
64+
string name)
65+
{
66+
var success = await _rpcClient.ShowToolWindowAsync(name);
67+
68+
if (success)
69+
{
70+
return $"Shown tool window: {name}";
71+
}
72+
73+
var supported = string.Join(", ", SupportedToolWindows);
74+
return $"Unknown tool window: {name}. Supported names: {supported}";
75+
}
76+
77+
[McpServerTool(Name = "toolwindow_hide", Destructive = false, Idempotent = true)]
78+
[Description("Hide (close) a tool window by its caption. Use window_list to find available window captions.")]
79+
public async Task<string> HideToolWindowAsync(
80+
[Description("The caption/title of the tool window to hide. Case-insensitive.")]
81+
string caption)
82+
{
83+
var success = await _rpcClient.HideToolWindowAsync(caption);
84+
return success
85+
? $"Hidden tool window: {caption}"
86+
: $"Tool window not found: {caption}";
87+
}
88+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace CodingWithCalvin.MCPServer.Shared.Models;
2+
3+
public class WindowInfo
4+
{
5+
public string Caption { get; set; } = string.Empty;
6+
public string Kind { get; set; } = string.Empty; // "Document" or "Tool"
7+
public bool IsVisible { get; set; }
8+
public string ObjectKind { get; set; } = string.Empty; // Window GUID
9+
}

src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ public interface IVisualStudioRpc
6767
Task<OutputReadResult> ReadOutputPaneAsync(string paneIdentifier);
6868
Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false);
6969
Task<List<OutputPaneInfo>> GetOutputPanesAsync();
70+
71+
// Window management tools
72+
Task<List<WindowInfo>> GetWindowsAsync();
73+
Task<bool> ActivateWindowAsync(string caption);
74+
Task<bool> ShowToolWindowAsync(string name);
75+
Task<bool> HideToolWindowAsync(string caption);
7076
}
7177

7278
/// <summary>

src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,9 @@ public interface IVisualStudioService
6262
Task<OutputReadResult> ReadOutputPaneAsync(string paneIdentifier);
6363
Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false);
6464
Task<List<OutputPaneInfo>> GetOutputPanesAsync();
65+
66+
Task<List<WindowInfo>> GetWindowsAsync();
67+
Task<bool> ActivateWindowAsync(string caption);
68+
Task<bool> ShowToolWindowAsync(string name);
69+
Task<bool> HideToolWindowAsync(string caption);
6570
}

src/CodingWithCalvin.MCPServer/Services/RpcServer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,9 @@ public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxR
221221
public Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false)
222222
=> _vsService.WriteOutputPaneAsync(paneIdentifier, message, activate);
223223
public Task<List<OutputPaneInfo>> GetOutputPanesAsync() => _vsService.GetOutputPanesAsync();
224+
225+
public Task<List<WindowInfo>> GetWindowsAsync() => _vsService.GetWindowsAsync();
226+
public Task<bool> ActivateWindowAsync(string caption) => _vsService.ActivateWindowAsync(caption);
227+
public Task<bool> ShowToolWindowAsync(string name) => _vsService.ShowToolWindowAsync(name);
228+
public Task<bool> HideToolWindowAsync(string caption) => _vsService.HideToolWindowAsync(caption);
224229
}

src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,4 +2166,153 @@ private bool IsWellKnownPane(Guid paneGuid)
21662166
paneGuid == VSConstants.OutputWindowPaneGuid.DebugPane_guid ||
21672167
paneGuid == VSConstants.OutputWindowPaneGuid.GeneralPane_guid;
21682168
}
2169+
2170+
private static readonly Dictionary<string, string> ToolWindowCommands = new(StringComparer.OrdinalIgnoreCase)
2171+
{
2172+
["SolutionExplorer"] = "View.SolutionExplorer",
2173+
["ErrorList"] = "View.ErrorList",
2174+
["Output"] = "View.Output",
2175+
["TeamExplorer"] = "View.TeamExplorer",
2176+
["Terminal"] = "View.Terminal",
2177+
["TaskList"] = "View.TaskList",
2178+
["Properties"] = "View.PropertiesWindow",
2179+
["Toolbox"] = "View.Toolbox",
2180+
["FindResults"] = "View.FindResults1",
2181+
["Bookmarks"] = "View.BookmarkWindow",
2182+
};
2183+
2184+
public async Task<List<Shared.Models.WindowInfo>> GetWindowsAsync()
2185+
{
2186+
using var activity = VsixTelemetry.Tracer.StartActivity("GetWindows");
2187+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
2188+
var dte = await GetDteAsync();
2189+
2190+
try
2191+
{
2192+
var windows = new List<Shared.Models.WindowInfo>();
2193+
2194+
foreach (Window window in dte.Windows)
2195+
{
2196+
try
2197+
{
2198+
windows.Add(new Shared.Models.WindowInfo
2199+
{
2200+
Caption = window.Caption,
2201+
Kind = window.Document != null ? "Document" : "Tool",
2202+
IsVisible = window.Visible,
2203+
ObjectKind = window.ObjectKind ?? string.Empty,
2204+
});
2205+
}
2206+
catch (Exception)
2207+
{
2208+
// Some windows may not be accessible
2209+
}
2210+
}
2211+
2212+
return windows;
2213+
}
2214+
catch (Exception ex)
2215+
{
2216+
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
2217+
activity?.RecordException(ex);
2218+
return new List<Shared.Models.WindowInfo>();
2219+
}
2220+
}
2221+
2222+
public async Task<bool> ActivateWindowAsync(string caption)
2223+
{
2224+
using var activity = VsixTelemetry.Tracer.StartActivity("ActivateWindow");
2225+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
2226+
var dte = await GetDteAsync();
2227+
2228+
try
2229+
{
2230+
foreach (Window window in dte.Windows)
2231+
{
2232+
try
2233+
{
2234+
if (string.Equals(window.Caption, caption, StringComparison.OrdinalIgnoreCase))
2235+
{
2236+
window.Activate();
2237+
return true;
2238+
}
2239+
}
2240+
catch (Exception)
2241+
{
2242+
// Some windows may not be accessible
2243+
}
2244+
}
2245+
2246+
return false;
2247+
}
2248+
catch (Exception ex)
2249+
{
2250+
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
2251+
activity?.RecordException(ex);
2252+
return false;
2253+
}
2254+
}
2255+
2256+
public async Task<bool> ShowToolWindowAsync(string name)
2257+
{
2258+
using var activity = VsixTelemetry.Tracer.StartActivity("ShowToolWindow");
2259+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
2260+
var dte = await GetDteAsync();
2261+
2262+
try
2263+
{
2264+
if (!ToolWindowCommands.TryGetValue(name, out var command))
2265+
{
2266+
return false;
2267+
}
2268+
2269+
dte.ExecuteCommand(command);
2270+
return true;
2271+
}
2272+
catch (Exception ex)
2273+
{
2274+
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
2275+
activity?.RecordException(ex);
2276+
return false;
2277+
}
2278+
}
2279+
2280+
public async Task<bool> HideToolWindowAsync(string caption)
2281+
{
2282+
using var activity = VsixTelemetry.Tracer.StartActivity("HideToolWindow");
2283+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
2284+
var dte = await GetDteAsync();
2285+
2286+
try
2287+
{
2288+
foreach (Window window in dte.Windows)
2289+
{
2290+
try
2291+
{
2292+
if (string.Equals(window.Caption, caption, StringComparison.OrdinalIgnoreCase))
2293+
{
2294+
window.Close();
2295+
return true;
2296+
}
2297+
}
2298+
catch (Exception)
2299+
{
2300+
// Some windows may not be accessible
2301+
}
2302+
}
2303+
2304+
return false;
2305+
}
2306+
catch (Exception ex)
2307+
{
2308+
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
2309+
activity?.RecordException(ex);
2310+
return false;
2311+
}
2312+
}
2313+
2314+
public static IReadOnlyCollection<string> GetSupportedToolWindowNames()
2315+
{
2316+
return ToolWindowCommands.Keys;
2317+
}
21692318
}

0 commit comments

Comments
 (0)