Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ dotnet build src/CodingWithCalvin.VSMCP/CodingWithCalvin.VSMCP.csproj
- `Services/TestExplorerInterop.cs` - Reflection bridge to Test Explorer (see the file's remarks for why it cannot be a compile-time reference)
- `Services/TerminalInterop.cs` - Reflection bridge to the integrated terminal, over the brokered service container
- `Services/CoverageInterop.cs` - Reflection bridge to the code coverage file reader
- `Services/DialogAutomation.cs` - Finds and answers modal dialogs via Win32 and UI Automation, without ever touching the UI thread the dialog is blocking
- `Server/Tools/*.cs` - MCP tool definitions

## MCP Tools Available
Expand Down Expand Up @@ -234,6 +235,12 @@ Terminal output is not captured. The Visual Studio terminal is a raw PTY with no
- `window_activate` - Activate (focus) a window by caption
- `window_list` - List all open windows with caption, kind, visibility, and GUID

### Dialog Tools
- `dialog_list` - List the modal dialogs Visual Studio is waiting on, with their message and buttons
- `dialog_respond` - Answer a dialog by clicking one of its buttons

A modal dialog holds the VS UI thread, so every other tool call that needs it waits until the dialog is answered. The dialog tools must never switch to the UI thread: they run on the thread pool and stay responsive while the blocked call is still waiting.

## Technology Stack

- .NET Framework 4.8 (VSIX)
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@
| `window_activate` | Activate (focus) a window by caption |
| `window_list` | List all open windows with caption, kind, visibility, and GUID |

### 💬 Dialog Tools

| Tool | Description |
|------|-------------|
| `dialog_list` | List the modal dialogs Visual Studio is waiting on, with their message and buttons |
| `dialog_respond` | Answer a dialog by clicking one of its buttons |

> 💡 **Stuck on a dialog?** A modal dialog, like "The source code is different from the original
> version" or "Hot Reload changes not supported", blocks Visual Studio until someone answers it.
> If a tool call hangs or times out, `dialog_list` shows what Visual Studio is waiting on and
> `dialog_respond` answers it. Both work while the blocked call is still waiting.

## 🛠️ 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 @@ -104,7 +104,8 @@ static async Task RunServerAsync(string pipeName, string host, int port, string
.WithTools<WindowTools>()
.WithTools<TestTools>()
.WithTools<TerminalTools>()
.WithTools<CoverageTools>();
.WithTools<CoverageTools>()
.WithTools<DialogTools>();

var app = builder.Build();

Expand Down
6 changes: 5 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), typeof(Tools.WindowTools), typeof(Tools.TestTools), typeof(Tools.TerminalTools), typeof(Tools.CoverageTools) };
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools), typeof(Tools.DebuggerTools), typeof(Tools.DiagnosticsTools), typeof(Tools.WindowTools), typeof(Tools.TestTools), typeof(Tools.TerminalTools), typeof(Tools.CoverageTools), typeof(Tools.DialogTools) };

foreach (var toolType in toolTypes)
{
Expand Down Expand Up @@ -202,4 +202,8 @@ public Task<TerminalResult> CreateTerminalAsync(string? name, string? workingDir
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);

public Task<DialogListResult> GetDialogsAsync() => Proxy.GetDialogsAsync();
public Task<DialogResponseResult> RespondToDialogAsync(string button, string? dialogId)
=> Proxy.RespondToDialogAsync(button, dialogId);
}
48 changes: 48 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/DialogTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks;
using ModelContextProtocol.Server;

namespace CodingWithCalvin.MCPServer.Server.Tools;

[McpServerToolType]
public class DialogTools
{
private readonly RpcClient _rpcClient;
private readonly JsonSerializerOptions _jsonOptions;

public DialogTools(RpcClient rpcClient)
{
_rpcClient = rpcClient;
_jsonOptions = new JsonSerializerOptions { WriteIndented = true };
}

[McpServerTool(Name = "dialog_list", ReadOnly = true)]
[Description("List the modal dialogs Visual Studio is waiting on, such as message boxes and prompts raised by the debugger, Hot Reload or a build. A modal dialog blocks Visual Studio until someone answers it, so if another tool call hangs or times out, call this to check for one. Works while another tool call is still blocked. Returns each dialog's id, title, message and buttons; answer it with dialog_respond.")]
public async Task<string> ListDialogsAsync()
{
var result = await _rpcClient.GetDialogsAsync();

if (result.Dialogs.Count == 0)
{
return result.Message ?? "No dialogs are open. Visual Studio is not waiting on a dialog.";
}

return JsonSerializer.Serialize(result.Dialogs, _jsonOptions);
}

[McpServerTool(Name = "dialog_respond", Destructive = true)]
[Description("Answer a modal dialog by clicking one of its buttons, exactly as a user would. Read the dialog with dialog_list first: the answer takes effect immediately and can discard changes, stop debugging or start a rebuild. Returns whether the dialog closed and the dialogs still open, because an answer can raise a follow-up prompt.")]
public async Task<string> RespondToDialogAsync(
[Description("Name of the button to click, as listed by dialog_list, for example \"Yes\", \"No\" or \"Cancel\". Case-insensitive.")] string button,
[Description("Id of the dialog to answer, from dialog_list. Only needed when more than one dialog is open.")] string? dialogId = null)
{
if (string.IsNullOrWhiteSpace(button))
{
return "A button name is required. Call dialog_list to see the dialog's buttons.";
}

var result = await _rpcClient.RespondToDialogAsync(button, dialogId);
return JsonSerializer.Serialize(result, _jsonOptions);
}
}
76 changes: 76 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/Models/DialogModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections.Generic;

namespace CodingWithCalvin.MCPServer.Shared.Models;

/// <summary>
/// A modal dialog that Visual Studio is waiting on.
/// </summary>
public class DialogInfo
{
/// <summary>
/// Window handle of the dialog in hexadecimal, used to pick a dialog when more than one is open.
/// </summary>
public string Id { get; set; } = string.Empty;

public string Title { get; set; } = string.Empty;

/// <summary>
/// The dialog's text, usually the question being asked or the error being reported.
/// </summary>
public string Message { get; set; } = string.Empty;

public List<DialogButtonInfo> Buttons { get; set; } = new();

/// <summary>
/// Populated when the dialog's contents could not be read, for example because it stopped
/// responding. The dialog is still listed so the caller knows something is blocking.
/// </summary>
public string? Note { get; set; }
}

public class DialogButtonInfo
{
public string Name { get; set; } = string.Empty;
public bool IsEnabled { get; set; }
}

/// <summary>
/// The modal dialogs currently open in Visual Studio.
/// </summary>
public class DialogListResult
{
public List<DialogInfo> Dialogs { get; set; } = new();

/// <summary>
/// Populated when the dialogs could not be enumerated at all, which is distinct from there
/// being none open.
/// </summary>
public string? Message { get; set; }
}

/// <summary>
/// Outcome of answering a modal dialog by clicking one of its buttons.
/// </summary>
public class DialogResponseResult
{
public bool Clicked { get; set; }

public string? DialogId { get; set; }

/// <summary>
/// Name of the button that was clicked, as the dialog displays it.
/// </summary>
public string? Button { get; set; }

/// <summary>
/// True when the dialog had closed by the time this result was produced.
/// </summary>
public bool DialogClosed { get; set; }

public string? Message { get; set; }

/// <summary>
/// Dialogs still open afterwards. Answering one dialog can raise a follow-up prompt.
/// </summary>
public List<DialogInfo> OpenDialogs { get; set; } = new();
}
4 changes: 4 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ Task<bool> DebugAddBreakpointAsync(
Task<bool> ActivateWindowAsync(string caption);
Task<bool> ShowToolWindowAsync(string name);
Task<bool> HideToolWindowAsync(string caption);

// Dialog tools
Task<DialogListResult> GetDialogsAsync();
Task<DialogResponseResult> RespondToDialogAsync(string button, string? dialogId);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,14 @@
<ProjectReference Include="..\CodingWithCalvin.MCPServer\CodingWithCalvin.MCPServer.csproj" />
</ItemGroup>

<ItemGroup>
<!-- Real Win32 and WPF dialogs for DialogAutomationTests to find and answer. -->
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xaml" />
<Reference Include="WindowsBase" />
</ItemGroup>

</Project>
Loading
Loading