Skip to content
24 changes: 24 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ public ValueTask<PingResult> PingAsync(
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A list of all available tools as <see cref="McpClientTool"/> instances.</returns>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// This overload aggregates every page into a single list and does not surface the per-result caching hints
/// (<see cref="ListToolsResult.TimeToLive"/> and <see cref="ListToolsResult.CacheScope"/>). To read those hints,
/// use the <see cref="ListToolsAsync(ListToolsRequestParams, CancellationToken)"/> overload, which returns the
/// raw <see cref="ListToolsResult"/> for each page.
/// </remarks>
public async ValueTask<IList<McpClientTool>> ListToolsAsync(
RequestOptions? options = null,
CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -256,6 +262,12 @@ public ValueTask<ListToolsResult> ListToolsAsync(
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A list of all available prompts as <see cref="McpClientPrompt"/> instances.</returns>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// This overload aggregates every page into a single list and does not surface the per-result caching hints
/// (<see cref="ListPromptsResult.TimeToLive"/> and <see cref="ListPromptsResult.CacheScope"/>). To read those hints,
/// use the <see cref="ListPromptsAsync(ListPromptsRequestParams, CancellationToken)"/> overload, which returns the
/// raw <see cref="ListPromptsResult"/> for each page.
/// </remarks>
public async ValueTask<IList<McpClientPrompt>> ListPromptsAsync(
RequestOptions? options = null,
CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -366,6 +378,12 @@ public ValueTask<GetPromptResult> GetPromptAsync(
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A list of all available resource templates as <see cref="ResourceTemplate"/> instances.</returns>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// This overload aggregates every page into a single list and does not surface the per-result caching hints
/// (<see cref="ListResourceTemplatesResult.TimeToLive"/> and <see cref="ListResourceTemplatesResult.CacheScope"/>). To read those hints,
/// use the <see cref="ListResourceTemplatesAsync(ListResourceTemplatesRequestParams, CancellationToken)"/> overload, which returns the
/// raw <see cref="ListResourceTemplatesResult"/> for each page.
/// </remarks>
public async ValueTask<IList<McpClientResourceTemplate>> ListResourceTemplatesAsync(
RequestOptions? options = null,
CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -422,6 +440,12 @@ public ValueTask<ListResourceTemplatesResult> ListResourceTemplatesAsync(
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A list of all available resources as <see cref="Resource"/> instances.</returns>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// This overload aggregates every page into a single list and does not surface the per-result caching hints
/// (<see cref="ListResourcesResult.TimeToLive"/> and <see cref="ListResourcesResult.CacheScope"/>). To read those hints,
/// use the <see cref="ListResourcesAsync(ListResourcesRequestParams, CancellationToken)"/> overload, which returns the
/// raw <see cref="ListResourcesResult"/> for each page.
/// </remarks>
public async ValueTask<IList<McpClientResource>> ListResourcesAsync(
RequestOptions? options = null,
CancellationToken cancellationToken = default)
Expand Down
28 changes: 0 additions & 28 deletions src/ModelContextProtocol.Core/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,6 @@
<Right>lib/net10.0/ModelContextProtocol.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:ModelContextProtocol.Protocol.TimeSpanMillisecondsConverter</Target>
<Left>lib/net10.0/ModelContextProtocol.Core.dll</Left>
<Right>lib/net10.0/ModelContextProtocol.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:ModelContextProtocol.Protocol.ToolExecution</Target>
Expand Down Expand Up @@ -295,13 +288,6 @@
<Right>lib/net8.0/ModelContextProtocol.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:ModelContextProtocol.Protocol.TimeSpanMillisecondsConverter</Target>
<Left>lib/net8.0/ModelContextProtocol.Core.dll</Left>
<Right>lib/net8.0/ModelContextProtocol.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:ModelContextProtocol.Protocol.ToolExecution</Target>
Expand Down Expand Up @@ -456,13 +442,6 @@
<Right>lib/net9.0/ModelContextProtocol.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:ModelContextProtocol.Protocol.TimeSpanMillisecondsConverter</Target>
<Left>lib/net9.0/ModelContextProtocol.Core.dll</Left>
<Right>lib/net9.0/ModelContextProtocol.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:ModelContextProtocol.Protocol.ToolExecution</Target>
Expand Down Expand Up @@ -617,13 +596,6 @@
<Right>lib/netstandard2.0/ModelContextProtocol.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:ModelContextProtocol.Protocol.TimeSpanMillisecondsConverter</Target>
<Left>lib/netstandard2.0/ModelContextProtocol.Core.dll</Left>
<Right>lib/netstandard2.0/ModelContextProtocol.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:ModelContextProtocol.Protocol.ToolExecution</Target>
Expand Down
1 change: 1 addition & 0 deletions src/ModelContextProtocol.Core/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(PingResult))]
[JsonSerializable(typeof(ReadResourceRequestParams))]
[JsonSerializable(typeof(ReadResourceResult))]
[JsonSerializable(typeof(CacheScope))]
[JsonSerializable(typeof(SetLevelRequestParams))]
[JsonSerializable(typeof(SubscribeRequestParams))]
[JsonSerializable(typeof(UnsubscribeRequestParams))]
Expand Down
44 changes: 44 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/CacheScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol;

/// <summary>
/// Indicates the intended scope of a cached response, analogous to the HTTP
/// <c>Cache-Control: public</c> and <c>Cache-Control: private</c> directives.
/// </summary>
/// <remarks>
/// <para>
/// This is used by <see cref="ICacheableResult.CacheScope"/> to control who may cache a
/// response returned by <c>tools/list</c>, <c>prompts/list</c>, <c>resources/list</c>,
/// <c>resources/templates/list</c>, and <c>resources/read</c>.
/// </para>
/// <para>
/// When the field is absent from a response, clients should treat it as <see cref="Public"/>.
/// </para>
/// </remarks>
[JsonConverter(typeof(JsonStringEnumConverter<CacheScope>))]
public enum CacheScope
{
/// <summary>
/// The response does not contain user-specific data. Any client, shared gateway, or caching
/// proxy may store and serve the cached response to any user.
/// </summary>
/// <remarks>
/// This is appropriate for lists of tools, prompts, and resource templates that are identical
/// for all users.
/// </remarks>
[JsonStringEnumMemberName("public")]
Public,

/// <summary>
/// The response contains user-specific data. Only the requesting user's client may cache it.
/// Shared caches (for example, multi-tenant gateways) must not serve the cached response to a
/// different user.
/// </summary>
/// <remarks>
/// This is appropriate for <c>resources/read</c> results that depend on the authenticated user,
/// or for filtered list results that vary per user.
/// </remarks>
[JsonStringEnumMemberName("private")]
Private
}
70 changes: 70 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/CacheScopeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol;

/// <summary>
/// Serializes <see cref="CacheScope"/> caching-scope hints, tolerating unknown or future values on read.
/// </summary>
/// <remarks>
/// <para>
/// SEP-2549 introduces <c>cacheScope</c> as a forward-looking caching hint. If a server sends an
/// unrecognized scope string (for example, a value added in a later revision of the specification) or a
/// non-string token, this converter maps it to <see langword="null"/> rather than throwing. This prevents
/// a single unexpected hint from breaking deserialization of the entire result (for example, the whole
/// tool list). A <see langword="null"/> result is the same as an absent field, which clients treat as
/// <see cref="CacheScope.Public"/>.
/// </para>
/// <para>
/// This converter is applied per-property on the cacheable result types. The <see cref="CacheScope"/>
/// enum itself retains a standard string converter for any standalone serialization.
/// </para>
/// </remarks>
internal sealed class CacheScopeConverter : JsonConverter<CacheScope?>
{
public override CacheScope? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.String)
{
string? value = reader.GetString();

// Match case-insensitively so a non-conforming casing of "private" (a security-relevant hint)
// is honored rather than falling through to null, which clients would treat as "public" and
// could cache user-specific data in a shared cache. Genuinely unknown values still map to null.
if (string.Equals(value, "public", StringComparison.OrdinalIgnoreCase))
{
return CacheScope.Public;
}

if (string.Equals(value, "private", StringComparison.OrdinalIgnoreCase))
{
return CacheScope.Private;
}

return null;
}

// Any non-string token (number, bool, object, array) is an unrecognized hint. Consume the whole
// value, including the contents of an object or array, so the reader is left correctly positioned
// before mapping to null. Skipping is required for container tokens: returning without consuming
// them would leave the reader mispositioned and break deserialization of the enclosing result.
reader.Skip();
return null;
}
Comment thread
tarekgh marked this conversation as resolved.

public override void Write(Utf8JsonWriter writer, CacheScope? value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}

writer.WriteStringValue(value switch
{
CacheScope.Public => "public",
CacheScope.Private => "private",
_ => throw new JsonException($"Unsupported {nameof(CacheScope)} value: {value}."),
});
}
}
58 changes: 58 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/ICacheableResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
namespace ModelContextProtocol.Protocol;

/// <summary>
/// Represents a result that carries time-to-live (TTL) caching hints, allowing clients to cache
/// the response for a period of time before re-fetching.
/// </summary>
/// <remarks>
/// <para>
/// This interface corresponds to the <c>CacheableResult</c> type in the Model Context Protocol
/// schema and is implemented by the results of <c>tools/list</c>, <c>prompts/list</c>,
/// <c>resources/list</c>, <c>resources/templates/list</c>, and <c>resources/read</c>.
/// </para>
/// <para>
/// The TTL is a freshness hint, not a guarantee. It supplements rather than replaces the existing
/// <c>list_changed</c> and <c>resources/updated</c> notification mechanisms; both can coexist. A
/// relevant notification invalidates a cached response regardless of any remaining TTL.
/// </para>
/// </remarks>
public interface ICacheableResult
{
/// <summary>
/// Gets or sets a hint indicating how long the client may cache this response before re-fetching.
/// </summary>
/// <remarks>
/// <para>
/// The semantics are analogous to the HTTP <c>Cache-Control: max-age</c> directive. The value is
/// serialized as an integer number of milliseconds under the <c>ttlMs</c> JSON property.
/// </para>
/// <para>
/// A value of <see cref="TimeSpan.Zero"/> indicates the response should be considered immediately
/// stale; a positive value indicates the client should consider the response fresh for that
/// duration from the time it was received.
/// </para>
/// <para>
/// When this property is <see langword="null"/> (the field was absent from the response), clients
/// should assume a default of <see cref="TimeSpan.Zero"/> (immediately stale) and rely on their
/// own caching heuristics or notifications. The SDK preserves whatever value the server sent and
/// does not coerce it; a client that receives a negative value should treat it as immediately stale.
/// </para>
/// </remarks>
TimeSpan? TimeToLive { get; set; }

/// <summary>
/// Gets or sets the intended scope of the cached response.
/// </summary>
/// <remarks>
/// <para>
/// When this property is <see langword="null"/> (the field was absent from the response), clients
/// should treat the response as <see cref="Protocol.CacheScope.Public"/>.
/// </para>
/// <para>
/// An unrecognized or future scope value sent by a server (or a non-string value) is tolerated and
/// surfaced as <see langword="null"/> rather than causing deserialization of the whole result to
/// fail, so a single unexpected hint never prevents a client from reading the result.
/// </para>
/// </remarks>
CacheScope? CacheScope { get; set; }
}
12 changes: 11 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/ListPromptsResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ namespace ModelContextProtocol.Protocol;
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </para>
/// </remarks>
public sealed class ListPromptsResult : PaginatedResult
public sealed class ListPromptsResult : PaginatedResult, ICacheableResult
{
/// <summary>
/// Gets or sets a list of prompts or prompt templates that the server offers.
/// </summary>
[JsonPropertyName("prompts")]
public IList<Prompt> Prompts { get; set; } = [];

/// <inheritdoc />
[JsonPropertyName("ttlMs")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }

/// <inheritdoc />
[JsonPropertyName("cacheScope")]
[JsonConverter(typeof(CacheScopeConverter))]
public CacheScope? CacheScope { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Protocol;
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </para>
/// </remarks>
public sealed class ListResourceTemplatesResult : PaginatedResult
public sealed class ListResourceTemplatesResult : PaginatedResult, ICacheableResult
{
/// <summary>
/// Gets or sets a list of resource templates that the server offers.
Expand All @@ -32,4 +32,14 @@ public sealed class ListResourceTemplatesResult : PaginatedResult
/// </remarks>
[JsonPropertyName("resourceTemplates")]
public IList<ResourceTemplate> ResourceTemplates { get; set; } = [];

/// <inheritdoc />
[JsonPropertyName("ttlMs")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }

/// <inheritdoc />
[JsonPropertyName("cacheScope")]
[JsonConverter(typeof(CacheScopeConverter))]
public CacheScope? CacheScope { get; set; }
}
12 changes: 11 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/ListResourcesResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ namespace ModelContextProtocol.Protocol;
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </para>
/// </remarks>
public sealed class ListResourcesResult : PaginatedResult
public sealed class ListResourcesResult : PaginatedResult, ICacheableResult
{
/// <summary>
/// Gets or sets a list of resources that the server offers.
/// </summary>
[JsonPropertyName("resources")]
public IList<Resource> Resources { get; set; } = [];

/// <inheritdoc />
[JsonPropertyName("ttlMs")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }

/// <inheritdoc />
[JsonPropertyName("cacheScope")]
[JsonConverter(typeof(CacheScopeConverter))]
public CacheScope? CacheScope { get; set; }
}
12 changes: 11 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/ListToolsResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ namespace ModelContextProtocol.Protocol;
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </para>
/// </remarks>
public sealed class ListToolsResult : PaginatedResult
public sealed class ListToolsResult : PaginatedResult, ICacheableResult
Comment thread
tarekgh marked this conversation as resolved.
{
/// <summary>
/// Gets or sets the server's response to a tools/list request from the client.
/// </summary>
[JsonPropertyName("tools")]
public IList<Tool> Tools { get; set; } = [];

/// <inheritdoc />
[JsonPropertyName("ttlMs")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }
Comment thread
tarekgh marked this conversation as resolved.

/// <inheritdoc />
[JsonPropertyName("cacheScope")]
[JsonConverter(typeof(CacheScopeConverter))]
public CacheScope? CacheScope { get; set; }
}
Loading