Thank you for your interest in contributing to RoslynMcp! This document provides guidelines and instructions for contributing.
All contributors will be acknowledged! Your contributions — whether code, documentation, bug reports, or ideas — help make RoslynMcp better.
- Code contributors: Listed in release notes and CHANGELOG.md
- Significant contributions: May be acknowledged in README.md or a dedicated CONTRIBUTORS file
- First-time contributors: Especially welcome — we're happy to help you get started!
By contributing, you're helping AI agents work better with C# code. That's worth celebrating. 🎉
- Search existing issues before creating a new one
- Use the issue templates provided
- Include:
- Clear description of the problem or feature request
- Steps to reproduce (for bugs)
- Expected vs. actual behavior
- Environment details (.NET version, OS, MCP client)
- Relevant logs or error messages
- Fork the repository and create a feature branch from
dev - Follow the code style (see
AGENTS.md) - Add tests for new tools or significant changes
- Update documentation (README.md, AGENTS.md, CHANGELOG.md, etc.)
- Run the test suite and ensure all tests pass:
# AI agents should use roslyn_build_project / roslyn_get_diagnostics. dotnet build src/RoslynMcp/RoslynMcp.csproj -f net10.0 dotnet run --project src/TestHarness/TestHarness.csproj -f net10.0 - Commit with clear messages — describe what and why, not how
- Submit PR against
devbranch (notmain)
- .NET 10 or 11 SDK
- Git
- Your preferred code editor (Visual Studio, VS Code, Rider, etc.)
After cloning, run this one-liner to activate the pre-commit hook that strips UTF-8 BOMs from staged .cs files:
git config core.hooksPath .githooksWhy? VS 17.14+ re-adds BOMs to .cs files via the Roslyn language service on background reload. The hook detects and strips them automatically on each commit, keeping the repository BOM-free.
The .githooks/pre-commit script is committed to the repo — core.hooksPath tells git to use it instead of .git/hooks/.
# Clone your fork
git clone https://github.com/MadQ/RoslynMcp.git
cd RoslynMcp
# AI agents should use roslyn_build_project.
# CLI alternative for human contributors:
dotnet build src/RoslynMcp/RoslynMcp.csproj -f net10.0# Run the comprehensive integration test suite
dotnet run --project src/TestHarness/TestHarness.csproj -f net10.0Tests run RoslynMcp against itself (dogfooding). All tests should pass before submitting a PR.
Publish a Release build and configure your MCP client to use it:
-
Publish RoslynMcp:
dotnet publish src/RoslynMcp/RoslynMcp.csproj -c Release -f net10.0 -o ./publish/net10.0
-
Configure your MCP client:
{ "servers": { "MadQ.RoslynMcp": { "type": "stdio", "command": "/absolute/path/to/RoslynMcp/publish/net10.0/RoslynMcp.exe", "args": ["/path/to/test/project"] } } } -
Test tool calls interactively
Note: Avoid Visual Studio's "Publish" UI (may trigger WebToolsException). Use dotnet publish command line.
See AGENTS.md § Code Style for the canonical style rules.
This document intentionally does not duplicate those rules. AGENTS.md is the source of truth for formatting, naming, blank-line rules, the "Right Code" principle, and RoslynMcp-specific tool conventions.
Code style enforcement is custom, not generic. RoslynMcp uses scripts/Test-CodeStyle.ps1 for style auditing and RoslynMcp.Analyzers for compile-time rules instead of a repository-wide .editorconfig linter setup.
PRs that add .editorconfig files will not be approved. These create the same conflicts with the project's intentional style choices.
Custom analyzers are acceptable if they enforce narrow, high-value rules. RoslynMcp.Analyzers includes error-severity rules (RMCP003: missing BeginTool scope, RMCP004: return bypasses scope terminal, RMCP005: BeginTool name mismatch, RMCP007: missing [Description] on tool method, RMCP008: missing [Description] on tool parameter, RMCP009: string projectPath must use [Description(ProjectPathDescription)]) and warning-severity rules (RMCP001/RMCP002: prefer nint/nuint over IntPtr/UIntPtr, RMCP006: TODO placeholder in scope.Outcome/scope.Failed detail strings). New analyzer contributions follow the same pattern.
-
Create the tool class in the appropriate folder under
src/RoslynMcp/Tools/:Analysis/for read-only semantic queriesSearch/for file/content discoveryEditing/for file mutation toolsRename/andRefactoring/for preview/apply workflowsBuild/for build / restore / clean tooling
[McpServerToolType] internal sealed class MyNewTool : RoslynMcpTool { public MyNewTool(WorkspaceResolver workspace, FileLogger logger, PaginationCache paginationCache) : base(workspace, logger, paginationCache) { } [McpServerTool(Name = "roslyn_my_tool", ReadOnly = true)] [Description("...")] public object MyToolMethod( [Description("...")] string parameter, [Description(ProjectPathDescription)] string projectPath) { using var scope = BeginTool("roslyn_my_tool", parameter); if(!TryGetCompilation(projectPath, out var compilation, out var error)) return scope.Error(error!); // Use Roslyn APIs here // Typed result records are preferred over anonymous objects return scope.Outcome("summary of result", new MyToolResult(...)); } }
Required rules (enforced by analyzer errors):
using var scope = BeginTool(...)must be the first statement — ensures every exit path logs timing (RMCP003)- Every return must flow through
scope.Error(error),scope.Outcome(detail, value), orscope.Failed(reason, value)— barereturnbypasses logging (RMCP004) [Description("...")]is required on the tool method and every parameter (RMCP007/RMCP008)string projectPathmust use[Description(ProjectPathDescription)], not an inline string (RMCP009)
-
No manual DI registration needed —
WithToolsFromAssembly()insrc/RoslynMcp/Program.csauto-discovers all[McpServerToolType]classes -
Add tests under
src/TestHarness/Tests/and wire them intosrc/TestHarness/TestHarnessProgram.csif the new coverage needs a new test section -
Update documentation:
- README.md (tool catalog table)
- AGENTS.md (architecture table and tool count)
Tools are the user-facing API surface. Exception handling must be explicit, informative, and never silent.
✅ Do:
- Catch specific exception types (
ArgumentException,IOException,UnauthorizedAccessException, etc.) - Return tool errors through the scope terminal with structured error objects:
return scope.Error(new ErrorResult("Short description", Hint: "..."));
❌ Don't:
- Bare
catch { }without explanation — always catch specific types or add a comment explaining why broad catch is needed - Swallow exceptions that indicate programming errors (
NullReferenceException,InvalidOperationExceptionfrom bugs) - Return generic "something went wrong" messages — be specific about what failed
Good — specific exception, structured error:
try {
var regex = new Regex(pattern);
}
catch(ArgumentException ex) {
return scope.Error(new ErrorResult($"Invalid regex pattern: {ex.Message}"));
}Acceptable — broad catch with clear justification:
try {
AddOrUpdateDocument(adhoc, projectId, fullPath);
}
catch {
// File may be locked mid-write by another process;
// next FileSystemWatcher event will retry automatically.
}Bad — silent, broad catch with no context:
try {
return ParseXmlDocumentation(symbol);
}
catch {
return null; // Why? What failed? Should the user know?
}User-facing tools (methods with [McpServerTool]) must return explicit errors.
Internal helpers (private methods, file watchers, background processing) may use broader catches if:
- Failure is non-fatal and recoverable
- A comment explains the failure scenario and why it's safe to ignore
- The outer system remains in a valid state
When in doubt, catch specific types and log or return the error.
Exception filters (when clauses) have a reputation for being obscure or "too clever." That's mostly cargo-cult thinking. They're just another tool — and a good one when the alternative is copy-pasting the same catch block five times. Try them. You might be pleasantly surprised.
Use when clauses to consolidate multiple related exception types or add conditional logic:
// Consolidate multiple file system exceptions
catch(Exception ex) when(ex is FileNotFoundException or DirectoryNotFoundException or UnauthorizedAccessException) {
return scope.Error(new ErrorResult("File system error", Hint: ex.Message));
}
// Conditional catch based on exception state
catch(IOException ex) when(IsTransientError(ex)) {
// Retry logic or ignore
}
// Log-and-rethrow pattern (filter returns false, so catch never executes)
catch(Exception ex) when(LogError(ex)) {
// Never reached — filter logs and returns false
}
static bool LogError(Exception ex) {
Console.Error.WriteLine($"[ERROR] {ex}");
return false; // Don't catch, just observe
}Exception filters are especially useful for:
- DRYing up multiple catch blocks with similar handling
- Conditional catching based on exception properties (error codes, inner exceptions)
- Logging without catching (filter returns false after logging)
RoslynMcp/
├── src/
│ ├── RoslynMcp/ # Main MCP server project
│ │ ├── Tools/ # Tool implementations
│ │ ├── Program.cs # MCP protocol + DI setup
│ │ ├── WorkspaceManager*.cs # Workspace caching (partial: .cs, .Resolution.cs, .Instance.cs)
│ │ ├── ApprovalStore.cs # Rename approval state
│ │ └── SolutionDiff.cs # Unified diff generation
│ └── TestHarness/ # Test suite
├── .github/ # GitHub-specific files
├── .meta/ # Project metadata
├── docs/ # Project documentation
│ ├── process/ # Checklists and process guides
│ ├── sessions/ # Session handoff notes and archived session docs
│ ├── MSBUILD_API_ANALYSIS.md # MSBuild vs Roslyn architecture rationale
│ └── ScratchPad.md # Owner scratchpad
├── README.md # Main documentation
├── INSTALLATION.md # Setup instructions
└── CHANGELOG.md # Version history
MCP transmits tool parameters as JSON, which uses \n (LF) for newlines. But files on Windows use \r\n (CRLF). Any tool that does literal string matching on file content — find-and-replace, anchor-based insertion, pattern search — will silently fail when a multi-line pattern arrives with LF but the file contains CRLF.
This is not specific to RoslynMcp. Any MCP server that matches tool input against file content is affected. The pattern (content from disk with platform line endings, pattern from JSON with LF-only) is universal.
RoslynMcp addresses this with BuildLiteralRegex in the tool base class, which replaces literal \n in escaped patterns with \r?\n so they match both line ending styles. Writing tools also offer a normalizeLineEndings parameter (default true) that adjusts replacement text to match the file's existing convention.
If you're building MCP tools that edit files, consider handling this in your implementation.
- Open an issue for questions or discussions
- Check existing issues and documentation first
- Be respectful and constructive
- Be professional and respectful
- Focus on technical merit
- Assume good intent
- Help others learn
By contributing, you agree that your contributions will be licensed under the MIT License.
You retain copyright to your contributions, but grant the project and users the rights specified in the MIT License. Your name will appear in the git history and (for significant contributions) in release notes and acknowledgments.