RoslynMcp automatically detects the best workspace mode based on your project structure. This document explains the three workspace modes (SDK, VS, and Adhoc), when each is used, and their tradeoffs. The mode can be overridden with the --workspace CLI flag or the ROSLYNMCP_WORKSPACE environment variable.
| Mode | Trigger / Override | Typical Cost | Type Resolution | Multi-Project | Best For |
|---|---|---|---|---|---|
| MSBuildWorkspace (SDK) | .csproj found + auto-detected SDK-style project, or --workspace sdk |
Highest | Full (NuGet + source) | ✅ Yes | Modern SDK-style projects |
| MSBuildWorkspace (VS) | Auto-detected legacy/Framework-style project, or --workspace vs |
Higher | Full (NuGet + source) | ✅ Yes | .NET Framework / legacy Visual Studio projects |
| AdhocWorkspace | No .csproj found, or --workspace adhoc |
Lowest | Source-only | ❌ No | Quick scripts, demos |
RoslynMcp automatically selects the appropriate mode—you don't need to configure anything for most projects. To override, pass --workspace sdk|vs|adhoc|auto on the command line, or set the ROSLYNMCP_WORKSPACE environment variable to the same values.
Activated when RoslynMcp finds a .csproj file and either:
- auto-detection (
WorkspaceMode.Auto) classifies it as SDK-style viaMSBuildBootstrap.DetectProjectStyle(), or - you explicitly force
--workspace sdk.
MSBuild discovery then runs through MSBuildBootstrap.EnsureReady().
- ✅ NuGet package type resolution —
List<T>,HttpClient, Entity Framework, etc. all resolve correctly - ✅ Multi-project support — follows
<ProjectReference>and loads referenced projects - ✅ .NET 6+ projects — best fit for modern SDK-style projects
- ✅ Correct preprocessor symbols — respects
<DefineConstants>from.csproj - ✅ Project-specific language version — uses
<LangVersion>from project file - ✅ Nullable reference types — respects
<Nullable>setting - ✅ ImplicitUsings — resolves global usings from SDK
- MSBuild discoverable — most commonly because the .NET SDK is on PATH
- Or
DOTNET_ROOTpoints at a valid SDK install - Or
ROSLYNMCP_MSBUILD_PATHpoints at a directory containingMSBuild.dll - NuGet packages restored — run
dotnet restorebefore first use (or useroslyn_restore_packagestool)
- Initial load: varies with project size, restore state, and SDK discovery path
- Subsequent calls: Instant (workspace is cached in memory)
- File changes: Automatically detected and incrementally updated
Perfect for:
- ASP.NET Core applications
- Class libraries with NuGet dependencies
- Multi-project solutions
- Production codebases with complex dependencies
Issue: Types from NuGet packages not resolving
Solution: Ensure packages are restored:
dotnet restore YourProject.csproj
# OR use the tool:
roslyn_restore_packages --projectPath src/YourProjectIssue: "MSBuild not found"
Solution: Install .NET SDK. MSBuild is included.
Activated via --workspace vs / ROSLYNMCP_WORKSPACE=vs, or auto-detected when DetectLoadStyle() sees legacy project markers such as ToolsVersion= or TargetFrameworkVersion in the loaded .csproj — or, for a .sln/.slnx, in any project the solution references (one legacy project is enough, since Roslyn loads it through the .NET Framework BuildHost regardless of its siblings). Projects are read from the solution file itself; only when it lists nothing readable does detection fall back to a nearest-first directory scan that skips bin/, obj/, packages/, node_modules/, and dot-folders. Uses the Visual Studio MSBuild instance located via vswhere.
Same as SDK mode, plus:
- ✅ .NET Framework projects — supports .NET Framework 4.6.1+ (requires Visual Studio)
- ✅ Legacy project formats — handles non-SDK-style
.csprojfiles
- Windows — VS mode is Windows-only
- Visual Studio installed — any edition or Build Tools installation discoverable via
vswhere vswhere.exe— bundled with Visual Studio; used to locate the MSBuild instance
- Initial load: usually slower than SDK mode because Visual Studio MSBuild discovery adds overhead
- Subsequent calls: Instant (workspace is cached in memory)
# CLI flag
RoslynMcp.exe --workspace vs .
# Environment variable
$env:ROSLYNMCP_WORKSPACE = "vs"
RoslynMcp.exe .Legacy projects load through Roslyn's out-of-process .NET Framework BuildHost, which picks the highest-versioned Visual Studio it can find — on its own, regardless of what the server resolved. When that newest install is one the BuildHost cannot load (a VS 2026 / 18.x install has been observed to fail with a Microsoft.Build.Shared.XMakeElements type-initializer error), pin the version instead of uninstalling:
| Source | Example |
|---|---|
| CLI | --vs-version 17 |
| Environment | ROSLYNMCP_VS_VERSION=17 |
| Project file | "vsVersion": "17" in .madq_roslynmcp.json |
Accepted forms: a major (17 → [17.0,18.0)), a major.minor (17.14 → [17.14,17.15)), an exact three- or four-part version, or a raw vswhere -version range such as [17.0,18.0). vswhere picks the newest install inside the range.
The pin applies in every mode except adhoc. In vs mode it selects the instance the server resolves and fails with a clear message when nothing matches. In auto/sdk mode the server keeps the .NET SDK for itself and only steers the BuildHost — the part that actually loads legacy projects. --msbuild-path, being more specific, still wins over the version pin. A version is portable across machines, which is why — unlike msbuildPath — it is honored from the committed project file. Precedence: CLI arg > env var > project file > newest installed.
Activated when RoslynMcp resolves your projectPath to a directory with no .csproj, or when you explicitly force --workspace adhoc. It creates an in-memory C# project rooted at that directory and scans for .cs files directly.
- ✅ Fast startup — no MSBuild overhead
- ✅ Source-defined type resolution — all types in loaded
.csfiles resolve - ✅ Syntax analysis — full syntax tree parsing
- ✅ Semantic analysis — symbol resolution for source-defined types
- ✅ File watching — detects
.csand.editorconfigchanges viaFileSystemWatcher - ✅
.editorconfig— every.editorconfigunder the root, and those in the directories above it, are loaded as in a project (see Code style of refactorings and fixes below)
- ❌ No NuGet type resolution —
List<T>appears asList<T>(generic),HttpClientunresolved - ❌ Fixed parse options — C# preview language version,
DEBUGpreprocessor symbol defined - ❌ Single directory tree — doesn't follow project references
- ❌ No project-specific settings — can't respect nullable/language version from project file
- Initial load: usually much faster than MSBuild modes because no MSBuild discovery or project evaluation runs
- Subsequent calls: Instant
- File changes: Automatically detected via
FileSystemWatcher
Perfect for:
- Quick C# scripts without project files
- Demonstration code snippets
- Learning/tutorial code
- Temporary scratch files
AdhocWorkspace includes protection against accidental misuse:
- Root directory protection — fails fast if you point it at a drive root like
C:\orD:\(prevents scanning entire drives) - UnauthorizedAccessException handling — skips directories it can't access
- Exclusion patterns — automatically skips:
node_modules/bin/,obj/.git/.vs/packages/- Hidden directories
- System directories
By default RoslynMcp auto-detects the best mode. To override:
| Method | Syntax |
|---|---|
| CLI flag | RoslynMcp.exe --workspace sdk|vs|adhoc|auto <path> |
| Environment variable | ROSLYNMCP_WORKSPACE=sdk|vs|adhoc|auto |
| Project file | .madq_roslynmcp.json at repo root: { "workspace": "sdk|vs|adhoc" } |
Priority for a concrete mode: CLI flag → env var → project file → auto-detect; an explicit auto leaves the project-file setting eligible.
MSBuild path override (force a specific MSBuild location):
| Method | Syntax |
|---|---|
| CLI flag | RoslynMcp.exe --msbuild-path <dir> |
| Environment variable | ROSLYNMCP_MSBUILD_PATH=<dir> |
Pass a .NET SDK directory or a VS MSBuild\Current\Bin directory. Honored in auto, sdk, and vs modes; ignored in adhoc. An invalid path is not fatal — the server logs it and falls through to normal discovery.
Priority: CLI flag → env var.
BuildHost pinning control (pin Roslyn's out-of-process .NET Framework BuildHost to a specific VS instance):
| Method | Syntax |
|---|---|
| CLI flag | RoslynMcp.exe --no-buildhost-pin |
| Environment variable | ROSLYNMCP_NO_BUILDHOST_PIN=true |
By default, RoslynMcp pins the BuildHost when a Visual Studio MSBuild instance is selected (or a vsVersion pin selects one in auto/sdk), fixing a bug where .NET Framework projects failed with a TypeInitializationException when multiple VS installs exist. Pass --no-buildhost-pin to disable pinning (let the BuildHost choose on its own).
Priority: CLI flag → env var (default: pinning enabled).
Symbol ambiguity handling (enable interactive picker for ambiguous symbol matches):
| Method | Syntax |
|---|---|
| CLI flag | RoslynMcp.exe --elicit |
| Environment variable | ROSLYNMCP_ELICIT=true |
| Project file | .madq_roslynmcp.json at repo root: { "elicit": true } |
By default, tools like roslyn_preview_rename return a structured list of candidate symbols when the name is ambiguous, so agents can self-recover via containingType or file location. Pass --elicit to instead prompt the user interactively (requires an MCP client that supports elicitation). The project file can pin a default for the repo.
Priority: CLI flag → env var → project file (default: interactive prompts disabled).
- ✅ You have a
.csprojfile - ✅ You need NuGet type resolution
- ✅ You're working on production code
- ✅ You need multi-project support
- ✅ You need project-specific settings (nullable, language version)
How: Just point RoslynMcp at a directory containing a .csproj file. It auto-detects.
- ✅ You're working with standalone
.csfiles - ✅ You need fastest possible startup
- ✅ You're doing quick experiments or demos
- ✅ You don't need external dependencies to resolve
How: Point RoslynMcp at a directory containing .cs files but no .csproj. It auto-detects.
RoslynMcp auto-detects the mode per projectPath. To force a specific mode for a session, use the --workspace flag or ROSLYNMCP_WORKSPACE env var (see Overriding the Mode above).
Every tool takes a projectPath parameter, so you can work with multiple modes in a single session:
// Example: workspace with both project types
{
"servers": {
"MadQ.RoslynMcp": {
"command": "/path/to/RoslynMcp.exe",
"args": ["--root", "/path/to/repo"]
}
}
}Then in tool calls:
// MSBuildWorkspace project (has .csproj)
roslyn_get_diagnostics({ projectPath: "src/MyApp" })
// AdhocWorkspace project (no .csproj)
roslyn_get_diagnostics({ projectPath: "scripts" })
// Omitted: the default workspace from --root (search, read, edit, and build tools)
roslyn_search_files({ pattern: "TODO" })A tool call that omits projectPath uses the session's default workspace, resolved once from startup inputs only — never from whichever workspaces happen to be cached:
- Root:
--root <path>(or a bare positional path) >ROSLYNMCP_ROOT> the server's working directory. - Workspace: a
.sln/.slnx/.csprojroot is used as-is. A directory resolves to the solution named by"solution"in.madq_roslynmcp.json, else the one.sln/.slnxin it, else the nearest solution above it, else its single.csproj. - Relative paths: a relative
projectPathis resolved against the default workspace's directory when it names something there, and against the server's working directory otherwise. Tools report paths relative to the workspace root, so those can be passed back as they are.roslyn_inforeports the default workspace and which setting chose its root. - Guards: drive roots and system directories are rejected, the search never goes downward, and the default is never an AdhocWorkspace. With no usable default, the call fails with
missing_project_pathand a hint to passprojectPathor--root.
File-based tools (roslyn_read_file, roslyn_get_file_outline, the editing tools, …) then pick the project in that solution that contains the file, so a file in any project works. Build, clean, and restore target the whole solution. A .sln/.slnx path is also accepted as an explicit projectPath.
| Operation | MSBuildWorkspace (SDK) | MSBuildWorkspace (VS) | AdhocWorkspace |
|---|---|---|---|
| Initial load | Variable | Variable (usually slower than SDK) | Usually fastest |
| Type resolution | Full (NuGet + source) | Full (NuGet + source) | Source-only |
| Memory usage | Project-dependent | Project-dependent | Lower, but still project-dependent |
| File change detection | Roslyn internal + FileSystemWatcher | Roslyn internal + FileSystemWatcher | FileSystemWatcher |
| Multi-project | ✅ Yes | ✅ Yes | ❌ No |
| .NET Framework | ✅ Yes | ❌ No | |
| Code refactorings and fixes (#389) | ✅ Built-in, project (analyzer references) and configured providers | ✅ Same | |
.editorconfig (#413) |
✅ The files the build hands the compiler | ✅ Same | ✅ Every one under the root and in the directories above it |
The code a refactoring or a code fix writes (roslyn_apply_refactoring, roslyn_apply_code_fix) is formatted by Roslyn, and Roslyn takes the style from your .editorconfig: indent_style, csharp_new_line_before_open_brace, csharp_space_after_keywords_in_control_flow_statements and the rest of the formatting options. Without one it uses its own defaults (four spaces, every brace on its own line, foreach (), whatever the file around the change looks like. This holds in all three modes. The file does not have to be committed: one that .gitignore lists is read like any other, so a private .editorconfig at the repository root is enough.
As in a project, an .editorconfig also sets diagnostic severities (dotnet_diagnostic.<id>.severity) in an AdhocWorkspace.
An AdhocWorkspace keeps the files under its root current as they change. One in a directory above the root is read when the workspace loads; a change to it is picked up by the next server start. An MSBuild workspace reloads when an .editorconfig it tracks changes.
Implementation: Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace
Initialization:
var workspace = MSBuildWorkspace.Create();
var project = await workspace.OpenProjectAsync(projectPath);Project loading:
- Resolves NuGet packages via MSBuild
- Follows
<ProjectReference>and loads dependencies - Respects all project settings (language version, nullable, preprocessor symbols)
- Uses Roslyn's internal file change detection
Implementation: Microsoft.CodeAnalysis.AdhocWorkspace
Initialization:
var workspace = new AdhocWorkspace();
var project = workspace.AddProject("MyProject", LanguageNames.CSharp);File loading:
- Enumerates
.csfiles in directory tree - Creates in-memory project with fixed parse options
- Uses
FileSystemWatcherfor change detection - No external dependency resolution
RoslynMcp's WorkspaceManager (split into WorkspaceManager.cs, .Resolution.cs, .Instance.cs and .Instance.*.cs) loads the full solution when a single .slnx or .sln is found while walking upward from a resolved .csproj path, and caches workspace instances with LRU eviction:
- Cache key: Solution path (or .csproj/directory if no solution found)
- Cache size: Configurable via
ROSLYNMCP_MAX_CACHED_WORKSPACES(default 5) - Path inference cache: Relative-path inference can be disabled via
ROSLYNMCP_DISABLE_PATH_CACHE=true - Thread-safety: Lock-protected cache lookups with short critical section; loading outside lock to avoid contention
- Invalidation: FileSystemWatcher (both MSBuild and Adhoc) + manual via
InvalidateFile() - Per-project compilation cache — each project in a solution has its own cached compilation
A: No. The --workspace sdk and --workspace vs flags select which MSBuild instance to use, but both still require a .csproj to load. Without one, RoslynMcp falls back to AdhocWorkspace regardless of the flag. To use MSBuildWorkspace, create a minimal .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>A: Yes — pass --workspace adhoc (or set ROSLYNMCP_WORKSPACE=adhoc) to force AdhocWorkspace even when a .csproj exists. This skips MSBuild entirely for faster startup with reduced semantics (no NuGet resolution, no project references).
A: MSBuildWorkspace:
- Invokes MSBuild to resolve the project
- Downloads/restores NuGet packages (if not cached)
- Parses all source files
- Builds the semantic model
Subsequent calls are instant because the workspace is cached.
A: Yes. AdhocWorkspace uses LangVersion.Preview by default, so all latest C# features are supported at the syntax level. Semantic features that require external types (e.g., required with data annotations) may not resolve if those types aren't in loaded source files.
A: MSBuildWorkspace takes precedence. The .cs files included in the .csproj via <Compile Include="..."> (or implicitly via SDK glob patterns) will be loaded. Loose .cs files not referenced by the project are ignored.
A: When multiple sources specify a workspace mode or MSBuild path:
- CLI arguments —
--workspace,--msbuild-path,--no-buildhost-pin(highest precedence) - Environment variables —
ROSLYNMCP_WORKSPACE,ROSLYNMCP_MSBUILD_PATH,ROSLYNMCP_NO_BUILDHOST_PIN - Project file —
.madq_roslynmcp.jsonat repo root (only forworkspacemode, theelicitflag, and thevsVersionpin; see below) - Auto-detection — RoslynMcp chooses the best mode based on
.csprojstructure (lowest precedence)
Example: If .madq_roslynmcp.json specifies "workspace": "adhoc" but you start the server with --workspace sdk, the CLI flag wins.
A: Yes — create a .madq_roslynmcp.json file at the repository root:
{
"workspace": "vs",
"elicit": true,
"vsVersion": "17"
}Note: Only the portable keys workspace, elicit, and vsVersion can be set in the project file. Machine-specific settings — MSBuild path (msbuild-path) and no-buildhost-pin — are deliberately never read from a committed file (a hostile repo must not point the server at arbitrary local paths); pass them via CLI args or environment variables instead. A Visual Studio version is allowed because it selects among installs vswhere already knows about and never names a path.
Priority: CLI arg > env var > project file > default.
- README.md § Workspace modes — quick overview
- INSTALLATION.md § Troubleshooting — setup issues
- AGENTS.md § Architecture — WorkspaceManager implementation details
Last Updated: 2026-07-18 (v0.8.1-beta)