Subchapter 143.10
references/testing.mdMarkdown6 KBView on GitHub
Three workflows: interactive testing with MCP Inspector, in-process integration tests, and CI-friendly unit tests.
MCP Inspector (opens in a new tab) is the go-to tool for trying out a server by hand. It launches your server, connects via STDIO or HTTP, and gives you a UI to list/call tools, view resources, fire elicitations, see logs, and inspect raw JSON-RPC frames.
npx @modelcontextprotocol/inspector dotnet run --project ./MyMcpServerPass env vars or args after --:
npx @modelcontextprotocol/inspector \
dotnet run --project ./MyMcpServer -- \
--some-flag valueStart the server normally (dotnet run), then in Inspector pick “Streamable HTTP” and enter the URL (e.g. http://localhost:3001).
The cleanest test setup uses InMemoryTransport (or the lower-level StreamServerTransport / StreamClientTransport) to wire a real server and a real client together in the same process. No subprocesses, no network.
using System.IO.Pipelines;
using ModelContextProtocol;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using Xunit;
public class WeatherToolsTests
{
[Fact]
public async Task GetWeather_returns_text()
{
var clientToServer = new Pipe();
var serverToClient = new Pipe();
await using var server = McpServer.Create(
new StreamServerTransport(
clientToServer.Reader.AsStream(),
serverToClient.Writer.AsStream()),
new McpServerOptions
{
ToolCollection =
[
McpServerTool.Create(
(string city) => $"{city}: 18°C",
new() { Name = "GetWeather" })
]
});
var serverTask = server.RunAsync();
await using var client = await McpClient.CreateAsync(
new StreamClientTransport(
clientToServer.Writer.AsStream(),
serverToClient.Reader.AsStream()));
var tools = await client.ListToolsAsync();
var tool = tools.Single(t => t.Name == "GetWeather");
var result = await tool.CallAsync(new Dictionary<string, object?>
{
["city"] = "Brussels"
});
Assert.False(result.IsError);
var text = result.Content.OfType<TextContentBlock>().Single().Text;
Assert.Equal("Brussels: 18°C", text);
}
}This style lets you assert on the exposed behaviour (what a real client sees), not internal details.
(Sampling and roots are deprecated on 2.x — expect MCP9005 warnings in test projects that exercise them; suppress in the test csproj if you’re deliberately covering legacy paths.)
Inject the MCP server, but supply mock client capabilities. With the in-memory pattern above, register handlers on the client:
await using var client = await McpClient.CreateAsync(clientTransport, new McpClientOptions
{
Capabilities = new()
{
Sampling = new()
{
SamplingHandler = (req, progress, ct) =>
Task.FromResult(new CreateMessageResult
{
Content = [new TextContentBlock { Text = "MOCK SUMMARY" }]
})
},
Elicitation = new()
{
ElicitationHandler = (req, ct) =>
Task.FromResult(new ElicitResult
{
Action = "accept",
Content = JsonSerializer.SerializeToNode(new { confirm = true })
.AsObject().ToDictionary(kv => kv.Key, kv => JsonDocument.Parse(kv.Value!.ToJsonString()).RootElement)
})
}
}
});Now your tool’s server.SampleAsync / server.ElicitAsync calls hit deterministic mocks.
For pure logic with no MCP-specific behaviour, just test the class:
[Fact]
public void Echo_prepends_hello()
{
Assert.Equal("hello world", EchoTool.Echo("world"));
}The [McpServerTool] attribute doesn’t affect runtime behaviour outside MCP wiring — your methods are just methods.
For end-to-end “feels-like-the-real-thing” testing:
dotnet publish -c Release (or just dotnet build and use dotnet run).dotnet run --project .... See transport-stdio.md for the config snippets.When iterating, set up dotnet watch run --project ... so the server restarts on edit; the host typically reconnects on the next tool call.
A typical CI pipeline:
- run: dotnet restore
- run: dotnet build --no-restore
- run: dotnet test --no-build --logger "trx;LogFileName=test-results.trx"Nothing MCP-specific. The in-memory transport tests run anywhere dotnet test runs — no Node, no Docker.
client.ListToolsAsync() in a quick test and dump the names. If your tool isn’t there, the registration is wrong.[Description].app.MapMcp() is called and you’re hitting the right path. MapMcp("/mcp") means the URL is http://host/mcp, not http://host/.