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 audio 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 McpAudioContent 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 audio bytes and their MIME type. 061 * 062 * @param data audio bytes, defensively copied 063 * @param mimeType audio MIME type 064 * @return audio-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 McpAudioContent(@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 audio 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 audio 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 McpAudioContent 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 McpAudioContent}. 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 audio content */ 169 @NonNull 170 public McpAudioContent build() { 171 return new McpAudioContent(this); 172 } 173 } 174}