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.net.URI; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.Objects; 028import java.util.Optional; 029 030import static com.soklet.McpContentValueSupport.annotationsEqual; 031import static com.soklet.McpContentValueSupport.annotationsHashCode; 032import static com.soklet.McpContentValueSupport.iconListHashCode; 033import static com.soklet.McpContentValueSupport.iconListsEqual; 034import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata; 035import static java.util.Objects.requireNonNull; 036 037/** 038 * Immutable MCP content block linking to a resource. 039 * 040 * <p>When returned as tool content, optional icons are omitted from the 041 * {@code 2025-06-18} wire projection and retained for {@code 2025-11-25} and 042 * the modern revision. Projection does not mutate this value.</p> 043 * 044 * @author <a href="https://www.revetkn.com">Mark Allen</a> 045 */ 046@ThreadSafe 047public final class McpResourceLink implements McpContentBlock { 048 @NonNull 049 private final URI uri; 050 @NonNull 051 private final String name; 052 @Nullable 053 private final String title; 054 @Nullable 055 private final String description; 056 @Nullable 057 private final String mimeType; 058 @NonNull 059 private final List<@NonNull McpIcon> icons; 060 @Nullable 061 private final McpContentAnnotations annotations; 062 @Nullable 063 private final Long sizeInBytes; 064 @NonNull 065 private final McpJsonObject metadata; 066 067 /** 068 * Vends a builder primed with the linked resource URI and name. 069 * 070 * @param uri absolute normalized resource URI in ASCII wire form 071 * @param name resource name 072 * @return resource-link builder 073 * @throws IllegalArgumentException if the URI is relative, not normalized, 074 * not in ASCII wire form, or the name is blank 075 */ 076 @NonNull 077 public static Builder withUriAndName(@NonNull URI uri, 078 @NonNull String name) { 079 return new Builder(uri, name); 080 } 081 082 /** 083 * Creates a resource-link content block from an existing concrete resource 084 * descriptor. Every descriptor property is preserved. 085 * 086 * @param resourceDescriptor resource descriptor to copy 087 * @return immutable resource link 088 * @throws NullPointerException if {@code resourceDescriptor} is null 089 */ 090 @NonNull 091 public static McpResourceLink fromResourceDescriptor( 092 @NonNull McpResourceDescriptor resourceDescriptor) { 093 return new McpResourceLink(resourceDescriptor); 094 } 095 096 private McpResourceLink( 097 @NonNull McpResourceDescriptor resourceDescriptor) { 098 McpResourceDescriptor descriptor = requireNonNull(resourceDescriptor); 099 this.uri = descriptor.getUri(); 100 this.name = descriptor.getName(); 101 this.title = descriptor.getTitle().orElse(null); 102 this.description = descriptor.getDescription().orElse(null); 103 this.mimeType = descriptor.getMimeType().orElse(null); 104 this.icons = List.copyOf(descriptor.getIcons()); 105 this.annotations = descriptor.getAnnotations().orElse(null); 106 this.sizeInBytes = descriptor.getSizeInBytes().orElse(null); 107 this.metadata = descriptor.getMetadata(); 108 } 109 110 private McpResourceLink(@NonNull Builder builder) { 111 this.uri = builder.uri; 112 this.name = builder.name; 113 this.title = builder.title; 114 this.description = builder.description; 115 this.mimeType = builder.mimeType; 116 this.icons = List.copyOf(builder.icons); 117 this.annotations = builder.annotations; 118 this.sizeInBytes = builder.sizeInBytes; 119 this.metadata = requireApplicationMetadata(builder.metadata); 120 } 121 122 /** @return absolute normalized linked-resource URI in ASCII wire form */ 123 @NonNull 124 public URI getUri() { 125 return this.uri; 126 } 127 128 /** @return linked resource name */ 129 @NonNull 130 public String getName() { 131 return this.name; 132 } 133 134 /** @return human-readable title, if supplied */ 135 @NonNull 136 public Optional<@NonNull String> getTitle() { 137 return Optional.ofNullable(this.title); 138 } 139 140 /** @return human-readable description, if supplied */ 141 @NonNull 142 public Optional<@NonNull String> getDescription() { 143 return Optional.ofNullable(this.description); 144 } 145 146 /** @return resource MIME type, if supplied */ 147 @NonNull 148 public Optional<@NonNull String> getMimeType() { 149 return Optional.ofNullable(this.mimeType); 150 } 151 152 /** @return immutable icon list in registration order */ 153 @NonNull 154 public List<@NonNull McpIcon> getIcons() { 155 return this.icons; 156 } 157 158 /** @return content annotations, if supplied */ 159 @Override 160 @NonNull 161 public Optional<@NonNull McpContentAnnotations> getAnnotations() { 162 return Optional.ofNullable(this.annotations); 163 } 164 165 /** @return resource size in bytes, if supplied */ 166 @NonNull 167 public Optional<@NonNull Long> getSizeInBytes() { 168 return Optional.ofNullable(this.sizeInBytes); 169 } 170 171 /** @return immutable extension metadata */ 172 @Override 173 @NonNull 174 public McpJsonObject getMetadata() { 175 return this.metadata; 176 } 177 178 /** @return whether every linked-resource property is structurally equal */ 179 @Override 180 public boolean equals(@Nullable Object other) { 181 if (this == other) 182 return true; 183 if (!(other instanceof McpResourceLink link)) 184 return false; 185 return this.uri.equals(link.uri) 186 && this.name.equals(link.name) 187 && Objects.equals(this.title, link.title) 188 && Objects.equals(this.description, link.description) 189 && Objects.equals(this.mimeType, link.mimeType) 190 && iconListsEqual(this.icons, link.icons) 191 && annotationsEqual(this.annotations, link.annotations) 192 && Objects.equals(this.sizeInBytes, link.sizeInBytes) 193 && this.metadata.equals(link.metadata); 194 } 195 196 /** @return structural linked-resource hash code */ 197 @Override 198 public int hashCode() { 199 int result = Objects.hash(this.uri, this.name, this.title, 200 this.description, this.mimeType); 201 result = 31 * result + iconListHashCode(this.icons); 202 result = 31 * result + annotationsHashCode(this.annotations); 203 result = 31 * result + Objects.hashCode(this.sizeInBytes); 204 result = 31 * result + this.metadata.hashCode(); 205 return result; 206 } 207 208 /** 209 * Mutable builder for immutable {@link McpResourceLink}. 210 * 211 * @author <a href="https://www.revetkn.com">Mark Allen</a> 212 */ 213 @NotThreadSafe 214 public static final class Builder { 215 @NonNull 216 private final URI uri; 217 @NonNull 218 private final String name; 219 @Nullable 220 private String title; 221 @Nullable 222 private String description; 223 @Nullable 224 private String mimeType; 225 @NonNull 226 private final List<@NonNull McpIcon> icons = new ArrayList<>(); 227 @Nullable 228 private McpContentAnnotations annotations; 229 @Nullable 230 private Long sizeInBytes; 231 @NonNull 232 private McpJsonObject metadata = McpJsonObject.emptyInstance(); 233 234 private Builder(@NonNull URI uri, @NonNull String name) { 235 this.uri = McpResourceValueSupport 236 .requireAbsoluteNormalizedUri(uri); 237 this.name = McpResourceValueSupport.requireNonBlank(name, 238 "MCP resource name"); 239 } 240 241 /** @param title human-readable title 242 * @return this builder */ 243 @NonNull 244 public Builder title(@NonNull String title) { 245 this.title = McpContentValueSupport.requireWellFormedString(title); 246 return this; 247 } 248 249 /** @param description human-readable description 250 * @return this builder */ 251 @NonNull 252 public Builder description(@NonNull String description) { 253 this.description = McpContentValueSupport.requireWellFormedString(description); 254 return this; 255 } 256 257 /** @param mimeType syntactically valid resource media type 258 * @return this builder 259 * @throws IllegalArgumentException if {@code mimeType} is not a syntactically valid media type 260 */ 261 @NonNull 262 public Builder mimeType(@NonNull String mimeType) { 263 this.mimeType = McpContentValueSupport.requireMimeType(mimeType); 264 return this; 265 } 266 267 /** 268 * Appends an icon. 269 * 270 * @param icon icon descriptor 271 * @return this builder 272 */ 273 @NonNull 274 public Builder addIcon(@NonNull McpIcon icon) { 275 this.icons.add(requireNonNull(icon)); 276 return this; 277 } 278 279 /** @param annotations content annotations 280 * @return this builder */ 281 @NonNull 282 public Builder annotations(@NonNull McpContentAnnotations annotations) { 283 this.annotations = requireNonNull(annotations); 284 return this; 285 } 286 287 /** 288 * Sets the resource size in bytes. 289 * 290 * @param sizeInBytes nonnegative byte count 291 * @return this builder 292 * @throws NullPointerException if {@code sizeInBytes} is null 293 * @throws IllegalArgumentException if {@code sizeInBytes} is negative 294 */ 295 @NonNull 296 public Builder sizeInBytes(@NonNull Long sizeInBytes) { 297 requireNonNull(sizeInBytes); 298 if (sizeInBytes < 0) 299 throw new IllegalArgumentException( 300 "Resource size must not be negative."); 301 this.sizeInBytes = sizeInBytes; 302 return this; 303 } 304 305 /** @param metadata protocol extension metadata 306 * @return this builder */ 307 @NonNull 308 public Builder metadata(@NonNull McpJsonObject metadata) { 309 this.metadata = requireNonNull(metadata); 310 return this; 311 } 312 313 /** @return immutable resource link */ 314 @NonNull 315 public McpResourceLink build() { 316 return new McpResourceLink(this); 317 } 318 } 319}