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
3 changes: 2 additions & 1 deletion src/CodingWithCalvin.MCPServer.Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ static async Task RunServerAsync(string pipeName, string host, int port, string
.WithTools<SolutionTools>()
.WithTools<DocumentTools>()
.WithTools<BuildTools>()
.WithTools<NavigationTools>();
.WithTools<NavigationTools>()
.WithTools<DebuggerTools>();

var app = builder.Build();

Expand Down
18 changes: 17 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) };

foreach (var toolType in toolTypes)
{
Expand Down Expand Up @@ -132,4 +132,20 @@ 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();
}
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);
}
}
37 changes: 35 additions & 2 deletions src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks;
Expand Down Expand Up @@ -65,10 +66,42 @@ public async Task<string> CloseDocumentAsync(
[McpServerTool(Name = "document_read", ReadOnly = true)]
[Description("Read the contents of a document. If the document is open in VS, reads the current editor buffer (including unsaved changes); otherwise reads from disk.")]
public async Task<string> ReadDocumentAsync(
[Description("The full absolute path to the document. Supports forward slashes (/) or backslashes (\\).")] string path)
[Description("The full absolute path to the document. Supports forward slashes (/) or backslashes (\\).")] string path,
[Description("The line number to start reading from (1-based). Defaults to 1.")] int offset = 1,
[Description("Maximum number of lines to read. Defaults to 500. Use smaller values for large files.")] int limit = 500)
{
var content = await _rpcClient.ReadDocumentAsync(path);
return content ?? $"Could not read document: {path}";
if (content == null)
{
return $"Could not read document: {path}";
}

var lines = content.Split('\n');
var totalLines = lines.Length;
var startIndex = Math.Max(0, offset - 1);
var count = Math.Min(limit, totalLines - startIndex);

if (startIndex >= totalLines)
{
return $"Offset {offset} is beyond end of file ({totalLines} lines)";
}

var selectedLines = new string[count];
Array.Copy(lines, startIndex, selectedLines, 0, count);

var result = new System.Text.StringBuilder();
for (int i = 0; i < selectedLines.Length; i++)
{
result.AppendLine($"{startIndex + i + 1}\t{selectedLines[i].TrimEnd('\r')}");
}

var header = $"Lines {startIndex + 1}-{startIndex + count} of {totalLines}";
if (startIndex + count < totalLines)
{
header += $" (use offset={startIndex + count + 1} to read more)";
}

return $"{header}\n{result}";
}

[McpServerTool(Name = "document_write", Destructive = true)]
Expand Down
42 changes: 42 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace CodingWithCalvin.MCPServer.Shared.Models;

public class DebuggerStatus
{
public string Mode { get; set; } = string.Empty;
public bool IsDebugging { get; set; }
public string LastBreakReason { get; set; } = string.Empty;
public string CurrentProcessName { get; set; } = string.Empty;
public string CurrentFile { get; set; } = string.Empty;
public int CurrentLine { get; set; }
public string CurrentFunction { get; set; } = string.Empty;
}

public class BreakpointInfo
{
public string File { get; set; } = string.Empty;
public int Line { get; set; }
public int Column { get; set; }
public string FunctionName { get; set; } = string.Empty;
public string Condition { get; set; } = string.Empty;
public bool Enabled { get; set; }
public int CurrentHits { get; set; }
}

public class LocalVariableInfo
{
public string Name { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public bool IsValidValue { get; set; }
}

public class CallStackFrameInfo
{
public int Depth { get; set; }
public string FunctionName { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public int LineNumber { get; set; }
public string Module { get; set; } = string.Empty;
public string Language { get; set; } = string.Empty;
public string ReturnType { get; set; } = string.Empty;
}
16 changes: 16 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ public interface IVisualStudioRpc
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column);
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);

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

Task<bool> DebugAddBreakpointAsync(string file, int line);
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
Task<List<CallStackFrameInfo>> DebugGetCallStackAsync();
}

/// <summary>
Expand Down
16 changes: 16 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,20 @@ public interface IVisualStudioService
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column);
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);

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

Task<bool> DebugAddBreakpointAsync(string file, int line);
Task<bool> DebugRemoveBreakpointAsync(string file, int line);
Task<List<BreakpointInfo>> DebugGetBreakpointsAsync();
Task<List<LocalVariableInfo>> DebugGetLocalsAsync();
Task<List<CallStackFrameInfo>> DebugGetCallStackAsync();
}
16 changes: 16 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/RpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,20 @@ public Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int col
=> _vsService.GoToDefinitionAsync(path, line, column);
public Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100)
=> _vsService.FindReferencesAsync(path, line, column, maxResults);

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

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