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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="$(System10Version)" />
<PackageVersion Include="xunit.v3" Version="2.0.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions ModelContextProtocol.sln
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EverythingServer", "samples
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNetCore", "src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj", "{37B6A5E0-9995-497D-8B43-3BC6870CC716}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNetCore.Tests", "tests\ModelContextProtocol.AspNetCore.Tests\ModelContextProtocol.AspNetCore.Tests.csproj", "{85557BA6-3D29-4C95-A646-2A972B1C2F25}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -104,6 +106,10 @@ Global
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Release|Any CPU.Build.0 = Release|Any CPU
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85557BA6-3D29-4C95-A646-2A972B1C2F25}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -121,6 +127,7 @@ Global
{0D1552DC-E6ED-4AAC-5562-12F8352F46AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{17B8453F-AB72-99C5-E5EA-D0B065A6AE65} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{37B6A5E0-9995-497D-8B43-3BC6870CC716} = {A2F1F52A-9107-4BF8-8C3F-2F6670E7D0AD}
{85557BA6-3D29-4C95-A646-2A972B1C2F25} = {2A77AF5C-138A-4EBB-9A13-9205DCD67928}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}
Expand Down
2 changes: 1 addition & 1 deletion samples/TestServerWithHosting/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@
}
finally
{
await Log.CloseAndFlushAsync();
Log.CloseAndFlush();
}
2 changes: 1 addition & 1 deletion samples/TestServerWithHosting/TestServerWithHosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net9.0;net8.0;net472</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!--
Expand Down
24 changes: 24 additions & 0 deletions src/Common/Polyfills/System/Diagnostics/ProcessExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,28 @@ public static void Kill(this Process process, bool entireProcessTree)
_ = entireProcessTree;
process.Kill();
}

public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default)
{
if (process.HasExited)
{
return;
}

var tcs = new TaskCompletionSource<bool>();
void ProcessExitedHandler(object? sender, EventArgs e) => tcs.TrySetResult(true);

try
{
process.EnableRaisingEvents = true;
process.Exited += ProcessExitedHandler;

using var _ = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken));
await tcs.Task.ConfigureAwait(false);
}
finally
{
process.Exited -= ProcessExitedHandler;
}
}
}
16 changes: 1 addition & 15 deletions src/Common/Polyfills/System/IO/TextReaderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,7 @@ internal static class TextReaderExtensions
{
public static Task<string> ReadLineAsync(this TextReader reader, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<string>(cancellationToken);
}

cancellationToken.ThrowIfCancellationRequested();
return reader.ReadLineAsync();
}

public static Task<string> ReadToEndAsync(this TextReader reader, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<string>(cancellationToken);
}

return reader.ReadToEndAsync();
}
}
27 changes: 0 additions & 27 deletions src/Common/Polyfills/System/IO/TextWriterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,6 @@ namespace System.IO;

internal static class TextWriterExtensions
{
public static async Task WriteLineAsync(this TextWriter writer, ReadOnlyMemory<char> value, CancellationToken cancellationToken)
{
Throw.IfNull(writer);

if (value.IsEmpty)
{
return;
}

cancellationToken.ThrowIfCancellationRequested();

if (MemoryMarshal.TryGetString(value, out string str, out int start, out int length) &&
start == 0 && length == str.Length)
{
await writer.WriteLineAsync(str).ConfigureAwait(false);
}
else if (MemoryMarshal.TryGetArray(value, out ArraySegment<char> array) &&
array.Array is not null && array.Offset == 0 && array.Count == array.Array.Length)
{
await writer.WriteLineAsync(array.Array).ConfigureAwait(false);
}
else
{
await writer.WriteLineAsync(value.ToArray()).ConfigureAwait(false);
}
}

public static async Task FlushAsync(this TextWriter writer, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Expand Down
11 changes: 8 additions & 3 deletions src/Common/Polyfills/System/Threading/Tasks/TaskExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ public static Task WaitAsync(this Task task, CancellationToken cancellationToken
return WaitAsync(task, Timeout.InfiniteTimeSpan, cancellationToken);
}

public static async Task<T> WaitAsync<T>(this Task<T> task, CancellationToken cancellationToken)
public static Task<T> WaitAsync<T>(this Task<T> task, CancellationToken cancellationToken)
{
await WaitAsync(task, Timeout.InfiniteTimeSpan, cancellationToken);
return WaitAsync(task, Timeout.InfiniteTimeSpan, cancellationToken);
}

public static async Task<T> WaitAsync<T>(this Task<T> task, TimeSpan timeout, CancellationToken cancellationToken = default)
{
await WaitAsync((Task)task, timeout, cancellationToken).ConfigureAwait(false);
return task.Result;
}

public static async Task WaitAsync(this Task task, TimeSpan timeout, CancellationToken cancellationToken)
public static async Task WaitAsync(this Task task, TimeSpan timeout, CancellationToken cancellationToken = default)
{
Throw.IfNull(task);

Expand Down
File renamed without changes.
4 changes: 4 additions & 0 deletions src/ModelContextProtocol/ModelContextProtocol.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Common\Throw.cs" Link="Utils\Throw.cs" />
</ItemGroup>

<!-- Dependencies only needed by netstandard2.0 -->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<Compile Include="..\Common\Polyfills\**\*.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>Latest</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>ModelContextProtocol.AspNetCore.Tests</RootNamespace>
</PropertyGroup>

<PropertyGroup>
<!-- Without this, tests are currently not showing results until all tests complete
https://xunit.net/docs/getting-started/v3/microsoft-testing-platform
-->
<DisableTestingPlatformServerCapability>true</DisableTestingPlatformServerCapability>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="GitHubActionsTestLogger">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<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">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
<ProjectReference Include="..\ModelContextProtocol.TestSseServer\ModelContextProtocol.TestSseServer.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Builder;
using ModelContextProtocol.Tests.Utils;

namespace ModelContextProtocol.Tests.Server;
namespace ModelContextProtocol.AspNetCore.Tests.Server;

public class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelInMemoryTest(testOutputHelper)
{
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace ModelContextProtocol.Tests.Utils;

public class DelegatingTestOutputHelper : ITestOutputHelper
{
public ITestOutputHelper? CurrentTestOutputHelper { get; set; }

public string Output => CurrentTestOutputHelper?.Output ?? string.Empty;

public void Write(string message) => CurrentTestOutputHelper?.Write(message);
public void Write(string format, params object[] args) => CurrentTestOutputHelper?.Write(format, args);
public void WriteLine(string message) => CurrentTestOutputHelper?.WriteLine(message);
public void WriteLine(string format, params object[] args) => CurrentTestOutputHelper?.WriteLine(format, args);
}
31 changes: 31 additions & 0 deletions tests/ModelContextProtocol.AspNetCore.Tests/Utils/LoggedTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Test.Utils;

namespace ModelContextProtocol.Tests.Utils;

public class LoggedTest : IDisposable
{
private readonly DelegatingTestOutputHelper _delegatingTestOutputHelper;

public LoggedTest(ITestOutputHelper testOutputHelper)
{
_delegatingTestOutputHelper = new()
{
CurrentTestOutputHelper = testOutputHelper,
};
LoggerProvider = new XunitLoggerProvider(_delegatingTestOutputHelper);
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
{
builder.AddProvider(LoggerProvider);
});
}

public ITestOutputHelper TestOutputHelper => _delegatingTestOutputHelper;
public ILoggerFactory LoggerFactory { get; }
public ILoggerProvider LoggerProvider { get; }

public virtual void Dispose()
{
_delegatingTestOutputHelper.CurrentTestOutputHelper = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace ModelContextProtocol.Tests.Utils;

public class MockHttpHandler : HttpMessageHandler
{
public Func<HttpRequestMessage, Task<HttpResponseMessage>>? RequestHandler { get; set; }

protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (RequestHandler == null)
throw new InvalidOperationException($"No {nameof(RequestHandler)} was set! Please set handler first and make request afterwards.");

cancellationToken.ThrowIfCancellationRequested();

var result = await RequestHandler.Invoke(request);

cancellationToken.ThrowIfCancellationRequested();

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using System.Text.Json;
using System.Threading.Channels;

namespace ModelContextProtocol.Tests.Utils;

public class TestServerTransport : ITransport
{
private readonly Channel<IJsonRpcMessage> _messageChannel;

public bool IsConnected { get; set; }

public ChannelReader<IJsonRpcMessage> MessageReader => _messageChannel;

public List<IJsonRpcMessage> SentMessages { get; } = [];

public Action<IJsonRpcMessage>? OnMessageSent { get; set; }

public TestServerTransport()
{
_messageChannel = Channel.CreateUnbounded<IJsonRpcMessage>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = true,
});
IsConnected = true;
}

public ValueTask DisposeAsync()
{
_messageChannel.Writer.TryComplete();
IsConnected = false;
return ValueTask.CompletedTask;
}

public async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
SentMessages.Add(message);
if (message is JsonRpcRequest request)
{
if (request.Method == RequestMethods.RootsList)
await ListRoots(request, cancellationToken);
else if (request.Method == RequestMethods.SamplingCreateMessage)
await Sampling(request, cancellationToken);
else
await WriteMessageAsync(request, cancellationToken);
}
else if (message is JsonRpcNotification notification)
{
await WriteMessageAsync(notification, cancellationToken);
}

OnMessageSent?.Invoke(message);
}

private async Task ListRoots(JsonRpcRequest request, CancellationToken cancellationToken)
{
await WriteMessageAsync(new JsonRpcResponse
{
Id = request.Id,
Result = JsonSerializer.SerializeToNode(new ListRootsResult
{
Roots = []
}),
}, cancellationToken);
}

private async Task Sampling(JsonRpcRequest request, CancellationToken cancellationToken)
{
await WriteMessageAsync(new JsonRpcResponse
{
Id = request.Id,
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new(), Model = "model", Role = "role" }),
}, cancellationToken);
}

private async Task WriteMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
await _messageChannel.Writer.WriteAsync(message, cancellationToken);
}
}
Loading
Loading