Skip to main content

Development Guide

Prerequisites​

  • .NET 10 SDK
  • Visual Studio 2022, Rider, or VS Code
  • Git

Building​

# Clone repository
git clone https://github.com/jkolo/debug-mcp.net
cd debug-mcp.net

# Restore and build
dotnet build

# Run tests
dotnet test

# Run locally
dotnet run --project src/DebugMcp

Project Structure​

DebugMcp/
├── DebugMcp.slnx # Solution file
├── README.md
├── docs/
│ ├── ARCHITECTURE.md
│ ├── DEBUGGER.md
│ ├── MCP_TOOLS.md
│ └── DEVELOPMENT.md
├── src/
│ └── DebugMcp/
│ ├── DebugMcp.csproj # Main project
│ ├── Program.cs # Entry point
│ ├── Tools/ # MCP tool implementations
│ │ ├── SessionTools.cs
│ │ ├── BreakpointTools.cs
│ │ ├── ExecutionTools.cs
│ │ └── InspectionTools.cs
│ ├── Debugger/ # Core debugging logic
│ │ ├── DebugSession.cs
│ │ ├── DebugEventHandler.cs
│ │ ├── BreakpointManager.cs
│ │ ├── ExpressionEvaluator.cs
│ │ └── SourceMapper.cs
│ └── Infrastructure/ # Platform support
│ ├── DbgShimLoader.cs
│ └── CorDebugFactory.cs
└── tests/
└── DebugMcp.Tests/
├── DebugMcp.Tests.csproj
├── Tools/ # Tool unit tests
├── Debugger/ # Debugger tests
└── Integration/ # Integration tests

Testing​

Unit Tests​

dotnet test --filter Category=Unit

Integration Tests​

Integration tests require a running sample application:

# Build test target
dotnet build tests/TestTarget

# Run integration tests
dotnet test --filter Category=Integration

Manual Testing with MCP Inspector​

# Install MCP Inspector
npm install -g @modelcontextprotocol/inspector

# Run DebugMcp with inspector
mcp-inspector dotnet run --project src/DebugMcp

Debugging DebugMcp Itself​

VS Code​

  1. Open in VS Code
  2. Select "DebugMcp (Debug)" launch configuration
  3. Set breakpoints in Tool or Debugger code
  4. F5 to start debugging

Rider​

  1. Open solution in Rider
  2. Select DebugMcp run configuration
  3. Set breakpoints
  4. Debug (Shift+F9)

Testing with Claude Desktop​

  1. Build: dotnet build -c Release
  2. Configure in claude_desktop_config.json:
    {
    "mcpServers": {
    "dotnet-debugger-dev": {
    "command": "dotnet",
    "args": ["run", "--project", "/path/to/DebugMcp/src/DebugMcp"]
    }
    }
    }
  3. Restart Claude Desktop
  4. Test by asking Claude to debug a .NET app

Adding a New Tool​

1. Create tool method​

Add to appropriate class in Tools/*.cs:

[McpTool("my_new_tool", "Description of what it does")]
public async Task<MyResponse> MyNewToolAsync(
[McpParameter("param1", "Description", required: true)] string param1,
[McpParameter("param2", "Description")] int? param2 = null,
CancellationToken cancellationToken = default)
{
// Validate session state
if (_session.State == DebugState.NotAttached)
return new MyResponse(false, "not_attached", "No debugging session");

// Perform operation
var result = await _session.DoSomethingAsync(param1, cancellationToken);

// Return structured response
return new MyResponse(true, null, result);
}

2. Add response model​

public record MyResponse(
bool Success,
string? ErrorCode,
string Message);

3. Write tests​

[Fact]
[Trait("Category", "Unit")]
public async Task MyNewTool_WithValidInput_ReturnsSuccess()
{
// Arrange
var mockSession = new Mock<IDebugSession>();
mockSession.Setup(s => s.State).Returns(DebugState.Stopped);
mockSession.Setup(s => s.DoSomethingAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("result");

var tools = new MyToolsClass(mockSession.Object);

// Act
var result = await tools.MyNewToolAsync("test", 42);

// Assert
Assert.True(result.Success);
Assert.Null(result.ErrorCode);
}

[Fact]
[Trait("Category", "Unit")]
public async Task MyNewTool_WhenNotAttached_ReturnsError()
{
// Arrange
var mockSession = new Mock<IDebugSession>();
mockSession.Setup(s => s.State).Returns(DebugState.NotAttached);

var tools = new MyToolsClass(mockSession.Object);

// Act
var result = await tools.MyNewToolAsync("test");

// Assert
Assert.False(result.Success);
Assert.Equal("not_attached", result.ErrorCode);
}

4. Document​

Add entry to docs/MCP_TOOLS.md with:

  • Description
  • Parameters table
  • Example request
  • Example response
  • Error codes

Code Style​

General​

  • Follow .NET naming conventions
  • Use nullable reference types (#nullable enable)
  • Prefer records for DTOs and immutable data
  • Use CancellationToken for async operations

Async​

  • Async methods end with Async suffix
  • Always pass CancellationToken through the call chain
  • Prefer ValueTask for hot paths that often complete synchronously

Logging​

// Use structured logging with semantic parameters
_logger.LogDebug("Setting breakpoint at {File}:{Line}", file, line);
_logger.LogWarning("Breakpoint {Id} not verified: {Reason}", id, reason);
_logger.LogError(ex, "Failed to evaluate expression {Expression}", expr);

Important: Logs go to stderr (not stdout, which is reserved for MCP JSON-RPC).

Error Handling​

// Use result types for expected failures
public record BreakpointResult(
bool Success,
int? Id = null,
string? ErrorCode = null,
string? ErrorMessage = null);

// Throw exceptions for unexpected failures
if (process == null)
throw new InvalidOperationException("Process unexpectedly null");

Debugging Tips​

COM Interop Issues​

  • ICorDebug requires STA thread for callbacks
  • Use Marshal.GetLastWin32Error() after failed P/Invoke
  • Check HRESULT values with Marshal.GetExceptionForHR()

Debugging Hangs​

  • Check if Continue() was called after callback
  • Verify callback handler doesn't block
  • Use process.Stop(0) with timeout for diagnosis

Symbol Loading​

  • Enable PDB loading diagnostics: DOTNET_STARTUP_HOOKS
  • Check symbol cache location
  • Verify PDB matches assembly version

Contributing​

Workflow​

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Make changes with tests
  4. Run all tests: dotnet test
  5. Commit with descriptive message
  6. Push: git push origin feature/my-feature
  7. Create Pull Request

Commit Messages​

Follow conventional commits:

feat: add conditional breakpoint support
fix: handle null thread in stacktrace
docs: update MCP_TOOLS with evaluate examples
refactor: extract SourceMapper from BreakpointManager
test: add integration tests for attach scenario

Pull Request Checklist​

  • All tests pass
  • New code has tests
  • Documentation updated
  • No breaking changes (or documented if necessary)
  • Changelog entry added

Release Process​

Versioning​

Follow SemVer:

  • MAJOR: Breaking API changes
  • MINOR: New features, backward compatible
  • PATCH: Bug fixes, backward compatible

Release Steps​

# Update version in DebugMcp.csproj
# <Version>1.2.3</Version>

# Update CHANGELOG.md

# Commit and tag
git add .
git commit -m "release: v1.2.3"
git tag v1.2.3
git push origin main v1.2.3

GitHub Actions will:

  • Run tests
  • Build NuGet package
  • Publish to nuget.org
  • Create GitHub Release

Troubleshooting​

"Cannot find dbgshim"​

Ensure the correct platform package is installed:

<PackageReference Include="Microsoft.Diagnostics.DbgShim.linux-x64" Version="9.0.652701" />

"Process already has a debugger attached"​

Only one debugger can attach at a time. Detach VS/Rider first.

"Breakpoint not bound"​

  • Check if module is loaded (debug_state shows loaded modules)
  • Verify source file path matches exactly
  • Check PDB is available and matches assembly

"Expression evaluation timeout"​

  • Complex expressions take time
  • LINQ queries may enumerate collections
  • Method calls execute in debuggee context