You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I'd like to pick up the Tasks extension for the Java SDK. Some of the tools on the MCP servers I run in production need a human approval before they execute, so I'm also looking at SEP-2848, which builds on Tasks. @chemicL should this wait for #1170 to land, or is it fine to start against the current server API?
@liotur thanks. #1170 is just the first step, we need to follow up with the client implementation. From then on we can think of implementing tasks. Please do contribute here in the form of ideas around the design for the extension. In the modern server primitives I tried to come up with more flexible interfaces for pluggability in mind so feedback on that will be very valuable.
@chemicL thanks. I read through #1170 with Tasks in mind. Most of it fits as a plain feature: a TasksFeature can serve tasks/get, tasks/update and tasks/cancel, advertise the extension and check ctx.clientCapabilities().extensions(), and since CallToolOutcome is open, tools/call can already return a CreateTaskResult. A few questions:
Where should the decision to create a task live? Today it's either a feature that wraps ToolsFeature, or the repository returning CreateTaskResult. With the repository route, the result that tasks/get serves later skips the outputSchema check, since that's private to ToolsFeature. Either way, it's up to each implementation not to return a task to a client that didn't declare the extension on that request. McpServer already rejects InputRequired from methods that don't support it. Would a similar check for task results fit? (Add ability to intercept tool call executions. #720 looks related.)
Whatever runs the tool after CreateTaskResult can only reuse the original McpRequestContext (final, package-private constructor, no attributes). So there's no way to hand the tool anything task-specific, like a cancel flag for blocking code. Is reusing the original context the intended pattern?
Every tasks/* call needs an auth check, but the only principal hook is the extractor on HmacRequestStateCodec, and it defaults to null. Would a server-level principal resolver that features can read make sense? SEP-2848 would need the same principal in requestState and in the task.
Smaller: SEP-2663 has clients send Mcp-Name set to the taskId on tasks/*, but the transport checks Mcp-Name only for three hard-coded methods, so an extension can't opt in.
Happy to open separate issues for any of these if you'd rather track them outside #1013.
I think your intuition is good: you'd wrap the ToolsFeature. Returning a CreateTaskResultcan be in a normal handler in the tools repo. However, everything else, including task/get, needs to be in a feature. In there, you can then validate the output schema of task/get's result. We haven't planned interceptors, as this can be modeled with features directly. The API is low level and needs a bit of Reactor, but your feature will probably do something like:
publicclassCustomTasksFeatureimplementsMcpFeature {
privatefinalToolsFeaturedelegate = null; // todo: inject the real thing@OverridepublicMono<? extendsMcpAsyncResponse<? extendsMcpSchema.Result>> handle(McpRequestContextctx, Objectparams) {
returnpreProcess(ctx, params)
// then optionally call the tool
.flatMap(preProcessed -> {
// you can do if/else here, or in a deferred monoif (preProcessed.someDecision()) {
returnMono.just(McpAsyncResponse.result(McpSchema.CallToolResult.builder().isError(true).build()));
}
else {
returndelegate.handle(ctx, params);
}
})
// post-processing
.map(response -> {
if (responseinstanceofMcpAsyncResponse.Result<? extendsMcpSchema.Result> result
&& result.result() instanceofMcpSchema.CallToolResulttoolResult) {
// here you could validate the output schemavarupdatedResult = postProcess(toolResult);
returnMcpAsyncResponse.result(updatedResult);
}
returnresponse;
});
}
privateMcpSchema.CallToolResultpostProcess(McpSchema.CallToolResulttoolResult) {
returnnull;
}
privateMono<PreProcessed> preProcess(McpRequestContextctx, Objectparams) {
returnnull;
}
recordPreProcessed(McpRequestContextctx, Objectparams, ObjectotherStuff) {
booleansomeDecision() {
returntrue;
}
}
// ...
}
Correct, context is known at task creation time and that's about it - if the client doesn't poll, we don't know anything else. Note that context is mostly information about the request, e.g. client capabilities, name and version. The context, in turn, context an McpTransportContext which can hold arbitrary data injected through an McpContextExtractor. This allows you to pass, say, the principal executing the task, or a tenant header, etc, but that's request level. I'm not sure what you mean by cancel flag; but I imagine you want to pass "extra arguments" to the tool. In that case, in the "preprocessing" step above, you change the CallToolRequest to have more arguments, maybe gated in _meta or something.
Weeeeelllll tasks are vague about auth. SEP says "Servers MUST perform authentication and authorization checks" but in STDIO there is no concept of auth. The hook for getting a principal is the contextExtractor, as stated above. It's server-level (transport-level, technically), and ends up in McpRequestContext.transportContext() under whatever key you choose for your principal.
Mcp-Name: that's a gap indeed. That's an annoying abstraction leak between the transport layer and server layer - what about tasks on STDIO ?
You could solve it again with a transport extractor + feature-level validation, but the whole idea of a feature is to self-contain extensions.
I think for Mcp-Name we'll probably take a shortcut and carry it forward. Opened #1317 for this purpose.
@Kehrlann thanks, that's exactly what I needed. I went through the sketch against main and it works; three things I ran into that are worth writing down for whoever does this next.
The delegate.ToolsFeature.ofAsync(repository) is public, so there's nothing to inject — the wrapper builds its own and is registered in place of it. McpServer.Builder.build() doesn't add a ToolsFeature of its own, and byMethod.putIfAbsent throws when two features claim the same method, so registering both isn't an option anyway. That's also what makes the wrapper reliably in the path rather than something a request can go around.
What the wrapper has to forward.methods() and handle are the only abstract members, so a wrapper that implements just those compiles and silently drops the rest: capabilities (ToolsFeature does builder.tools(false), so tools stop being advertised) and inputRequiredMethods. Both are defaults on McpFeature, so nothing warns. Forwarding them to the delegate is enough.
Output schema. Validating in the wrapper after delegate.handle only covers the synchronous result. For the result tasks/get serves later the wrapper has to validate on the way in to the store, since by then there's no CallToolResult coming back through it — which lines up with keeping tasks/get in a feature.
On 2 and 3, that's clear — request-level context, and extra arguments go in by rewriting the CallToolRequest during pre-processing. For the principal it's McpTransportContextExtractor into transportContext(), which works for both tasks and the SEP-2848 case since they read the same context.
Mcp-Name: carrying it forward sounds right to me. Happy to open a separate issue for just the transport-level part — the check is hard-coded to three methods today, so an extension can't opt in — if you'd like it tracked outside this one. I'll leave 1–3 here since they're answered.
I'd prefer Tasks to be a feature in its own right that replaces ToolsFeature instead of wrapping it.
The Tasks feature would take two repositories: the regular tool repository you already have, and a new one for task tools. Regular tools are served as they are today. A task tool is still a tool to the client, listed in tools/list, but its handler receives a task handle (cancel signal, input requests) and returns a task. The feature serves tools/list, tools/call and tasks/*, so deciding to create a task, checking the client's capability, and validating the stored result all happen in one place. Migrating a tool to a task means moving it to the task repository.
For that to work without duplicating ToolsFeature, we can make its building blocks reusable. Separately, Mcp-Name validation should work for extension methods too, so tasks/* can be checked against taskId.
An extensible subscriptions/listen for taskIds and notifications/tasks is already part of my client work for #1011.
@Kehrlann if you agree with this direction, we can create an issue for making the ToolsFeature building blocks reusable (not needed for GA).
Activity
Kehrlann commentedon Jun 24, 2026
We will prioritize extensions in the SDK after we have the core spec covered.
If you need hooks to provide your own implementation, please open a new issue and link to this one.
anushree1808 commentedon Sep 1, 2026
Hi. What is the estimated timeline for the new protocol version and task extensions to be available?
liotur commentedon Oct 12, 2026
I'd like to pick up the Tasks extension for the Java SDK. Some of the tools on the MCP servers I run in production need a human approval before they execute, so I'm also looking at SEP-2848, which builds on Tasks. @chemicL should this wait for #1170 to land, or is it fine to start against the current server API?
chemicL commentedon Oct 15, 2026
@liotur thanks. #1170 is just the first step, we need to follow up with the client implementation. From then on we can think of implementing tasks. Please do contribute here in the form of ideas around the design for the extension. In the modern server primitives I tried to come up with more flexible interfaces for pluggability in mind so feedback on that will be very valuable.
liotur commentedon Oct 16, 2026
@chemicL thanks. I read through #1170 with Tasks in mind. Most of it fits as a plain feature: a
TasksFeaturecan servetasks/get,tasks/updateandtasks/cancel, advertise the extension and checkctx.clientCapabilities().extensions(), and sinceCallToolOutcomeis open,tools/callcan already return aCreateTaskResult. A few questions:Where should the decision to create a task live? Today it's either a feature that wraps
ToolsFeature, or the repository returningCreateTaskResult. With the repository route, the result thattasks/getserves later skips the outputSchema check, since that's private toToolsFeature. Either way, it's up to each implementation not to return a task to a client that didn't declare the extension on that request.McpServeralready rejectsInputRequiredfrom methods that don't support it. Would a similar check for task results fit? (Add ability to intercept tool call executions. #720 looks related.)Whatever runs the tool after
CreateTaskResultcan only reuse the originalMcpRequestContext(final, package-private constructor, no attributes). So there's no way to hand the tool anything task-specific, like a cancel flag for blocking code. Is reusing the original context the intended pattern?Every
tasks/*call needs an auth check, but the only principal hook is the extractor onHmacRequestStateCodec, and it defaults tonull. Would a server-level principal resolver that features can read make sense? SEP-2848 would need the same principal inrequestStateand in the task.Smaller: SEP-2663 has clients send
Mcp-Nameset to thetaskIdontasks/*, but the transport checksMcp-Nameonly for three hard-coded methods, so an extension can't opt in.Happy to open separate issues for any of these if you'd rather track them outside #1013.
Kehrlann commentedon Nov 6, 2026
@liotur
ToolsFeature. Returning aCreateTaskResultcan be in a normal handler in the tools repo. However, everything else, includingtask/get, needs to be in a feature. In there, you can then validate the output schema oftask/get's result. We haven't planned interceptors, as this can be modeled with features directly. The API is low level and needs a bit of Reactor, but your feature will probably do something like:McpTransportContextwhich can hold arbitrary data injected through anMcpContextExtractor. This allows you to pass, say, the principal executing the task, or a tenant header, etc, but that's request level. I'm not sure what you mean by cancel flag; but I imagine you want to pass "extra arguments" to the tool. In that case, in the "preprocessing" step above, you change theCallToolRequestto have more arguments, maybe gated in_metaor something.contextExtractor, as stated above. It's server-level (transport-level, technically), and ends up inMcpRequestContext.transportContext()under whatever key you choose for your principal.Mcp-Name: that's a gap indeed. That's an annoying abstraction leak between the transport layer and server layer - what about tasks on STDIO ?You could solve it again with a transport extractor + feature-level validation, but the whole idea of a feature is to self-contain extensions.
I think for Mcp-Name we'll probably take a shortcut and carry it forward. Opened #1317 for this purpose.
liotur commentedon Nov 8, 2026
@Kehrlann thanks, that's exactly what I needed. I went through the sketch against
mainand it works; three things I ran into that are worth writing down for whoever does this next.The delegate.
ToolsFeature.ofAsync(repository)is public, so there's nothing to inject — the wrapper builds its own and is registered in place of it.McpServer.Builder.build()doesn't add aToolsFeatureof its own, andbyMethod.putIfAbsentthrows when two features claim the same method, so registering both isn't an option anyway. That's also what makes the wrapper reliably in the path rather than something a request can go around.What the wrapper has to forward.
methods()andhandleare the only abstract members, so a wrapper that implements just those compiles and silently drops the rest:capabilities(ToolsFeaturedoesbuilder.tools(false), so tools stop being advertised) andinputRequiredMethods. Both are defaults onMcpFeature, so nothing warns. Forwarding them to the delegate is enough.Output schema. Validating in the wrapper after
delegate.handleonly covers the synchronous result. For the resulttasks/getserves later the wrapper has to validate on the way in to the store, since by then there's noCallToolResultcoming back through it — which lines up with keepingtasks/getin a feature.On 2 and 3, that's clear — request-level context, and extra arguments go in by rewriting the
CallToolRequestduring pre-processing. For the principal it'sMcpTransportContextExtractorintotransportContext(), which works for both tasks and the SEP-2848 case since they read the same context.Mcp-Name: carrying it forward sounds right to me. Happy to open a separate issue for just the transport-level part — the check is hard-coded to three methods today, so an extension can't opt in — if you'd like it tracked outside this one. I'll leave 1–3 here since they're answered.chemicL commentedon Nov 9, 2026
@liotur thanks, that's a useful perspective!
I'd prefer Tasks to be a feature in its own right that replaces
ToolsFeatureinstead of wrapping it.The Tasks feature would take two repositories: the regular tool repository you already have, and a new one for task tools. Regular tools are served as they are today. A task tool is still a tool to the client, listed in tools/list, but its handler receives a task handle (cancel signal, input requests) and returns a task. The feature serves tools/list, tools/call and tasks/*, so deciding to create a task, checking the client's capability, and validating the stored result all happen in one place. Migrating a tool to a task means moving it to the task repository.
For that to work without duplicating
ToolsFeature, we can make its building blocks reusable. Separately,Mcp-Namevalidation should work for extension methods too, so tasks/* can be checked against taskId.An extensible subscriptions/listen for taskIds and notifications/tasks is already part of my client work for #1011.
@Kehrlann if you agree with this direction, we can create an issue for making the
ToolsFeaturebuilding blocks reusable (not needed for GA).Kehrlann commentedon Nov 9, 2026
Agreed, opened #1320