WIP: feat: MCP Tasks extension (full: protocol, server execution, client) - #234
Draft
sideeffffect wants to merge 15 commits into
Draft
WIP: feat: MCP Tasks extension (full: protocol, server execution, client)#234sideeffffect wants to merge 15 commits into
sideeffffect wants to merge 15 commits into
Conversation
Add core protocol types for the experimental MCP Tasks extension (SEP-2663, `io.modelcontextprotocol/tasks`): TaskStatus, CreateTaskResult, GetTaskResult, the tasks/get, tasks/cancel and tasks/update request/param types, TaskAck and the notifications/tasks notification, with circe codecs matching the reference wire format. Add the requestor-side client methods getTask, cancelTask and updateTask to McpClient, so a client can poll a durable task handle returned by a task-supporting server, retrieve its result, cancel it, or answer an input request. Server-side task execution (returning a task from tools/call, task storage and driving) is intentionally left for a follow-up; this establishes the protocol vocabulary and requestor side first. Tracks softwaremill#163. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the Tasks extension (io.modelcontextprotocol/tasks) with the server-side subsystem and client augmentation on top of the protocol types: Server: - TaskStore[F] (in-memory default) and TaskExecutor[F] (thread-pool executor for synchronous servers) with best-effort cancellation. - TaskSupport bundle attached via McpServer/StreamingMcpServer.withTasks, with per-tool useTask / requireTask policies. - McpHandler answers tools/call with a CreateTaskResult when the client declares the tasks capability in _meta and runs the tool in the background, transitioning the task to completed (with the CallToolResult) or failed; a late completion never overwrites a cancellation. Handles tasks/get, tasks/cancel and tasks/update, returns -32003 when a required task capability is absent, and advertises the extension in capabilities. Client: - callToolWithTasks declares the capability and returns either a CallToolResult or a CreateTaskResult task handle. Core: - MissingRequiredClientCapability (-32003), ServerCapabilities.extensions, and the client-capability _meta helpers. Server-initiated input_required flows remain a follow-up. Tracks softwaremill#163. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace primitive-typed task fields with domain types, keeping the exact wire format: - taskId: opaque TaskId instead of String, throughout core, client and server. - createdAt / lastUpdatedAt: java.time.Instant instead of String (ISO 8601). - ttl / pollInterval: FiniteDuration instead of Long; still serialized as the integer-millisecond wire fields ttlMs / pollIntervalMs via Codec.forProductN. - GetTaskResult.error: JSONRPCErrorObject instead of raw Json. - inputRequests / UpdateTaskParams.inputResponses: Map[String, Json] instead of raw Json. - TaskSupport.ttl / pollInterval: FiniteDuration; TaskStore / TaskExecutor keyed by TaskId. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Use java.time.Duration for ttl / pollInterval, to match the java.time.Instant already used for the task timestamps. The Tasks spec fixes ttlMs / pollIntervalMs as integer milliseconds (not ISO-8601 durations), so the wire encoding is unchanged: the file-private codec maps Duration to/from milliseconds, verified by a test asserting the integer-millisecond wire fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add reusable ISO-8601 duration codecs (DurationCodecs) for both java.time.Duration and scala.concurrent.duration.FiniteDuration, the latter bridging through java.time.Duration (the JDK's ISO-8601 duration type). Encoding always produces an ISO-8601 string (e.g. "PT1H"); decoding also accepts a bare JSON number read as milliseconds, so chimp still interoperates with peers that send the ext-tasks bare-millisecond ttlMs / pollIntervalMs form. The task ttl / pollInterval fields now serialize as ISO-8601 strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…in codecs Use scala.concurrent.duration.FiniteDuration for the task ttl / pollInterval fields and TaskSupport, and expose only a Codec[FiniteDuration]. java.time.Duration is now confined to DurationCodecs, where it is used solely as the JDK's ISO-8601 duration parser and formatter (Duration.parse / Duration.ofNanos(..).toString). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Tasks extension fixes ttlMs / pollIntervalMs as integer milliseconds, so revert the wire encoding from ISO-8601 back to integer milliseconds. The domain type stays scala.concurrent.duration.FiniteDuration; only the codec changes. Drop the now-unused ISO-8601 DurationCodecs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Tasks extension fixes ttlMs / pollIntervalMs as integer milliseconds, so revert the wire encoding from ISO-8601 back to integer milliseconds. The domain type stays scala.concurrent.duration.FiniteDuration; only the codec changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Default TaskExecutor.threadPool to Executors.newVirtualThreadPerTaskExecutor() (JDK 21+), which fits the blocking tool logic tasks run and scales to many concurrent tasks. chimp's synchronous stack (ox, tapir-netty-server-sync) already requires JDK 21 at runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The task executor uses Executors.newVirtualThreadPerTaskExecutor() (JDK 21+), but the reusable publish workflow otherwise compiles on JDK 11 by default, so pin the publish job to java-version 21. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ire keys Replace the hand-written Codec.forProductN for CreateTaskResult and GetTaskResult with `derives Codec`, naming the duration fields ttlMs / pollIntervalMs (FiniteDuration) so the derived keys match the spec wire without a manual codec that could drift. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the loose status / result / error / inputRequests optional fields on GetTaskResult with a sealed TaskOutcome (Working, InputRequired(inputRequests), Completed(result), Failed(error), Cancelled), mirroring the ext-tasks DetailedTask union. status is derived from the outcome, so result/error/ inputRequests can only exist with the matching status. A hand-written codec flattens the outcome onto the wire (status plus result/error/inputRequests) and rejects, for example, a completed task with no result. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace Either[CallToolResult, CreateTaskResult] with a named sealed type ToolCallResponse (Immediate(result) / Deferred(task)), decoded from the resultType discriminator, so call sites match on meaningful cases instead of Right/Left. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change start(taskId, body: () => F[Unit]) to start(taskId)(body: => F[Unit]), so callers pass the task body as a by-name argument rather than an explicit thunk. Deferral of a synchronous (Identity) body to the worker thread is preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A tool registered with addTaskTool (defined via Tool.taskLogic) receives a TaskContext and can call requestInput(key, request) while running as a task. This moves the task to input_required, surfacing key -> request via tasks/get; the tool parks (on the virtual-thread executor) until the client answers with tasks/update, then resumes, transitioning back to working (or completing once all inputs are resolved). - TaskContext capability + Tool.taskLogic builder + McpServer/StreamingMcpServer addTaskTool and a taskTools list. - TaskInputCoordinator parks the worker and delivers answers; the waiter is registered before input_required is advertised, so a fast tasks/update is never lost. Cancelling a task unblocks its waiters. - tasks/update now delivers inputResponses; task tools are always answered with a task and require the client capability (-32003 otherwise). Task tools also appear in tools/list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Complete implementation of the Tasks extension (
io.modelcontextprotocol/tasks, SEP-2663) from the MCP roadmap — "call now, fetch later" for long-running requests. Relates to #163.Protocol (core)
Types matching the
2026-07-28ext-tasks schema, with domain-typed fields:TaskId(opaque),TaskStatus, andTaskOutcome— a sealed union (Working/InputRequired(inputRequests)/Completed(result)/Failed(error)/Cancelled) so the status-specific data can only exist with its status;GetTaskResultcarries it and derivesstatus.CreateTaskResult,tasks/get|cancel|updaterequests,TaskAck,notifications/tasks, andToolCallResponse(Immediate/Deferred) forcallToolWithTasks.Instanttimestamps,FiniteDurationttlMs/pollIntervalMs(serialized as integer milliseconds per spec),JSONRPCErrorObjecterrors,-32003, and the_metacapability helpers.Server
TaskStore(in-memory default) +TaskExecutor(virtual-thread-per-task, JDK 21) with best-effort cancellation;TaskSupportattached via.withTasks, with per-tooluseTask/requireTaskpolicies.McpHandleranswerstools/callwith aCreateTaskResultwhen the client declares the capability, runs the tool in the background, transitions tocompleted/failed(a late completion never clobbers a cancellation), handlestasks/get/tasks/cancel/tasks/update, returns-32003for a required-but-undeclared capability, and advertises the extension.input_required: a tool defined with.taskLogicand registered via.addTaskToolgets aTaskContextand canrequestInput(key, request)mid-run — moving the task toinput_required(surfaced viatasks/get), parking on the virtual-thread worker until the client answers withtasks/update, then resuming. The waiter is registered beforeinput_requiredis advertised, so a fasttasks/updateis never lost.Client
getTask/cancelTask/updateTask, andcallToolWithTasksreturningToolCallResponse.Tests
TasksSpec/DurationCodecsbehaviour — codec round-trips vs. the spec example, status wire strings, outcome flattening, integer-millis durations.TaskServerSpec— end-to-end viaMcpHandler+ the virtual-thread executor: sync fallback, create→poll→complete, throw→failed, cancel,-32003, capability advertisement, and the full input_required round trip (request →tasks/getshows it →tasks/updateanswers → completes).TasksClientSpec—getTask/cancelTask/callToolWithTasks.2025-11-25conformance + container integration).Marked WIP.
🤖 Generated with Claude Code