Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ModelContextProtocol.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<Project Path="src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" />
<Project Path="src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Tasks/ModelContextProtocol.Extensions.Tasks.csproj" />
<Project Path="src/ModelContextProtocol/ModelContextProtocol.csproj" />
</Folder>
<Folder Name="/tests/">
Expand Down
22 changes: 13 additions & 9 deletions samples/TasksExtension/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// is self-contained — no separate server process or HTTP transport required.

using ModelContextProtocol.Client;
using ModelContextProtocol.Extensions.Tasks;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
Expand All @@ -21,22 +22,25 @@

Pipe clientToServerPipe = new(), serverToClientPipe = new();

McpServerOptions serverOptions = new()
{
ToolCollection = [McpServerTool.Create(SlowTools.RunReport, new() { Name = "run-report" })],
};

// Calling WithTasks is all that's needed for [McpServerTool]-attributed tools to be
// automatically wrapped as background tasks when the client opts in.
serverOptions.WithTasks(new InMemoryMcpTaskStore { DefaultPollIntervalMs = 250 });

await using McpServer server = McpServer.Create(
new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()),
new McpServerOptions
{
// Setting TaskStore is all that's needed for [McpServerTool]-attributed tools to be
// automatically wrapped as background tasks when the client opts in.
TaskStore = new InMemoryMcpTaskStore { DefaultPollIntervalMs = 250 },
ToolCollection = [McpServerTool.Create(SlowTools.RunReport, new() { Name = "run-report" })],
});
serverOptions);
_ = server.RunAsync();

await using McpClient client = await McpClient.CreateAsync(
new StreamClientTransport(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream()));

Console.WriteLine("=== CallToolAsync (auto-poll) ===");
var auto = await client.CallToolAsync(
Console.WriteLine("=== CallToolAsTaskAsync (auto-poll) ===");
var auto = await client.CallToolAsTaskAsync(
new CallToolRequestParams { Name = "run-report" });
Console.WriteLine($" result: {((TextContentBlock)auto.Content[0]).Text}");
Console.WriteLine();
Expand Down
3 changes: 2 additions & 1 deletion samples/TasksExtension/TasksExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
<NoWarn>$(NoWarn);MCPEXP001;MCPEXP002;MCPEXP004</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
<ProjectReference Include="..\..\src\ModelContextProtocol.Extensions.Tasks\ModelContextProtocol.Extensions.Tasks.csproj" />
</ItemGroup>

</Project>
365 changes: 7 additions & 358 deletions src/ModelContextProtocol.Core/Client/McpClient.Methods.cs

Large diffs are not rendered by default.

12 changes: 3 additions & 9 deletions src/ModelContextProtocol.Core/Client/McpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ protected McpClient()
public abstract Task<ClientCompletionDetails> Completion { get; }

/// <summary>
/// Resolves input requests embedded in an <see cref="InputRequiredTaskResult"/> by dispatching
/// Resolves input requests embedded in a task that requires input by dispatching
/// each request to the appropriate registered handler.
/// </summary>
/// <param name="inputRequests">
Expand All @@ -82,16 +82,10 @@ protected McpClient()
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A dictionary of responses keyed by the same identifiers as the input requests.</returns>
private protected abstract ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
[System.Diagnostics.CodeAnalysis.Experimental(Experimentals.Subclassing_DiagnosticId, UrlFormat = Experimentals.Subclassing_Url)]
public abstract ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
IDictionary<string, InputRequest> inputRequests, CancellationToken cancellationToken);

/// <summary>
/// Gets the maximum number of consecutive stuck-in-<see cref="McpTaskStatus.InputRequired"/> polls
/// allowed by <see cref="PollTaskToCompletionAsync"/> before the client cancels and throws.
/// Sourced from <see cref="McpClientOptions.MaxConsecutiveStuckPolls"/>.
/// </summary>
private protected abstract int MaxConsecutiveStuckPolls { get; }

/// <summary>
/// Registers one or more tool definitions in the client's tool cache, enabling the transport
/// to send <c>Mcp-Param-*</c> headers for those tools without requiring a prior <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/> call.
Expand Down
6 changes: 1 addition & 5 deletions src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ internal sealed partial class McpClientImpl : McpClient
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
/// <param name="loggerFactory">The logger factory.</param>
internal McpClientImpl(ITransport transport, string endpointName, McpClientOptions? options, ILoggerFactory? loggerFactory)
#pragma warning restore MCPEXP002
{
options ??= new();

Expand Down Expand Up @@ -178,10 +177,7 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not
public override Task<ClientCompletionDetails> Completion => _sessionHandler.CompletionTask;

/// <inheritdoc/>
private protected override int MaxConsecutiveStuckPolls => _options.MaxConsecutiveStuckPolls;

/// <inheritdoc/>
private protected override async ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
public override async ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
IDictionary<string, InputRequest> inputRequests,
CancellationToken cancellationToken)
{
Expand Down
38 changes: 0 additions & 38 deletions src/ModelContextProtocol.Core/Client/McpClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,42 +146,4 @@ public McpClientHandlers Handlers
field = value;
}
}

/// <summary>
/// Gets or sets the maximum number of consecutive task polls during which a task may report
/// <see cref="McpTaskStatus.InputRequired"/> without publishing any new input requests, before
/// the client treats the task as stuck, issues a best-effort <c>tasks/cancel</c>, and throws
/// an <see cref="McpException"/>.
/// </summary>
/// <value>
/// The maximum number of consecutive stuck polls allowed. The default value is <c>60</c>.
/// </value>
/// <remarks>
/// <para>
/// This guard prevents an unbounded poll loop when the server keeps a task in
/// <see cref="McpTaskStatus.InputRequired"/> but never publishes new input requests after the
/// client has already responded to every previously surfaced request. It only affects the
/// long-poll path used by <see cref="McpClient.CallToolAsync(CallToolRequestParams, CancellationToken)"/>;
/// it does not affect direct <see cref="McpClient.GetTaskAsync(string, CancellationToken)"/> calls.
/// </para>
/// <para>
/// Callers should size this value with the configured server-side poll interval in mind: the
/// effective wall-clock timeout is roughly <c>MaxConsecutiveStuckPolls * pollIntervalMs</c>.
/// Setting this to a very small value can cause false positives for servers that are slow to
/// surface follow-up input requests; setting it too large can mask misbehaving servers.
/// </para>
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">The value is less than <c>1</c>.</exception>
public int MaxConsecutiveStuckPolls
{
get;
set
{
if (value < 1)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "must be greater than or equal to 1.");
}
field = value;
}
} = 60;
}
19 changes: 0 additions & 19 deletions src/ModelContextProtocol.Core/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,6 @@ internal static bool IsValidToolOutputSchema(JsonElement element) =>
[JsonSerializable(typeof(ResourceUpdatedNotificationParams))]
[JsonSerializable(typeof(RootsListChangedNotificationParams))]
[JsonSerializable(typeof(ToolListChangedNotificationParams))]
[JsonSerializable(typeof(TaskStatusNotificationParams))]
[JsonSerializable(typeof(WorkingTaskNotificationParams))]
[JsonSerializable(typeof(CompletedTaskNotificationParams))]
[JsonSerializable(typeof(FailedTaskNotificationParams))]
[JsonSerializable(typeof(CancelledTaskNotificationParams))]
[JsonSerializable(typeof(InputRequiredTaskNotificationParams))]

// MCP Request Params / Results
[JsonSerializable(typeof(CallToolRequestParams))]
Expand Down Expand Up @@ -174,19 +168,6 @@ internal static bool IsValidToolOutputSchema(JsonElement element) =>
[JsonSerializable(typeof(IDictionary<string, InputRequest>))]
[JsonSerializable(typeof(IDictionary<string, InputResponse>))]

[JsonSerializable(typeof(GetTaskRequestParams))]
[JsonSerializable(typeof(GetTaskResult))]
[JsonSerializable(typeof(WorkingTaskResult))]
[JsonSerializable(typeof(CompletedTaskResult))]
[JsonSerializable(typeof(FailedTaskResult))]
[JsonSerializable(typeof(CancelledTaskResult))]
[JsonSerializable(typeof(InputRequiredTaskResult))]
[JsonSerializable(typeof(UpdateTaskRequestParams))]
[JsonSerializable(typeof(UpdateTaskResult))]
[JsonSerializable(typeof(CancelTaskRequestParams))]
[JsonSerializable(typeof(CancelTaskResult))]
[JsonSerializable(typeof(CreateTaskResult))]

// MCP Content
[JsonSerializable(typeof(ContentBlock))]
[JsonSerializable(typeof(TextContentBlock))]
Expand Down
10 changes: 10 additions & 0 deletions src/ModelContextProtocol.Core/McpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ public abstract partial class McpSession : IAsyncDisposable
internal bool IsJuly2026OrLaterProtocol() =>
McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(NegotiatedProtocolVersion);

/// <summary>
/// Gets a value indicating whether the negotiated protocol version supports the draft protocol
/// features (the <c>2026-07-28</c> revision or later).
/// </summary>
/// <remarks>
/// This is a public seam used by bolt-on extensions (such as the Tasks extension) to gate
/// functionality that requires the <c>2026-07-28</c> or later protocol revision.
/// </remarks>
public bool IsDraftProtocol() => IsJuly2026OrLaterProtocol();

/// <summary>
/// Sends a JSON-RPC request to the connected session and waits for a response.
/// </summary>
Expand Down
20 changes: 0 additions & 20 deletions src/ModelContextProtocol.Core/Protocol/MetaKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,4 @@ public static class MetaKeys
/// belonging to different subscriptions on a shared channel (especially STDIO).
/// </remarks>
public const string SubscriptionId = "io.modelcontextprotocol/subscriptionId";

/// <summary>
/// The metadata key used to associate requests, responses, and notifications with a task.
/// </summary>
/// <remarks>
/// <para>
/// This constant defines the key <c>"io.modelcontextprotocol/related-task"</c> used in the
/// <c>_meta</c> field to associate messages with their originating task across the entire
/// request lifecycle.
/// </para>
/// <para>
/// For example, an elicitation that a task-augmented tool call depends on must share the
/// same related task ID with that tool call's task.
/// </para>
/// <para>
/// For <c>tasks/get</c>, <c>tasks/list</c>, and <c>tasks/cancel</c> operations, this
/// metadata should not be included as the taskId is already present in the message structure.
/// </para>
/// </remarks>
public const string RelatedTask = "io.modelcontextprotocol/related-task";
}
10 changes: 0 additions & 10 deletions src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,6 @@ public static class NotificationMethods
/// </remarks>
public const string CancelledNotification = "notifications/cancelled";

/// <summary>
/// The name of the notification sent by the server to push task status updates to subscribed clients.
/// </summary>
/// <remarks>
/// Part of the <c>io.modelcontextprotocol/tasks</c> extension.
/// Each notification carries a complete task state for the current status, identical to what
/// <c>tasks/get</c> would have returned at that moment.
/// </remarks>
public const string TaskStatusNotification = "notifications/tasks/status";

/// <summary>
/// The name of the notification sent first on a <see cref="RequestMethods.SubscriptionsListen"/>
/// response stream to indicate which notification types the server agreed to deliver.
Expand Down
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/Protocol/NotificationParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace ModelContextProtocol.Protocol;
/// </summary>
public abstract class NotificationParams
{
/// <summary>Prevent external derivations.</summary>
private protected NotificationParams()
/// <summary>Allow derivations only within the SDK and its extension packages.</summary>
protected NotificationParams()
{
}

Expand Down
27 changes: 0 additions & 27 deletions src/ModelContextProtocol.Core/Protocol/RequestMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,33 +125,6 @@ public static class RequestMethods
/// </remarks>
public const string Initialize = "initialize";

/// <summary>
/// The name of the request method sent from the client to poll for task completion.
/// </summary>
/// <remarks>
/// Part of the <c>io.modelcontextprotocol/tasks</c> extension.
/// Clients poll for task status by sending this request with the task ID.
/// </remarks>
public const string TasksGet = "tasks/get";

/// <summary>
/// The name of the request method sent from the client to provide input responses to a task.
/// </summary>
/// <remarks>
/// Part of the <c>io.modelcontextprotocol/tasks</c> extension.
/// Used when a task has <c>input_required</c> status and the client needs to fulfill outstanding requests.
/// </remarks>
public const string TasksUpdate = "tasks/update";

/// <summary>
/// The name of the request method sent from the client to signal intent to cancel a task.
/// </summary>
/// <remarks>
/// Part of the <c>io.modelcontextprotocol/tasks</c> extension.
/// Cancellation is cooperative — the server decides whether and when to honor it.
/// </remarks>
public const string TasksCancel = "tasks/cancel";

/// <summary>
/// The name of the request method sent from the client to discover the server's protocol versions,
/// capabilities, and metadata.
Expand Down
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/Protocol/RequestParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ namespace ModelContextProtocol.Protocol;
/// </remarks>
public abstract class RequestParams
{
/// <summary>Prevent external derivations.</summary>
private protected RequestParams()
/// <summary>Allow derivations only within the SDK and its extension packages.</summary>
protected RequestParams()
{
}

Expand Down
8 changes: 4 additions & 4 deletions src/ModelContextProtocol.Core/Protocol/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace ModelContextProtocol.Protocol;
/// </summary>
public abstract class Result
{
/// <summary>Prevent external derivations.</summary>
private protected Result()
/// <summary>Allow derivations only within the SDK and its extension packages.</summary>
protected Result()
{
}

Expand All @@ -30,8 +30,8 @@ private protected Result()
/// When absent or set to <c>"complete"</c>, the result is a normal completed response.
/// When set to <c>"input_required"</c>, the result is an <see cref="InputRequiredResult"/> indicating
/// that additional input is needed before the request can be completed.
/// When set to <c>"task"</c>, the result is a <see cref="CreateTaskResult"/> indicating that the server
/// has created a long-running task in lieu of returning the result directly.
/// When set to <c>"task"</c>, the server has created a long-running task in lieu of returning the
/// result directly.
/// </para>
/// </remarks>
/// <value>Defaults to <see langword="null"/>, which is equivalent to <c>"complete"</c>.</value>
Expand Down
45 changes: 0 additions & 45 deletions src/ModelContextProtocol.Core/RequestHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,49 +45,4 @@ public void Set<TParams, TResult>(
return JsonSerializer.SerializeToNode(result, responseTypeInfo);
};
}

/// <summary>
/// Registers a handler that may return either a standard result or a <see cref="CreateTaskResult"/>
/// for task-augmented execution.
/// </summary>
public void SetTaskAugmented<TParams, TResult>(
string method,
Func<TParams, JsonRpcRequest, CancellationToken, ValueTask<ResultOrCreatedTask<TResult>>> handler,
JsonTypeInfo<TParams> requestTypeInfo,
JsonTypeInfo<TResult> responseTypeInfo,
JsonTypeInfo<CreateTaskResult> taskResultTypeInfo)
where TResult : Result
{
Throw.IfNull(method);
Throw.IfNull(handler);
Throw.IfNull(requestTypeInfo);
Throw.IfNull(responseTypeInfo);
Throw.IfNull(taskResultTypeInfo);

this[method] = async (request, cancellationToken) =>
{
TParams typedRequest = JsonSerializer.Deserialize(request.Params, requestTypeInfo)!;
var augmented = await handler(typedRequest, request, cancellationToken).ConfigureAwait(false);

if (augmented.IsTask)
{
// Guard against a misconfiguration where a handler opts into task-augmented
// execution but the server has no task lifecycle handlers wired up. Without
// tasks/get, a client that received a CreateTaskResult would have no way to
// poll the task to completion. Configure McpServerOptions.TaskStore or set
// the task handlers explicitly via McpServerOptions.Handlers.
if (!ContainsKey(RequestMethods.TasksGet))
{
throw new InvalidOperationException(
$"Handler for '{method}' returned a {nameof(CreateTaskResult)}, but the server has no " +
$"'{RequestMethods.TasksGet}' handler registered. Configure McpServerOptions.TaskStore " +
"or set the task handlers explicitly in McpServerOptions.Handlers before starting the server.");
}

return JsonSerializer.SerializeToNode(augmented.TaskCreated!, taskResultTypeInfo);
}

return JsonSerializer.SerializeToNode(augmented.Result!, responseTypeInfo);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, C
{
// When an MRTR context is active, intercept server-to-client requests (sampling, elicitation, roots)
// and route them through the MRTR mechanism instead of sending them over the wire.
// Task-augmented requests (SampleAsTaskAsync/ElicitAsTaskAsync) have a "task" property on their params
// and expect a CreateTaskResult response, so they must bypass MRTR and go over the wire.
// Requests carrying a "task" property on their params are part of a bolt-on task extension flow
// and expect a task-creation response, so they must bypass MRTR and go over the wire.
if (ActiveMrtrContext is { } mrtrContext &&
!(request.Params is JsonObject paramsObj && paramsObj.ContainsKey("task")))
{
Expand Down
Loading