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.time.Duration; 025import java.util.List; 026import java.util.Objects; 027import java.util.Optional; 028 029import static com.soklet.McpContentValueSupport.resourceContentsEqual; 030import static com.soklet.McpContentValueSupport.resourceContentsHashCode; 031import static java.util.Objects.requireNonNull; 032 033/** 034 * Immutable output of one completed MCP resource-read request. 035 * 036 * <p>The matching resource registration owns the fixed cache scope and 037 * default time to live. An output may override only the time to live for this 038 * response. A localization-enabled server conservatively publishes a private, 039 * zero-time-to-live policy regardless of an output override. 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043@ThreadSafe 044public final class McpResourceOutput implements McpCompletePayload { 045 @NonNull 046 private final List<@NonNull McpResourceContents> contents; 047 @Nullable 048 private final Duration cacheTimeToLiveOverride; 049 050 /** 051 * Vends a builder containing its required first resource value. 052 * 053 * @param resourceContents resource contents 054 * @return resource-output builder 055 * @throws NullPointerException if {@code resourceContents} is null 056 */ 057 @NonNull 058 public static Builder withContent( 059 @NonNull McpResourceContents resourceContents) { 060 return withContents(List.of(requireNonNull(resourceContents))); 061 } 062 063 /** 064 * Creates output containing exactly one resource value. 065 * 066 * @param resourceContents resource contents 067 * @return immutable resource output 068 * @throws NullPointerException if {@code resourceContents} is null 069 */ 070 @NonNull 071 public static McpResourceOutput fromContent( 072 @NonNull McpResourceContents resourceContents) { 073 return withContent(resourceContents).build(); 074 } 075 076 /** 077 * Vends a builder containing an ordered snapshot of resource contents. 078 * 079 * @param resourceContents nonempty resource contents in response order 080 * @return resource-output builder 081 * @throws NullPointerException if the list or any item is null 082 * @throws IllegalArgumentException if the list is empty 083 */ 084 @NonNull 085 public static Builder withContents( 086 @NonNull List<? extends @NonNull McpResourceContents> resourceContents) { 087 return new Builder(resourceContents); 088 } 089 090 /** 091 * Creates output from an ordered snapshot of resource contents. 092 * 093 * @param resourceContents nonempty resource contents in response order 094 * @return immutable resource output 095 * @throws NullPointerException if the list or any item is null 096 * @throws IllegalArgumentException if the list is empty 097 */ 098 @NonNull 099 public static McpResourceOutput fromContents( 100 @NonNull List<? extends @NonNull McpResourceContents> resourceContents) { 101 return withContents(resourceContents).build(); 102 } 103 104 private McpResourceOutput(@NonNull Builder builder) { 105 this.contents = requireContents(builder.contents); 106 this.cacheTimeToLiveOverride = builder.cacheTimeToLiveOverride; 107 } 108 109 @NonNull 110 private static List<@NonNull McpResourceContents> requireContents( 111 @NonNull List<? extends @NonNull McpResourceContents> resourceContents) { 112 List<McpResourceContents> copiedContents = 113 List.copyOf(requireNonNull(resourceContents)); 114 if (copiedContents.isEmpty()) 115 throw new IllegalArgumentException( 116 "At least one MCP resource-content value is required."); 117 return copiedContents; 118 } 119 120 /** @return immutable resource contents in insertion order */ 121 @NonNull 122 public List<@NonNull McpResourceContents> getContents() { 123 return this.contents; 124 } 125 126 /** 127 * Returns this response's time-to-live override. 128 * 129 * @return override, or empty to use the registration default 130 */ 131 @NonNull 132 public Optional<@NonNull Duration> getCacheTimeToLiveOverride() { 133 return Optional.ofNullable(this.cacheTimeToLiveOverride); 134 } 135 136 /** @return whether contents and cache override are structurally equal */ 137 @Override 138 public boolean equals(@Nullable Object other) { 139 if (this == other) 140 return true; 141 if (!(other instanceof McpResourceOutput output)) 142 return false; 143 return contentsEqual(this.contents, output.contents) 144 && Objects.equals(this.cacheTimeToLiveOverride, 145 output.cacheTimeToLiveOverride); 146 } 147 148 /** @return structural resource-output hash code */ 149 @Override 150 public int hashCode() { 151 int contentsHashCode = 1; 152 for (McpResourceContents resourceContents : this.contents) 153 contentsHashCode = 31 * contentsHashCode 154 + resourceContentsHashCode(resourceContents); 155 return Objects.hash(contentsHashCode, this.cacheTimeToLiveOverride); 156 } 157 158 private static boolean contentsEqual( 159 @NonNull List<@NonNull McpResourceContents> first, 160 @NonNull List<@NonNull McpResourceContents> second) { 161 if (first.size() != second.size()) 162 return false; 163 for (int index = 0; index < first.size(); ++index) { 164 if (!resourceContentsEqual(first.get(index), second.get(index))) 165 return false; 166 } 167 return true; 168 } 169 170 /** 171 * Mutable builder for immutable {@link McpResourceOutput}. 172 * 173 * @author <a href="https://www.revetkn.com">Mark Allen</a> 174 */ 175 @NotThreadSafe 176 public static final class Builder { 177 @NonNull 178 private List<@NonNull McpResourceContents> contents; 179 @Nullable 180 private Duration cacheTimeToLiveOverride; 181 182 private Builder( 183 @NonNull List<? extends @NonNull McpResourceContents> resourceContents) { 184 this.contents = requireContents(resourceContents); 185 } 186 187 /** 188 * Replaces all resource contents with an ordered immutable snapshot. 189 * A failed replacement leaves the previous contents unchanged. 190 * 191 * @param resourceContents nonempty resource contents in response order 192 * @return this builder 193 * @throws NullPointerException if the list or any item is null 194 * @throws IllegalArgumentException if the list is empty 195 */ 196 @NonNull 197 public Builder contents( 198 @NonNull List<? extends @NonNull McpResourceContents> resourceContents) { 199 this.contents = requireContents(resourceContents); 200 return this; 201 } 202 203 /** 204 * Overrides only the registration's default time to live. 205 * 206 * @param cacheTimeToLiveOverride nonnegative whole-millisecond duration 207 * @return this builder 208 * @throws IllegalArgumentException if the duration is negative or has 209 * sub-millisecond precision 210 */ 211 @NonNull 212 public Builder cacheTimeToLiveOverride( 213 @NonNull Duration cacheTimeToLiveOverride) { 214 this.cacheTimeToLiveOverride = 215 McpCachePolicy.requireTimeToLive(cacheTimeToLiveOverride); 216 return this; 217 } 218 219 /** @return immutable resource output */ 220 @NonNull 221 public McpResourceOutput build() { 222 return new McpResourceOutput(this); 223 } 224 } 225}