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}