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.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.util.Objects;
025import java.util.Optional;
026
027import static java.util.Objects.requireNonNull;
028
029/**
030 * Immutable MCP client hints describing a tool's behavior.
031 *
032 * <p>Hints are advisory and must never be used as authorization or safety
033 * controls. Each boolean preserves the difference between an omitted hint and
034 * an explicitly supplied {@code false} value.
035 *
036 * @author <a href="https://www.revetkn.com">Mark Allen</a>
037 */
038@ThreadSafe
039public final class McpToolAnnotations {
040        @Nullable
041        private final String title;
042        @Nullable
043        private final Boolean readOnlyHint;
044        @Nullable
045        private final Boolean destructiveHint;
046        @Nullable
047        private final Boolean idempotentHint;
048        @Nullable
049        private final Boolean openWorldHint;
050
051        /** @return an empty tool-annotation builder */
052        @NonNull
053        public static Builder builder() {
054                return new Builder();
055        }
056
057        private McpToolAnnotations(@NonNull Builder builder) {
058                this.title = builder.title;
059                this.readOnlyHint = builder.readOnlyHint;
060                this.destructiveHint = builder.destructiveHint;
061                this.idempotentHint = builder.idempotentHint;
062                this.openWorldHint = builder.openWorldHint;
063        }
064
065        /** @return human-readable title hint, if supplied */
066        @NonNull
067        public Optional<@NonNull String> getTitle() {
068                return Optional.ofNullable(this.title);
069        }
070
071        /** @return read-only hint, preserving omission */
072        @NonNull
073        public Optional<@NonNull Boolean> getReadOnlyHint() {
074                return Optional.ofNullable(this.readOnlyHint);
075        }
076
077        /** @return destructive hint, preserving omission */
078        @NonNull
079        public Optional<@NonNull Boolean> getDestructiveHint() {
080                return Optional.ofNullable(this.destructiveHint);
081        }
082
083        /** @return idempotent hint, preserving omission */
084        @NonNull
085        public Optional<@NonNull Boolean> getIdempotentHint() {
086                return Optional.ofNullable(this.idempotentHint);
087        }
088
089        /** @return open-world interaction hint, preserving omission */
090        @NonNull
091        public Optional<@NonNull Boolean> getOpenWorldHint() {
092                return Optional.ofNullable(this.openWorldHint);
093        }
094
095        /** @return whether every tool hint is structurally equal */
096        @Override
097        public boolean equals(@Nullable Object other) {
098                if (this == other)
099                        return true;
100                if (!(other instanceof McpToolAnnotations annotations))
101                        return false;
102                return Objects.equals(this.title, annotations.title)
103                                && Objects.equals(this.readOnlyHint, annotations.readOnlyHint)
104                                && Objects.equals(this.destructiveHint,
105                                                annotations.destructiveHint)
106                                && Objects.equals(this.idempotentHint,
107                                                annotations.idempotentHint)
108                                && Objects.equals(this.openWorldHint, annotations.openWorldHint);
109        }
110
111        /** @return structural tool-hint hash code */
112        @Override
113        public int hashCode() {
114                return Objects.hash(this.title, this.readOnlyHint, this.destructiveHint,
115                                this.idempotentHint, this.openWorldHint);
116        }
117
118        /**
119         * Mutable builder for immutable {@link McpToolAnnotations}.
120         *
121         * @author <a href="https://www.revetkn.com">Mark Allen</a>
122         */
123        @NotThreadSafe
124        public static final class Builder {
125                @Nullable
126                private String title;
127                @Nullable
128                private Boolean readOnlyHint;
129                @Nullable
130                private Boolean destructiveHint;
131                @Nullable
132                private Boolean idempotentHint;
133                @Nullable
134                private Boolean openWorldHint;
135
136                private Builder() {
137                }
138
139                /**
140                 * Sets the human-readable title hint.
141                 *
142                 * @param title title hint
143                 * @return this builder
144                 */
145                @NonNull
146                public Builder title(@NonNull String title) {
147                        this.title = McpContentValueSupport.requireWellFormedString(title);
148                        return this;
149                }
150
151                /**
152                 * @param readOnlyHint whether the tool is read-only
153                 * @return this builder
154                 * @throws NullPointerException if {@code readOnlyHint} is null
155                 */
156                @NonNull
157                public Builder readOnlyHint(@NonNull Boolean readOnlyHint) {
158                        this.readOnlyHint = requireNonNull(readOnlyHint);
159                        return this;
160                }
161
162                /**
163                 * @param destructiveHint whether the tool may perform destructive updates
164                 * @return this builder
165                 * @throws NullPointerException if {@code destructiveHint} is null
166                 */
167                @NonNull
168                public Builder destructiveHint(@NonNull Boolean destructiveHint) {
169                        this.destructiveHint = requireNonNull(destructiveHint);
170                        return this;
171                }
172
173                /**
174                 * @param idempotentHint whether repeated calls have no additional effect
175                 * @return this builder
176                 * @throws NullPointerException if {@code idempotentHint} is null
177                 */
178                @NonNull
179                public Builder idempotentHint(@NonNull Boolean idempotentHint) {
180                        this.idempotentHint = requireNonNull(idempotentHint);
181                        return this;
182                }
183
184                /**
185                 * @param openWorldHint whether the tool may interact with external entities
186                 * @return this builder
187                 * @throws NullPointerException if {@code openWorldHint} is null
188                 */
189                @NonNull
190                public Builder openWorldHint(@NonNull Boolean openWorldHint) {
191                        this.openWorldHint = requireNonNull(openWorldHint);
192                        return this;
193                }
194
195                /** @return immutable tool annotations */
196                @NonNull
197                public McpToolAnnotations build() {
198                        return new McpToolAnnotations(this);
199                }
200        }
201}