Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="$(System10Version)" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0" />
<PackageVersion Include="xunit.v3" Version="2.0.0" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an unresolved issue with xunit.v3 >= 2.0.0 and Rider. I would recommend using 1.0.x for now until it gets resolved.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<PackageVersion Include="xunit.v3" Version="2.0.0" />
<PackageVersion Include="xunit.v3" Version="1.1.0" />

<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
</ItemGroup>
</Project>
1 change: 0 additions & 1 deletion tests/ModelContextProtocol.TestServer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text;
using System.Text.Json;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
Expand Down
36 changes: 17 additions & 19 deletions tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,28 @@ public class McpClientFactoryTests
[Fact]
public async Task CreateAsync_WithInvalidArgs_Throws()
{
await Assert.ThrowsAsync<ArgumentNullException>("serverConfig", () => McpClientFactory.CreateAsync((McpServerConfig)null!, _defaultOptions));
await Assert.ThrowsAsync<ArgumentNullException>("serverConfig", () => McpClientFactory.CreateAsync((McpServerConfig)null!, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken));

await Assert.ThrowsAsync<ArgumentNullException>("clientOptions", () => McpClientFactory.CreateAsync(
new McpServerConfig()
await Assert.ThrowsAsync<ArgumentNullException>("clientOptions", () => McpClientFactory.CreateAsync(new McpServerConfig()
{
Name = "name",
Id = "id",
TransportType = TransportTypes.StdIo,
}, (McpClientOptions)null!));
}, (McpClientOptions)null!, cancellationToken: TestContext.Current.CancellationToken));

await Assert.ThrowsAsync<ArgumentException>("serverConfig", () => McpClientFactory.CreateAsync(
new McpServerConfig()
await Assert.ThrowsAsync<ArgumentException>("serverConfig", () => McpClientFactory.CreateAsync(new McpServerConfig()
{
Name = "name",
Id = "id",
TransportType = "somethingunsupported",
},
_defaultOptions));
}, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken));

await Assert.ThrowsAsync<InvalidOperationException>(() => McpClientFactory.CreateAsync(
new McpServerConfig()
await Assert.ThrowsAsync<InvalidOperationException>(() => McpClientFactory.CreateAsync(new McpServerConfig()
{
Name = "name",
Id = "id",
TransportType = TransportTypes.StdIo,
},
_defaultOptions,
(_, __) => null!));
}, _defaultOptions, (_, __) => null!, cancellationToken: TestContext.Current.CancellationToken));
}

[Fact]
Expand All @@ -68,7 +62,8 @@ public async Task CreateAsync_WithValidStdioConfig_CreatesNewClient()
await using var client = await McpClientFactory.CreateAsync(
serverConfig,
_defaultOptions,
(_, __) => new NopTransport());
(_, __) => new NopTransport(),
cancellationToken: TestContext.Current.CancellationToken);

// Assert
Assert.NotNull(client);
Expand All @@ -91,7 +86,8 @@ public async Task CreateAsync_WithNoTransportOptions_CreatesNewClient()
await using var client = await McpClientFactory.CreateAsync(
serverConfig,
_defaultOptions,
(_, __) => new NopTransport());
(_, __) => new NopTransport(),
cancellationToken: TestContext.Current.CancellationToken);

// Assert
Assert.NotNull(client);
Expand All @@ -114,7 +110,8 @@ public async Task CreateAsync_WithValidSseConfig_CreatesNewClient()
await using var client = await McpClientFactory.CreateAsync(
serverConfig,
_defaultOptions,
(_, __) => new NopTransport());
(_, __) => new NopTransport(),
cancellationToken: TestContext.Current.CancellationToken);

// Assert
Assert.NotNull(client);
Expand Down Expand Up @@ -144,7 +141,8 @@ public async Task CreateAsync_WithSse_CreatesCorrectTransportOptions()
await using var client = await McpClientFactory.CreateAsync(
serverConfig,
_defaultOptions,
(_, __) => new NopTransport());
(_, __) => new NopTransport(),
cancellationToken: TestContext.Current.CancellationToken);

// Assert
Assert.NotNull(client);
Expand All @@ -171,7 +169,7 @@ public async Task McpFactory_WithInvalidTransportOptions_ThrowsFormatException(s
};

// act & assert
await Assert.ThrowsAsync<ArgumentException>(() => McpClientFactory.CreateAsync(config, _defaultOptions));
await Assert.ThrowsAsync<ArgumentException>(() => McpClientFactory.CreateAsync(config, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken));
}

private sealed class NopTransport : IClientTransport
Expand All @@ -191,7 +189,7 @@ public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancella
{
switch (message)
{
case JsonRpcRequest request:
case JsonRpcRequest:
_channel.Writer.TryWrite(new JsonRpcResponse
{
Id = ((JsonRpcRequest)message).Id,
Expand Down
78 changes: 44 additions & 34 deletions tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using ModelContextProtocol.Client;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using Microsoft.Extensions.AI;
using OpenAI;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Protocol.Messages;
using System.Text.Json;
using ModelContextProtocol.Configuration;
using ModelContextProtocol.Protocol.Transport;
using Xunit.Sdk;

namespace ModelContextProtocol.Tests;

Expand Down Expand Up @@ -61,8 +62,8 @@ public async Task ListTools_Stdio(string clientId)

// act
await using var client = await _fixture.CreateClientAsync(clientId);
var tools = await client.ListToolsAsync().ToListAsync();
var aiFunctions = await client.GetAIFunctionsAsync();
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken).ToListAsync(TestContext.Current.CancellationToken);
var aiFunctions = await client.GetAIFunctionsAsync(TestContext.Current.CancellationToken);

// assert
Assert.NotEmpty(tools);
Expand Down Expand Up @@ -102,9 +103,9 @@ public async Task CallTool_Stdio_ViaAIFunction_EchoServer(string clientId)

// act
await using var client = await _fixture.CreateClientAsync(clientId);
var aiFunctions = await client.GetAIFunctionsAsync();
var aiFunctions = await client.GetAIFunctionsAsync(TestContext.Current.CancellationToken);
var echo = aiFunctions.Single(t => t.Name == "echo");
var result = await echo.InvokeAsync([new KeyValuePair<string, object?>("message", "Hello MCP!")]);
var result = await echo.InvokeAsync([new KeyValuePair<string, object?>("message", "Hello MCP!")], TestContext.Current.CancellationToken);

// assert
Assert.NotNull(result);
Expand All @@ -119,7 +120,7 @@ public async Task ListPrompts_Stdio(string clientId)

// act
await using var client = await _fixture.CreateClientAsync(clientId);
var prompts = await client.ListPromptsAsync().ToListAsync();
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken).ToListAsync(TestContext.Current.CancellationToken);

// assert
Assert.NotEmpty(prompts);
Expand Down Expand Up @@ -251,7 +252,7 @@ public async Task SubscribeResource_Stdio()
await client.SubscribeToResourceAsync("test://static/resource/1", CancellationToken.None);

// notifications happen every 5 seconds, so we wait for 10 seconds to ensure we get at least one notification
await Task.Delay(10000);
await Task.Delay(10000, TestContext.Current.CancellationToken);

// assert
Assert.True(counter > 0);
Expand All @@ -276,17 +277,17 @@ public async Task UnsubscribeResource_Stdio()
await client.SubscribeToResourceAsync("test://static/resource/1", CancellationToken.None);

// notifications happen every 5 seconds, so we wait for 10 seconds to ensure we get at least one notification
await Task.Delay(10000);
await Task.Delay(10000, TestContext.Current.CancellationToken);

// reset counter
int counterAfterSubscribe = counter;

// unsubscribe
await client.UnsubscribeFromResourceAsync("test://static/resource/1", CancellationToken.None);
counter = 0;

// notifications happen every 5 seconds, so we wait for 10 seconds to ensure we would've gotten at least one notification
await Task.Delay(10000);
await Task.Delay(10000, TestContext.Current.CancellationToken);

// assert
Assert.True(counterAfterSubscribe > 0);
Expand Down Expand Up @@ -340,7 +341,7 @@ public async Task GetCompletion_Stdio_PromptReference(string clientId)
[Theory]
[MemberData(nameof(GetClients))]
public async Task Sampling_Stdio(string clientId)
{
{
// Set up the sampling handler
int samplingHandlerCalls = 0;
await using var client = await _fixture.CreateClientAsync(clientId, new()
Expand Down Expand Up @@ -375,8 +376,8 @@ public async Task Sampling_Stdio(string clientId)
{
["prompt"] = "Test prompt",
["maxTokens"] = 100
}
);
},
TestContext.Current.CancellationToken);

// assert
Assert.NotNull(result);
Expand Down Expand Up @@ -423,8 +424,8 @@ public async Task Notifications_Stdio(string clientId)
await using var client = await _fixture.CreateClientAsync(clientId);

// Verify we can send notifications without errors
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification);
await client.SendNotificationAsync("test/notification", new { test = true });
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification, cancellationToken: TestContext.Current.CancellationToken);
await client.SendNotificationAsync("test/notification", new { test = true }, TestContext.Current.CancellationToken);

// assert
// no response to check, if no exception is thrown, it's a success
Expand Down Expand Up @@ -452,13 +453,17 @@ public async Task CallTool_Stdio_MemoryServer()
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
};

await using var client = await McpClientFactory.CreateAsync(serverConfig, clientOptions, loggerFactory: _fixture.LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(
serverConfig,
clientOptions,
loggerFactory: _fixture.LoggerFactory,
cancellationToken: TestContext.Current.CancellationToken);

// act
var result = await client.CallToolAsync(
"read_graph",
[]
);
[],
TestContext.Current.CancellationToken);

// assert
Assert.NotNull(result);
Expand All @@ -471,14 +476,14 @@ public async Task CallTool_Stdio_MemoryServer()
[Fact]
public async Task GetAIFunctionsAsync_UsingEverythingServer_ToolsAreProperlyCalled()
{
if (s_openAIKey is null)
{
return; // Skip the test if the OpenAI key is not provided
}
SkipTestIfNoOpenAIKey();

// Get the MCP client and tools from it.
await using var client = await McpClientFactory.CreateAsync(_fixture.EverythingServerConfig, _fixture.DefaultOptions); ;
var mappedTools = await client.GetAIFunctionsAsync();
await using var client = await McpClientFactory.CreateAsync(
_fixture.EverythingServerConfig,
_fixture.DefaultOptions,
cancellationToken: TestContext.Current.CancellationToken);
var mappedTools = await client.GetAIFunctionsAsync(TestContext.Current.CancellationToken);

// Create the chat client.
using IChatClient chatClient = new OpenAIClient(s_openAIKey).AsChatClient("gpt-4o-mini")
Expand All @@ -495,7 +500,7 @@ public async Task GetAIFunctionsAsync_UsingEverythingServer_ToolsAreProperlyCall
messages.Add(new(ChatRole.User, "Please call the echo tool with the string 'Hello MCP!' and output the response ad verbatim."));

// Call the chat client
var response = await chatClient.GetResponseAsync(messages, new() { Tools = [.. mappedTools], Temperature = 0 });
var response = await chatClient.GetResponseAsync(messages, new() { Tools = [.. mappedTools], Temperature = 0 }, TestContext.Current.CancellationToken);

// Assert
Assert.Contains("Echo: Hello MCP!", response.Text);
Expand All @@ -504,10 +509,7 @@ public async Task GetAIFunctionsAsync_UsingEverythingServer_ToolsAreProperlyCall
[Fact]
public async Task SamplingViaChatClient_RequestResponseProperlyPropagated()
{
if (s_openAIKey is null)
{
return; // Skip the test if the OpenAI key is not provided
}
SkipTestIfNoOpenAIKey();

await using var client = await McpClientFactory.CreateAsync(_fixture.EverythingServerConfig, new()
{
Expand All @@ -519,17 +521,25 @@ public async Task SamplingViaChatClient_RequestResponseProperlyPropagated()
SamplingHandler = new OpenAIClient(s_openAIKey).AsChatClient("gpt-4o-mini").CreateSamplingHandler(),
},
},
});
}, cancellationToken: TestContext.Current.CancellationToken);

var result = await client.CallToolAsync("sampleLLM", new()
{
["prompt"] = "In just a few words, what is the most famous tower in Paris?",
});
}, TestContext.Current.CancellationToken);

Assert.NotNull(result);
Assert.NotEmpty(result.Content);
Assert.Equal("text", result.Content[0].Type);
Assert.Contains("LLM sampling result:", result.Content[0].Text);
Assert.Contains("Eiffel", result.Content[0].Text);
}

private static void SkipTestIfNoOpenAIKey()
{
if (string.IsNullOrEmpty(s_openAIKey))
{
throw SkipException.ForSkip("No OpenAI key provided. Skipping test.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
Loading