-
Notifications
You must be signed in to change notification settings - Fork 607
Implement MCP task support (SEP-1686) #1170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Implement MCP task support (SEP-1686) #1170
Conversation
| /// </para> | ||
| /// </remarks> | ||
| [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)] | ||
| public async ValueTask<(McpTask Task, JsonElement Result)> WaitForTaskResultAsync( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the peculiar return signature on this method. This is intentionally copying the signature used by the TS sdk. Alternatives include returning just the JsonElement or using a named envelope type containing both the task and its result. I think this is the simpler solution also due to the similarity with existing SDKs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we choose a tuple over a named type only because it's more similar to the TS SDK? If that's the only reason, I'd just as soon create a named McpTaskResult type or something.
It also seems a bit unusual to want a McpTask and result together anyway after you've already got the result. And if that's something you really care about, you can call PollTaskUntilCompleteAsync and GetTaskResultAsync yourself without much effort.
My vote would be first just to remove this method, but if we have to keep it, we should name the return type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing altogether is something I considered as well. Another reason why I think it might be worth keeping is mechanical sympathy with the TS SDK. It means that if somebody is using an LLM to translate a TS codebase into a C# it can easily replace one method with the other. Obviously though that's less of a selling point given we're diverging on a bunch of other places.
@mikekistler @stephentoub thoughts?
| // If neither is done, resolving IMcpTaskStore will throw. | ||
| services.TryAddSingleton<IMcpTaskStore>(sp => | ||
| { | ||
| var options = sp.GetRequiredService<IOptions<McpServerOptions>>().Value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like some feedback from @halter73 on this one. This is automatically registering the IMcpTaskStore if one has been configured in the server options. We could of course skip this and have users rely on just the server options being available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The normal way to do this would be add a IMcpTaskStore? taskStore = null parameter to McpServerOptionsSetup and then set options.TaskStore = taskStore in the Configure method.
As far as I can tell, this is the only line of code that currently resolves IMcpTaskStore from the service provider, so another option would be to just undo the AddMcpServer changes and leave it to the app developer to resolve their IMcpTaskStore and set McpServerOptions.TaskStore themselves. This is what @MackinnonBuck did in #1077 for the ISseEventStreamStore.
However, I do like the convenience of just being able to add a singleton to DI and have it light up, so I think we should probably also add an HttpServerTransportOptionsSetup that initializes the EventStreamStore from DI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The normal way to do this would be add a IMcpTaskStore? taskStore = null parameter to McpServerOptionsSetup and then set options.TaskStore = taskStore in the Configure method.
Oh, that already is the case today. What this code does is conditionally populate the DI entry for IMcpTaskStore if options.TaskStore is populated. Meaning you can add IMcpTaskStore as a tool constructor parameter directly instead of probing the guts of McpServerOptions.
| /// </para> | ||
| /// </remarks> | ||
| [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)] | ||
| public async ValueTask<(McpTask Task, JsonElement Result)> WaitForTaskResultAsync( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we choose a tuple over a named type only because it's more similar to the TS SDK? If that's the only reason, I'd just as soon create a named McpTaskResult type or something.
It also seems a bit unusual to want a McpTask and result together anyway after you've already got the result. And if that's something you really care about, you can call PollTaskUntilCompleteAsync and GetTaskResultAsync yourself without much effort.
My vote would be first just to remove this method, but if we have to keep it, we should name the return type.
| Task support levels: | ||
| - `Forbidden` (default for sync methods): Tool cannot be called with task augmentation | ||
| - `Optional` (default for async methods): Tool can be called with or without task augmentation | ||
| - `Required`: Tool must be called with task augmentation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to add support to configure this via attributes? Can we add an experimental property to [McpServerTool]?
|
|
||
| // Stream enumeration - filter by session, exclude expired, apply keyset pagination | ||
| var query = _tasks.Values | ||
| .Where(e => sessionId == null || e.SessionId == sessionId) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .Where(e => sessionId == null || e.SessionId == sessionId) | |
| .Where(e => sessionId == e.SessionId) |
| TimeSpan? maxTtl = null, | ||
| TimeSpan? pollInterval = null, | ||
| TimeSpan? cleanupInterval = null, | ||
| int pageSize = 100) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that this is public. Do we want to add the ability to configure a max number of Task stored globally and/or per-session?
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Text.Json; | ||
|
|
||
| namespace ModelContextProtocol.Server; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| namespace ModelContextProtocol.Server; | |
| namespace ModelContextProtocol; |
Nit: We should probably move the folder and InMemoryMcpTaskStore too since it's supported on the client.
| } | ||
|
|
||
| // Retrieve the stored result - already stored as JsonElement | ||
| return await taskStore.GetTaskResultAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the IMcpTaskStore offer a version of this method that asynchronously blocks until a terminal result is ready? We could have a default interface implementation that polls like this getTaskResultHandler does or switch to an abstract class with a default implementation.
It could save a lot of unnecessary polling in the in-memory case and could even be useful in distributed settings where you have some sort of subscription or push notification.
| }; | ||
|
|
||
| // Register handlers | ||
| SetHandler( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do we think about adding McpServerHandlers and/or McpServerFilters for "tasks/get", "tasks/result", "tasks/list" and "tasks/cancel"? It feels like it would be appropriate to add both for all of these.
I can kinda see the argument that the "IMcpTaskStore" is low level enough you don't need to be able to specify direct handlers, but I'm not sure. Take the forced polling for example "tasks/result". It might not be worth the effort to expand the interface, but a low-level handler would let power users avoid polling. And filters seem useful for logging and the like.
| /// </remarks> | ||
| public bool Cancel(string taskId) | ||
| { | ||
| if (_disposed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's weird to check _disposed here but not in Complete. It also doesn't look like the return value is used for anything anyway. Does the McpTaskCancellationTokenProvider even need to be disposable at all?
The finally bock should ensure everything gets removed without increasing the risk of ODEs. and if for some reason the continuation that was supposed to run the finally gets GC'd because a custom task store did something really weird, the GC should take care of all of this too.
| var inputRequiredTask = await store.UpdateTaskStatusAsync( | ||
| task.TaskId, | ||
| McpTaskStatus.InputRequired, | ||
| "Waiting for user confirmation", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the server set the input_required state automatically when the tool call handler has a server-to-client request like elicitation or sampling pending?
Co-authored-by: Stephen Halter <halter73@gmail.com>
Implements MCP task support. See the included tasks.md for a high-level introduction to the new APIs.
Fix #943.