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
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,43 @@
| `build_cancel` | Cancel a running build |
| `build_status` | Get current build status |

### 🧭 Navigation Tools

| Tool | Description |
|------|-------------|
| `goto_definition` | Navigate to the definition of a symbol |
| `find_references` | Find all references to a symbol |
| `symbol_document` | Get all symbols defined in a document |
| `symbol_workspace` | Search for symbols across the solution |

### 🐛 Debugger Tools

| Tool | Description |
|------|-------------|
| `debugger_status` | Get current debugger state |
| `debugger_launch` | Start debugging (F5) |
| `debugger_launch_without_debugging` | Start without debugger (Ctrl+F5) |
| `debugger_continue` | Continue execution (F5) |
| `debugger_break` | Pause execution (Ctrl+Alt+Break) |
| `debugger_stop` | Stop debugging (Shift+F5) |
| `debugger_step_over` | Step over (F10) |
| `debugger_step_into` | Step into (F11) |
| `debugger_step_out` | Step out (Shift+F11) |
| `debugger_add_breakpoint` | Add a breakpoint at a file and line |
| `debugger_remove_breakpoint` | Remove a breakpoint |
| `debugger_list_breakpoints` | List all breakpoints |
| `debugger_get_locals` | Get local variables in current frame |
| `debugger_get_callstack` | Get the call stack |

### 🔍 Diagnostics Tools

| Tool | Description |
|------|-------------|
| `errors_list` | Read build errors, warnings, and messages from the Error List |
| `output_read` | Read content from an Output window pane |
| `output_write` | Write a message to an Output window pane |
| `output_list_panes` | List all available Output window panes |

## 🛠️ Installation

### Visual Studio Marketplace
Expand All @@ -103,20 +140,36 @@ Download the latest `.vsix` from the [Releases](https://github.com/CodingWithCal
2. Go to **Tools > MCP Server > Start Server** (or enable auto-start in settings)
3. The MCP server starts on `http://localhost:5050`

### 🤖 Configuring Claude Desktop
### 🤖 Configuring Claude Desktop & Claude Code

Add this to your Claude Desktop MCP settings:
Add this to your Claude Desktop or Claude Code MCP settings (preferred HTTP method):

```json
{
"mcpServers": {
"visual-studio": {
"visualstudio": {
"type": "http",
"url": "http://localhost:5050"
}
}
}
```

**Legacy SSE method** (deprecated, but still supported):

```json
{
"mcpServers": {
"visualstudio": {
"type": "sse",
"url": "http://localhost:5050/sse"
}
}
}
```

> ℹ️ **Note:** The HTTP method is the preferred standard. SSE (Server-Sent Events) is a legacy protocol and should only be used for backward compatibility.

### ⚙️ Settings

Configure the extension at **Tools > Options > MCP Server**:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ModelContextProtocol" Version="0.2.0-preview.2" />
<PackageReference Include="ModelContextProtocol" Version="0.8.0-preview.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.2.0-preview.2" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.8.0-preview.1" />
<PackageReference Include="StreamJsonRpc" Version="2.20.20" />
</ItemGroup>

Expand Down
6 changes: 5 additions & 1 deletion src/CodingWithCalvin.MCPServer.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@

static async Task RunServerAsync(string pipeName, string host, int port, string serverName, string logLevel)
{
#pragma warning disable VSTHRD103 // Console.Error.WriteLine is appropriate in console app context
// Parse log level
var msLogLevel = logLevel switch
{
Expand Down Expand Up @@ -97,7 +98,9 @@ static async Task RunServerAsync(string pipeName, string host, int port, string
.WithTools<SolutionTools>()
.WithTools<DocumentTools>()
.WithTools<BuildTools>()
.WithTools<NavigationTools>();
.WithTools<NavigationTools>()
.WithTools<DebuggerTools>()
.WithTools<DiagnosticsTools>();

var app = builder.Build();

Expand All @@ -115,4 +118,5 @@ static async Task RunServerAsync(string pipeName, string host, int port, string
await app.RunAsync();

Console.Error.WriteLine("Server shutdown complete");
#pragma warning restore VSTHRD103
}
27 changes: 26 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) };
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools), typeof(Tools.DebuggerTools), typeof(Tools.DiagnosticsTools) };

foreach (var toolType in toolTypes)
{
Expand Down Expand Up @@ -96,8 +96,10 @@ public Task<List<ToolInfo>> GetAvailableToolsAsync()

public Task ShutdownAsync()
{
#pragma warning disable VSTHRD103 // Console.Error.WriteLine is fine in console app context
Console.Error.WriteLine("Shutdown requested via RPC");
_shutdownCts.Cancel();
#pragma warning restore VSTHRD103
return Task.CompletedTask;
}

Expand Down Expand Up @@ -132,4 +134,27 @@ public Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int col
=> Proxy.GoToDefinitionAsync(path, line, column);
public Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100)
=> Proxy.FindReferencesAsync(path, line, column, maxResults);

public Task<DebuggerStatus> GetDebuggerStatusAsync() => Proxy.GetDebuggerStatusAsync();
public Task<bool> DebugLaunchAsync() => Proxy.DebugLaunchAsync();
public Task<bool> DebugLaunchWithoutDebuggingAsync() => Proxy.DebugLaunchWithoutDebuggingAsync();
public Task<bool> DebugContinueAsync() => Proxy.DebugContinueAsync();
public Task<bool> DebugBreakAsync() => Proxy.DebugBreakAsync();
public Task<bool> DebugStopAsync() => Proxy.DebugStopAsync();
public Task<bool> DebugStepOverAsync() => Proxy.DebugStepOverAsync();
public Task<bool> DebugStepIntoAsync() => Proxy.DebugStepIntoAsync();
public Task<bool> DebugStepOutAsync() => Proxy.DebugStepOutAsync();

public Task<bool> DebugAddBreakpointAsync(string file, int line) => Proxy.DebugAddBreakpointAsync(file, line);
public Task<bool> DebugRemoveBreakpointAsync(string file, int line) => Proxy.DebugRemoveBreakpointAsync(file, line);
public Task<List<BreakpointInfo>> DebugGetBreakpointsAsync() => Proxy.DebugGetBreakpointsAsync();
public Task<List<Shared.Models.LocalVariableInfo>> DebugGetLocalsAsync() => Proxy.DebugGetLocalsAsync();
public Task<List<CallStackFrameInfo>> DebugGetCallStackAsync() => Proxy.DebugGetCallStackAsync();

public Task<ErrorListResult> GetErrorListAsync(string? severity = null, int maxResults = 100)
=> Proxy.GetErrorListAsync(severity, maxResults);
public Task<OutputReadResult> ReadOutputPaneAsync(string paneIdentifier) => Proxy.ReadOutputPaneAsync(paneIdentifier);
public Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false)
=> Proxy.WriteOutputPaneAsync(paneIdentifier, message, activate);
public Task<List<OutputPaneInfo>> GetOutputPanesAsync() => Proxy.GetOutputPanesAsync();
}
131 changes: 131 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks;
using ModelContextProtocol.Server;

namespace CodingWithCalvin.MCPServer.Server.Tools;

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

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

[McpServerTool(Name = "debugger_status", ReadOnly = true)]
[Description("Get the current debugger state. Returns the mode (Design = not debugging, Run = executing, Break = paused at breakpoint/step), break reason, current source location (file, line, function), and debugged process name. Always succeeds regardless of debugger state.")]
public async Task<string> GetDebuggerStatusAsync()
{
var status = await _rpcClient.GetDebuggerStatusAsync();
return JsonSerializer.Serialize(status, _jsonOptions);
}

[McpServerTool(Name = "debugger_launch", Destructive = false)]
[Description("Start debugging the current startup project (equivalent to F5). A solution must be open with a valid startup project configured. Use debugger_status to check the resulting state.")]
public async Task<string> DebugLaunchAsync()
{
var success = await _rpcClient.DebugLaunchAsync();
return success ? "Debugging started" : "Failed to start debugging (is a solution open with a startup project configured?)";
}

[McpServerTool(Name = "debugger_launch_without_debugging", Destructive = false)]
[Description("Start the current startup project without the debugger attached (equivalent to Ctrl+F5). The application runs normally without breakpoints or stepping. A solution must be open with a valid startup project configured.")]
public async Task<string> DebugLaunchWithoutDebuggingAsync()
{
var success = await _rpcClient.DebugLaunchWithoutDebuggingAsync();
return success ? "Started without debugging" : "Failed to start without debugging (is a solution open with a startup project configured?)";
}

[McpServerTool(Name = "debugger_continue", Destructive = false)]
[Description("Continue execution after a break (equivalent to F5 while paused). Only works when the debugger is in Break mode (paused at a breakpoint or after stepping). Use debugger_status to verify the debugger is in Break mode first.")]
public async Task<string> DebugContinueAsync()
{
var success = await _rpcClient.DebugContinueAsync();
return success ? "Execution continued" : "Cannot continue (debugger is not in Break mode)";
}

[McpServerTool(Name = "debugger_break", Destructive = false)]
[Description("Pause execution of the running program (equivalent to Ctrl+Alt+Break). Only works when the debugger is in Run mode. Use debugger_status to verify the debugger is in Run mode first.")]
public async Task<string> DebugBreakAsync()
{
var success = await _rpcClient.DebugBreakAsync();
return success ? "Execution paused" : "Cannot break (debugger is not in Run mode)";
}

[McpServerTool(Name = "debugger_stop", Destructive = true)]
[Description("Stop the current debugging session (equivalent to Shift+F5). Terminates the debugged process. Only works when a debugging session is active (Run or Break mode).")]
public async Task<string> DebugStopAsync()
{
var success = await _rpcClient.DebugStopAsync();
return success ? "Debugging stopped" : "Cannot stop (no active debugging session)";
}

[McpServerTool(Name = "debugger_step_over", Destructive = false)]
[Description("Step over the current statement (equivalent to F10). Executes the current line and stops at the next line in the same function. Only works when the debugger is in Break mode.")]
public async Task<string> DebugStepOverAsync()
{
var success = await _rpcClient.DebugStepOverAsync();
return success ? "Stepped over" : "Cannot step over (debugger is not in Break mode)";
}

[McpServerTool(Name = "debugger_step_into", Destructive = false)]
[Description("Step into the current statement (equivalent to F11). If the current line contains a function call, steps into that function. Only works when the debugger is in Break mode.")]
public async Task<string> DebugStepIntoAsync()
{
var success = await _rpcClient.DebugStepIntoAsync();
return success ? "Stepped into" : "Cannot step into (debugger is not in Break mode)";
}

[McpServerTool(Name = "debugger_step_out", Destructive = false)]
[Description("Step out of the current function (equivalent to Shift+F11). Continues execution until the current function returns, then breaks at the caller. Only works when the debugger is in Break mode.")]
public async Task<string> DebugStepOutAsync()
{
var success = await _rpcClient.DebugStepOutAsync();
return success ? "Stepped out" : "Cannot step out (debugger is not in Break mode)";
}

[McpServerTool(Name = "debugger_add_breakpoint", Destructive = false)]
[Description("Add a breakpoint at a specific file and line number. Works in any debugger mode (Design, Run, or Break). The file path must be absolute.")]
public async Task<string> DebugAddBreakpointAsync(string path, int line)
{
var success = await _rpcClient.DebugAddBreakpointAsync(path, line);
return success ? $"Breakpoint added at {path}:{line}" : $"Failed to add breakpoint at {path}:{line}";
}

[McpServerTool(Name = "debugger_remove_breakpoint", Destructive = true)]
[Description("Remove a breakpoint at a specific file and line number. Returns whether a breakpoint was found and removed.")]
public async Task<string> DebugRemoveBreakpointAsync(string path, int line)
{
var success = await _rpcClient.DebugRemoveBreakpointAsync(path, line);
return success ? $"Breakpoint removed from {path}:{line}" : $"No breakpoint found at {path}:{line}";
}

[McpServerTool(Name = "debugger_list_breakpoints", ReadOnly = true)]
[Description("List all breakpoints in the current solution. Returns file, line, column, function name, condition, enabled state, and hit count for each breakpoint.")]
public async Task<string> DebugListBreakpointsAsync()
{
var breakpoints = await _rpcClient.DebugGetBreakpointsAsync();
return JsonSerializer.Serialize(breakpoints, _jsonOptions);
}

[McpServerTool(Name = "debugger_get_locals", ReadOnly = true)]
[Description("Get local variables in the current stack frame. Only works when the debugger is in Break mode. Returns name, value, type, and validity for each local variable.")]
public async Task<string> DebugGetLocalsAsync()
{
var locals = await _rpcClient.DebugGetLocalsAsync();
return JsonSerializer.Serialize(locals, _jsonOptions);
}

[McpServerTool(Name = "debugger_get_callstack", ReadOnly = true)]
[Description("Get the call stack of the current thread. Only works when the debugger is in Break mode. Returns depth, function name, file name, line number, module, language, and return type for each frame.")]
public async Task<string> DebugGetCallStackAsync()
{
var callStack = await _rpcClient.DebugGetCallStackAsync();
return JsonSerializer.Serialize(callStack, _jsonOptions);
}
}
79 changes: 79 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/DiagnosticsTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks;
using ModelContextProtocol.Server;

namespace CodingWithCalvin.MCPServer.Server.Tools;

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

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

[McpServerTool(Name = "errors_list", ReadOnly = true)]
[Description("Get errors, warnings, and messages from the Error List. Returns diagnostics with file, line, description, and severity. Filter by severity to focus on specific issues.")]
public async Task<string> GetErrorListAsync(
[Description("Filter by severity: \"Error\", \"Warning\", \"Message\", or null for all. Case-insensitive.")]
string? severity = null,
[Description("Maximum number of items to return. Defaults to 100.")]
int maxResults = 100)
{
var result = await _rpcClient.GetErrorListAsync(severity, maxResults);

// Always return the JSON result (includes debug info if TotalCount is 0)
return JsonSerializer.Serialize(result, _jsonOptions);
}

[McpServerTool(Name = "output_read", ReadOnly = true)]
[Description("Read content from an Output window pane. Specify pane by GUID or well-known name (\"Build\", \"Debug\", \"General\"). Note: Some panes may not support reading due to VS API limitations.")]
public async Task<string> ReadOutputPaneAsync(
[Description("Output pane identifier: GUID string or well-known name (\"Build\", \"Debug\", \"General\").")]
string paneIdentifier)
{
var result = await _rpcClient.ReadOutputPaneAsync(paneIdentifier);

if (string.IsNullOrEmpty(result.Content))
{
return $"Output pane '{paneIdentifier}' is empty or does not support reading";
}

return JsonSerializer.Serialize(result, _jsonOptions);
}

[McpServerTool(Name = "output_write", Destructive = false, Idempotent = false)]
[Description("Write a message to an Output window pane. Custom panes are auto-created. System panes (Build, Debug) must already exist. Message is appended to existing content.")]
public async Task<string> WriteOutputPaneAsync(
[Description("Output pane identifier: GUID string or name. Custom GUIDs/names will create new panes if needed.")]
string paneIdentifier,
[Description("Message to write. Appended to existing content.")]
string message,
[Description("Whether to activate (bring to front) the Output window. Defaults to false.")]
bool activate = false)
{
var success = await _rpcClient.WriteOutputPaneAsync(paneIdentifier, message, activate);
return success
? $"Message written to output pane: {paneIdentifier}"
: $"Failed to write to output pane: {paneIdentifier}";
}

[McpServerTool(Name = "output_list_panes", ReadOnly = true)]
[Description("List available Output window panes. Returns well-known panes (Build, Debug, General) with their names and GUIDs.")]
public async Task<string> GetOutputPanesAsync()
{
var panes = await _rpcClient.GetOutputPanesAsync();

if (panes.Count == 0)
{
return "No output panes available";
}

return JsonSerializer.Serialize(panes, _jsonOptions);
}
}
Loading