Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="$(MicrosoftExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="$(System10Version)" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(System10Version)" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(System10Version)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(System10Version)" />
</ItemGroup>
Expand Down
5 changes: 5 additions & 0 deletions src/ModelContextProtocol.Core/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.AI;
using ModelContextProtocol.Authentication;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -158,6 +159,10 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(BlobResourceContents))]
[JsonSerializable(typeof(TextResourceContents))]

// Distributed cache event stream store
[JsonSerializable(typeof(DistributedCacheEventStreamStore.StreamMetadata))]
[JsonSerializable(typeof(DistributedCacheEventStreamStore.StoredEvent))]

// Other MCP Types
[JsonSerializable(typeof(IReadOnlyDictionary<string, object>))]
[JsonSerializable(typeof(ProgressToken))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
Copy link
Contributor

Choose a reason for hiding this comment

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

Is ModelContextProtocol.Core the right assembly for this, or should it instead live in ModelContextProtocol or ModelContextProtocol.AspNetCore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It could probably live in ModelContextProtocol.AspNetCore for the sake of minimizing dependencies in the .Core project.

</ItemGroup>

<!-- Reference analyzers -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// This is a shared source file included in both ModelContextProtocol.Core and the test project.
// Do not reference symbols internal to the core project, as they won't be available in tests.

#if NET
using System.Buffers;
using System.Buffers.Text;
using System.Diagnostics.CodeAnalysis;

#endif
using System.Text;

namespace ModelContextProtocol.Server;

/// <summary>
/// Provides methods for formatting and parsing event IDs used by <see cref="DistributedCacheEventStreamStore"/>.
/// </summary>
/// <remarks>
/// Event IDs are formatted as "{base64(sessionId)}:{base64(streamId)}:{sequence}".
/// </remarks>
internal static class DistributedCacheEventIdFormatter
{
private const char Separator = ':';

/// <summary>
/// Formats session ID, stream ID, and sequence number into an event ID string.
/// </summary>
public static string Format(string sessionId, string streamId, long sequence)
{
// Base64-encode session and stream IDs so the event ID can be parsed
// even if the original IDs contain the ':' separator character
var sessionBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(sessionId));
var streamBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(streamId));
Copy link
Contributor

Choose a reason for hiding this comment

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

Separate from this PR, we should really add Base64 overloads that handle this without the intermediate byte[]. I will follow up.

return $"{sessionBase64}{Separator}{streamBase64}{Separator}{sequence}";
}

/// <summary>
/// Attempts to parse an event ID into its component parts.
/// </summary>
public static bool TryParse(string eventId, out string sessionId, out string streamId, out long sequence)
{
sessionId = string.Empty;
streamId = string.Empty;
sequence = 0;

#if NET
ReadOnlySpan<char> eventIdSpan = eventId.AsSpan();
Span<Range> partRanges = stackalloc Range[4];
int rangeCount = eventIdSpan.Split(partRanges, Separator);
if (rangeCount != 3)
{
return false;
}

try
{
ReadOnlySpan<char> sessionBase64 = eventIdSpan[partRanges[0]];
ReadOnlySpan<char> streamBase64 = eventIdSpan[partRanges[1]];
ReadOnlySpan<char> sequenceSpan = eventIdSpan[partRanges[2]];

if (!TryDecodeBase64ToString(sessionBase64, out sessionId!) ||
!TryDecodeBase64ToString(streamBase64, out streamId!))
{
return false;
}

return long.TryParse(sequenceSpan, out sequence);
}
catch
{
return false;
}
#else
var parts = eventId.Split(Separator);
if (parts.Length != 3)
{
return false;
}

try
{
sessionId = Encoding.UTF8.GetString(Convert.FromBase64String(parts[0]));
streamId = Encoding.UTF8.GetString(Convert.FromBase64String(parts[1]));
return long.TryParse(parts[2], out sequence);
}
catch
{
return false;
}
#endif
}

#if NET
private static bool TryDecodeBase64ToString(ReadOnlySpan<char> base64Chars, [NotNullWhen(true)] out string? result)
{
// Use a single buffer: base64 chars are ASCII (1:1 with UTF8 bytes),
// and decoded data is always smaller than encoded, so we can decode in-place.
int bufferLength = base64Chars.Length;
Span<byte> buffer = bufferLength <= 256
? stackalloc byte[bufferLength]
: new byte[bufferLength];

Encoding.UTF8.GetBytes(base64Chars, buffer);

OperationStatus status = Base64.DecodeFromUtf8InPlace(buffer, out int bytesWritten);
if (status != OperationStatus.Done)
{
result = null;
return false;
}

result = Encoding.UTF8.GetString(buffer[..bytesWritten]);
return true;
}
#endif
}
Loading
Loading