001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020 021import javax.annotation.concurrent.ThreadSafe; 022import java.util.Optional; 023 024/** 025 * Thread-safe application authority for MCP task state and client actions. 026 * 027 * <p>A production implementation normally delegates to application-owned 028 * durable storage and job infrastructure shared by every eligible Soklet 029 * node. It owns task-ID generation, persistence, retention, task-to-principal 030 * and tenant binding, legal atomic state transitions, client-input 031 * deduplication, cancelation intent, and publication of already-durable status 032 * changes. Soklet does not close the manager or start, stop, schedule, retry, 033 * lease, or fence application workers. 034 * 035 * <p>Every operation is independently admitted before this interface is 036 * called, but admission alone does not authorize a task. Implementations must 037 * atomically compare the current request's endpoint and admitted application 038 * identity with their durable task binding. Unknown and unauthorized task IDs 039 * must be indistinguishable: {@link #findTask(McpTaskRequestContext)} returns 040 * an empty optional for either case, while mutation methods throw 041 * {@link McpTaskNotFoundException} for either case. Other failures are treated 042 * as internal errors and must not expose exception-derived data to the client. 043 * 044 * <p>A task must be durably readable through {@link #findTask} before its 045 * handler returns {@link McpTaskCreatedResult}. Correct production behavior 046 * cannot depend on the original HTTP connection, sticky routing, or the 047 * originating process remaining alive. Implementations should assume 048 * at-least-once work execution and use leases, fencing, idempotency, and an 049 * outbox or equivalent recovery mechanism where their workload requires them. 050 * 051 * <p>Soklet validates returned snapshots at delivery time. Invalid stored 052 * output or undeclared input requests fail polling with a fixed internal 053 * error and suppress notification projection without mutating task state, 054 * publishing a state change, or notifying an application worker. Current 055 * access policy, sanitization, and transport limits can also prevent delivery 056 * without proving that the underlying work failed. 057 * Applications must validate their output and input requests before storing 058 * them; preserve the framework origin as opaque data rather than interpreting 059 * its persisted members. This interface supplies no worker-side validation 060 * callback. 061 * 062 * @author <a href="https://www.revetkn.com">Mark Allen</a> 063 */ 064@ThreadSafe 065public interface McpTaskManager { 066 /** 067 * Creates Soklet's explicit process-local task manager with documented 068 * bounded defaults. 069 * 070 * <p>The returned manager is intended for development, tests, simulation, 071 * and deliberately ephemeral single-process applications. Its state is lost 072 * on restart and is invisible to other nodes. It provides no worker, 073 * executor, queue, outbox, leases, fencing, failover, or crash recovery and 074 * is never selected implicitly by an {@link McpServer}. 075 * 076 * @return a new independent in-memory task manager 077 */ 078 @NonNull 079 static McpInMemoryTaskManager fromInMemoryDefaults() { 080 return new McpInMemoryTaskManager(); 081 } 082 083 /** 084 * Returns the optional application-owned publisher for durable task-state 085 * changes. 086 * <p> 087 * When present, Soklet subscribes while the server is running and may project 088 * matching events onto {@code subscriptions/listen} streams as 089 * {@code notifications/tasks}. The publisher carries task IDs only; Soklet 090 * performs a fresh, authorized lookup through this manager before sending a 091 * current snapshot. Polling through {@code tasks/get} remains authoritative. 092 * <p> 093 * This is a stable manager property: every invocation for the manager's 094 * lifetime must return either an empty optional or an optional containing the 095 * same publisher instance. Each built {@link McpServer} snapshots the property 096 * once during construction; it does not observe a later publisher change. 097 * This lets production and simulator servers backed by the same manager attach 098 * independently to one shared event topology. 099 * <p> 100 * The default returns an empty optional, enabling polling without task 101 * notifications. Soklet neither starts nor closes the publisher. 102 * 103 * @return task-event publisher, or the empty optional when task notifications 104 * are disabled 105 */ 106 @NonNull 107 default Optional<@NonNull McpTaskEventPublisher> getTaskEventPublisher() { 108 return Optional.empty(); 109 } 110 111 /** 112 * Finds the current authoritative task snapshot after atomically 113 * authorizing the request. 114 * <p> 115 * Soklet may invoke this method asynchronously to project a task-change event. 116 * A subscription can disconnect while that lookup is in progress, so 117 * implementations should apply finite storage deadlines and respond promptly 118 * to thread interruption; a result may be discarded when the subscription is 119 * no longer active. 120 * <p> 121 * After an authorized lookup, Soklet checks outstanding input requests 122 * against the polling request's current client capabilities. Missing support 123 * fails that poll with the missing-capability protocol error, without sending 124 * a partial snapshot or changing authoritative state. Working and terminal 125 * snapshots do not require capabilities for unused input declarations. 126 * 127 * @param taskRequestContext independently admitted lookup context 128 * @return current snapshot, or empty when the task is unknown or unauthorized 129 * @throws Exception if task storage or authorization fails unexpectedly 130 */ 131 @NonNull 132 Optional<@NonNull McpTask> findTask( 133 @NonNull McpTaskRequestContext taskRequestContext) throws Exception; 134 135 /** 136 * Atomically authorizes and accepts client responses to outstanding task 137 * input requests. 138 * 139 * <p>A request may supply any partial subset of outstanding responses. 140 * Unknown, already-consumed, and superseded response keys are ignored, as is 141 * a response whose MCP union variant does not match its outstanding request. 142 * Implementations may perform that correlation through 143 * {@link McpInputRequest#matchesInputResponse(McpJsonValue)}. 144 * The update acknowledgement may precede a worker's subsequent observable 145 * state change. 146 * 147 * @param taskUpdateContext independently admitted task update 148 * @throws McpTaskNotFoundException if the task is unknown or unauthorized 149 * @throws Exception if task storage, authorization, or update processing 150 * fails unexpectedly 151 */ 152 void updateTask(@NonNull McpTaskUpdateContext taskUpdateContext) throws Exception; 153 154 /** 155 * Atomically authorizes and records a client's durable task-cancelation 156 * intent. 157 * 158 * <p>A successful return acknowledges intent only. A worker may later honor 159 * it, or completion or failure may win the race. This method is unrelated to 160 * request-scoped {@link CancelationToken} and 161 * {@code notifications/cancelled} behavior. A known terminal task must be 162 * accepted idempotently; only an unknown or unauthorized task uses the 163 * not-found signal. 164 * 165 * @param taskRequestContext independently admitted task-cancelation request 166 * @throws McpTaskNotFoundException if the task is unknown or unauthorized 167 * @throws Exception if task storage, authorization, or intent recording 168 * fails unexpectedly 169 */ 170 void requestTaskCancelation( 171 @NonNull McpTaskRequestContext taskRequestContext) throws Exception; 172}