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}