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.LinkedHashMap;
023import java.util.Map;
024import java.util.Optional;
025
026import static java.util.Objects.requireNonNull;
027
028/**
029 * Immutable lookup for optional features associated with one MCP invocation.
030 * The lookup carrier is safe for concurrent access; feature values retain their
031 * own lifecycle, mutability, and thread-safety contracts. This type's
032 * {@link ThreadSafe} annotation does not make feature values thread-safe.
033 *
034 * <p>Features are registered and found by their exact public feature-interface
035 * class. Assignable supertypes and subtypes are not searched. Feature types
036 * are added without changing handler or context signatures.
037 *
038 * <p>Soklet supplies one {@link CancelationToken} for every selected MCP
039 * application handler. Its cancelation reason, when present, is one fixed
040 * {@link StreamTerminationReason} value; the framework-supplied MCP token does
041 * not expose an underlying throwable through
042 * {@link CancelationToken#getCancelationCause()}. Applications may record the
043 * fixed reason under their own logging policy, but should never substitute an
044 * untrusted free-form cause or use cancelation details as metric dimensions.
045 *
046 * @author <a href="https://www.revetkn.com">Mark Allen</a>
047 */
048@ThreadSafe
049public interface McpInvocationFeatures {
050        /**
051         * Creates an immutable feature lookup, primarily for application tests.
052         *
053         * <p>The map and its entries are defensively copied. Each value must be an
054         * instance of its key and retains its own mutability and thread-safety
055         * contract.
056         *
057         * @param featuresByType exact feature-interface keys and matching values
058         * @return immutable feature lookup
059         * @throws NullPointerException if the map, a key, or a value is null
060         * @throws IllegalArgumentException if a value is not an instance of its key
061         */
062        @NonNull
063        static McpInvocationFeatures fromFeatures(
064                        @NonNull Map<@NonNull Class<?>, @NonNull Object> featuresByType) {
065                requireNonNull(featuresByType);
066                Map<@NonNull Class<?>, @NonNull Object> copiedFeatures =
067                                new LinkedHashMap<>();
068                for (Map.Entry<@NonNull Class<?>, @NonNull Object> entry
069                                : featuresByType.entrySet()) {
070                        Class<?> featureType = requireNonNull(entry.getKey());
071                        Object feature = requireNonNull(entry.getValue());
072                        if (!featureType.isInstance(feature))
073                                throw new IllegalArgumentException(
074                                                "Feature value is not an instance of "
075                                                                + featureType.getName() + ".");
076                        copiedFeatures.put(featureType, feature);
077                }
078                Map<@NonNull Class<?>, @NonNull Object> immutableFeatures =
079                                Map.copyOf(copiedFeatures);
080
081                return new McpInvocationFeatures() {
082                        @Override
083                        @NonNull
084                        public <T> Optional<@NonNull T> find(
085                                        @NonNull Class<@NonNull T> featureType) {
086                                Class<@NonNull T> requiredType = requireNonNull(featureType);
087                                return Optional.ofNullable(requiredType.cast(
088                                                immutableFeatures.get(requiredType)));
089                        }
090                };
091        }
092
093        /**
094         * Finds a feature by its exact registered feature-interface class.
095         *
096         * <p>An empty result means that the feature is unknown, does not apply to
097         * this operation, or is unavailable for this request. Repeated lookup of a
098         * present feature returns the same instance for the invocation's lifetime.
099         *
100         * @param featureType exact feature-interface class
101         * @param <T> feature type
102         * @return feature instance, if available
103         * @throws NullPointerException if {@code featureType} is null
104         */
105        @NonNull
106        <T> Optional<@NonNull T> find(
107                        @NonNull Class<@NonNull T> featureType);
108
109        /**
110         * Returns the cooperative cancelation signal for this invocation.
111         *
112         * <p>Soklet supplies this built-in feature for every selected MCP
113         * application handler. The returned value is the same instance exposed by
114         * {@link #require(Class) require(CancelationToken.class)}.
115         *
116         * @return invocation cancelation token
117         * @throws IllegalStateException if an implementation does not supply the
118         * built-in cancelation token
119         */
120        @NonNull
121        default CancelationToken getCancelationToken() {
122                return require(CancelationToken.class);
123        }
124
125        /**
126         * Returns the progress reporter available for this invocation.
127         *
128         * <p>The reporter is present only when the initiating request supplied a
129         * valid progress token and Soklet can safely emit request-scoped progress.
130         * When present, the returned value is the same instance exposed by
131         * {@link #find(Class) find(McpProgressReporter.class)}.
132         *
133         * @return invocation progress reporter, if available
134         */
135        @NonNull
136        default Optional<@NonNull McpProgressReporter> getProgressReporter() {
137                return find(McpProgressReporter.class);
138        }
139
140        /**
141         * Returns the context for durably creating an MCP task from this invocation.
142         *
143         * <p>The context is present only when the current operation supports task
144         * augmentation, a task manager is configured, the selected endpoint enables
145         * Tasks for this revision, and the client declared the MCP Tasks extension
146         * capability on this request. Presence does not mean that the client prefers
147         * or requires a task; the server remains the sole per-request decision maker.
148         * An absent task creation context requires inline completion. A handler or
149         * interceptor that nevertheless returns a task handle fails with the fixed
150         * JSON-RPC internal error. Statically task-required tools reject a missing
151         * client capability before application execution.
152         *
153         * @return invocation task creation context, if task creation is permitted
154         */
155        @NonNull
156        default Optional<@NonNull McpTaskCreationContext> getTaskCreationContext() {
157                return find(McpTaskCreationContext.class);
158        }
159
160        /**
161         * Requires a feature using the same exact-class lookup as
162         * {@link #find(Class)}.
163         *
164         * @param featureType exact feature-interface class
165         * @param <T> feature type
166         * @return feature instance
167         * @throws NullPointerException if {@code featureType} is null
168         * @throws IllegalStateException if the feature is unavailable
169         */
170        @NonNull
171        default <T> T require(@NonNull Class<@NonNull T> featureType) {
172                Class<@NonNull T> requiredFeatureType = requireNonNull(featureType);
173                return find(requiredFeatureType).orElseThrow(() ->
174                                new IllegalStateException(
175                                                "Required MCP invocation feature is unavailable: "
176                                                                + requiredFeatureType.getName()));
177        }
178}