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:
parent
330e526446
commit
973c7c093f
8 changed files with 332 additions and 831 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue