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}