Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions tests/Common/Utils/TestServerTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,12 @@ private async Task WriteMessageAsync(JsonRpcMessage message, CancellationToken c
{
await _messageChannel.Writer.WriteAsync(message, cancellationToken);
}

/// <summary>
/// Sends a message from the client to the server (simulating client-to-server communication).
/// </summary>
public async Task SendClientMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
await _messageChannel.Writer.WriteAsync(message, cancellationToken);
}
}
102 changes: 0 additions & 102 deletions tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using ModelContextProtocol.Client;
using ModelContextProtocol.Server;
using System.Net;
using System.Reflection;
using System.Security.Claims;
using Xunit.Sdk;

Expand Down Expand Up @@ -751,105 +750,4 @@ await McpClient.CreateAsync(

Assert.Contains("does not match", ex.Message);
}

[Fact]
public void CloneResourceMetadataClonesAllProperties()
{
var propertyNames = typeof(ProtectedResourceMetadata).GetProperties().Select(property => property.Name).ToList();

// Set metadata properties to non-default values to verify they're copied.
var metadata = new ProtectedResourceMetadata
{
Resource = new Uri("https://example.com/resource"),
AuthorizationServers = [new Uri("https://auth1.example.com"), new Uri("https://auth2.example.com")],
BearerMethodsSupported = ["header", "body", "query"],
ScopesSupported = ["read", "write", "admin"],
JwksUri = new Uri("https://example.com/.well-known/jwks.json"),
ResourceSigningAlgValuesSupported = ["RS256", "ES256"],
ResourceName = "Test Resource",
ResourceDocumentation = new Uri("https://docs.example.com"),
ResourcePolicyUri = new Uri("https://example.com/policy"),
ResourceTosUri = new Uri("https://example.com/terms"),
TlsClientCertificateBoundAccessTokens = true,
AuthorizationDetailsTypesSupported = ["payment_initiation", "account_information"],
DpopSigningAlgValuesSupported = ["RS256", "PS256"],
DpopBoundAccessTokensRequired = true
};

// Use reflection to call the internal CloneResourceMetadata method
var handlerType = typeof(McpAuthenticationHandler);
var cloneMethod = handlerType.GetMethod("CloneResourceMetadata", BindingFlags.Static | BindingFlags.NonPublic);
Comment thread
eiriktsarpalis marked this conversation as resolved.
Assert.NotNull(cloneMethod);

var clonedMetadata = (ProtectedResourceMetadata?)cloneMethod.Invoke(null, [metadata, null]);
Assert.NotNull(clonedMetadata);

// Ensure the cloned metadata is not the same instance
Assert.NotSame(metadata, clonedMetadata);

// Verify Resource property
Assert.Equal(metadata.Resource, clonedMetadata.Resource);
Assert.True(propertyNames.Remove(nameof(metadata.Resource)));

// Verify AuthorizationServers list is cloned and contains the same values
Assert.NotSame(metadata.AuthorizationServers, clonedMetadata.AuthorizationServers);
Assert.Equal(metadata.AuthorizationServers, clonedMetadata.AuthorizationServers);
Assert.True(propertyNames.Remove(nameof(metadata.AuthorizationServers)));

// Verify BearerMethodsSupported list is cloned and contains the same values
Assert.NotSame(metadata.BearerMethodsSupported, clonedMetadata.BearerMethodsSupported);
Assert.Equal(metadata.BearerMethodsSupported, clonedMetadata.BearerMethodsSupported);
Assert.True(propertyNames.Remove(nameof(metadata.BearerMethodsSupported)));

// Verify ScopesSupported list is cloned and contains the same values
Assert.NotSame(metadata.ScopesSupported, clonedMetadata.ScopesSupported);
Assert.Equal(metadata.ScopesSupported, clonedMetadata.ScopesSupported);
Assert.True(propertyNames.Remove(nameof(metadata.ScopesSupported)));

// Verify JwksUri property
Assert.Equal(metadata.JwksUri, clonedMetadata.JwksUri);
Assert.True(propertyNames.Remove(nameof(metadata.JwksUri)));

// Verify ResourceSigningAlgValuesSupported list is cloned (nullable list)
Assert.NotSame(metadata.ResourceSigningAlgValuesSupported, clonedMetadata.ResourceSigningAlgValuesSupported);
Assert.Equal(metadata.ResourceSigningAlgValuesSupported, clonedMetadata.ResourceSigningAlgValuesSupported);
Assert.True(propertyNames.Remove(nameof(metadata.ResourceSigningAlgValuesSupported)));

// Verify ResourceName property
Assert.Equal(metadata.ResourceName, clonedMetadata.ResourceName);
Assert.True(propertyNames.Remove(nameof(metadata.ResourceName)));

// Verify ResourceDocumentation property
Assert.Equal(metadata.ResourceDocumentation, clonedMetadata.ResourceDocumentation);
Assert.True(propertyNames.Remove(nameof(metadata.ResourceDocumentation)));

// Verify ResourcePolicyUri property
Assert.Equal(metadata.ResourcePolicyUri, clonedMetadata.ResourcePolicyUri);
Assert.True(propertyNames.Remove(nameof(metadata.ResourcePolicyUri)));

// Verify ResourceTosUri property
Assert.Equal(metadata.ResourceTosUri, clonedMetadata.ResourceTosUri);
Assert.True(propertyNames.Remove(nameof(metadata.ResourceTosUri)));

// Verify TlsClientCertificateBoundAccessTokens property
Assert.Equal(metadata.TlsClientCertificateBoundAccessTokens, clonedMetadata.TlsClientCertificateBoundAccessTokens);
Assert.True(propertyNames.Remove(nameof(metadata.TlsClientCertificateBoundAccessTokens)));

// Verify AuthorizationDetailsTypesSupported list is cloned (nullable list)
Assert.NotSame(metadata.AuthorizationDetailsTypesSupported, clonedMetadata.AuthorizationDetailsTypesSupported);
Assert.Equal(metadata.AuthorizationDetailsTypesSupported, clonedMetadata.AuthorizationDetailsTypesSupported);
Assert.True(propertyNames.Remove(nameof(metadata.AuthorizationDetailsTypesSupported)));

// Verify DpopSigningAlgValuesSupported list is cloned (nullable list)
Assert.NotSame(metadata.DpopSigningAlgValuesSupported, clonedMetadata.DpopSigningAlgValuesSupported);
Assert.Equal(metadata.DpopSigningAlgValuesSupported, clonedMetadata.DpopSigningAlgValuesSupported);
Assert.True(propertyNames.Remove(nameof(metadata.DpopSigningAlgValuesSupported)));

// Verify DpopBoundAccessTokensRequired property
Assert.Equal(metadata.DpopBoundAccessTokensRequired, clonedMetadata.DpopBoundAccessTokensRequired);
Assert.True(propertyNames.Remove(nameof(metadata.DpopBoundAccessTokensRequired)));

// Ensure we've checked every property. When new properties get added, we'll have to update this test along with the CloneResourceMetadata implementation.
Assert.Empty(propertyNames);
}
}
77 changes: 58 additions & 19 deletions tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,18 @@ public async Task SampleAsync_Should_Throw_Exception_If_Client_Does_Not_Support_
// Arrange
await using var transport = new TestServerTransport();
await using var server = McpServer.Create(transport, _options, LoggerFactory);
SetClientCapabilities(server, new ClientCapabilities());
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken);

var action = async () => await server.SampleAsync(
new CreateMessageRequestParams { Messages = [], MaxTokens = 1000 },
CancellationToken.None);

// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(action);

await transport.DisposeAsync();
await runTask;
}

[Fact]
Expand All @@ -144,9 +148,8 @@ public async Task SampleAsync_Should_SendRequest()
// Arrange
await using var transport = new TestServerTransport();
await using var server = McpServer.Create(transport, _options, LoggerFactory);
SetClientCapabilities(server, new ClientCapabilities { Sampling = new SamplingCapability() });

var runTask = server.RunAsync(TestContext.Current.CancellationToken);
await InitializeServerAsync(transport, new ClientCapabilities { Sampling = new SamplingCapability() }, TestContext.Current.CancellationToken);

// Act
var result = await server.SampleAsync(
Expand All @@ -155,8 +158,10 @@ public async Task SampleAsync_Should_SendRequest()

Assert.NotNull(result);
Assert.NotEmpty(transport.SentMessages);
Assert.IsType<JsonRpcRequest>(transport.SentMessages[0]);
Assert.Equal(RequestMethods.SamplingCreateMessage, ((JsonRpcRequest)transport.SentMessages[0]).Method);
// First message is the initialize response, second is the sampling request
Comment thread
eiriktsarpalis marked this conversation as resolved.
Assert.True(transport.SentMessages.Count >= 2, "Expected at least 2 messages (initialize response and sampling request)");
var samplingRequest = Assert.IsType<JsonRpcRequest>(transport.SentMessages[1]);
Assert.Equal(RequestMethods.SamplingCreateMessage, samplingRequest.Method);

await transport.DisposeAsync();
await runTask;
Expand All @@ -168,12 +173,16 @@ public async Task RequestRootsAsync_Should_Throw_Exception_If_Client_Does_Not_Su
// Arrange
await using var transport = new TestServerTransport();
await using var server = McpServer.Create(transport, _options, LoggerFactory);
SetClientCapabilities(server, new ClientCapabilities());
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken);

// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await server.RequestRootsAsync(
new ListRootsRequestParams(),
CancellationToken.None));

await transport.DisposeAsync();
await runTask;
}

[Fact]
Expand All @@ -182,17 +191,19 @@ public async Task RequestRootsAsync_Should_SendRequest()
// Arrange
await using var transport = new TestServerTransport();
await using var server = McpServer.Create(transport, _options, LoggerFactory);
SetClientCapabilities(server, new ClientCapabilities { Roots = new RootsCapability() });
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
await InitializeServerAsync(transport, new ClientCapabilities { Roots = new RootsCapability() }, TestContext.Current.CancellationToken);

// Act
var result = await server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None);

// Assert
Assert.NotNull(result);
Assert.NotEmpty(transport.SentMessages);
Assert.IsType<JsonRpcRequest>(transport.SentMessages[0]);
Assert.Equal(RequestMethods.RootsList, ((JsonRpcRequest)transport.SentMessages[0]).Method);
// First message is the initialize response, second is the roots request
Assert.True(transport.SentMessages.Count >= 2, "Expected at least 2 messages (initialize response and roots request)");
var rootsRequest = Assert.IsType<JsonRpcRequest>(transport.SentMessages[1]);
Assert.Equal(RequestMethods.RootsList, rootsRequest.Method);

await transport.DisposeAsync();
await runTask;
Expand All @@ -204,12 +215,16 @@ public async Task ElicitAsync_Should_Throw_Exception_If_Client_Does_Not_Support_
// Arrange
await using var transport = new TestServerTransport();
await using var server = McpServer.Create(transport, _options, LoggerFactory);
SetClientCapabilities(server, new ClientCapabilities());
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken);

// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await server.ElicitAsync(
new ElicitRequestParams { Message = "" },
CancellationToken.None));

await transport.DisposeAsync();
await runTask;
}

[Fact]
Expand All @@ -218,23 +233,25 @@ public async Task ElicitAsync_Should_SendRequest()
// Arrange
await using var transport = new TestServerTransport();
await using var server = McpServer.Create(transport, _options, LoggerFactory);
SetClientCapabilities(server, new ClientCapabilities
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
await InitializeServerAsync(transport, new ClientCapabilities
{
Elicitation = new()
{
Form = new(),
},
});
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
}, TestContext.Current.CancellationToken);

// Act
var result = await server.ElicitAsync(new ElicitRequestParams { Message = "", RequestedSchema = new() }, CancellationToken.None);

// Assert
Assert.NotNull(result);
Assert.NotEmpty(transport.SentMessages);
Assert.IsType<JsonRpcRequest>(transport.SentMessages[0]);
Assert.Equal(RequestMethods.ElicitationCreate, ((JsonRpcRequest)transport.SentMessages[0]).Method);
// First message is the initialize response, second is the elicit request
Assert.True(transport.SentMessages.Count >= 2, "Expected at least 2 messages (initialize response and elicit request)");
var elicitRequest = Assert.IsType<JsonRpcRequest>(transport.SentMessages[1]);
Assert.Equal(RequestMethods.ElicitationCreate, elicitRequest.Method);

await transport.DisposeAsync();
await runTask;
Expand Down Expand Up @@ -844,11 +861,33 @@ public async Task Can_SendMessage_Before_RunAsync()
Assert.Same(logNotification, transport.SentMessages[0]);
}

private static void SetClientCapabilities(McpServer server, ClientCapabilities capabilities)
private static async Task InitializeServerAsync(TestServerTransport transport, ClientCapabilities capabilities, CancellationToken cancellationToken = default)
{
FieldInfo? field = server.GetType().GetField("_clientCapabilities", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(field);
field.SetValue(server, capabilities);
var initializeRequest = new JsonRpcRequest
{
Id = new RequestId("init-1"),
Method = RequestMethods.Initialize,
Params = JsonSerializer.SerializeToNode(new InitializeRequestParams
{
ProtocolVersion = "2024-11-05",
Capabilities = capabilities,
ClientInfo = new Implementation { Name = "test-client", Version = "1.0.0" }
}, McpJsonUtilities.DefaultOptions)
};

var tcs = new TaskCompletionSource<bool>();
transport.OnMessageSent = (message) =>
{
if (message is JsonRpcResponse response && response.Id == initializeRequest.Id)
{
tcs.TrySetResult(true);
}
};

await transport.SendClientMessageAsync(initializeRequest, cancellationToken);

// Wait for the initialize response to be sent
await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
}

private sealed class TestServerForIChatClient(bool supportsSampling) : McpServer
Expand Down