|
8 | 8 |
|
9 | 9 | import io.modelcontextprotocol.server.McpAsyncServerExchange; |
10 | 10 | import io.modelcontextprotocol.spec.McpSchema; |
| 11 | +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; |
| 12 | +import io.modelcontextprotocol.spec.McpSchema.TaskStatus; |
11 | 13 | import reactor.core.publisher.Mono; |
12 | 14 |
|
13 | 15 | /** |
|
18 | 20 | * |
19 | 21 | * <pre>{@code |
20 | 22 | * CreateTaskHandler handler = (args, extra) -> { |
21 | | - * // Decide TTL based on request or use a default |
22 | | - * long ttl = extra.requestTtl() != null |
23 | | - * ? Math.min(extra.requestTtl(), Duration.ofMinutes(30).toMillis()) |
24 | | - * : Duration.ofMinutes(5).toMillis(); |
| 23 | + * return extra.createTask(opts -> opts.pollInterval(500L)).flatMap(task -> { |
| 24 | + * // Start async work that will complete the task later |
| 25 | + * doAsyncWork(args) |
| 26 | + * .flatMap(result -> extra.completeTask(task.taskId(), result)) |
| 27 | + * .onErrorResume(e -> extra.failTask(task.taskId(), e.getMessage())) |
| 28 | + * .subscribe(); |
25 | 29 | * |
26 | | - * return extra.taskStore() |
27 | | - * .createTask(CreateTaskOptions.builder() |
28 | | - * .requestedTtl(ttl) |
29 | | - * .sessionId(extra.sessionId()) |
30 | | - * .build()) |
31 | | - * .flatMap(task -> { |
32 | | - * // Use exchange for client communication |
33 | | - * startBackgroundWork(task.taskId(), args, extra.exchange()).subscribe(); |
34 | | - * return Mono.just(new McpSchema.CreateTaskResult(task, null)); |
35 | | - * }); |
| 30 | + * return Mono.just(McpSchema.CreateTaskResult.builder().task(task).build()); |
| 31 | + * }); |
36 | 32 | * }; |
37 | 33 | * }</pre> |
38 | 34 | * |
|
47 | 43 | * |
48 | 44 | * @see CreateTaskHandler |
49 | 45 | * @see SyncCreateTaskExtra |
50 | | - * @see TaskStore |
51 | | - * @see TaskMessageQueue |
52 | 46 | */ |
53 | 47 | public interface CreateTaskExtra { |
54 | 48 |
|
55 | | - /** |
56 | | - * The task store for creating and managing tasks. |
57 | | - * |
58 | | - * <p> |
59 | | - * Tools use this to create tasks with their desired configuration: |
60 | | - * |
61 | | - * <pre>{@code |
62 | | - * extra.taskStore().createTask(CreateTaskOptions.builder() |
63 | | - * .requestedTtl(Duration.ofMinutes(5).toMillis()) |
64 | | - * .pollInterval(Duration.ofSeconds(1).toMillis()) |
65 | | - * .sessionId(extra.sessionId()) |
66 | | - * .build()); |
67 | | - * }</pre> |
68 | | - * @return the TaskStore instance |
69 | | - */ |
70 | | - TaskStore<McpSchema.ServerTaskPayloadResult> taskStore(); |
71 | | - |
72 | | - /** |
73 | | - * The message queue for task communication during INPUT_REQUIRED state. |
74 | | - * |
75 | | - * <p> |
76 | | - * Use this for interactive tasks that need to communicate with the client during |
77 | | - * execution. |
78 | | - * @return the TaskMessageQueue instance, or null if not configured |
79 | | - */ |
80 | | - TaskMessageQueue taskMessageQueue(); |
81 | | - |
82 | 49 | /** |
83 | 50 | * The server exchange for client interaction. |
84 | 51 | * |
@@ -130,81 +97,116 @@ public interface CreateTaskExtra { |
130 | 97 | McpSchema.Request originatingRequest(); |
131 | 98 |
|
132 | 99 | // -------------------------- |
133 | | - // Convenience Methods |
| 100 | + // Task Creation |
134 | 101 | // -------------------------- |
135 | 102 |
|
136 | 103 | /** |
137 | 104 | * Convenience method to create a task with default options derived from this context. |
138 | 105 | * |
139 | 106 | * <p> |
140 | 107 | * This method automatically uses {@link #originatingRequest()}, {@link #sessionId()}, |
141 | | - * and {@link #requestTtl()} from this context, eliminating common boilerplate: |
| 108 | + * and {@link #requestTtl()} from this context. |
142 | 109 | * |
143 | 110 | * <pre>{@code |
144 | | - * // Instead of: |
145 | | - * extra.taskStore().createTask(CreateTaskOptions.builder(extra.originatingRequest()) |
146 | | - * .sessionId(extra.sessionId()) |
147 | | - * .requestedTtl(extra.requestTtl()) |
148 | | - * .build()) |
149 | | - * |
150 | | - * // You can simply use: |
151 | | - * extra.createTask() |
| 111 | + * extra.createTask().flatMap(task -> { |
| 112 | + * doAsyncWork(args) |
| 113 | + * .flatMap(result -> extra.completeTask(task.taskId(), result)) |
| 114 | + * .subscribe(); |
| 115 | + * return Mono.just(McpSchema.CreateTaskResult.builder().task(task).build()); |
| 116 | + * }); |
152 | 117 | * }</pre> |
153 | | - * @return Mono that completes with the created Task |
| 118 | + * @return Mono that completes with the created task |
154 | 119 | */ |
155 | | - default Mono<McpSchema.Task> createTask() { |
156 | | - return taskStore().createTask(CreateTaskOptions.builder(originatingRequest()) |
157 | | - .sessionId(sessionId()) |
158 | | - .requestedTtl(requestTtl()) |
159 | | - .build()); |
160 | | - } |
| 120 | + Mono<McpSchema.Task> createTask(); |
161 | 121 |
|
162 | 122 | /** |
163 | 123 | * Convenience method to create a task with custom options, but inheriting session |
164 | 124 | * context. |
165 | 125 | * |
166 | 126 | * <p> |
167 | 127 | * This method pre-populates the builder with {@link #originatingRequest()}, |
168 | | - * {@link #sessionId()}, and {@link #requestTtl()}, then allows customization: |
| 128 | + * {@link #sessionId()}, and {@link #requestTtl()}, then allows customization. |
169 | 129 | * |
170 | 130 | * <pre>{@code |
171 | 131 | * // Create a task with custom poll interval: |
172 | | - * extra.createTask(opts -> opts.pollInterval(500L)) |
173 | | - * |
174 | | - * // Create a task with custom TTL (ignoring client request): |
175 | | - * extra.createTask(opts -> opts.requestedTtl(Duration.ofMinutes(10).toMillis())) |
| 132 | + * extra.createTask(opts -> opts.pollInterval(500L)).flatMap(task -> { |
| 133 | + * // Pass task ID explicitly for side-channeling |
| 134 | + * extra.exchange().createElicitation(request, task.taskId()).subscribe(); |
| 135 | + * return Mono.just(McpSchema.CreateTaskResult.builder().task(task).build()); |
| 136 | + * }); |
176 | 137 | * }</pre> |
177 | 138 | * @param customizer function to customize options beyond the defaults |
178 | | - * @return Mono that completes with the created Task |
| 139 | + * @return Mono that completes with the created task |
| 140 | + */ |
| 141 | + Mono<McpSchema.Task> createTask(Consumer<CreateTaskOptions.Builder> customizer); |
| 142 | + |
| 143 | + // -------------------------- |
| 144 | + // Task Lifecycle |
| 145 | + // -------------------------- |
| 146 | + |
| 147 | + /** |
| 148 | + * Complete a task with a successful result. |
| 149 | + * |
| 150 | + * <p> |
| 151 | + * This marks the task as {@link TaskStatus#COMPLETED} and stores the result for |
| 152 | + * client retrieval. |
| 153 | + * |
| 154 | + * <pre>{@code |
| 155 | + * extra.createTask().flatMap(task -> { |
| 156 | + * doAsyncWork(args) |
| 157 | + * .flatMap(result -> extra.completeTask(task.taskId(), result)) |
| 158 | + * .subscribe(); |
| 159 | + * return Mono.just(McpSchema.CreateTaskResult.builder().task(task).build()); |
| 160 | + * }); |
| 161 | + * }</pre> |
| 162 | + * @param taskId the ID of the task to complete |
| 163 | + * @param result the tool result to store |
| 164 | + * @return Mono that completes when the task is updated |
| 165 | + */ |
| 166 | + Mono<Void> completeTask(String taskId, CallToolResult result); |
| 167 | + |
| 168 | + /** |
| 169 | + * Mark a task as failed with an error message. |
| 170 | + * |
| 171 | + * <p> |
| 172 | + * This marks the task as {@link TaskStatus#FAILED} with the provided message. |
| 173 | + * |
| 174 | + * <pre>{@code |
| 175 | + * extra.createTask().flatMap(task -> { |
| 176 | + * doAsyncWork(args) |
| 177 | + * .flatMap(result -> extra.completeTask(task.taskId(), result)) |
| 178 | + * .onErrorResume(e -> extra.failTask(task.taskId(), e.getMessage())) |
| 179 | + * .subscribe(); |
| 180 | + * return Mono.just(McpSchema.CreateTaskResult.builder().task(task).build()); |
| 181 | + * }); |
| 182 | + * }</pre> |
| 183 | + * @param taskId the ID of the task to fail |
| 184 | + * @param message the error message describing what went wrong |
| 185 | + * @return Mono that completes when the task is updated |
179 | 186 | */ |
180 | | - default Mono<McpSchema.Task> createTask(Consumer<CreateTaskOptions.Builder> customizer) { |
181 | | - CreateTaskOptions.Builder builder = CreateTaskOptions.builder(originatingRequest()) |
182 | | - .sessionId(sessionId()) |
183 | | - .requestedTtl(requestTtl()); |
184 | | - customizer.accept(builder); |
185 | | - return taskStore().createTask(builder.build()); |
186 | | - } |
| 187 | + Mono<Void> failTask(String taskId, String message); |
187 | 188 |
|
188 | 189 | /** |
189 | | - * Create a TaskContext for managing the given task's lifecycle. |
| 190 | + * Set a task to INPUT_REQUIRED status, triggering side-channel delivery. |
190 | 191 | * |
191 | 192 | * <p> |
192 | | - * This convenience method creates a TaskContext that uses this extra's task store and |
193 | | - * message queue, reducing boilerplate in task handlers: |
| 193 | + * When a task is in {@link TaskStatus#INPUT_REQUIRED}, the client will poll via |
| 194 | + * {@code tasks/result} and receive any queued notifications or requests via |
| 195 | + * side-channeling. |
194 | 196 | * |
195 | 197 | * <pre>{@code |
196 | | - * extra.createTask() |
197 | | - * .map(task -> extra.createTaskContext(task)) |
198 | | - * .flatMap(ctx -> { |
199 | | - * // Use ctx to update status, send messages, etc. |
200 | | - * return ctx.complete(result); |
201 | | - * }); |
| 198 | + * extra.createTask().flatMap(task -> { |
| 199 | + * // Queue a notification for side-channel delivery |
| 200 | + * extra.exchange().loggingNotification(notification, task.taskId()) |
| 201 | + * .then(extra.setInputRequired(task.taskId(), "Waiting for user input")) |
| 202 | + * .subscribe(); |
| 203 | + * return Mono.just(McpSchema.CreateTaskResult.builder().task(task).build()); |
| 204 | + * }); |
202 | 205 | * }</pre> |
203 | | - * @param task the task to create a context for |
204 | | - * @return a TaskContext bound to the given task and this extra's infrastructure |
| 206 | + * @param taskId the ID of the task |
| 207 | + * @param message a status message describing what input is required |
| 208 | + * @return Mono that completes when the task is updated |
205 | 209 | */ |
206 | | - default TaskContext createTaskContext(McpSchema.Task task) { |
207 | | - return new DefaultTaskContext<>(task.taskId(), sessionId(), taskStore(), taskMessageQueue()); |
208 | | - } |
| 210 | + Mono<Void> setInputRequired(String taskId, String message); |
209 | 211 |
|
210 | 212 | } |
0 commit comments