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}