Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(System10Version)" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(System10Version)" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="$(System10Version)" />
Expand Down
9 changes: 9 additions & 0 deletions ModelContextProtocol.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
<Folder Name="/docs/concepts/progress/samples/server/">
<Project Path="docs/concepts/progress/samples/server/Progress.csproj" />
</Folder>
<Folder Name="/docs/concepts/transports/" />
<Folder Name="/docs/concepts/transports/samples/" />
<Folder Name="/docs/concepts/transports/samples/integration-testing/" />
<Folder Name="/docs/concepts/transports/samples/integration-testing/server/">
<Project Path="docs/concepts/transports/samples/integration-testing/server/IntegrationTestingMcpServer.csproj" />
</Folder>
<Folder Name="/docs/concepts/transports/samples/integration-testing/tests/">
<Project Path="docs/concepts/transports/samples/integration-testing/tests/IntegrationTestingMcpServer.Tests.csproj" />
</Folder>
<Folder Name="/samples/">
<Project Path="samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj" />
<Project Path="samples/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../../../../../../src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// <snippet_IntegrationTestServer>
using System.ComponentModel;
using ModelContextProtocol.Server;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<EchoTools>();

var app = builder.Build();
app.MapMcp("/mcp");
app.Run();

// WebApplicationFactory<TEntryPoint> needs a public entry point.
public partial class Program;

[McpServerToolType]
public sealed class EchoTools
{
[McpServerTool(Name = "echo"), Description("Returns the supplied message.")]
public static string Echo(string message) => $"Echo: {message}";
}
// </snippet_IntegrationTestServer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../server/IntegrationTestingMcpServer.csproj" />
<ProjectReference Include="../../../../../../src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// <snippet_IntegrationTest>
using Microsoft.AspNetCore.Mvc.Testing;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using Xunit;

namespace IntegrationTestingMcpServer.Tests;

public class McpServerTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task EchoTool_RoundTripsThroughStreamableHttp()
{
using HttpClient httpClient = factory.CreateClient();
await using var transport = new HttpClientTransport(
new HttpClientTransportOptions
{
Endpoint = new Uri(httpClient.BaseAddress!, "/mcp"),
TransportMode = HttpTransportMode.StreamableHttp,
},
httpClient);

await using McpClient client = await McpClient.CreateAsync(
transport,
cancellationToken: TestContext.Current.CancellationToken);

var result = await client.CallToolAsync(
"echo",
new Dictionary<string, object?> { ["message"] = "Hello MCP" },
cancellationToken: TestContext.Current.CancellationToken);

var text = Assert.Single(result.Content.OfType<TextContentBlock>());
Assert.Equal("Echo: Hello MCP", text.Text);
}
}
// </snippet_IntegrationTest>
12 changes: 12 additions & 0 deletions docs/concepts/transports/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ app.Run();

By default, the HTTP transport runs **statelessly** — the server does not assign an `Mcp-Session-Id` or track transport session state in memory. This simplifies deployment, enables horizontal scaling without session affinity, and matches the `2026-07-28` Streamable HTTP wire format. Set `SessionMode = HttpServerSessionMode.Stateful` explicitly when your server needs stateful sessions for unsolicited notifications, resource subscriptions, or per-client isolation. For a detailed guide on when to use stateless vs. stateful mode, configure session options, and understand [cancellation and disposal](xref:stateless#cancellation-and-disposal) behavior during shutdown, see [Stateless and Stateful](xref:stateless).

#### Integration testing an ASP.NET Core MCP server

Use `Microsoft.AspNetCore.Mvc.Testing` to host the complete ASP.NET Core application in process and exercise its Streamable HTTP endpoint without opening a network port. When the server uses top-level statements, expose its generated entry point to the test project by adding `public partial class Program`:

[!code-csharp[](samples/integration-testing/server/Program.cs?name=snippet_IntegrationTestServer)]

Create an `HttpClient` from `WebApplicationFactory<TEntryPoint>` and pass that same client to <xref:ModelContextProtocol.Client.HttpClientTransport>. The factory client is backed by the in-memory test server; constructing `HttpClientTransport` without it would create a separate `HttpClient` that cannot reach the in-process application.

[!code-csharp[](samples/integration-testing/tests/McpServerTests.cs?name=snippet_IntegrationTest)]

The test project needs references to the server project and `ModelContextProtocol.Core`, plus the `Microsoft.AspNetCore.Mvc.Testing` package. Keep ownership of the factory-created `HttpClient` in the test, dispose the MCP client and transport asynchronously, and pass the test cancellation token to MCP operations so a failed connection cannot hang the test run.

#### Host name validation

For local HTTP servers, keep the set of accepted host names limited to loopback values. This helps protect against DNS rebinding, where a browser reaches a local server through an attacker-controlled DNS name while sending that DNS name in the HTTP `Host` header. ASP.NET Core's Kestrel server doesn't validate `Host` headers by default, so configure `AllowedHosts` with known host names rather than `"*"`. This also avoids reflecting untrusted host names through ASP.NET Core features such as absolute URL generation. See [Host filtering with ASP.NET Core Kestrel web server | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/host-filtering) and [URL generation concepts | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/routing#url-generation-concepts).
Expand Down