This document provides guidance for AI orchestrators and MCP clients on which .NET MCP Server tools can safely run in parallel.
The .NET MCP Server provides 10 consolidated tools (8 domain tools + 2 utilities) with multiple actions each. Understanding which operations can run concurrently is essential for:
- AI orchestrators that execute multiple operations simultaneously
- MCP clients that batch or parallelize requests
- Performance optimization when working with large solutions or multiple projects
As of v1.0+, the server implements automatic concurrency control for long-running and mutating operations. Conflicting operations are automatically rejected with a CONCURRENCY_CONFLICT error code.
| Can Run in Parallel | Tool Categories | Implementation |
|---|---|---|
| ✅ Yes - Safe | Read-only operations (Info, List, Search, Check) | No locking needed |
| Mutating operations on different files/projects | Automatic conflict detection | |
| ❌ No - Unsafe | Mutating operations on same file/project, long-running operations | Returns CONCURRENCY_CONFLICT error |
The .NET MCP Server automatically prevents conflicting operations from running simultaneously. When a conflict is detected, the server returns a structured error:
{
"success": false,
"errors": [{
"code": "CONCURRENCY_CONFLICT",
"message": "Cannot execute 'build' on '/path/to/project.csproj' because a conflicting operation is already in progress: build on /path/to/project.csproj (started at 2025-11-01 12:34:56)",
"category": "Concurrency",
"hint": "Wait for the conflicting operation to complete, or cancel it before retrying this operation."
}],
"exitCode": -1
}This applies to:
- Long-running operations:
dotnet_projectactions: Build, Run, Test, Publish, Watch - Mutating operations:
dotnet_packageactions: Add, Remove;dotnet_solutionactions: Add, Remove - Global operations:
dotnet_sdkactions: ClearTemplateCache, InstallTemplatePack, UninstallTemplatePack;dotnet_dev_certsactions: CertificateTrust, CertificateClean
These operations can always run in parallel with any other operations, including themselves. They do not modify state and are safe to execute concurrently.
| Tool | Actions | Description | Parallel Safe |
|---|---|---|---|
| dotnet_sdk | Version, Info, ListSdks, ListRuntimes, ListTemplates, SearchTemplates, TemplateInfo, ListTemplatePacks, FrameworkInfo, CacheMetrics | Query SDK, runtime, template, and framework information | ✅ Always |
| dotnet_package | Search, List | Search NuGet packages and list package references | ✅ Always |
| dotnet_solution | List | List projects in solution | ✅ Always |
| dotnet_tool | List, Search | List and search for .NET tools | ✅ Always |
| dotnet_dev_certs | CertificateCheck, SecretsList | Check certificate status and list user secrets | ✅ Always |
| dotnet_project | Analyze, Dependencies, Validate | Analyze project metadata and dependencies | ✅ Always |
| dotnet_ef | MigrationsList, DbContextList, DbContextInfo | List migrations and DbContext information | ✅ Always |
| dotnet_workload | List, Info, Search | List and search workloads | ✅ Always |
| dotnet_help | (all) | Get help for dotnet commands | ✅ Always |
| dotnet_server_capabilities | (all) | Get MCP server capabilities | ✅ Always |
Key Characteristics:
- No file system modifications
- No state changes
- Idempotent operations
- Can be cached safely
- Multiple concurrent executions produce identical results
These operations can run in parallel IF they operate on different targets (different projects, packages, or solutions). Running them on the same target concurrently may cause conflicts.
| Tool | Actions | Parallel Conditions | Risk Level |
|---|---|---|---|
| dotnet_project | Build, Restore, Clean, Test, Publish, Pack, Format | Different projects or solutions | |
| dotnet_package | Add, Remove, Update, AddReference, RemoveReference, ClearCache | Different projects or cache types | |
| dotnet_solution | Add, Remove | Different solutions | |
| dotnet_tool | Install, Uninstall, Update, Restore | Different tools or scopes (global vs local) | |
| dotnet_dev_certs | CertificateExport, SecretsSet, SecretsRemove, SecretsClear | Different projects or output paths | |
| dotnet_ef | MigrationsAdd, MigrationsRemove, MigrationsScript, DatabaseUpdate, DatabaseDrop, DbContextScaffold | Different projects or databases | |
| dotnet_workload | Install, Uninstall, Update | Different workloads |
Safety Guidelines:
- ✅ Safe:
dotnet_projectBuild on Project A and Project B simultaneously (if no dependencies between them) - ✅ Safe:
dotnet_packageAdd different packages to different projects concurrently - ❌ Unsafe:
dotnet_packageAdd two packages to the same project concurrently - ❌ Unsafe:
dotnet_projectBuild the same project twice simultaneously - ❌ Unsafe:
dotnet_solutionAdd projects from multiple operations to same solution file
These operations should NEVER run in parallel with themselves or similar operations, as they modify global state, run indefinitely, or create file system conflicts.
| Tool | Actions | Reason | Risk Level |
|---|---|---|---|
| dotnet_project | New, Run, Watch | Creates files/directories or long-running process | ❌ Critical |
| dotnet_solution | Create | Creates solution file | ❌ Critical |
| dotnet_tool | Run | May be long-running; depends on tool | ❌ High |
| dotnet_dev_certs | CertificateTrust, CertificateClean | Modifies system trust store | ❌ Critical |
| dotnet_sdk | ClearTemplateCache, InstallTemplatePack, UninstallTemplatePack | Modifies global template state and/or clears caches | ❌ High |
| dotnet_ef | DatabaseUpdate, DatabaseDrop | Modifies database schema | ❌ Critical |
| dotnet_workload | Install, Update | Modifies global SDK installation | ❌ High |
Key Considerations:
- These operations often modify global state (certificates, global tools, caches)
- File watchers run indefinitely and should not be duplicated
- Project creation can conflict if output directories overlap
- Running applications hold ports and resources
Beyond simple parallelization, consider project dependencies:
❌ UNSAFE: Parallel Execution Ignoring Dependencies
┌─────────────────┐ ┌─────────────────┐
│ Build Project A │ │ Build Project B │
│ (references B) │ ──X→│ │
└─────────────────┘ └─────────────────┘
Run simultaneously? NO - A depends on B
✅ SAFE: Sequential Execution Respecting Dependencies
┌─────────────────┐ ┌─────────────────┐
│ Build Project B │ ──→ │ Build Project A │
│ │ │ (references B) │
└─────────────────┘ └─────────────────┘
Step 1: Build B first, then A
✅ SAFE: Parallel Independent Projects
┌─────────────────┐ ┌─────────────────┐
│ Build Project A │ │ Build Project C │
│ │ │ │
└─────────────────┘ └─────────────────┘
No dependencies? Safe to run in parallel
Rules:
- Always build dependencies before dependent projects
- Test projects can usually run in parallel if they don't share state
- Multiple independent projects in a solution can build concurrently
Many operations have implicit ordering requirements:
- dotnet_project Restore action must complete before Build action
- dotnet_tool Restore action must complete before Run action
- dotnet_package Add action should complete before dotnet_project Build action
Thread 1: dotnet_package { action: "Add", packageId: "PackageA", project: "MyProject.csproj" }
Thread 2: dotnet_package { action: "Add", packageId: "PackageB", project: "MyProject.csproj" }
Result: Race condition, possible corruption or lost changes
Thread 1: dotnet_solution { action: "Add", projects: ["ProjectA.csproj"], solution: "MySolution.sln" }
Thread 2: dotnet_solution { action: "Add", projects: ["ProjectB.csproj"], solution: "MySolution.sln" }
Result: One operation may be lost or file corrupted
Thread 1: dotnet_project { action: "Build", project: "ProjectA.csproj" }
Thread 2: dotnet_project { action: "Build", project: "ProjectB.csproj" }
Result: Generally safe if no interdependencies, but MSBuild may serialize internally
Thread 1: dotnet_project { action: "Publish", project: "ProjectA.csproj", output: "/output" }
Thread 2: dotnet_project { action: "Publish", project: "ProjectB.csproj", output: "/output" }
Result: File conflicts, overwritten outputs
Running multiple web applications simultaneously can cause port conflicts:
❌ UNSAFE:
Thread 1: dotnet_project { action: "Run", project: "WebAppA.csproj" } (uses port 5000)
Thread 2: dotnet_project { action: "Run", project: "WebAppB.csproj" } (tries to use port 5000)
Result: Second process fails with "address already in use"
The global NuGet cache can handle concurrent access, but operations may serialize:
⚠️ SLOWED BUT SAFE:
Thread 1: dotnet_package { action: "Add", packageId: "PackageX" } (downloads package X)
Thread 2: dotnet_package { action: "Add", packageId: "PackageY" } (downloads package Y)
Result: Both succeed but may be slower due to NuGet lock files
Scenario: Gather information about the development environment
✅ SAFE - Execute in Parallel:
┌──────────────────────────────────────┐
│ dotnet_sdk { action: "ListSdks" } │ ───┐
└──────────────────────────────────────┘ │
┌──────────────────────────────────────┐ │
│ dotnet_sdk { action: "ListTemplates" }───┤ All execute
└──────────────────────────────────────┘ │ concurrently
┌──────────────────────────────────────┐ │
│ dotnet_package { action: "Search" } │ ───┘
└──────────────────────────────────────┘
Scenario: Add multiple packages to a project
✅ SAFE - Execute Sequentially:
┌───────────────────────────────────────────────────────┐
│ dotnet_package { action: "Add", packageId: "Pkg1" } │
└───────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────┐
│ dotnet_package { action: "Add", packageId: "Pkg2" } │
└───────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────┐
│ dotnet_project { action: "Restore" } │
└───────────────────────────────┘
Scenario: Build multiple independent projects
✅ SAFE - Execute in Parallel (if no dependencies):
┌──────────────────────────────────────────┐ ┌──────────────────────────────────────────┐
│ dotnet_project { action: "Build", │ │ dotnet_project { action: "Build", │
│ project: "ProjA.csproj"} │ │ project: "ProjC.csproj"}│
└──────────────────────────────────────────┘ └──────────────────────────────────────────┘
Scenario: Build projects with dependencies
✅ SAFE - Respect Dependencies:
┌──────────────────────────────────────────┐
│ dotnet_project { action: "Build", │
│ project: "ProjB.csproj"}│
│ (no dependencies) │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ dotnet_project { action: "Build", │
│ project: "ProjA.csproj"}│
│ (references B) │
└──────────────────────────────────────────┘
The .NET MCP Server implements caching for read-only resources:
- Templates: Cached for 5 minutes (300 seconds)
- SDK Info: Cached for 5 minutes (300 seconds)
- Runtime Info: Cached for 5 minutes (300 seconds)
✅ Thread-Safe: All cache operations use SemaphoreSlim for async locking
✅ Concurrent Reads: Multiple parallel reads are safe and efficient
dotnet_sdk { action: "ClearTemplateCache" }, InstallTemplatePack, and UninstallTemplatePack should not run concurrently with template operations
| Tool Type | Same Target | Different Targets | With Any Tool |
|---|---|---|---|
| Read-only | ✅ Safe | ✅ Safe | ✅ Safe |
| Mutating (projects) | ❌ Unsafe | ||
| Long-running (watch, run) | ❌ Unsafe | ❌ Unsafe | ❌ Unsafe |
| Global state (cache, certs) | ❌ Unsafe | ❌ Unsafe | ❌ Unsafe |
* Conditional on no dependencies or shared resources
- Default to Sequential - When in doubt, execute operations sequentially
- Parallelize Reads - Read-only operations can always be parallelized safely
- Check Dependencies - Analyze project references before parallel builds
- Separate by Scope - Parallel operations should work on separate projects/solutions
- Batch Similar Operations - Group package additions into a single sequential batch
- Avoid Parallel Writes - Never modify the same file from multiple threads
- Monitor Long-Running - Track and manage long-running operations separately
- Respect Lock Files - Be aware that NuGet and MSBuild use lock files internally
When parallel operations fail, consider these common causes:
| Error Pattern | Likely Cause | Solution |
|---|---|---|
| CONCURRENCY_CONFLICT | Attempting to run conflicting operations simultaneously | Wait for the first operation to complete before starting the second |
| "File is being used by another process" | Concurrent writes to same file (rare with v1.1+ automatic control) | Should not occur with automatic concurrency control |
| "Port already in use" | Multiple run commands | Use different ports or wait for first to complete |
| "Project file could not be loaded" | Simultaneous solution modifications | Automatic conflict detection prevents this |
| "Unable to acquire lock" | NuGet package restore conflicts | Retry or serialize restore operations |
| OPERATION_CANCELLED | Operation was cancelled via CancellationToken | Normal cancellation - no action needed |
When you receive a CONCURRENCY_CONFLICT error:
- Check the conflicting operation - The error message identifies what's blocking your operation
- Wait for completion - Most operations complete quickly; retry after a short delay
- Cancel if needed - Use cancellation tokens to terminate long-running operations
- Use different targets - Operate on different projects/solutions to avoid conflicts
Example retry logic (plain text mode):
var maxRetries = 3;
var retryDelay = TimeSpan.FromSeconds(2);
for (int i = 0; i < maxRetries; i++)
{
var result = await dotnetProjectBuild(project: "MyProject.csproj", machineReadable: false);
if (!result.Contains("CONCURRENCY_CONFLICT"))
break; // Success or different error
if (i < maxRetries - 1)
await Task.Delay(retryDelay);
}Example retry logic (machine-readable mode):
using System.Text.Json;
var maxRetries = 3;
var retryDelay = TimeSpan.FromSeconds(2);
for (int i = 0; i < maxRetries; i++)
{
var result = await dotnetProjectBuild(project: "MyProject.csproj", machineReadable: true);
// Parse the JSON result and check for CONCURRENCY_CONFLICT error code
try
{
using var doc = JsonDocument.Parse(result);
var root = doc.RootElement;
// Check if it's a success response
if (root.TryGetProperty("success", out var success) && success.GetBoolean())
break; // Success
// Check for CONCURRENCY_CONFLICT error code
if (root.TryGetProperty("errors", out var errors) && errors.GetArrayLength() > 0)
{
var firstError = errors[0];
if (firstError.TryGetProperty("code", out var code) &&
code.GetString() != "CONCURRENCY_CONFLICT")
break; // Different error, don't retry
}
}
catch (JsonException)
{
// Not valid JSON, treat as different error
break;
}
if (i < maxRetries - 1)
await Task.Delay(retryDelay);
}To test concurrent tool execution:
- Use
dotnet_sdk { action: "CacheMetrics" }to verify caching behavior - Monitor file system for lock contention
- Check process handles for resource conflicts
- Review MCP server logs for race conditions
- SDK Integration Details - Learn about caching implementation
- Advanced Topics - Performance optimization and logging
- Model Context Protocol - Official MCP specification
- v1.0 (2026-01-09) - Initial release with consolidated tools and automatic concurrency control
- Introduced
ConcurrencyManagerfor conflict detection - Added
CONCURRENCY_CONFLICTerror code - Consolidated tool interface with action-based parameters
- Implemented
CancellationTokensupport throughout execution chain - Added
isLongRunningmetadata to appropriate tools
- Introduced
- v1.0 (2025-10-31) - Initial concurrency safety documentation
The .NET MCP Server supports graceful cancellation of long-running operations via CancellationToken. When a cancellation is requested:
- Process termination - The underlying dotnet process is killed (with entire process tree)
- Partial results - Any output captured before cancellation is included in the response
- Structured error - Returns
OPERATION_CANCELLEDerror code in machine-readable mode
// Start a long-running test operation
var cts = new CancellationTokenSource();
var testTask = DotNetCommandExecutor.ExecuteCommandAsync(
"test MyProject.Tests.csproj",
logger,
machineReadable: true,
cts.Token
);
// Cancel after 30 seconds if not complete
cts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
var result = await testTask;
// Process result
}
catch (OperationCanceledException)
{
// Operation was cancelled
}{
"success": false,
"errors": [{
"code": "OPERATION_CANCELLED",
"message": "The operation was cancelled by the user",
"category": "Cancellation",
"hint": "The command was terminated before completion",
"rawOutput": "Partial test output..."
}],
"exitCode": -1
}All operations support cancellation, but it's most useful for:
- Long-running tests - Large test suites that take minutes to complete
- Build operations - Complex solutions with many projects
- Run operations - Applications that would otherwise run indefinitely
- Watch operations - File watchers that run until cancelled
- Publish operations - Deployment tasks with long durations
The .NET MCP Server provides process session management for tracking and controlling long-running operations like dotnet run and dotnet watch. This feature helps prevent file-lock errors (MSB3026/MSB3027) that occur when multiple builds access the same output files.
The dotnet_project tool includes a Stop action that can terminate long-running process sessions by their session ID.
// Stop a running process session
await callTool("dotnet_project", {
action: "Stop",
sessionId: "abc-123-def-456"
});Response (Success):
{
"success": true,
"output": "Successfully stopped session 'abc-123-def-456'",
"exitCode": 0,
"metadata": {
"sessionId": "abc-123-def-456",
"stopped": "true"
}
}Response (Session Not Found):
{
"success": false,
"errors": [{
"code": "INVALID_PARAMS",
"message": "Session 'abc-123-def-456' not found. It may have already completed or been stopped.",
"category": "Validation",
"hint": "Verify the session ID is correct and the process hasn't already exited"
}]
}When a session is stopped:
- The entire process tree is terminated using
Process.Kill(entireProcessTree: true) - All child processes are cleaned up to prevent orphaned processes
- The session is removed from tracking
- No lingering file locks remain
Problem: Long-running dotnet run processes cause build errors:
MSB3026: Could not copy "MyApp.dll" to "bin\Debug\net10.0\MyApp.dll".
The process cannot access the file because it is being used by another process.
Solution: Use the Stop action to cleanly terminate running processes:
- For manually registered sessions: Use ProcessSessionManager.RegisterSession() to track processes and obtain a session ID
- Call
dotnet_projectwithaction: "Stop"and the session ID to terminate the process - Proceed with build/test operations without file lock conflicts
Note: Automatic session registration for Run/Watch actions is planned for a future enhancement.
Session IDs are GUIDs (e.g., "550e8400-e29b-41d4-a716-446655440000") generated when a process session is registered.
Note: The current implementation provides the infrastructure for process session management (ProcessSessionManager class with RegisterSession, TryStopSession, etc.), but Run and Watch actions do not yet automatically register sessions or return session IDs in their output. The Stop action is available and functional for any manually registered sessions. A future enhancement will integrate session registration into Run/Watch operations to provide automatic session ID tracking.
Note: This documentation applies to .NET MCP Server v1.2+. Concurrency characteristics may change in future versions based on .NET SDK updates and MCP protocol enhancements.