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