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.Arrays; 026import java.util.Optional; 027 028import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata; 029import static java.util.Objects.requireNonNull; 030 031/** 032 * Immutable binary contents for one MCP resource. 033 * 034 * <p>The built-in server can send at most 786,432 raw bytes per binary content 035 * item: Base64 encoding must fit the 1,048,576-character JSON string limit. 036 * The complete response must also fit the 4 MiB UTF-8 JSON limit. Construction 037 * defensively copies data but does not prevalidate the eventual response size. 038 * Prefer {@link McpResourceLink} for larger payloads; an MCP resource read 039 * returning these contents remains subject to the same limits.</p> 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043@ThreadSafe 044public final class McpBlobResourceContents implements McpResourceContents { 045 @NonNull 046 private final URI uri; 047 private final byte @NonNull [] data; 048 @Nullable 049 private final String mimeType; 050 @Nullable 051 private final McpAppResourceMetadata appResourceMetadata; 052 @NonNull 053 private final McpJsonObject metadata; 054 055 /** 056 * Vends a builder primed with the resource URI and binary data. 057 * 058 * @param uri absolute normalized resource URI in ASCII wire form 059 * @param data resource bytes, defensively copied 060 * @return resource-content builder 061 * @throws IllegalArgumentException if the URI is relative, not normalized, 062 * or not in ASCII wire form 063 */ 064 @NonNull 065 public static Builder withUriAndData(@NonNull URI uri, 066 byte @NonNull [] data) { 067 return new Builder(uri, data); 068 } 069 070 private McpBlobResourceContents(@NonNull Builder builder) { 071 this.uri = builder.uri; 072 this.data = Arrays.copyOf(builder.data, builder.data.length); 073 this.mimeType = builder.mimeType; 074 this.appResourceMetadata = builder.appResourceMetadata; 075 this.metadata = requireApplicationMetadata(builder.metadata); 076 } 077 078 /** 079 * Returns the URI that identifies this resource. 080 * 081 * @return absolute normalized resource URI in ASCII wire form 082 */ 083 @Override 084 @NonNull 085 public URI getUri() { 086 return this.uri; 087 } 088 089 /** 090 * Returns a defensive copy of the resource bytes. 091 * 092 * @return resource bytes 093 */ 094 public byte @NonNull [] getData() { 095 return Arrays.copyOf(this.data, this.data.length); 096 } 097 098 int dataLength() { 099 return this.data.length; 100 } 101 102 boolean dataEquals(@NonNull McpBlobResourceContents other) { 103 return Arrays.equals(this.data, requireNonNull(other).data); 104 } 105 106 int dataHashCode() { 107 return Arrays.hashCode(this.data); 108 } 109 110 /** 111 * Returns the resource MIME type, if one was supplied. 112 * 113 * @return resource MIME type, if available 114 */ 115 @Override 116 @NonNull 117 public Optional<@NonNull String> getMimeType() { 118 return Optional.ofNullable(this.mimeType); 119 } 120 121 /** 122 * Returns separately configured Apps policy and presentation hints. 123 * 124 * @return typed Apps resource metadata, if supplied 125 */ 126 @Override 127 @NonNull 128 public Optional<@NonNull McpAppResourceMetadata> getAppResourceMetadata() { 129 return Optional.ofNullable(this.appResourceMetadata); 130 } 131 132 /** 133 * Returns protocol extension metadata associated with this resource. 134 * 135 * @return immutable metadata object 136 */ 137 @Override 138 @NonNull 139 public McpJsonObject getMetadata() { 140 return this.metadata; 141 } 142 143 /** @return whether every resource-content property is structurally equal */ 144 @Override 145 public boolean equals(@Nullable Object other) { 146 if (this == other) 147 return true; 148 if (!(other instanceof McpBlobResourceContents contents)) 149 return false; 150 return this.uri.equals(contents.uri) 151 && dataEquals(contents) 152 && java.util.Objects.equals(this.mimeType, contents.mimeType) 153 && java.util.Objects.equals(this.appResourceMetadata, contents.appResourceMetadata) 154 && this.metadata.equals(contents.metadata); 155 } 156 157 /** @return structural resource-content hash code */ 158 @Override 159 public int hashCode() { 160 return java.util.Objects.hash(McpBlobResourceContents.class, this.uri, 161 dataHashCode(), this.mimeType, this.appResourceMetadata, this.metadata); 162 } 163 164 /** 165 * Mutable builder for immutable {@link McpBlobResourceContents}. 166 * 167 * @author <a href="https://www.revetkn.com">Mark Allen</a> 168 */ 169 @NotThreadSafe 170 public static final class Builder { 171 @NonNull 172 private final URI uri; 173 private final byte @NonNull [] data; 174 @Nullable 175 private String mimeType; 176 @Nullable 177 private McpAppResourceMetadata appResourceMetadata; 178 @NonNull 179 private McpJsonObject metadata = McpJsonObject.emptyInstance(); 180 181 private Builder(@NonNull URI uri, byte @NonNull [] data) { 182 this.uri = McpResourceValueSupport 183 .requireAbsoluteNormalizedUri(uri); 184 this.data = Arrays.copyOf(requireNonNull(data), data.length); 185 } 186 187 /** 188 * Sets the resource MIME type. 189 * 190 * @param mimeType syntactically valid media type 191 * @return this builder 192 * @throws IllegalArgumentException if {@code mimeType} is not a syntactically valid media type 193 */ 194 @NonNull 195 public Builder mimeType(@NonNull String mimeType) { 196 this.mimeType = McpContentValueSupport.requireMimeType(mimeType); 197 return this; 198 } 199 200 /** 201 * Sets Apps policy and presentation hints for a resource read. 202 * 203 * <p>The built contents must use a normalized ASCII {@code ui://} URI 204 * and the {@code text/html;profile=mcp-app} MIME type. Typed Apps 205 * metadata is kept separate from raw extension metadata; declaring 206 * typed-owned fields in both is rejected regardless of setter order. 207 * 208 * @param appResourceMetadata immutable Apps resource metadata 209 * @return this builder 210 * @throws IllegalArgumentException if raw metadata declares a typed-owned field 211 */ 212 @NonNull 213 public Builder appResourceMetadata(@NonNull McpAppResourceMetadata appResourceMetadata) { 214 McpAppMetadataSupport.effectiveResourceMetadata(this.metadata, 215 requireNonNull(appResourceMetadata)); 216 this.appResourceMetadata = appResourceMetadata; 217 return this; 218 } 219 220 /** 221 * Sets protocol extension metadata. 222 * 223 * @param metadata immutable metadata object 224 * @return this builder 225 */ 226 @NonNull 227 public Builder metadata(@NonNull McpJsonObject metadata) { 228 McpAppMetadataSupport.effectiveResourceMetadata(requireNonNull(metadata), 229 this.appResourceMetadata); 230 this.metadata = metadata; 231 return this; 232 } 233 234 /** @return immutable binary resource contents */ 235 @NonNull 236 public McpBlobResourceContents build() { 237 McpAppMetadataSupport.requireResourceContents(this.uri, this.mimeType, 238 this.metadata, this.appResourceMetadata); 239 return new McpBlobResourceContents(this); 240 } 241 } 242}