McpServerFactory 1.0.0
dotnet add package McpServerFactory --version 1.0.0
NuGet\Install-Package McpServerFactory -Version 1.0.0
<PackageReference Include="McpServerFactory" Version="1.0.0" />
<PackageVersion Include="McpServerFactory" Version="1.0.0" />
<PackageReference Include="McpServerFactory" />
paket add McpServerFactory --version 1.0.0
#r "nuget: McpServerFactory, 1.0.0"
#:package McpServerFactory@1.0.0
#addin nuget:?package=McpServerFactory&version=1.0.0
#tool nuget:?package=McpServerFactory&version=1.0.0
McpServerFactory
In-memory integration test harness for .NET Model Context Protocol (MCP) servers.
Your MCP server's real contract isn't with your code — it's with the model. The tool names
and JSON schemas, the descriptions the agent reads, the error text it sees, the sampling
round-trips it triggers. McpServerFactory boots your server in-process and connects a real
McpClient over in-memory pipes, so you can assert on that contract in a plain unit test:
breakpoints on both sides, dependencies swapped for fakes, no subprocess, no ports, no Docker,
no live model.
Think WebApplicationFactory<T> — but for MCP.
// one process: a real client ⇄ your real server, over in-memory pipes
McpTestClient client = await factory.CreateTestClientAsync();
Assert.Equal("hello", await client.CallToolForTextAsync(
"echo", new Dictionary<string, object?> { ["message"] = "hello" }));
What you can actually verify
- A tool exists, and its input schema/description are what you think (
GetToolAsync,McpAssert.ToolExistsAsync). - A tool returns the right text or typed JSON (
CallToolForTextAsync,CallToolForJsonAsync<T>). - A tool fails the way you intend —
IsErrorresults throw instead of quietly passing a test (CallToolExpectingErrorAsync). - Server-initiated sampling does the right thing, answered by a fake model you control (
FakeSamplingHandler). - The expected notifications fire — logging, progress, list-changed (
NotificationRecorder). - Your real DI graph wires up, with the slow/external bits mocked (
configureServices/ConfigureHost).
Scope: by default the factory hosts the tool/resource/prompt classes you register over an in-memory transport. It does not auto-run your server's
Program.cs. To exercise your real composition root (configuration, options, hosted services), use theConfigureHosthook.
Installation
dotnet add package McpServerFactory
Template-based scaffolding
Install the template pack to bootstrap an MCP integration test project:
dotnet new install McpServerFactory.Templates
dotnet new mcp-itest -n MyServer.Tests
The mcp-itest template accepts --McpServerFactoryVersion to override the
McpServerFactory package version; the default tracks the version of the
template pack you installed.
Quick start
using McpServerFactory.Testing;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Server;
using Xunit;
[McpServerToolType]
public sealed class EchoTools
{
[McpServerTool(Name = "echo")]
public string Echo(string message) => message;
}
public class EchoTests
{
[Fact]
public async Task Echo_ReturnsInput()
{
await using McpServerIntegrationFactory factory = new(
configureMcpServer: builder => builder.WithTools<EchoTools>());
// The factory owns the client; you do not dispose it yourself.
McpTestClient client = await factory.CreateTestClientAsync();
string text = await client.CallToolForTextAsync(
"echo",
new Dictionary<string, object?> { ["message"] = "hello" });
Assert.Equal("hello", text);
}
}
Using xUnit? The companion McpServerFactory.Xunit package
boots the server once per test class via IClassFixture<T>.
Why use this library
- Fast and in-process — no subprocess, ports, Docker, or stdio plumbing.
- Real protocol flow — a real
McpClientover a real (in-memory) transport:tools/list,tools/call, resources, prompts, and server-initiated sampling all run end-to-end. - Easy dependency overrides — substitute services via
configureServices. - Debuggable — set breakpoints on both sides; it's all one process.
- Framework-agnostic core — works with xUnit, NUnit, and MSTest.
How it compares
| Approach | Speed | No process/port | Breakpoints both sides | DI substitution | Exercises real Program.cs |
|---|---|---|---|---|---|
| McpServerFactory | fast | ✅ | ✅ | ✅ | via ConfigureHost |
stdio subprocess + McpClient |
slow | ❌ | server only | ❌ | ✅ |
raw pipes + StreamClientTransport |
fast | ✅ | ✅ | ✅ (manual) | ❌ |
| MCP Inspector (manual) | n/a | ❌ | ❌ | ❌ | ✅ |
Use it when you want fast, automated, in-process tests of your tools/resources/prompts and handlers with dependency substitution. Reach for a stdio subprocess instead when you must validate the actual published binary, its real transport configuration, or process-level startup.
Used in the wild
stash-mcp — a 40-tool MCP server for Bitbucket
Server — tests its tools with McpServerFactory. It subclasses the factory, registers its real tool
assembly, and swaps the live Bitbucket client, cache, and resilience services for fakes, so the
whole tool surface is exercised in-process without ever touching a Bitbucket instance:
public sealed class StashMcpTestFactory(Action<StashMcpTestFactory>? configureMocks = null)
: McpServerFactory.Testing.McpServerFactory
{
public IBitbucketClient BitbucketClient { get; } = Substitute.For<IBitbucketClient>();
protected override void ConfigureMcpServer(IMcpServerBuilder builder) =>
builder.WithToolsFromAssembly(typeof(ProjectTools).Assembly);
protected override void ConfigureServices(IServiceCollection services) =>
services.AddSingleton(BitbucketClient); // ...plus cache, settings, resilience fakes
}
Testing tools, resources, prompts, and structured output
McpTestClient wraps a real McpClient with test-shaped helpers (all pagination-safe):
McpTestClient client = await factory.CreateTestClientAsync();
string[] tools = await client.GetToolNamesAsync();
string greeting = await client.CallToolForTextAsync("greet");
MyDto result = await client.CallToolForJsonAsync<MyDto>("compute", args); // structured or JSON-text
string contents = await client.ReadResourceTextAsync("resource://config");
string[] prompts = await client.GetPromptNamesAsync();
Tool failures (IsError) no longer pass silently: CallToolForTextAsync throws
McpToolCallException, and CallToolExpectingErrorAsync asserts the negative path. The
framework-agnostic McpAssert helpers (ToolExistsAsync, Succeeded, IsError, TextEquals)
work under any test framework.
Testing server-initiated sampling
If your server calls the model (server-initiated sampling), wire a deterministic fake so tests never need a real LLM:
FakeSamplingHandler sampling = FakeSamplingHandler.Returning("42");
await using McpServerIntegrationFactory factory = new(
configureMcpServer: builder => builder.WithTools<AskTools>(),
options: new McpServerFactoryOptions
{
ConfigureClient = client => client.UseSamplingHandler(sampling),
});
McpTestClient client = await factory.CreateTestClientAsync();
string answer = await client.CallToolForTextAsync(
"ask", new Dictionary<string, object?> { ["question"] = "..." });
Assert.Single(sampling.ReceivedRequests); // assert what the server asked the model
ConfigureClient exposes the full McpClientOptions, so you can also declare elicitation, roots,
or notification handlers. Capture server-sent notifications (logging, progress, list-changed) with
NotificationRecorder.Attach(client.Inner) and await recorder.WaitForMethodAsync(...).
Testing your real composition root
By default the factory hosts the classes you register. To exercise the same registration your
server's Program.cs uses — real configuration binding, options, and hosted services — supply
ConfigureHost. The factory always owns the MCP server registration and the in-memory transport,
so configure everything except the transport; register tools/resources/prompts via
configureMcpServer:
await using McpServerIntegrationFactory factory = new(
configureMcpServer: builder => builder.WithTools<MyTools>(),
options: new McpServerFactoryOptions
{
ConfigureHost = builder =>
{
builder.Configuration.AddInMemoryCollection(/* test config */);
builder.Services.AddMyDomainServices(); // your real registration, minus the transport
},
});
Boot once per class with xUnit
Install the companion package and derive a fixture:
dotnet add package McpServerFactory.Xunit
using McpServerFactory.Testing.Xunit;
public sealed class EchoFixture : McpServerFixture
{
protected override void ConfigureMcpServer(IMcpServerBuilder builder) => builder.WithTools<EchoTools>();
}
public sealed class EchoTests(EchoFixture fixture) : IClassFixture<EchoFixture>
{
[Fact]
public async Task Echoes() =>
Assert.Equal("hi", await fixture.TestClient.CallToolForTextAsync(
"echo", new Dictionary<string, object?> { ["message"] = "hi" }));
}
Behavioral guarantees
CreateClientAsync/CreateTestClientAsyncare thread-safe and idempotent per factory instance.- Concurrent calls return the same connected client; the factory owns and disposes it — you do not need to dispose the returned client yourself.
- Startup failures do not leak the temporary host instance or its pipes.
DisposeAsyncis safe to call multiple times and is bounded byShutdownTimeout(it will not hang teardown).CreateClientAsyncthrowsObjectDisposedExceptionafter disposal.- Need independent server instances (isolation)? Create multiple factory instances — each owns its own host. A single factory exposes one in-memory session.
Compatibility and support
- Target frameworks:
net8.0,net9.0,net10.0. - MCP SDK dependency:
ModelContextProtocol1.4.0. - Compatibility promise: each package release is validated against the pinned MCP SDK version on all target frameworks.
- Upgrade policy: MCP SDK bumps are explicit and called out in CHANGELOG.md. Now that
the MCP SDK is stable,
McpServerFactoryfollows Semantic Versioning: an MCP SDK change that breaks this library's public surface ships as a new major version.
| McpServerFactory | MCP SDK | Target frameworks |
|---|---|---|
| 1.0.x | 1.4.0 | net8.0, net9.0, net10.0 |
| 0.2.x | 0.4.0-preview.3 | net8.0, net9.0, net10.0 |
| 0.1.x | 0.4.0-preview.3 | net10.0 |
API overview
McpServerFactory/McpServerIntegrationFactory- Start an in-process server host; create a connected client via
CreateClientAsync()or a factory-owned wrapper viaCreateTestClientAsync(). - Expose
Servicesfor DI validation after startup.
- Start an in-process server host; create a connected client via
McpServerFactoryOptions- Configure server identity, timeouts, instructions, logging, the client (
ConfigureClient), and the host composition root (ConfigureHost).
- Configure server identity, timeouts, instructions, logging, the client (
McpTestClient- Wrapper with tool, resource, prompt, structured-output, and server-metadata helpers.
FakeSamplingHandler/NotificationRecorder/McpAssert- Deterministic sampling, notification capture, and framework-agnostic assertions.
McpServerFactory.Xunit(separate package)McpServerFixtureforIClassFixture<T>boot-once-per-class.
Logging in test output
By default, host logging providers are suppressed to keep test output clean. To enable custom logging during tests:
using Microsoft.Extensions.Logging;
var options = new McpServerFactoryOptions
{
SuppressHostLogging = false,
ConfigureLogging = logging => logging.SetMinimumLevel(LogLevel.Debug),
};
Release notes
See CHANGELOG.md for release history and upcoming changes.
Samples
- Minimal runnable sample:
samples/MinimalSmoke
Repository layout
src/McpServerFactory— reusable factory library (framework-agnostic core).src/McpServerFactory.Xunit— xUnit fixtures (McpServerFixture).tests/McpServerFactory.Tests— unit/integration-focused library tests.templates/McpServerFactory.Templates—dotnet newtemplate pack.samples/MinimalSmoke— runnable sample console app.docs— architecture notes and usage guidance.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.Hosting (>= 10.0.0)
- ModelContextProtocol (>= 1.4.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
- Microsoft.Extensions.Hosting (>= 8.0.1)
- ModelContextProtocol (>= 1.4.0)
-
net9.0
- Microsoft.Extensions.DependencyInjection (>= 9.0.0)
- Microsoft.Extensions.Hosting (>= 9.0.0)
- ModelContextProtocol (>= 1.4.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on McpServerFactory:
| Package | Downloads |
|---|---|
|
McpServerFactory.Xunit
xUnit fixtures for McpServerFactory: boot an in-memory MCP server once per test class via IClassFixture. |
GitHub repositories
This package is not used by any popular GitHub repositories.