A local Model Context Protocol server, built in C#/.NET, that lets an AI client (Claude Code, Claude Desktop, VS Code, Visual Studio, etc.) inspect and operate on a local .NET solution through a controlled set of tools — list projects, read and search source, run builds and tests, check for outdated NuGet packages, and get structured build/test failure diagnostics.
It communicates over stdio using the official MCP C# SDK, and is packaged as a .NET tool so it can be installed once and invoked by command name.
See docs/implementation-plan.md for the full design, phase-by-phase implementation history, and architecture notes.
Functional and tested against real .NET solutions, including this repo's own. Packaged as a
.NET tool and published to NuGet.org
via trusted publishing (OIDC) whenever a v* tag is pushed — see
release.yml.
- .NET 10 SDK on the machine running the
server (it shells out to the
dotnetCLI for build/test/package operations).
Install as a global .NET tool from NuGet.org:
dotnet tool install --global DeveloperProjectAssistant.McpServerOr build and install from source instead:
git clone https://github.com/kl00t/developer-project-assistant.git
cd developer-project-assistant
dotnet pack src/DeveloperProjectAssistant.McpServer -c Release
dotnet tool install --global --add-source src/DeveloperProjectAssistant.McpServer/bin/Release --version <version-in-csproj> DeveloperProjectAssistant.McpServerEither way this installs a developer-project-assistant command on your PATH. See
src/DeveloperProjectAssistant.McpServer/README.md
for more on packaging, including a gotcha with prerelease versions and dotnet tool install.
Every tool operates relative to a single project root, fixed when the server starts (see Configuration below) — there is no per-call path to point it at a different repo.
| Tool | What it does | Key parameters |
|---|---|---|
ping |
Checks the server is running. | — |
get_server_info |
Returns the server version and configured project root. | — |
list_projects |
Lists the projects in the .slnx/.sln solution. |
solutionRelativePath (only needed if there's more than one solution file) |
read_file |
Reads a source file, truncating beyond a byte limit. | path, maxBytes (default 512 KB) |
search_symbols |
Syntactic search for classes/methods by name — no build required. | query, kind (Class/Method/Any), exactMatch, caseSensitive, maxResults |
run_build |
Runs dotnet build; returns structured diagnostics by default. |
projectOrSolutionRelativePath, configuration, includeRawOutput, timeoutSeconds |
run_tests |
Runs dotnet test; returns pass/fail/skipped counts and structured failures. |
same as run_build |
report_outdated_packages |
Runs dotnet list package --outdated. Needs network access. |
projectOrSolutionRelativePath, includeRawOutput, timeoutSeconds |
summarize_last_failure |
Re-parses the most recent run_build/run_tests output into structured diagnostics, without re-running it. |
— |
search_symbols' kind: Class matches class declarations only, not record/struct types
(a syntactic-search limitation, not a bug — see the implementation plan for details).
Request:
{ "name": "run_build", "arguments": { "configuration": "Release" } }Response (compact by default — pass "includeRawOutput": true to also get the full stdout/stderr):
{
"success": false,
"exitCode": 1,
"diagnostics": [
{
"filePath": "src/Broken.cs",
"line": 7,
"column": 41,
"severity": "error",
"code": "CS1525",
"message": "Invalid expression term ';'",
"projectPath": "src/Broken.csproj"
}
],
"timedOut": false,
"outputTruncated": false
}Response:
{
"success": false,
"exitCode": 1,
"trxFound": true,
"total": 2,
"passed": 1,
"failed": 1,
"skipped": 0,
"failures": [
{
"testName": "SampleLib.Tests.CalculatorTests.AlwaysFails",
"errorMessage": "Assert.Equal() Failure: Values differ\r\nExpected: 999\r\nActual: 5",
"stackTrace": " at SampleLib.Tests.CalculatorTests.AlwaysFails() in ..."
}
]
}The project root is fixed at launch — one server instance inspects one solution, so a repo that wants this tool available adds its own config pointing at itself.
Option A — project-scoped .mcp.json (recommended): add a .mcp.json at the root of the
solution you want the server to inspect:
// Running from source during development:
{
"mcpServers": {
"developer-project-assistant": {
"command": "dotnet",
"args": [
"run", "--project", "<path-to>/src/DeveloperProjectAssistant.McpServer",
"--", "--project-root", "${workspaceFolder}"
]
}
}
}Option B — the claude mcp add CLI:
claude mcp add developer-project-assistant --scope project -- developer-project-assistant --project-root "<path-to-solution>"Flag names (--scope, argument separators) have changed across Claude Code versions — run
claude mcp add --help to confirm current syntax.
Verifying the connection: either option writes to (or is equivalent to) .mcp.json at the
solution root. After adding it, restart Claude Code (or run /mcp if available in your version)
and confirm developer-project-assistant shows as connected, then ask it to "list the projects
in this solution" to exercise list_projects end-to-end.
See docs/implementation-plan.md §6 for the same setup written up in more detail, plus notes on other MCP clients (VS Code, Visual Studio).
- Trust boundary:
run_buildandrun_testsexecute whatever build/test code exists in the target solution — equivalent in risk to runningdotnet build/dotnet testyourself locally. Only point this server at solutions you trust; this is not sandboxed. - Path confinement: every file-reading and search tool resolves paths through a guard that
refuses anything outside the configured project root, including symlink escapes (best-effort)
and
..traversal. - Process limits: every shelled-out
dotnetinvocation has a timeout (default 5 minutes, configurable per call) and an output-size cap, so a hung or noisy child process can't consume the session indefinitely. - No secret handling: the server never accepts or forwards credentials through tool arguments. Anything the build needs (NuGet feed auth, etc.) must already be available in the ambient environment the server process runs in.
- Errors are generic to the client, detailed in logs: the underlying MCP SDK doesn't forward exception messages to the calling client — every unhandled error becomes the same generic "An error occurred" result. Real error detail (rejected paths, malformed solution files, etc.) is logged to stderr instead, for local troubleshooting.
See docs/implementation-plan.md §7 for the full security notes as originally scoped.
- docs/implementation-plan.md — full design and phase-by-phase history, including deviations from the original plan and things found along the way.
- src/DeveloperProjectAssistant.McpServer/README.md — packaging, local publishing checks, and running from source.
- CLAUDE.md — commit message and branching conventions for this repo (all changes, including documentation, go through a feature branch and PR).
dotnet build
dotnet test