Add ModelContextProtocol.AspNetCore (#160)

* Add ModelContextProtocol.AspNetCore

- Remove IServerTransport and HttpListenerSseServerTransport
- Replace HttpListenerSseServerTransport usage in tests with ASP.NET Core.

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com>
This commit is contained in:
Stephen Halter 2025-03-31 12:01:04 -07:00 • committed by GitHub
commit 973c7c093f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 332 additions and 831 deletions

View file

@ -1,20 +1,20 @@
using ModelContextProtocol.Client;
using Microsoft.Extensions.AI;
using OpenAI;
using ModelContextProtocol.Protocol.Types;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Messages;
using System.Text.Json;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Tests.Utils;
using OpenAI;
using System.Text.Encodings.Web;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Tests;
public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegrationTestFixture>
{
private static readonly string? s_openAIKey = Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!;
private static readonly string? s_openAIKey = Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey");
public static bool NoOpenAIKeySet => string.IsNullOrWhiteSpace(s_openAIKey);

View file

@ -19,18 +19,4 @@ public class McpServerBuilderExtensionsTransportsTests
Assert.NotNull(transportType);
Assert.Equal(typeof(StdioServerTransport), transportType.ImplementationType);
}
[Fact]
public void WithHttpListenerSseServerTransport_Sets_Transport()
{
var services = new ServiceCollection();
var builder = new Mock<IMcpServerBuilder>();
builder.SetupGet(b => b.Services).Returns(services);
builder.Object.WithHttpListenerSseServerTransport();
var transportType = services.FirstOrDefault(s => s.ServiceType == typeof(IServerTransport));
Assert.NotNull(transportType);
Assert.Equal(typeof(HttpListenerSseServerTransport), transportType.ImplementationType);
}
}

View file

@ -32,22 +32,16 @@ public class McpServerFactoryTests : LoggedTest
}
[Fact]
public async Task Create_Throws_For_Null_ServerTransport()
public void Create_Throws_For_Null_ServerTransport()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>("transport", () => McpServerFactory.Create(null!, _options, LoggerFactory));
await Assert.ThrowsAsync<ArgumentNullException>("serverTransport", () =>
McpServerFactory.AcceptAsync(null!, _options, LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
public async Task Create_Throws_For_Null_Options()
public void Create_Throws_For_Null_Options()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>("serverOptions", () => McpServerFactory.Create(Mock.Of<ITransport>(), null!, LoggerFactory));
await Assert.ThrowsAsync<ArgumentNullException>("serverOptions", () =>
McpServerFactory.AcceptAsync(Mock.Of<IServerTransport>(), null!, LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
}
}

View file

@ -168,18 +168,6 @@ public class McpServerTests : LoggedTest
await runTask;
}
[Fact]
public async Task Throws_Exception_If_Not_Connected()
{
await using var server = McpServerFactory.Create(_serverTransport.Object, _options, LoggerFactory, _serviceProvider);
SetClientCapabilities(server, new ClientCapabilities { Roots = new RootsCapability() });
_serverTransport.SetupGet(t => t.IsConnected).Returns(false);
var action = async () => await server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None);
await Assert.ThrowsAsync<McpClientException>(action);
}
[Fact]
public async Task Can_Handle_Ping_Requests()
{

View file

@ -13,18 +13,11 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
private readonly CancellationTokenSource _stopCts = new();
private readonly DelegatingTestOutputHelper _delegatingTestOutputHelper = new();
private readonly ILoggerFactory _redirectingLoggerFactory;
public McpServerConfig DefaultConfig { get; }
public SseServerIntegrationTestFixture()
{
_redirectingLoggerFactory = LoggerFactory.Create(builder =>
{
Program.ConfigureSerilog(builder);
builder.AddProvider(new XunitLoggerProvider(_delegatingTestOutputHelper));
});
DefaultConfig = new McpServerConfig
{
Id = "test_server",
@ -34,7 +27,7 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
Location = "http://localhost:3001/sse"
};
_serverTask = Program.MainAsync([], _redirectingLoggerFactory, _stopCts.Token);
_serverTask = Program.MainAsync([], new XunitLoggerProvider(_delegatingTestOutputHelper), _stopCts.Token);
}
public static McpClientOptions CreateDefaultClientOptions() => new()
@ -63,7 +56,6 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
catch (OperationCanceledException)
{
}
_redirectingLoggerFactory.Dispose();
_stopCts.Dispose();
}
}

View file

@ -36,7 +36,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// Arrange
// Act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
await client.PingAsync(TestContext.Current.CancellationToken);
// Assert
@ -49,7 +49,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// Arrange
// Act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
// Assert
Assert.NotNull(client.ServerCapabilities);
@ -62,7 +62,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
// assert
@ -75,7 +75,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
var result = await client.CallToolAsync(
"echo",
new Dictionary<string, object?>
@ -98,7 +98,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
IList<Resource> allResources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
@ -112,7 +112,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
// Odd numbered resources are text in the everything server (despite the docs saying otherwise)
// 1 is index 0, which is "even" in the 0-based index
// We copied this oddity to the test server
@ -131,7 +131,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
// Even numbered resources are binary in the everything server (despite the docs saying otherwise)
// 2 is index 1, which is "odd" in the 0-based index
// We copied this oddity to the test server
@ -150,7 +150,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
// assert
@ -167,7 +167,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
var result = await client.GetPromptAsync("simple_prompt", null, TestContext.Current.CancellationToken);
// assert
@ -181,7 +181,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
var arguments = new Dictionary<string, object?>
{
{ "temperature", "0.7" },
@ -200,7 +200,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// arrange
// act
var client = await GetClientAsync();
await using var client = await GetClientAsync();
await Assert.ThrowsAsync<McpClientException>(() =>
client.GetPromptAsync("non_existent_prompt", null, TestContext.Current.CancellationToken));
}
@ -229,7 +229,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
}
};
};
var client = await GetClientAsync(options);
await using var client = await GetClientAsync(options);
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
// Call the server's sampleLLM tool which should trigger our sampling handler
@ -246,4 +246,29 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
Assert.Equal("text", textContent.Type);
Assert.False(string.IsNullOrEmpty(textContent.Text));
}
[Fact]
public async Task CallTool_Sse_EchoServer_Concurrently()
{
await using var client1 = await GetClientAsync();
await using var client2 = await GetClientAsync();
for (int i = 0; i < 4; i++)
{
var client = (i % 2 == 0) ? client1 : client2;
var result = await client.CallToolAsync(
"echo",
new Dictionary<string, object?>
{
["message"] = $"Hello MCP! {i}"
},
TestContext.Current.CancellationToken
);
Assert.NotNull(result);
Assert.False(result.IsError);
var textContent = Assert.Single(result.Content, c => c.Type == "text");
Assert.Equal($"Echo: Hello MCP! {i}", textContent.Text);
}
}
}

View file

@ -6,7 +6,6 @@ using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using ModelContextProtocol.Utils.Json;
using System.IO.Pipelines;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
@ -32,11 +31,9 @@ public class StdioServerTransportTests : LoggedTest
};
}
[Fact]
[Fact(Skip="https://github.com/modelcontextprotocol/csharp-sdk/issues/143")]
public async Task Constructor_Should_Initialize_With_Valid_Parameters()
{
Assert.SkipWhen(RuntimeInformation.IsOSPlatform(OSPlatform.Linux), "https://github.com/modelcontextprotocol/csharp-sdk/issues/143");
// Act
await using var transport = new StdioServerTransport(_serverOptions);

View file

@ -13,14 +13,16 @@ public class LoggedTest : IDisposable
{
CurrentTestOutputHelper = testOutputHelper,
};
LoggerProvider = new XunitLoggerProvider(_delegatingTestOutputHelper);
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
{
builder.AddProvider(new XunitLoggerProvider(_delegatingTestOutputHelper));
builder.AddProvider(LoggerProvider);
});
}
public ITestOutputHelper TestOutputHelper => _delegatingTestOutputHelper;
public ILoggerFactory LoggerFactory { get; }
public ILoggerProvider LoggerProvider { get; }
public virtual void Dispose()
{