diff --git a/src/ModelContextProtocol.Core/Protocol/CreateTaskResult.cs b/src/ModelContextProtocol.Core/Protocol/CreateTaskResult.cs
index 2e5bc0c41..6d9bac23c 100644
--- a/src/ModelContextProtocol.Core/Protocol/CreateTaskResult.cs
+++ b/src/ModelContextProtocol.Core/Protocol/CreateTaskResult.cs
@@ -55,10 +55,11 @@ public sealed class CreateTaskResult : Result
public required DateTimeOffset LastUpdatedAt { get; set; }
///
- /// Gets or sets the time-to-live duration from creation in milliseconds, or for unlimited.
+ /// Gets or sets the time-to-live duration from creation, or for unlimited.
///
[JsonPropertyName("ttlMs")]
- public long? TtlMs { get; set; }
+ [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
+ public TimeSpan? TimeToLive { get; set; }
///
/// Gets or sets the suggested polling interval in milliseconds.
diff --git a/src/ModelContextProtocol.Core/Protocol/GetTaskResult.cs b/src/ModelContextProtocol.Core/Protocol/GetTaskResult.cs
index 9366f2374..3fea8cc6b 100644
--- a/src/ModelContextProtocol.Core/Protocol/GetTaskResult.cs
+++ b/src/ModelContextProtocol.Core/Protocol/GetTaskResult.cs
@@ -64,10 +64,10 @@ private protected GetTaskResult()
public required DateTimeOffset LastUpdatedAt { get; set; }
///
- /// Gets or sets the time-to-live duration from creation in milliseconds, or for unlimited.
+ /// Gets or sets the time-to-live duration from creation, or for unlimited.
///
[JsonPropertyName("ttlMs")]
- public long? TtlMs { get; set; }
+ public TimeSpan? TimeToLive { get; set; }
///
/// Gets or sets the suggested polling interval in milliseconds.
@@ -245,7 +245,7 @@ internal sealed class Converter : JsonConverter
};
taskResult.StatusMessage = statusMessage;
- taskResult.TtlMs = ttlMs;
+ taskResult.TimeToLive = ttlMs is null ? null : TimeSpan.FromMilliseconds(ttlMs.Value);
taskResult.PollIntervalMs = pollIntervalMs;
taskResult.ResultType = resultType;
taskResult.Meta = meta;
@@ -287,9 +287,9 @@ public override void Write(Utf8JsonWriter writer, GetTaskResult value, JsonSeria
writer.WriteString("createdAt", value.CreatedAt);
writer.WriteString("lastUpdatedAt", value.LastUpdatedAt);
- if (value.TtlMs is not null)
+ if (value.TimeToLive is not null)
{
- writer.WriteNumber("ttlMs", value.TtlMs.Value);
+ writer.WriteNumber("ttlMs", (long)value.TimeToLive.Value.TotalMilliseconds);
}
if (value.PollIntervalMs is not null)
diff --git a/src/ModelContextProtocol.Core/Protocol/TaskStatusNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/TaskStatusNotificationParams.cs
index e1bbb2f73..4e859a22c 100644
--- a/src/ModelContextProtocol.Core/Protocol/TaskStatusNotificationParams.cs
+++ b/src/ModelContextProtocol.Core/Protocol/TaskStatusNotificationParams.cs
@@ -70,10 +70,10 @@ private protected TaskStatusNotificationParams()
public required DateTimeOffset LastUpdatedAt { get; set; }
///
- /// Gets or sets the time-to-live duration from creation in milliseconds, or for unlimited.
+ /// Gets or sets the time-to-live duration from creation, or for unlimited.
///
[JsonPropertyName("ttlMs")]
- public long? TtlMs { get; set; }
+ public TimeSpan? TimeToLive { get; set; }
///
/// Gets or sets the suggested polling interval in milliseconds.
@@ -247,7 +247,7 @@ internal sealed class Converter : JsonConverter
};
notification.StatusMessage = statusMessage;
- notification.TtlMs = ttlMs;
+ notification.TimeToLive = ttlMs is null ? null : TimeSpan.FromMilliseconds(ttlMs.Value);
notification.PollIntervalMs = pollIntervalMs;
notification.Meta = meta;
@@ -283,9 +283,9 @@ public override void Write(Utf8JsonWriter writer, TaskStatusNotificationParams v
writer.WriteString("createdAt", value.CreatedAt);
writer.WriteString("lastUpdatedAt", value.LastUpdatedAt);
- if (value.TtlMs is not null)
+ if (value.TimeToLive is not null)
{
- writer.WriteNumber("ttlMs", value.TtlMs.Value);
+ writer.WriteNumber("ttlMs", (long)value.TimeToLive.Value.TotalMilliseconds);
}
if (value.PollIntervalMs is not null)
diff --git a/src/ModelContextProtocol.Core/Protocol/TimeSpanMillisecondsConverter.cs b/src/ModelContextProtocol.Core/Protocol/TimeSpanMillisecondsConverter.cs
new file mode 100644
index 000000000..9ad646b48
--- /dev/null
+++ b/src/ModelContextProtocol.Core/Protocol/TimeSpanMillisecondsConverter.cs
@@ -0,0 +1,19 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol;
+
+///
+/// Provides a JSON converter for values that serializes and deserializes
+/// them as a whole-millisecond integer.
+///
+public sealed class TimeSpanMillisecondsConverter : JsonConverter
+{
+ ///
+ public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => TimeSpan.FromMilliseconds(reader.GetDouble());
+
+ ///
+ public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
+ => writer.WriteNumberValue((long)value.TotalMilliseconds);
+}
diff --git a/src/ModelContextProtocol.Core/Server/InMemoryMcpTaskStore.cs b/src/ModelContextProtocol.Core/Server/InMemoryMcpTaskStore.cs
index a13af13e9..00c69844b 100644
--- a/src/ModelContextProtocol.Core/Server/InMemoryMcpTaskStore.cs
+++ b/src/ModelContextProtocol.Core/Server/InMemoryMcpTaskStore.cs
@@ -32,9 +32,9 @@ public class InMemoryMcpTaskStore : IMcpTaskStore
public long DefaultPollIntervalMs { get; set; } = 1000;
///
- /// Gets or sets the default time-to-live in milliseconds for new tasks, or for unlimited.
+ /// Gets or sets the default time-to-live for new tasks, or for unlimited.
///
- public long? DefaultTtlMs { get; set; }
+ public TimeSpan? DefaultTimeToLive { get; set; }
///
public Task CreateTaskAsync(CancellationToken cancellationToken = default)
@@ -42,7 +42,7 @@ public Task CreateTaskAsync(CancellationToken cancellationToken = d
var taskId = Guid.NewGuid().ToString("N");
var now = DateTimeOffset.UtcNow;
- var info = new McpTaskInfo(taskId, McpTaskStatus.Working, now, now, DefaultTtlMs, DefaultPollIntervalMs);
+ var info = new McpTaskInfo(taskId, McpTaskStatus.Working, now, now, DefaultTimeToLive, DefaultPollIntervalMs);
_tasks[taskId] = info;
return Task.FromResult(info);
diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
index 85b1cc26a..1cce71c70 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
@@ -1009,7 +1009,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
Status = info.Status,
CreatedAt = info.CreatedAt,
LastUpdatedAt = info.LastUpdatedAt,
- TtlMs = info.TtlMs,
+ TimeToLive = info.TimeToLive,
PollIntervalMs = info.PollIntervalMs,
StatusMessage = info.StatusMessage,
ResultType = "task",
@@ -1022,7 +1022,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
TaskId = info.TaskId,
CreatedAt = info.CreatedAt,
LastUpdatedAt = info.LastUpdatedAt,
- TtlMs = info.TtlMs,
+ TimeToLive = info.TimeToLive,
PollIntervalMs = info.PollIntervalMs,
StatusMessage = info.StatusMessage,
ResultType = "complete",
@@ -1032,7 +1032,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
TaskId = info.TaskId,
CreatedAt = info.CreatedAt,
LastUpdatedAt = info.LastUpdatedAt,
- TtlMs = info.TtlMs,
+ TimeToLive = info.TimeToLive,
PollIntervalMs = info.PollIntervalMs,
StatusMessage = info.StatusMessage,
Result = info.Result ?? throw new InvalidOperationException($"Task '{info.TaskId}' is completed but has no result."),
@@ -1043,7 +1043,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
TaskId = info.TaskId,
CreatedAt = info.CreatedAt,
LastUpdatedAt = info.LastUpdatedAt,
- TtlMs = info.TtlMs,
+ TimeToLive = info.TimeToLive,
PollIntervalMs = info.PollIntervalMs,
StatusMessage = info.StatusMessage,
Error = info.Error ?? throw new InvalidOperationException($"Task '{info.TaskId}' is failed but has no error."),
@@ -1054,7 +1054,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
TaskId = info.TaskId,
CreatedAt = info.CreatedAt,
LastUpdatedAt = info.LastUpdatedAt,
- TtlMs = info.TtlMs,
+ TimeToLive = info.TimeToLive,
PollIntervalMs = info.PollIntervalMs,
StatusMessage = info.StatusMessage,
ResultType = "complete",
@@ -1064,7 +1064,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
TaskId = info.TaskId,
CreatedAt = info.CreatedAt,
LastUpdatedAt = info.LastUpdatedAt,
- TtlMs = info.TtlMs,
+ TimeToLive = info.TimeToLive,
PollIntervalMs = info.PollIntervalMs,
StatusMessage = info.StatusMessage,
// McpTaskInfo.InputRequests is IReadOnlyDictionary (covers immutable store
diff --git a/src/ModelContextProtocol.Core/Server/McpTaskInfo.cs b/src/ModelContextProtocol.Core/Server/McpTaskInfo.cs
index ab71d6505..87fa8ef1c 100644
--- a/src/ModelContextProtocol.Core/Server/McpTaskInfo.cs
+++ b/src/ModelContextProtocol.Core/Server/McpTaskInfo.cs
@@ -20,7 +20,7 @@ public sealed record McpTaskInfo(
McpTaskStatus Status,
DateTimeOffset CreatedAt,
DateTimeOffset LastUpdatedAt,
- long? TtlMs = null,
+ TimeSpan? TimeToLive = null,
long? PollIntervalMs = null,
string? StatusMessage = null,
JsonElement? Result = null,
diff --git a/tests/ModelContextProtocol.Tests/Protocol/TaskSerializationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/TaskSerializationTests.cs
index 556403add..86acc57f6 100644
--- a/tests/ModelContextProtocol.Tests/Protocol/TaskSerializationTests.cs
+++ b/tests/ModelContextProtocol.Tests/Protocol/TaskSerializationTests.cs
@@ -21,7 +21,7 @@ public static void CreateTaskResult_SerializationRoundTrip_PreservesAllPropertie
StatusMessage = "Processing...",
CreatedAt = new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero),
LastUpdatedAt = new DateTimeOffset(2025, 6, 1, 12, 5, 0, TimeSpan.Zero),
- TtlMs = 3600000,
+ TimeToLive = TimeSpan.FromHours(1),
PollIntervalMs = 5000,
ResultType = "task",
Meta = new JsonObject { ["key"] = "value" }
@@ -36,7 +36,7 @@ public static void CreateTaskResult_SerializationRoundTrip_PreservesAllPropertie
Assert.Equal("Processing...", deserialized.StatusMessage);
Assert.Equal(original.CreatedAt, deserialized.CreatedAt);
Assert.Equal(original.LastUpdatedAt, deserialized.LastUpdatedAt);
- Assert.Equal(3600000, deserialized.TtlMs);
+ Assert.Equal(TimeSpan.FromHours(1), deserialized.TimeToLive);
Assert.Equal(5000, deserialized.PollIntervalMs);
Assert.Equal("task", deserialized.ResultType);
Assert.NotNull(deserialized.Meta);
@@ -52,7 +52,7 @@ public static void CreateTaskResult_UsesCorrectWireFieldNames()
Status = McpTaskStatus.Working,
CreatedAt = DateTimeOffset.UtcNow,
LastUpdatedAt = DateTimeOffset.UtcNow,
- TtlMs = 60000,
+ TimeToLive = TimeSpan.FromMinutes(1),
PollIntervalMs = 1000,
ResultType = "task",
};
diff --git a/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs b/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs
index 7c87786eb..83afbfe23 100644
--- a/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs
@@ -55,13 +55,13 @@ public async Task CreateTaskAsync_UsesDefaultPollInterval()
}
[Fact]
- public async Task CreateTaskAsync_UsesDefaultTtl()
+ public async Task CreateTaskAsync_UsesDefaultTimeToLive()
{
- var store = new InMemoryMcpTaskStore { DefaultTtlMs = 30000 };
+ var store = new InMemoryMcpTaskStore { DefaultTimeToLive = TimeSpan.FromSeconds(30) };
var result = await store.CreateTaskAsync(CT);
- Assert.Equal(30000, result.TtlMs);
+ Assert.Equal(TimeSpan.FromSeconds(30), result.TimeToLive);
}
[Fact]
diff --git a/tests/ModelContextProtocol.Tests/Server/TaskCancellationIntegrationTests.cs b/tests/ModelContextProtocol.Tests/Server/TaskCancellationIntegrationTests.cs
index 1b424d002..0e8693353 100644
--- a/tests/ModelContextProtocol.Tests/Server/TaskCancellationIntegrationTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/TaskCancellationIntegrationTests.cs
@@ -34,7 +34,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
options.TaskStore = new InMemoryMcpTaskStore
{
DefaultPollIntervalMs = 50,
- DefaultTtlMs = 5000,
+ DefaultTimeToLive = TimeSpan.FromSeconds(5),
};
});