Add initial Activity / Metric support (#183)

This commit is contained in:
Stephen Toub 2025-04-02 11:09:05 -04:00 • committed by GitHub
commit 4b38d3e71b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 443 additions and 61 deletions

View file

@ -640,7 +640,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
Assert.Equal(10, array.Length);
for (int i = 0; i < array.Length; i++)
{
Assert.Equal("\"abc123\"", array[i].ProgressToken.ToString());
Assert.Equal("abc123", array[i].ProgressToken.ToString());
Assert.Equal(i, array[i].Progress.Progress);
Assert.Equal(10, array[i].Progress.Total);
Assert.Equal($"Progress {i}", array[i].Progress.Message);

View file

@ -0,0 +1,79 @@
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using OpenTelemetry.Trace;
using System.Diagnostics;
using System.IO.Pipelines;
namespace ModelContextProtocol.Tests;
[Collection(nameof(DisableParallelization))]
public class DiagnosticTests
{
[Fact]
public async Task Session_TracksActivities()
{
var activities = new List<Activity>();
using (var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource("ModelContextProtocol")
.AddInMemoryExporter(activities)
.Build())
{
await RunConnected(async (client, server) =>
{
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
Assert.NotNull(tools);
Assert.NotEmpty(tools);
var tool = tools.First(t => t.Name == "DoubleValue");
await tool.InvokeAsync(new Dictionary<string, object?>() { ["amount"] = 42 }, TestContext.Current.CancellationToken);
});
}
Assert.NotEmpty(activities);
Activity toolCallActivity = activities.First(a =>
a.Tags.Any(t => t.Key == "rpc.method" && t.Value == "tools/call"));
Assert.Equal("DoubleValue", toolCallActivity.Tags.First(t => t.Key == "mcp.request.params.name").Value);
}
private static async Task RunConnected(Func<IMcpClient, IMcpServer, Task> action)
{
Pipe clientToServerPipe = new(), serverToClientPipe = new();
StreamServerTransport serverTransport = new(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream());
StreamClientTransport clientTransport = new(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream());
Task serverTask;
await using (IMcpServer server = McpServerFactory.Create(serverTransport, new()
{
ServerInfo = new Implementation { Name = "TestServer", Version = "1.0.0" },
Capabilities = new()
{
Tools = new()
{
ToolCollection = [McpServerTool.Create((int amount) => amount * 2, new() { Name = "DoubleValue", Description = "Doubles the value." })],
}
}
}))
{
serverTask = server.RunAsync(TestContext.Current.CancellationToken);
await using (IMcpClient client = await McpClientFactory.CreateAsync(new()
{
Id = "TestServer",
Name = "TestServer",
TransportType = TransportTypes.StdIo,
},
createTransportFunc: (_, __) => clientTransport,
cancellationToken: TestContext.Current.CancellationToken))
{
await action(client, server);
}
}
await serverTask;
}
}

View file

@ -25,6 +25,8 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio">

View file

@ -0,0 +1,38 @@
using ModelContextProtocol.Protocol.Messages;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public class RequestIdTests
{
[Fact]
public void StringCtor_Roundtrips()
{
RequestId id = new("test-id");
Assert.Equal("test-id", id.ToString());
Assert.Equal("\"test-id\"", JsonSerializer.Serialize(id));
Assert.Same("test-id", id.Id);
Assert.True(id.Equals(new("test-id")));
Assert.False(id.Equals(new("tEst-id")));
Assert.Equal("test-id".GetHashCode(), id.GetHashCode());
Assert.Equal(id, JsonSerializer.Deserialize<RequestId>(JsonSerializer.Serialize(id)));
}
[Fact]
public void Int64Ctor_Roundtrips()
{
RequestId id = new(42);
Assert.Equal("42", id.ToString());
Assert.Equal("42", JsonSerializer.Serialize(id));
Assert.Equal(42, Assert.IsType<long>(id.Id));
Assert.True(id.Equals(new(42)));
Assert.False(id.Equals(new(43)));
Assert.False(id.Equals(new("42")));
Assert.Equal(42L.GetHashCode(), id.GetHashCode());
Assert.Equal(id, JsonSerializer.Deserialize<RequestId>(JsonSerializer.Serialize(id)));
}
}

View file

@ -684,7 +684,7 @@ public class McpServerTests : LoggedTest
var notification = await notificationReceived.Task;
var progress = (ProgressNotification)notification.Params!;
Assert.Equal("\"abc\"", progress.ProgressToken.ToString());
Assert.Equal("abc", progress.ProgressToken.ToString());
Assert.Equal(50, progress.Progress.Progress);
Assert.Equal(100, progress.Progress.Total);
Assert.Equal("Progress message", progress.Progress.Message);

View file

@ -1,2 +1,9 @@
// Uncomment to disable parallel test execution
//[assembly: CollectionBehavior(DisableTestParallelization = true)]
// Uncomment to disable parallel test execution for the whole assembly
//[assembly: CollectionBehavior(DisableTestParallelization = true)]
/// <summary>
/// Enables test classes to individually be attributed as [Collection(nameof(DisableParallelization))]
/// to have those tests run non-concurrently with any other tests.
/// </summary>
[CollectionDefinition(nameof(DisableParallelization), DisableParallelization = true)]
public sealed class DisableParallelization;

View file

@ -213,7 +213,7 @@ public class SseClientTransportTests : LoggedTest
Assert.True(session.MessageReader.TryRead(out var message));
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
Assert.Equal("\"44\"", ((JsonRpcRequest)message).Id.ToString());
Assert.Equal("44", ((JsonRpcRequest)message).Id.ToString());
}
[Fact]