Reduce dependencies to 8.x versions of packages (#195)

* Reduce dependencies to 8.x versions of packages

When building for netstandard2.0 or net8.0, the ModelContextProtocol library now only depends on 8.x versions of nuget packages, rather than using the newer 9.x versions. The exception to this are packages that don't have 8.x versions.

This also introduces a net9.0 tfm. When building for net9.0, it'll use 9.x versions of packages.

The PR temporarily removes the Microsoft.Extensions.AI.nupkg reference (the Microsoft.Extensions.AI.Abstractions reference is still very much there). The latest Microsoft.Extensions.AI package on nuget has a System.Text.Json 9.x reference, but the next build to be published drops that down to 8.x as well. Once that build is published, the dependency will be added back, and temporary polyfills (in particular TemporaryAIFunctionFactory) will be deleted.

* Fix StdioClientSessionTransport shutdown

StreamClientSessionTransport's ReadMessageAsync method does await CleanupAsync in its finally. CleanupAsync awaits _readTask. _readTask _is_ ReadMessageAsync. So it's waiting for itself to complete, which will then always timeout. We may want to refactor how this works more in the future, but for now, make sure it doesn't wait for itself.

* Fix dangling requests when server unexpectedly shuts down

If the client makes a request to the server and while waiting for the response the server shuts down, the client hangs. Make sure that when the client's message loops ends due to the server's output it's reading from ends, any pending requests are notified.

* Add net9.0 testing

* Avoid docker port conflicts from test suites running concurrently
This commit is contained in:
Stephen Toub 2025-04-03 14:01:46 -04:00 • committed by GitHub
commit c452dc8e26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 247 additions and 91 deletions

View file

@ -1,36 +1,59 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<SystemVersion>9.0.3</SystemVersion>
<System10Version>10.0.0-preview.2.25163.2</System10Version>
<MicrosoftExtensionsVersion>9.0.3</MicrosoftExtensionsVersion>
<MicrosoftExtensionsAIVersion>9.3.0-preview.1.25161.3</MicrosoftExtensionsAIVersion>
</PropertyGroup>
<!-- Product dependencies netstandard -->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageVersion Include="Microsoft.Bcl.Memory" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageVersion Include="System.IO.Pipelines" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="System.Threading.Channels" Version="8.0.0" />
</ItemGroup>
<!-- Product dependencies LTS -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageVersion Include="System.IO.Pipelines" Version="8.0.0" />
</ItemGroup>
<!-- Product dependencies .NET 9 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageVersion Include="System.IO.Pipelines" Version="9.0.0" />
</ItemGroup>
<!-- Product dependencies shared -->
<ItemGroup>
<!-- Product dependencies -->
<PackageVersion Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.Bcl.Memory" Version="$(SystemVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="$(System10Version)" />
<PackageVersion Include="System.Text.Json" Version="$(SystemVersion)" />
<PackageVersion Include="System.Threading.Channels" Version="$(SystemVersion)" />
</ItemGroup>
<ItemGroup>
<!-- Build Infra & Packaging -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<!-- Testing dependencies -->
<PackageVersion Include="Anthropic.SDK" Version="5.0.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="OpenTelemetry" Version="1.11.2" />

View file

@ -15,6 +15,7 @@
<ItemGroup>
<PackageReference Include="Anthropic.SDK" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.AI" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,17 @@
using System.Runtime.CompilerServices;
namespace System.Threading.Channels;
internal static class ChannelExtensions
{
public static async IAsyncEnumerable<T> ReadAllAsync<T>(this ChannelReader<T> reader, [EnumeratorCancellation] CancellationToken cancellationToken)
{
while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
{
while (reader.TryRead(out var item))
{
yield return item;
}
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>

View file

@ -24,7 +24,7 @@ public static class McpClientExtensions
return client.SendRequestAsync(
RequestMethods.Ping,
parameters: null,
McpJsonUtilities.JsonContext.Default.Object,
McpJsonUtilities.JsonContext.Default.Object!,
McpJsonUtilities.JsonContext.Default.Object,
cancellationToken: cancellationToken);
}
@ -52,7 +52,7 @@ public static class McpClientExtensions
{
var toolResults = await client.SendRequestAsync(
RequestMethods.ToolsList,
CreateCursorDictionary(cursor),
CreateCursorDictionary(cursor)!,
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListToolsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
@ -96,7 +96,7 @@ public static class McpClientExtensions
{
var toolResults = await client.SendRequestAsync(
RequestMethods.ToolsList,
CreateCursorDictionary(cursor),
CreateCursorDictionary(cursor)!,
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListToolsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
@ -128,7 +128,7 @@ public static class McpClientExtensions
{
var promptResults = await client.SendRequestAsync(
RequestMethods.PromptsList,
CreateCursorDictionary(cursor),
CreateCursorDictionary(cursor)!,
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
@ -166,7 +166,7 @@ public static class McpClientExtensions
{
var promptResults = await client.SendRequestAsync(
RequestMethods.PromptsList,
CreateCursorDictionary(cursor),
CreateCursorDictionary(cursor)!,
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
@ -230,7 +230,7 @@ public static class McpClientExtensions
{
var templateResults = await client.SendRequestAsync(
RequestMethods.ResourcesTemplatesList,
CreateCursorDictionary(cursor),
CreateCursorDictionary(cursor)!,
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
@ -271,7 +271,7 @@ public static class McpClientExtensions
{
var templateResults = await client.SendRequestAsync(
RequestMethods.ResourcesTemplatesList,
CreateCursorDictionary(cursor),
CreateCursorDictionary(cursor)!,
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
@ -304,7 +304,7 @@ public static class McpClientExtensions
{
var resourceResults = await client.SendRequestAsync(
RequestMethods.ResourcesList,
CreateCursorDictionary(cursor),
CreateCursorDictionary(cursor)!,
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
@ -345,7 +345,7 @@ public static class McpClientExtensions
{
var resourceResults = await client.SendRequestAsync(
RequestMethods.ResourcesList,
CreateCursorDictionary(cursor),
CreateCursorDictionary(cursor)!,
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
@ -374,7 +374,7 @@ public static class McpClientExtensions
return client.SendRequestAsync(
RequestMethods.ResourcesRead,
new Dictionary<string, object?> { ["uri"] = uri },
new Dictionary<string, object> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
cancellationToken: cancellationToken);
@ -401,7 +401,7 @@ public static class McpClientExtensions
return client.SendRequestAsync(
RequestMethods.CompletionComplete,
new Dictionary<string, object?>
new Dictionary<string, object>
{
["ref"] = reference,
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
@ -424,7 +424,7 @@ public static class McpClientExtensions
return client.SendRequestAsync(
RequestMethods.ResourcesSubscribe,
new Dictionary<string, object?> { ["uri"] = uri },
new Dictionary<string, object> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
@ -443,7 +443,7 @@ public static class McpClientExtensions
return client.SendRequestAsync(
RequestMethods.ResourcesUnsubscribe,
new Dictionary<string, object?> { ["uri"] = uri },
new Dictionary<string, object> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
@ -629,7 +629,7 @@ public static class McpClientExtensions
return client.SendRequestAsync(
RequestMethods.LoggingSetLevel,
new Dictionary<string, object?> { ["level"] = level },
new Dictionary<string, object> { ["level"] = level },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);

View file

@ -7,10 +7,20 @@ internal static class Diagnostics
{
internal static ActivitySource ActivitySource { get; } = new("Experimental.ModelContextProtocol");
internal static Meter Meter { get; } = new("Experimental.ModelContextProtocol");
internal static Histogram<double> CreateDurationHistogram(string name, string description, bool longBuckets) =>
Diagnostics.Meter.CreateHistogram<double>(name, "s", description
#if NET9_0_OR_GREATER
, advice: longBuckets ? LongSecondsBucketBoundaries : ShortSecondsBucketBoundaries
#endif
);
#if NET9_0_OR_GREATER
/// <summary>
/// Follows boundaries from http.server.request.duration/http.client.request.duration
/// </summary>
internal static InstrumentAdvice<double> ShortSecondsBucketBoundaries { get; } = new()
private static InstrumentAdvice<double> ShortSecondsBucketBoundaries { get; } = new()
{
HistogramBucketBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10],
};
@ -19,11 +29,9 @@ internal static class Diagnostics
/// Not based on a standard. Larger bucket sizes for longer lasting operations, e.g. HTTP connection duration.
/// See https://github.com/open-telemetry/semantic-conventions/issues/336
/// </summary>
internal static InstrumentAdvice<double> LongSecondsBucketBoundaries { get; } = new()
private static InstrumentAdvice<double> LongSecondsBucketBoundaries { get; } = new()
{
HistogramBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300],
};
internal static Meter Meter { get; } = new("Experimental.ModelContextProtocol");
#endif
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<PackageId>ModelContextProtocol</PackageId>
@ -12,15 +12,8 @@
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<!-- Dependencies only needed by netstandard2.0 -->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<Compile Include="..\Common\Polyfills\**\*.cs" />
<PackageReference Include="Microsoft.Bcl.Memory" />
@ -28,6 +21,24 @@
<PackageReference Include="System.Threading.Channels" />
</ItemGroup>
<!-- Dependencies only needed by netstandard2.0 or net8.0 -->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.IO.Pipelines" />
</ItemGroup>
<!-- Dependencies needed by all -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.Net.ServerSentEvents" />
<!-- Temporarily removed until new version can be picked up that
has reduced dependencies:
<PackageReference Include="Microsoft.Extensions.AI" />
-->
</ItemGroup>
<ItemGroup>
<None Include="..\..\README.md" pack="true" PackagePath="\" />
</ItemGroup>

View file

@ -53,7 +53,7 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string
return;
}
JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage!);
}
/// <inheritdoc/>

View file

@ -29,9 +29,16 @@ internal class StreamClientSessionTransport : TransportBase
_serverInput = serverInput;
EndpointName = endpointName;
// Start reading messages in the background
// Start reading messages in the background. We use the rarer pattern of new Task + Start
// in order to ensure that the body of the task will always see _readTask initialized.
// It is then able to reliably null it out on completion.
Logger.TransportReadingMessages(endpointName);
_readTask = Task.Run(() => ReadMessagesAsync(_shutdownCts.Token), CancellationToken.None);
var readTask = new Task<Task>(
thisRef => ((StreamClientSessionTransport)thisRef!).ReadMessagesAsync(_shutdownCts.Token),
this,
TaskCreationOptions.DenyChildAttach);
_readTask = readTask.Unwrap();
readTask.Start();
SetConnected(true);
}
@ -117,6 +124,7 @@ internal class StreamClientSessionTransport : TransportBase
}
finally
{
_readTask = null;
await CleanupAsync(cancellationToken).ConfigureAwait(false);
}
}

View file

@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol.Types;
/// A request to include context from one or more MCP servers (including the caller), to be attached to the prompt.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<ContextInclusion>))]
[JsonConverter(typeof(CustomizableJsonStringEnumConverter<ContextInclusion>))]
public enum ContextInclusion
{
/// <summary>

View file

@ -7,7 +7,7 @@ namespace ModelContextProtocol.Protocol.Types;
/// These map to syslog message severities, as specified in RFC-5424:
/// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<LoggingLevel>))]
[JsonConverter(typeof(CustomizableJsonStringEnumConverter<LoggingLevel>))]
public enum LoggingLevel
{
/// <summary>Detailed debug information, typically only valuable to developers.</summary>

View file

@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol.Types;
/// Represents the type of role in the conversation.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<Role>))]
[JsonConverter(typeof(CustomizableJsonStringEnumConverter<Role>))]
public enum Role
{
/// <summary>

View file

@ -133,9 +133,9 @@ internal static partial class TemporaryAIFunctionFactory
/// <remarks>
/// <para>
/// Return values are serialized to <see cref="JsonElement"/> using <paramref name="options"/>'s
/// <see cref="AIFunctionFactoryOptions.SerializerOptions"/>. Arguments that are not already of the expected type are
/// <see cref="TemporaryAIFunctionFactoryOptions.SerializerOptions"/>. Arguments that are not already of the expected type are
/// marshaled to the expected type via JSON and using <paramref name="options"/>'s
/// <see cref="AIFunctionFactoryOptions.SerializerOptions"/>. If the argument is a <see cref="JsonElement"/>,
/// <see cref="TemporaryAIFunctionFactoryOptions.SerializerOptions"/>. If the argument is a <see cref="JsonElement"/>,
/// <see cref="JsonDocument"/>, or <see cref="JsonNode"/>, it is deserialized directly. If the argument is anything else unknown,
/// it is round-tripped through JSON, serializing the object as JSON and then deserializing it to the expected type.
/// </para>

View file

@ -72,7 +72,7 @@ internal sealed class TemporaryAIFunctionFactoryOptions
/// <summary>Gets or sets a delegate used to determine the <see cref="object"/> returned by <see cref="AIFunction.InvokeAsync"/>.</summary>
/// <remarks>
/// <para>
/// By default, the return value of invoking the method wrapped into an <see cref="AIFunction"/> by <see cref="AIFunctionFactory"/>
/// By default, the return value of invoking the method wrapped into an <see cref="AIFunction"/> by <see cref="TemporaryAIFunctionFactory"/>
/// is then JSON serialized, with the resulting <see cref="JsonElement"/> returned from the <see cref="AIFunction.InvokeAsync"/> method.
/// This default behavior is ideal for the common case where the result will be passed back to an AI service. However, if the caller
/// requires more control over the result's marshaling, the <see cref="MarshalResult"/> property may be set to a delegate that is
@ -82,7 +82,7 @@ internal sealed class TemporaryAIFunctionFactoryOptions
/// <para>
/// When set, the delegate is invoked even for <see langword="void"/>-returning methods, in which case it is invoked with
/// a <see langword="null"/> argument. By default, <see langword="null"/> is returned from the <see cref="AIFunction.InvokeAsync"/>
/// method for <see cref="AIFunction"/> instances produced by <see cref="AIFunctionFactory"/> to wrap
/// method for <see cref="AIFunction"/> instances produced by <see cref="TemporaryAIFunctionFactory"/> to wrap
/// <see langword="void"/>-returning methods).
/// </para>
/// <para>

View file

@ -12,6 +12,7 @@ using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Channels;
namespace ModelContextProtocol.Shared;
@ -20,15 +21,14 @@ namespace ModelContextProtocol.Shared;
/// </summary>
internal sealed class McpSession : IDisposable
{
private static readonly Histogram<double> s_clientSessionDuration = Diagnostics.Meter.CreateHistogram<double>(
"mcp.client.session.duration", "s", "Measures the duration of a client session.", advice: Diagnostics.LongSecondsBucketBoundaries);
private static readonly Histogram<double> s_serverSessionDuration = Diagnostics.Meter.CreateHistogram<double>(
"mcp.server.session.duration", "s", "Measures the duration of a server session.", advice: Diagnostics.LongSecondsBucketBoundaries);
private static readonly Histogram<double> s_serverRequestDuration = Diagnostics.Meter.CreateHistogram<double>(
"rpc.server.duration", "s", "Measures the duration of inbound RPC.", advice: Diagnostics.ShortSecondsBucketBoundaries);
private static readonly Histogram<double> s_clientRequestDuration = Diagnostics.Meter.CreateHistogram<double>(
"rpc.client.duration", "s", "Measures the duration of outbound RPC.", advice: Diagnostics.ShortSecondsBucketBoundaries);
private static readonly Histogram<double> s_clientSessionDuration = Diagnostics.CreateDurationHistogram(
"mcp.client.session.duration", "Measures the duration of a client session.", longBuckets: true);
private static readonly Histogram<double> s_serverSessionDuration = Diagnostics.CreateDurationHistogram(
"mcp.server.session.duration", "Measures the duration of a server session.", longBuckets: true);
private static readonly Histogram<double> s_clientRequestDuration = Diagnostics.CreateDurationHistogram(
"rpc.client.duration", "Measures the duration of outbound RPC.", longBuckets: false);
private static readonly Histogram<double> s_serverRequestDuration = Diagnostics.CreateDurationHistogram(
"rpc.server.duration", "Measures the duration of inbound RPC.", longBuckets: false);
private readonly bool _isServer;
private readonly string _transportKind;
@ -174,6 +174,14 @@ internal sealed class McpSession : IDisposable
// Normal shutdown
_logger.EndpointMessageProcessingCancelled(EndpointName);
}
finally
{
// Fail any pending requests, as they'll never be satisfied.
foreach (var entry in _pendingRequests)
{
entry.Value.TrySetException(new InvalidOperationException("The server shut down unexpectedly."));
}
}
}
private async Task HandleMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken)

View file

@ -0,0 +1,64 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
// NOTE:
// This is a temporary workaround for lack of System.Text.Json's JsonStringEnumConverter<T>
// 9.x support for JsonStringEnumMemberNameAttribute. Once all builds use the System.Text.Json 9.x
// version, this whole file can be removed.
namespace System.Text.Json.Serialization;
internal sealed class CustomizableJsonStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> :
JsonStringEnumConverter<TEnum> where TEnum : struct, Enum
{
#if !NET9_0_OR_GREATER
public CustomizableJsonStringEnumConverter() :
base(namingPolicy: ResolveNamingPolicy())
{
}
private static JsonNamingPolicy? ResolveNamingPolicy()
{
var map = typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(f => (f.Name, AttributeName: f.GetCustomAttribute<JsonStringEnumMemberNameAttribute>()?.Name))
.Where(pair => pair.AttributeName != null)
.ToDictionary(pair => pair.Name, pair => pair.AttributeName);
return map.Count > 0 ? new EnumMemberNamingPolicy(map!) : null;
}
private sealed class EnumMemberNamingPolicy(Dictionary<string, string> map) : JsonNamingPolicy
{
public override string ConvertName(string name) =>
map.TryGetValue(name, out string? newName) ?
newName :
name;
}
#endif
}
#if !NET9_0_OR_GREATER
/// <summary>
/// Determines the string value that should be used when serializing an enum member.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
internal sealed class JsonStringEnumMemberNameAttribute : Attribute
{
/// <summary>
/// Creates new attribute instance with a specified enum member name.
/// </summary>
/// <param name="name">The name to apply to the current enum member.</param>
public JsonStringEnumMemberNameAttribute(string name)
{
Name = name;
}
/// <summary>
/// Gets the name of the enum member.
/// </summary>
public string Name { get; }
}
#endif

View file

@ -78,7 +78,6 @@ public static partial class McpJsonUtilities
// Keep in sync with CreateDefaultOptions above.
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>TestServer</AssemblyName>
@ -10,8 +10,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.File" />
</ItemGroup>

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>TestSseServer</AssemblyName>
@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.File" />
</ItemGroup>

View file

@ -377,10 +377,12 @@ public class Program
{
Console.WriteLine("Starting server...");
int port = args.Length > 0 && uint.TryParse(args[0], out var parsedPort) ? (int)parsedPort : 3001;
var builder = WebApplication.CreateSlimBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(3001);
options.ListenLocalhost(port);
});
ConfigureSerilog(builder.Logging);

Some files were not shown because too many files have changed in this diff Show more