AI Native  

Benchmarking MCP C# SDK 2.0 Under Concurrent Tool Workloads

Model Context Protocol has moved from a developer experiment into an important integration layer for AI applications. As MCP servers begin handling more simultaneous tool calls, performance questions become more important than simply asking whether a server works.

For .NET developers, the release of MCP C# SDK 2.0 is especially significant. The official C# SDK is maintained in collaboration with Microsoft, and version 2.0 aligns with the MCP 2026-07-28 protocol revision. The stable ModelContextProtocol 2.0.0 package is available on NuGet.

But SDK version changes should not be evaluated only by API differences. A production MCP server also needs to be evaluated under concurrent tool workloads.

This article presents a practical benchmark methodology for measuring latency, throughput, concurrency, failures, and resource usage in an MCP C# server. The examples are designed so developers can run the tests against their own environment instead of relying on fabricated benchmark numbers.

Why Benchmark MCP Tool Workloads?

A simple MCP test usually looks like this:

Client
   |
   v
MCP Server
   |
   v
Tool
   |
   v
Response

That tells you whether the integration works.

Production traffic looks different:

                    +--> Tool A
                    |
Client --> MCP Server --> Tool B
                    |
                    +--> Tool C
                    |
                    +--> Tool D

Several clients may call tools at the same time. Some tools may perform CPU-intensive work, while others wait on databases or HTTP APIs.

The resulting performance depends on more than the SDK itself.

Important variables include:

  • Number of concurrent requests

  • Tool execution time

  • JSON serialization and deserialization

  • HTTP transport behavior

  • Connection reuse

  • Server CPU and memory

  • Downstream service latency

  • Payload size

  • Client-side concurrency

  • Application-level locking

Therefore, an honest benchmark should measure the complete workload and clearly document the environment.

What Changed in MCP C# SDK 2.0?

The C# SDK 2.0 release aligns with the 2026-07-28 MCP specification. Among the major changes are discovery-first negotiation, stateless-by-default HTTP, multi-round-trip requests, caching hints, standardized headers, and dedicated extension packages for MCP Apps and Tasks. The SDK also maintains interoperability with earlier protocol versions through negotiation.

The architectural change that matters particularly for concurrent HTTP workloads is the move toward stateless servers.

The .NET blog demonstrates the new configuration:

builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

The SDK's HTTP transport is now stateless by default. A stateless server can be placed behind a load balancer without requiring transport-session synchronization between instances.

That makes concurrency testing especially relevant.

Building a Benchmark MCP Server

Start with the main package:

dotnet add package ModelContextProtocol --version 2.0.0
dotnet add package ModelContextProtocol.AspNetCore --version 2.0.0

The SDK separates functionality into packages. ModelContextProtocol.Core provides lower-level APIs, ModelContextProtocol provides hosting and discovery functionality, and ModelContextProtocol.AspNetCore adds HTTP server support.

Create a minimal ASP.NET Core MCP server:

using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.Stateless = true;
    })
    .WithToolsFromAssembly();

var app = builder.Build();

app.MapMcp();

app.Run("http://localhost:3001");

[McpServerToolType]
public static class BenchmarkTools
{
    [McpServerTool]
    [Description("Returns a small response for latency testing.")]
    public static string Ping(string value)
    {
        return $"pong:{value}";
    }
}

The tool is intentionally simple.

That is important because a benchmark should isolate the component being measured. If Ping performs a database query, an HTTP request, and JSON transformation, the benchmark becomes primarily a test of those dependencies.

Designing Useful Benchmark Workloads

One workload is not enough.

A practical benchmark should contain several scenarios.

WorkloadPurposeMain Metrics
Sequential callsEstablish baselineAverage latency
Low concurrencyNormal application trafficp50/p95 latency
Medium concurrencyTypical scaling testThroughput and latency
High concurrencySaturation testingp95/p99 and errors
Large payloadSerialization behaviorLatency and memory
Slow toolAsync behaviorConcurrency efficiency
Mixed toolsRealistic workloadOverall throughput

For example, start with:

Concurrency: 1
Concurrency: 10
Concurrency: 50
Concurrency: 100
Concurrency: 250

The exact values should be adapted to the environment being tested.

Do not describe these levels as production capacity. They are experimental workload levels.

Measuring Latency Correctly

Average latency is useful, but it should not be your only metric.

Suppose 99 requests complete quickly and one request takes several seconds. The average may hide that outlier.

At minimum, record:

  • p50 latency

  • p95 latency

  • p99 latency

  • Minimum latency

  • Maximum latency

  • Requests per second

  • Failed requests

A simple benchmark client can invoke the MCP tool concurrently using CallToolAsync. The SDK exposes CallToolAsync as an asynchronous API on McpClient.

A simplified benchmark loop looks like this:

var stopwatch = Stopwatch.StartNew();

var tasks = Enumerable.Range(0, concurrency)
    .Select(async i =>
    {
        var start = Stopwatch.GetTimestamp();

        try
        {
            await client.CallToolAsync(
                "Ping",
                new Dictionary<string, object?>
                {
                    ["value"] = i.ToString()
                });

            var elapsed = Stopwatch.GetElapsedTime(start);

            return new BenchmarkResult(
                elapsed.TotalMilliseconds,
                true);
        }
        catch
        {
            var elapsed = Stopwatch.GetElapsedTime(start);

            return new BenchmarkResult(
                elapsed.TotalMilliseconds,
                false);
        }
    });

var results = await Task.WhenAll(tasks);

stopwatch.Stop();

The important part is that all requests are issued asynchronously.

Using:

await CallToolAsync(...);
await CallToolAsync(...);
await CallToolAsync(...);

measures sequential behavior.

Using Task.WhenAll measures concurrent client demand.

Measuring Percentiles

Once the requests complete, sort successful latency measurements:

var latencies = results
    .Where(x => x.Success)
    .Select(x => x.LatencyMs)
    .OrderBy(x => x)
    .ToArray();

double Percentile(double[] values, double percentile)
{
    if (values.Length == 0)
        return double.NaN;

    var index = (int)Math.Ceiling(
        percentile / 100.0 * values.Length) - 1;

    index = Math.Clamp(index, 0, values.Length - 1);

    return values[index];
}

Console.WriteLine($"p50: {Percentile(latencies, 50):F2} ms");
Console.WriteLine($"p95: {Percentile(latencies, 95):F2} ms");
Console.WriteLine($"p99: {Percentile(latencies, 99):F2} ms");

This gives you a much more useful picture of tail latency.

In production MCP systems, tail latency matters because one slow tool invocation can delay an agent workflow even when most other requests are fast.

Measuring Throughput

Throughput can be calculated as:

Requests Per Second =
Successful Requests / Total Benchmark Duration

For example:

var requestsPerSecond =
    results.Length / stopwatch.Elapsed.TotalSeconds;

Console.WriteLine(
    $"Throughput: {requestsPerSecond:F2} requests/sec");

Do not compare throughput numbers between two SDK versions unless the test environment is identical.

The following should remain constant:

  • Machine or container resources

  • .NET runtime

  • Tool implementation

  • Payload

  • Transport

  • Network conditions

  • Client configuration

  • Concurrency level

  • Benchmark duration

Otherwise, the result is not a meaningful SDK comparison.

Testing Concurrency Saturation

The most interesting part of the benchmark is usually not the first test.

Run progressively higher concurrency.

1
10
25
50
100
250
500

Record the results:

ConcurrencyRequests/secp50p95p99Errors
1MeasureMeasureMeasureMeasureMeasure
10MeasureMeasureMeasureMeasureMeasure
25MeasureMeasureMeasureMeasureMeasure
50MeasureMeasureMeasureMeasureMeasure
100MeasureMeasureMeasureMeasureMeasure
250MeasureMeasureMeasureMeasureMeasure

The table should contain actual measurements from your environment.

Do not replace them with invented numbers.

The goal is to identify the point where increasing concurrency stops producing useful throughput and starts increasing latency or errors.

Testing I/O-Bound Tools

A realistic MCP server often calls databases or APIs.

Add a controlled asynchronous delay:

[McpServerTool]
[Description("Simulates an asynchronous downstream operation.")]
public static async Task<string> SlowOperation(
    int delayMilliseconds,
    CancellationToken cancellationToken)
{
    await Task.Delay(
        TimeSpan.FromMilliseconds(delayMilliseconds),
        cancellationToken);

    return "completed";
}

Now test:

Concurrency = 100
Delay = 100 ms

Then compare it with:

Concurrency = 100
Delay = 500 ms

This tells you whether the server continues processing other requests efficiently while individual tool operations are waiting.

This is more representative of many real services than a CPU-only benchmark.

Testing CPU-Bound Tools

I/O concurrency and CPU concurrency behave differently.

A CPU-heavy tool can be simulated carefully:

[McpServerTool]
[Description("Performs controlled CPU work.")]
public static long CpuOperation(int iterations)
{
    long result = 0;

    for (var i = 0; i < iterations; i++)
    {
        result = unchecked(
            result + (i * 31L));
    }

    return result;
}

Run the benchmark at increasing concurrency and monitor CPU utilization.

If throughput stops increasing while CPU approaches saturation, the limiting factor may be the workload rather than MCP transport.

That distinction is important when interpreting results.

Measuring Large Tool Payloads

Serialization can become relevant when tools return large results.

Create several payload sizes:

1 KB
10 KB
100 KB
1 MB

For example:

[McpServerTool]
[Description("Returns a generated payload.")]
public static string GeneratePayload(int sizeKb)
{
    return new string(
        'x',
        sizeKb * 1024);
}

Then measure latency and memory as payload size increases.

This can reveal whether the workload is becoming serialization- or memory-bound.

Benchmarking SDK Versions

A useful SDK benchmark compares the same application against two versions.

For example:

Test A
.NET Runtime
    |
MCP C# SDK 1.x
    |
Benchmark Suite

Test B
.NET Runtime
    |
MCP C# SDK 2.0
    |
Benchmark Suite

The application code should remain as similar as possible.

The benchmark should then compare:

MetricSDK Version ASDK Version B
p50 latencyMeasureMeasure
p95 latencyMeasureMeasure
p99 latencyMeasureMeasure
ThroughputMeasureMeasure
Error rateMeasureMeasure
CPUMeasureMeasure
MemoryMeasureMeasure

This is much stronger than saying that one version is "faster."

It gives readers enough information to reproduce and challenge the result.

Stateless HTTP and Concurrent Scaling

One of the most relevant architectural changes in SDK 2.0 is stateless-by-default HTTP.

Microsoft's announcement demonstrates that a stateless MCP server can be placed behind a round-robin load balancer without synchronization between server instances.

A production-style benchmark can therefore evolve from:

Client
  |
  v
MCP Server

to:

                +--> MCP Server 1
                |
Client --> Load Balancer
                |
                +--> MCP Server 2
                |
                +--> MCP Server 3

The benchmark should then measure:

  • Requests per second

  • Distribution across instances

  • p95 and p99 latency

  • Error rate

  • CPU per instance

  • Memory per instance

The important point is that stateless protocol behavior does not mean your application cannot maintain state. If an application needs state between calls, the SDK guidance is to make that state explicit, such as passing a basketId or other handle as a tool argument.

Common Benchmarking Mistakes

Testing Only One Request

A single successful request proves functionality, not scalability.

Measuring Only Average Latency

Tail latency can become the real bottleneck at higher concurrency.

Changing Multiple Variables

If you upgrade the SDK, .NET runtime, server hardware, and tool implementation simultaneously, you cannot identify what caused the difference.

Benchmarking a Mock Tool and Calling It Production Performance

A simple echo tool is useful for transport testing, but it does not represent database-heavy or AI-heavy workloads.

Ignoring Errors

A system that processes 20,000 requests per second while returning a significant number of errors is not actually achieving useful throughput.

Running the Benchmark Once

Performance measurements contain noise. Run repeated trials and report the methodology.

Troubleshooting Unexpected Results

If latency increases sharply with concurrency, check:

  1. CPU saturation

  2. Memory pressure

  3. Thread-pool behavior

  4. Connection limits

  5. Downstream API limits

  6. Database connection pools

  7. Serialization cost

  8. Network latency

  9. Client-side concurrency

  10. Load-balancer behavior

Also verify that logging is not dominating the benchmark.

A development environment with verbose console logging can behave very differently from a production deployment.

Production Benchmarking Best Practices

A reliable MCP performance study should follow a controlled process.

  1. Pin the .NET runtime version.

  2. Pin the MCP SDK version.

  3. Use identical server hardware or container limits.

  4. Keep the tool implementation unchanged between tests.

  5. Warm up the server before collecting measurements.

  6. Test multiple concurrency levels.

  7. Capture p50, p95, and p99 latency.

  8. Record throughput and failures.

  9. Monitor CPU and memory.

  10. Repeat each scenario.

  11. Separate transport benchmarks from downstream-service benchmarks.

  12. Publish the test configuration with the results.

These practices make the benchmark reproducible instead of turning it into a one-machine performance claim.

What Should You Actually Benchmark?

There is no single "MCP performance number."

A useful benchmark answers specific questions:

  • How does latency change as concurrency increases?

  • Where does throughput stop scaling?

  • What happens to p99 latency under load?

  • How does a large tool response affect memory?

  • How efficiently does the server handle I/O-bound tools?

  • What happens when downstream dependencies slow down?

  • Does stateless deployment scale across multiple instances?

  • How does SDK 2.0 compare with the previous version under the same workload?

Those questions produce engineering data that can actually influence architecture decisions.

Conclusion

MCP C# SDK 2.0 brings significant protocol and architectural changes to .NET MCP applications. The release aligns the SDK with the 2026-07-28 MCP specification and introduces discovery-first negotiation, stateless-by-default HTTP, multi-round-trip requests, and additional extension packages.

But the existence of new features does not automatically tell us how a workload will perform.

For that, developers need controlled benchmarks.

The most useful approach is to test progressively increasing concurrency while measuring throughput, p50/p95/p99 latency, failures, CPU, memory, and downstream behavior. Run the same suite against different SDK versions and keep the environment controlled.

That turns "MCP SDK 2.0 looks faster" into something much more valuable:

reproducible performance evidence that can guide production architecture.

Frequently Asked Questions

Is MCP C# SDK 2.0 production-ready?

The stable ModelContextProtocol 2.0.0 package is available on NuGet, and the official SDK repository identifies version 2.0.0 as the stable release aligned with the 2026-07-28 specification.

What should I measure first?

Start with p50, p95, p99 latency, throughput, and error rate. Then add CPU and memory measurements when investigating scaling behavior.

Should MCP benchmarks use real AI models?

Not for the initial transport benchmark. Start with deterministic tools to isolate MCP behavior. Add realistic AI or downstream-service workloads as a separate benchmark layer.

Is higher concurrency always better?

No. Increasing concurrency can improve throughput until a resource becomes saturated. Beyond that point, latency and error rates can increase without providing useful throughput gains.

Can stateless MCP servers run behind a load balancer?

Yes. SDK 2.0's HTTP transport is stateless by default, and Microsoft's guidance demonstrates scaling stateless MCP servers behind a round-robin load balancer.