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()); } }