Skip to content

Latest commit

 

History

History
443 lines (299 loc) · 21.2 KB

WORKSPACE_MODES.md

File metadata and controls

443 lines (299 loc) · 21.2 KB

Workspace Modes Reference

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.


Overview

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.


MSBuildWorkspace (SDK) — Full Resolution, SDK MSBuild

When Used

Activated when RoslynMcp finds a .csproj file and either:

  • auto-detection (WorkspaceMode.Auto) classifies it as SDK-style via MSBuildBootstrap.DetectProjectStyle(), or
  • you explicitly force --workspace sdk.

MSBuild discovery then runs through MSBuildBootstrap.EnsureReady().

Capabilities

  • ✅ 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

Requirements

  • MSBuild discoverable — most commonly because the .NET SDK is on PATH
  • Or DOTNET_ROOT points at a valid SDK install
  • Or ROSLYNMCP_MSBUILD_PATH points at a directory containing MSBuild.dll
  • NuGet packages restored — run dotnet restore before first use (or use roslyn_restore_packages tool)

Startup Performance

  • 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

Example Projects

Perfect for:

  • ASP.NET Core applications
  • Class libraries with NuGet dependencies
  • Multi-project solutions
  • Production codebases with complex dependencies

Troubleshooting

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/YourProject

Issue: "MSBuild not found"

Solution: Install .NET SDK. MSBuild is included.


MSBuildWorkspace (VS) — Full Resolution, Visual Studio MSBuild

When Used

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.

Capabilities

Same as SDK mode, plus:

  • ✅ .NET Framework projects — supports .NET Framework 4.6.1+ (requires Visual Studio)
  • ✅ Legacy project formats — handles non-SDK-style .csproj files

Requirements

  • 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

Startup Performance

  • Initial load: usually slower than SDK mode because Visual Studio MSBuild discovery adds overhead
  • Subsequent calls: Instant (workspace is cached in memory)

When to Force This Mode

# CLI flag
RoslynMcp.exe --workspace vs .

# Environment variable
$env:ROSLYNMCP_WORKSPACE = "vs"
RoslynMcp.exe .

Pinning the Visual Studio Version

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.


AdhocWorkspace (Fast, Source-Only)

When Used

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.

Capabilities

  • ✅ Fast startup — no MSBuild overhead
  • ✅ Source-defined type resolution — all types in loaded .cs files resolve
  • ✅ Syntax analysis — full syntax tree parsing
  • ✅ Semantic analysis — symbol resolution for source-defined types
  • ✅ File watching — detects .cs and .editorconfig changes via FileSystemWatcher
  • ✅ .editorconfig — every .editorconfig under the root, and those in the directories above it, are loaded as in a project (see Code style of refactorings and fixes below)

Limitations

  • ❌ No NuGet type resolution — List<T> appears as List<T> (generic), HttpClient unresolved
  • ❌ Fixed parse options — C# preview language version, DEBUG preprocessor symbol defined
  • ❌ Single directory tree — doesn't follow project references
  • ❌ No project-specific settings — can't respect nullable/language version from project file

Startup Performance

  • 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

Example Use Cases

Perfect for:

  • Quick C# scripts without project files
  • Demonstration code snippets
  • Learning/tutorial code
  • Temporary scratch files

Safety Features

AdhocWorkspace includes protection against accidental misuse:

  • Root directory protection — fails fast if you point it at a drive root like C:\ or D:\ (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

Overriding the Mode

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.

Additional Configuration Flags

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).


Choosing a Mode

Use MSBuildWorkspace When:

  • ✅ You have a .csproj file
  • ✅ 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.

Use AdhocWorkspace When:

  • ✅ You're working with standalone .cs files
  • ✅ 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.


Switching Modes

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).

Multi-Project Workflows

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" })

Default Workspace

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:

  1. Root: --root <path> (or a bare positional path) > ROSLYNMCP_ROOT > the server's working directory.
  2. Workspace: a .sln/.slnx/.csproj root is used as-is. A directory resolves to the solution named by "solution" in .madq_roslynmcp.json, else the one .sln/.slnx in it, else the nearest solution above it, else its single .csproj.
  3. Relative paths: a relative projectPath is 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_info reports the default workspace and which setting chose its root.
  4. 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_path and a hint to pass projectPath or --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.


Performance Comparison

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 ⚠️ Limited ✅ Yes ❌ No
Code refactorings and fixes (#389) ✅ Built-in, project (analyzer references) and configured providers ✅ Same ⚠️ Built-in and configured providers only — no project file, so no analyzer references; the source report says so
.editorconfig (#413) ✅ The files the build hands the compiler ✅ Same ✅ Every one under the root and in the directories above it

Code style of refactorings and fixes

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.


Technical Details

MSBuildWorkspace

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

AdhocWorkspace

Implementation: Microsoft.CodeAnalysis.AdhocWorkspace

Initialization:

var workspace = new AdhocWorkspace();
var project = workspace.AddProject("MyProject", LanguageNames.CSharp);

File loading:

  • Enumerates .cs files in directory tree
  • Creates in-memory project with fixed parse options
  • Uses FileSystemWatcher for change detection
  • No external dependency resolution

WorkspaceManager

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

FAQ

Q: Can I force MSBuildWorkspace even if no .csproj exists?

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>

Q: Can I use AdhocWorkspace for a project with .csproj?

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).

Q: Why is MSBuildWorkspace slower on first load?

A: MSBuildWorkspace:

  1. Invokes MSBuild to resolve the project
  2. Downloads/restores NuGet packages (if not cached)
  3. Parses all source files
  4. Builds the semantic model

Subsequent calls are instant because the workspace is cached.

Q: Does AdhocWorkspace support C# 13/14 features?

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.

Q: What happens if I have both .csproj and loose .cs 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.

Q: What's the precedence order for workspace configuration?

A: When multiple sources specify a workspace mode or MSBuild path:

  1. CLI arguments — --workspace, --msbuild-path, --no-buildhost-pin (highest precedence)
  2. Environment variables — ROSLYNMCP_WORKSPACE, ROSLYNMCP_MSBUILD_PATH, ROSLYNMCP_NO_BUILDHOST_PIN
  3. Project file — .madq_roslynmcp.json at repo root (only for workspace mode, the elicit flag, and the vsVersion pin; see below)
  4. Auto-detection — RoslynMcp chooses the best mode based on .csproj structure (lowest precedence)

Example: If .madq_roslynmcp.json specifies "workspace": "adhoc" but you start the server with --workspace sdk, the CLI flag wins.

Q: Can I use a project file to pin workspace settings?

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.


Related


Last Updated: 2026-07-18 (v0.8.1-beta)