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; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.ThreadSafe; 023import java.util.Collections; 024import java.util.LinkedHashSet; 025import java.util.Objects; 026import java.util.Set; 027 028import static java.util.Objects.requireNonNull; 029 030/** 031 * Immutable declaration of an MCP client request that an operation may emit. 032 * 033 * <p>The declaration binds a core client request method to the complete set 034 * of client capabilities required to emit it. Soklet supports only form and 035 * URL elicitation through {@code elicitation/create}; this type is not an 036 * extension escape hatch. 037 * 038 * @author <a href="https://www.revetkn.com">Mark Allen</a> 039 */ 040@ThreadSafe 041public final class McpInputRequestDeclaration { 042 @NonNull 043 private final McpInputRequestType inputRequestType; 044 @NonNull 045 private final String jsonRpcMethod; 046 @NonNull 047 private final Set<@NonNull McpClientCapability> capabilities; 048 @NonNull 049 private final McpInputRequirement requirement; 050 051 /** 052 * Creates and validates an input-request declaration. 053 * 054 * @param inputRequestType client input-request type 055 * @param capabilities required client capabilities 056 * @param requirement when the capabilities are required 057 * @throws NullPointerException if an argument or capability is null 058 * @throws IllegalArgumentException if the type or capability combination 059 * is not one of Soklet's supported core declarations 060 */ 061 private McpInputRequestDeclaration( 062 @NonNull McpInputRequestType inputRequestType, 063 @NonNull Set<@NonNull McpClientCapability> capabilities, 064 @NonNull McpInputRequirement requirement) { 065 requireNonNull(inputRequestType); 066 requireNonNull(capabilities); 067 requireNonNull(requirement); 068 Set<McpClientCapability> copiedCapabilities = 069 new LinkedHashSet<>(capabilities.size()); 070 for (McpClientCapability capability : capabilities) 071 copiedCapabilities.add(requireNonNull(capability)); 072 073 if (copiedCapabilities.isEmpty()) 074 throw new IllegalArgumentException( 075 "At least one input-request capability is required."); 076 077 Set<McpClientCapability> immutableCapabilities = 078 Collections.unmodifiableSet(copiedCapabilities); 079 validateCapabilities(inputRequestType, immutableCapabilities); 080 081 this.inputRequestType = inputRequestType; 082 this.jsonRpcMethod = jsonRpcMethod(inputRequestType); 083 this.capabilities = immutableCapabilities; 084 this.requirement = requirement; 085 } 086 087 /** 088 * Creates a form-based elicitation declaration. 089 * 090 * @param requirement when form elicitation support is required 091 * @return form-based elicitation declaration 092 */ 093 @NonNull 094 public static McpInputRequestDeclaration fromElicitationForm( 095 @NonNull McpInputRequirement requirement) { 096 return new McpInputRequestDeclaration(McpInputRequestType.ELICITATION_FORM, 097 Set.of(McpClientCapability.ELICITATION_FORM), requirement); 098 } 099 100 /** 101 * Creates a URL-based elicitation declaration. 102 * 103 * @param requirement when URL elicitation support is required 104 * @return URL-based elicitation declaration 105 */ 106 @NonNull 107 public static McpInputRequestDeclaration fromElicitationUrl( 108 @NonNull McpInputRequirement requirement) { 109 return new McpInputRequestDeclaration(McpInputRequestType.ELICITATION_URL, 110 Set.of(McpClientCapability.ELICITATION_URL), requirement); 111 } 112 113 /** @return core client-input request type */ 114 @NonNull 115 public McpInputRequestType getInputRequestType() { 116 return this.inputRequestType; 117 } 118 119 /** @return derived JSON-RPC client request method */ 120 @NonNull 121 public String getJsonRpcMethod() { 122 return this.jsonRpcMethod; 123 } 124 125 /** @return immutable required client capabilities */ 126 @NonNull 127 public Set<@NonNull McpClientCapability> getCapabilities() { 128 return this.capabilities; 129 } 130 131 /** @return when the client capabilities are required */ 132 @NonNull 133 public McpInputRequirement getRequirement() { 134 return this.requirement; 135 } 136 137 /** @return whether this value has the same type, capabilities, and requirement */ 138 @Override 139 public boolean equals(@Nullable Object other) { 140 if (this == other) 141 return true; 142 if (!(other instanceof McpInputRequestDeclaration declaration)) 143 return false; 144 return this.inputRequestType == declaration.inputRequestType 145 && this.capabilities.equals(declaration.capabilities) 146 && this.requirement == declaration.requirement; 147 } 148 149 /** @return value-based hash code */ 150 @Override 151 public int hashCode() { 152 return Objects.hash(this.inputRequestType, this.capabilities, 153 this.requirement); 154 } 155 156 /** @return value-based diagnostic rendering */ 157 @Override 158 @NonNull 159 public String toString() { 160 return "McpInputRequestDeclaration{inputRequestType=%s, jsonRpcMethod='%s', capabilities=%s, requirement=%s}" 161 .formatted(this.inputRequestType, this.jsonRpcMethod, 162 this.capabilities, this.requirement); 163 } 164 165 private static void validateCapabilities( 166 @NonNull McpInputRequestType inputRequestType, 167 @NonNull Set<@NonNull McpClientCapability> capabilities) { 168 switch (inputRequestType) { 169 case ELICITATION_FORM -> requireExactCapabilities(capabilities, 170 McpClientCapability.ELICITATION_FORM, 171 "Form-elicitation declarations require exactly the ELICITATION_FORM capability."); 172 case ELICITATION_URL -> requireExactCapabilities(capabilities, 173 McpClientCapability.ELICITATION_URL, 174 "URL-elicitation declarations require exactly the ELICITATION_URL capability."); 175 } 176 } 177 178 private static void requireExactCapabilities( 179 @NonNull Set<@NonNull McpClientCapability> capabilities, 180 @NonNull McpClientCapability capability, 181 @NonNull String message) { 182 if (!capabilities.equals(Set.of(capability))) 183 throw new IllegalArgumentException(message); 184 } 185 186 @NonNull 187 private static String jsonRpcMethod( 188 @NonNull McpInputRequestType inputRequestType) { 189 return switch (inputRequestType) { 190 case ELICITATION_FORM, ELICITATION_URL -> "elicitation/create"; 191 }; 192 } 193}