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.util.Arrays;
025import java.util.Optional;
026
027import static com.soklet.McpContentValueSupport.annotationsEqual;
028import static com.soklet.McpContentValueSupport.annotationsHashCode;
029import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata;
030import static java.util.Objects.requireNonNull;
031
032/**
033 * Immutable binary image MCP content block.
034 *
035 * <p>The MIME type is validated as an ASCII type/subtype with optional
036 * parameters, using the same syntax parser as MCP Apps. Malformed syntax,
037 * controls, non-ASCII characters, and duplicate parameter names are rejected.
038 * Its spelling is retained; Soklet does not inspect the bytes or check whether
039 * a particular client supports the declared format.</p>
040 *
041 * <p>The built-in server can send at most 786,432 raw bytes per binary content
042 * item: Base64 encoding must fit the 1,048,576-character JSON string limit.
043 * The complete response must also fit the 4 MiB UTF-8 JSON limit. Construction
044 * defensively copies data but does not prevalidate the eventual response size.
045 * Prefer {@link McpResourceLink} for larger payloads.</p>
046 *
047 * @author <a href="https://www.revetkn.com">Mark Allen</a>
048 */
049@ThreadSafe
050public final class McpImageContent implements McpContentBlock {
051        private final byte @NonNull [] data;
052        @NonNull
053        private final String mimeType;
054        @Nullable
055        private final McpContentAnnotations annotations;
056        @NonNull
057        private final McpJsonObject metadata;
058
059        /**
060         * Vends a builder primed with image bytes and their MIME type.
061         *
062         * @param data image bytes, defensively copied
063         * @param mimeType image MIME type
064         * @return image-content builder
065         * @throws IllegalArgumentException if the MIME type has malformed syntax
066         */
067        @NonNull
068        public static Builder withDataAndMimeType(byte @NonNull [] data,
069                        @NonNull String mimeType) {
070                return new Builder(data, mimeType);
071        }
072
073        private McpImageContent(@NonNull Builder builder) {
074                this.data = Arrays.copyOf(builder.data, builder.data.length);
075                this.mimeType = builder.mimeType;
076                this.annotations = builder.annotations;
077                this.metadata = requireApplicationMetadata(builder.metadata);
078        }
079
080        /** @return a defensive copy of the image bytes */
081        public byte @NonNull [] getData() {
082                return Arrays.copyOf(this.data, this.data.length);
083        }
084
085        int dataLength() {
086                return this.data.length;
087        }
088
089        /** @return image MIME type */
090        @NonNull
091        public String getMimeType() {
092                return this.mimeType;
093        }
094
095        /** @return content annotations, if supplied */
096        @Override
097        @NonNull
098        public Optional<@NonNull McpContentAnnotations> getAnnotations() {
099                return Optional.ofNullable(this.annotations);
100        }
101
102        /** @return immutable extension metadata */
103        @Override
104        @NonNull
105        public McpJsonObject getMetadata() {
106                return this.metadata;
107        }
108
109        /** @return whether every content property is structurally equal */
110        @Override
111        public boolean equals(@Nullable Object other) {
112                if (this == other)
113                        return true;
114                if (!(other instanceof McpImageContent content))
115                        return false;
116                return Arrays.equals(this.data, content.data)
117                                && this.mimeType.equals(content.mimeType)
118                                && annotationsEqual(this.annotations, content.annotations)
119                                && this.metadata.equals(content.metadata);
120        }
121
122        /** @return structural content hash code */
123        @Override
124        public int hashCode() {
125                int result = Arrays.hashCode(this.data);
126                result = 31 * result + this.mimeType.hashCode();
127                result = 31 * result + annotationsHashCode(this.annotations);
128                result = 31 * result + this.metadata.hashCode();
129                return result;
130        }
131
132        /**
133         * Mutable builder for immutable {@link McpImageContent}.
134         *
135         * @author <a href="https://www.revetkn.com">Mark Allen</a>
136         */
137        @NotThreadSafe
138        public static final class Builder {
139                private final byte @NonNull [] data;
140                @NonNull
141                private final String mimeType;
142                @Nullable
143                private McpContentAnnotations annotations;
144                @NonNull
145                private McpJsonObject metadata = McpJsonObject.emptyInstance();
146
147                private Builder(byte @NonNull [] data, @NonNull String mimeType) {
148                        this.data = Arrays.copyOf(requireNonNull(data), data.length);
149                        this.mimeType = McpContentValueSupport.requireMimeType(mimeType);
150                }
151
152                /** @param annotations content annotations
153                 * @return this builder */
154                @NonNull
155                public Builder annotations(@NonNull McpContentAnnotations annotations) {
156                        this.annotations = requireNonNull(annotations);
157                        return this;
158                }
159
160                /** @param metadata protocol extension metadata
161                 * @return this builder */
162                @NonNull
163                public Builder metadata(@NonNull McpJsonObject metadata) {
164                        this.metadata = requireNonNull(metadata);
165                        return this;
166                }
167
168                /** @return immutable image content */
169                @NonNull
170                public McpImageContent build() {
171                        return new McpImageContent(this);
172                }
173        }
174}