Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/CodingWithCalvin.MCPServer.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ static async Task RunServerAsync(string pipeName, string host, int port, string
.WithTools<BuildTools>()
.WithTools<NavigationTools>()
.WithTools<DebuggerTools>()
.WithTools<DiagnosticsTools>();
.WithTools<DiagnosticsTools>()
.WithTools<WindowTools>();

var app = builder.Build();

Expand Down
7 changes: 6 additions & 1 deletion src/CodingWithCalvin.MCPServer.Server/RpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public Task<List<ToolInfo>> GetAvailableToolsAsync()
}

var tools = new List<ToolInfo>();
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)
{
Expand Down Expand Up @@ -163,4 +163,9 @@ public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxR
public Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false)
=> Proxy.WriteOutputPaneAsync(paneIdentifier, message, activate);
public Task<List<OutputPaneInfo>> GetOutputPanesAsync() => Proxy.GetOutputPanesAsync();

public Task<List<WindowInfo>> GetWindowsAsync() => Proxy.GetWindowsAsync();
public Task<bool> ActivateWindowAsync(string caption) => Proxy.ActivateWindowAsync(caption);
public Task<bool> ShowToolWindowAsync(string name) => Proxy.ShowToolWindowAsync(name);
public Task<bool> HideToolWindowAsync(string caption) => Proxy.HideToolWindowAsync(caption);
}
88 changes: 88 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/WindowTools.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<string> 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<string> 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<string> 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}";
}
}
9 changes: 9 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/Models/WindowModels.cs
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ public interface IVisualStudioRpc
Task<OutputReadResult> ReadOutputPaneAsync(string paneIdentifier);
Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false);
Task<List<OutputPaneInfo>> GetOutputPanesAsync();

// Window management tools
Task<List<WindowInfo>> GetWindowsAsync();
Task<bool> ActivateWindowAsync(string caption);
Task<bool> ShowToolWindowAsync(string name);
Task<bool> HideToolWindowAsync(string caption);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,9 @@ public interface IVisualStudioService
Task<OutputReadResult> ReadOutputPaneAsync(string paneIdentifier);
Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false);
Task<List<OutputPaneInfo>> GetOutputPanesAsync();

Task<List<WindowInfo>> GetWindowsAsync();
Task<bool> ActivateWindowAsync(string caption);
Task<bool> ShowToolWindowAsync(string name);
Task<bool> HideToolWindowAsync(string caption);
}
5 changes: 5 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/RpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,9 @@ public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxR
public Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false)
=> _vsService.WriteOutputPaneAsync(paneIdentifier, message, activate);
public Task<List<OutputPaneInfo>> GetOutputPanesAsync() => _vsService.GetOutputPanesAsync();

public Task<List<WindowInfo>> GetWindowsAsync() => _vsService.GetWindowsAsync();
public Task<bool> ActivateWindowAsync(string caption) => _vsService.ActivateWindowAsync(caption);
public Task<bool> ShowToolWindowAsync(string name) => _vsService.ShowToolWindowAsync(name);
public Task<bool> HideToolWindowAsync(string caption) => _vsService.HideToolWindowAsync(caption);
}
149 changes: 149 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2166,4 +2166,153 @@ private bool IsWellKnownPane(Guid paneGuid)
paneGuid == VSConstants.OutputWindowPaneGuid.DebugPane_guid ||
paneGuid == VSConstants.OutputWindowPaneGuid.GeneralPane_guid;
}

private static readonly Dictionary<string, string> 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<List<Shared.Models.WindowInfo>> GetWindowsAsync()
{
using var activity = VsixTelemetry.Tracer.StartActivity("GetWindows");
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var dte = await GetDteAsync();

try
{
var windows = new List<Shared.Models.WindowInfo>();

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<Shared.Models.WindowInfo>();
}
}

public async Task<bool> 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<bool> 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<bool> 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<string> GetSupportedToolWindowNames()
{
return ToolWindowCommands.Keys;
}
}
Loading