using ModelContextProtocol.Protocol;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Protocol;
///
/// Targeted tests for the SEP-2549 caching hints (ttlMs and cacheScope) on
/// . Spec PR #2855 promotes both fields to required on the discover
/// response. has required CLR properties for
/// and ,
/// which prevents reuse of the parameterized
/// helper (it instantiates via reflection). This file covers the
/// same property-shape assertions for .
///
public static class DiscoverResultCacheableTests
{
private static DiscoverResult NewDiscoverResult() => new()
{
SupportedVersions = [McpProtocolVersions.November2025ProtocolVersion, McpProtocolVersions.July2026ProtocolVersion],
Capabilities = new ServerCapabilities(),
};
[Fact]
public static void DiscoverResult_SerializesTtlMsAsIntegerMilliseconds()
{
var result = NewDiscoverResult();
result.TimeToLive = TimeSpan.FromMilliseconds(300_000);
result.CacheScope = CacheScope.Public;
string json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var node = JsonNode.Parse(json)!.AsObject();
Assert.True(node.ContainsKey("ttlMs"));
Assert.Equal(JsonValueKind.Number, node["ttlMs"]!.GetValueKind());
Assert.Equal(300_000, node["ttlMs"]!.GetValue());
Assert.Equal("public", node["cacheScope"]!.GetValue());
var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)!;
Assert.Equal(TimeSpan.FromMilliseconds(300_000), deserialized.TimeToLive);
Assert.Equal(CacheScope.Public, deserialized.CacheScope);
}
[Fact]
public static void DiscoverResult_PrivateScope_RoundTrips()
{
var result = NewDiscoverResult();
result.TimeToLive = TimeSpan.Zero;
result.CacheScope = CacheScope.Private;
string json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var node = JsonNode.Parse(json)!.AsObject();
Assert.True(node.ContainsKey("ttlMs"));
Assert.Equal(0, node["ttlMs"]!.GetValue());
Assert.Equal("private", node["cacheScope"]!.GetValue());
var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)!;
Assert.Equal(TimeSpan.Zero, deserialized.TimeToLive);
Assert.Equal(CacheScope.Private, deserialized.CacheScope);
}
[Fact]
public static void DiscoverResult_OmitsCachingHints_WhenUnset()
{
var result = NewDiscoverResult();
string json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
var node = JsonNode.Parse(json)!.AsObject();
// Backward compatibility: servers that do not set the hints must not emit them.
Assert.False(node.ContainsKey("ttlMs"));
Assert.False(node.ContainsKey("cacheScope"));
var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)!;
Assert.Null(deserialized.TimeToLive);
Assert.Null(deserialized.CacheScope);
}
[Fact]
public static void DiscoverResult_DeserializesMissingHints_AsNull()
{
// A response from a pre-PR-#2855 server may omit both fields. Deserialization must succeed
// and surface them as null so callers can apply their own defaults.
string json =
"""
{
"supportedVersions": ["2025-11-25"],
"capabilities": {},
"serverInfo": {"name": "x", "version": "1"}
}
""";
var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)!;
Assert.Null(deserialized.TimeToLive);
Assert.Null(deserialized.CacheScope);
}
[Fact]
public static void DiscoverResult_DeserializesUnknownCacheScope_AsNull()
{
// A future or unknown cacheScope string must not break deserialization of the entire result.
string json =
"""
{
"supportedVersions": ["2025-11-25"],
"capabilities": {},
"serverInfo": {"name": "x", "version": "1"},
"cacheScope": "shared"
}
""";
var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)!;
Assert.Null(deserialized.CacheScope);
}
[Fact]
public static void DiscoverResult_ImplementsICacheableResult()
{
// Compile-time assertion that DiscoverResult participates in the shared cacheability surface
// alongside the list/read result types.
Assert.IsAssignableFrom(NewDiscoverResult());
}
}