Multi Round-Trip Requests (MRTR) (#1458)
This commit is contained in:
parent
8202bcc932
commit
f6dbe4372f
2 changed files with 5832 additions and 103 deletions
|
|
@ -205,6 +205,44 @@ public static class NodeHelpers
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the SEP-2322 (Multi Round-Trip Requests / IncompleteResult)
|
||||
/// conformance scenarios are available by reading the conformance package version
|
||||
/// from the repo's package.json. MRTR scenarios require a conformance package version
|
||||
/// that includes SEP-2322 support (see
|
||||
/// https://github.com/modelcontextprotocol/conformance/pull/188).
|
||||
/// </summary>
|
||||
public static bool HasMrtrScenarios()
|
||||
{
|
||||
try
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var packageJsonPath = Path.Combine(repoRoot, "package.json");
|
||||
if (!File.Exists(packageJsonPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var json = System.Text.Json.JsonDocument.Parse(File.ReadAllText(packageJsonPath));
|
||||
if (json.RootElement.TryGetProperty("dependencies", out var deps) &&
|
||||
deps.TryGetProperty("@modelcontextprotocol/conformance", out var versionElement))
|
||||
{
|
||||
var versionStr = versionElement.GetString();
|
||||
if (versionStr is not null && Version.TryParse(versionStr, out var version))
|
||||
{
|
||||
// SEP-2322 scenarios are expected in conformance package >= 0.2.0
|
||||
return version >= new Version(0, 2, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ProcessStartInfo NpmStartInfo(string arguments, string workingDirectory)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
|
|
|
|||
95
tests/Common/Utils/ServerMessageTracker.cs
Normal file
95
tests/Common/Utils/ServerMessageTracker.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json.Nodes;
|
||||
using Xunit;
|
||||
|
||||
namespace ModelContextProtocol.Tests.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks MRTR protocol mode via incoming and outgoing message filters.
|
||||
/// Used by MRTR tests to verify the correct protocol mode (MRTR vs legacy) was used.
|
||||
/// </summary>
|
||||
internal sealed class ServerMessageTracker
|
||||
{
|
||||
private static readonly HashSet<string> LegacyMrtrMethods =
|
||||
[
|
||||
RequestMethods.ElicitationCreate,
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
RequestMethods.RootsList,
|
||||
];
|
||||
|
||||
private readonly ConcurrentBag<string> _legacyRequestMethods = [];
|
||||
private int _mrtrRetryCount;
|
||||
private int _incompleteResultCount;
|
||||
|
||||
/// <summary>
|
||||
/// Adds incoming and outgoing message filters to track MRTR protocol usage.
|
||||
/// Call this in <c>services.Configure<McpServerOptions></c> or <c>AddMcpServer</c> callbacks.
|
||||
/// </summary>
|
||||
public void AddFilters(McpMessageFilters messageFilters)
|
||||
{
|
||||
// Track outgoing legacy JSON-RPC requests and InputRequiredResult responses.
|
||||
messageFilters.OutgoingFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
if (context.JsonRpcMessage is JsonRpcRequest request && LegacyMrtrMethods.Contains(request.Method))
|
||||
{
|
||||
_legacyRequestMethods.Add(request.Method);
|
||||
}
|
||||
else if (context.JsonRpcMessage is JsonRpcResponse response &&
|
||||
response.Result is JsonObject resultObj &&
|
||||
resultObj.TryGetPropertyValue("resultType", out var resultTypeNode) &&
|
||||
resultTypeNode?.GetValue<string>() == "input_required")
|
||||
{
|
||||
Interlocked.Increment(ref _incompleteResultCount);
|
||||
}
|
||||
|
||||
await next(context, cancellationToken);
|
||||
});
|
||||
|
||||
// Track incoming MRTR retries (requests with inputResponses or requestState in params).
|
||||
messageFilters.IncomingFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
if (context.JsonRpcMessage is JsonRpcRequest request &&
|
||||
request.Params is JsonObject paramsObj &&
|
||||
(paramsObj.ContainsKey("inputResponses") || paramsObj.ContainsKey("requestState")))
|
||||
{
|
||||
Interlocked.Increment(ref _mrtrRetryCount);
|
||||
}
|
||||
|
||||
await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that MRTR was used: at least one InputRequiredResult response was sent
|
||||
/// and no legacy JSON-RPC requests (elicitation/create, sampling/createMessage, roots/list) were sent.
|
||||
/// </summary>
|
||||
public void AssertMrtrUsed()
|
||||
{
|
||||
Assert.True(_incompleteResultCount > 0,
|
||||
"Expected at least one InputRequiredResult response (MRTR mode), but none were detected.");
|
||||
Assert.Empty(_legacyRequestMethods);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that MRTR was used at least once (at least one InputRequiredResult response was sent),
|
||||
/// independent of whether the session also issued any legacy server-to-client requests.
|
||||
/// </summary>
|
||||
public void AssertMrtrUsedAtLeastOnce()
|
||||
{
|
||||
Assert.True(_incompleteResultCount > 0,
|
||||
"Expected at least one InputRequiredResult response (MRTR mode), but none were detected.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that legacy mode was used: at least one legacy JSON-RPC request was sent
|
||||
/// and no MRTR retries or InputRequiredResult responses were detected.
|
||||
/// </summary>
|
||||
public void AssertMrtrNotUsed()
|
||||
{
|
||||
Assert.NotEmpty(_legacyRequestMethods);
|
||||
Assert.Equal(0, _mrtrRetryCount);
|
||||
Assert.Equal(0, _incompleteResultCount);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue