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.Objects; 026import java.util.Optional; 027 028import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata; 029import static java.util.Objects.requireNonNull; 030 031/** 032 * Immutable UTF-16 Java text contents for one MCP resource. 033 * 034 * <p>Construction rejects unpaired UTF-16 surrogates with 035 * {@link IllegalArgumentException}. Well-formed text is retained exactly, 036 * without normalization or replacement. Text must fit the independent JSON 037 * string/token and complete-response limits described by {@link McpJsonString}; 038 * construction does not prevalidate its eventual serialized size.</p> 039 * 040 * @author <a href="https://www.revetkn.com">Mark Allen</a> 041 */ 042@ThreadSafe 043public final class McpTextResourceContents implements McpResourceContents { 044 @NonNull 045 private final URI uri; 046 @NonNull 047 private final String text; 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 text. 057 * 058 * @param uri absolute normalized resource URI in ASCII wire form 059 * @param text resource text 060 * @return resource-content builder 061 * @throws IllegalArgumentException if the URI is relative, not normalized, 062 * or not in ASCII wire form, or the text contains an unpaired surrogate 063 */ 064 @NonNull 065 public static Builder withUriAndText(@NonNull URI uri, 066 @NonNull String text) { 067 return new Builder(uri, text); 068 } 069 070 private McpTextResourceContents(@NonNull Builder builder) { 071 this.uri = builder.uri; 072 this.text = builder.text; 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 /** @return resource text */ 090 @NonNull 091 public String getText() { 092 return this.text; 093 } 094 095 /** 096 * Returns the resource MIME type, if one was supplied. 097 * 098 * @return resource MIME type, if available 099 */ 100 @Override 101 @NonNull 102 public Optional<@NonNull String> getMimeType() { 103 return Optional.ofNullable(this.mimeType); 104 } 105 106 /** 107 * Returns separately configured Apps policy and presentation hints. 108 * 109 * @return typed Apps resource metadata, if supplied 110 */ 111 @Override 112 @NonNull 113 public Optional<@NonNull McpAppResourceMetadata> getAppResourceMetadata() { 114 return Optional.ofNullable(this.appResourceMetadata); 115 } 116 117 /** 118 * Returns protocol extension metadata associated with this resource. 119 * 120 * @return immutable metadata object 121 */ 122 @Override 123 @NonNull 124 public McpJsonObject getMetadata() { 125 return this.metadata; 126 } 127 128 /** @return whether every resource-content property is structurally equal */ 129 @Override 130 public boolean equals(@Nullable Object other) { 131 if (this == other) 132 return true; 133 if (!(other instanceof McpTextResourceContents contents)) 134 return false; 135 return this.uri.equals(contents.uri) 136 && this.text.equals(contents.text) 137 && Objects.equals(this.mimeType, contents.mimeType) 138 && Objects.equals(this.appResourceMetadata, contents.appResourceMetadata) 139 && this.metadata.equals(contents.metadata); 140 } 141 142 /** @return structural resource-content hash code */ 143 @Override 144 public int hashCode() { 145 return Objects.hash(McpTextResourceContents.class, this.uri, this.text, 146 this.mimeType, this.appResourceMetadata, this.metadata); 147 } 148 149 /** 150 * Mutable builder for immutable {@link McpTextResourceContents}. 151 * 152 * @author <a href="https://www.revetkn.com">Mark Allen</a> 153 */ 154 @NotThreadSafe 155 public static final class Builder { 156 @NonNull 157 private final URI uri; 158 @NonNull 159 private final String text; 160 @Nullable 161 private String mimeType; 162 @Nullable 163 private McpAppResourceMetadata appResourceMetadata; 164 @NonNull 165 private McpJsonObject metadata = McpJsonObject.emptyInstance(); 166 167 private Builder(@NonNull URI uri, @NonNull String text) { 168 this.uri = McpResourceValueSupport 169 .requireAbsoluteNormalizedUri(uri); 170 this.text = McpContentValueSupport.requireWellFormedString(text); 171 } 172 173 /** 174 * Sets the resource MIME type. 175 * 176 * @param mimeType syntactically valid media type 177 * @return this builder 178 * @throws IllegalArgumentException if {@code mimeType} is not a syntactically valid media type 179 */ 180 @NonNull 181 public Builder mimeType(@NonNull String mimeType) { 182 this.mimeType = McpContentValueSupport.requireMimeType(mimeType); 183 return this; 184 } 185 186 /** 187 * Sets Apps policy and presentation hints for a resource read. 188 * 189 * <p>The built contents must use a normalized ASCII {@code ui://} URI 190 * and the {@code text/html;profile=mcp-app} MIME type. Typed Apps 191 * metadata is kept separate from raw extension metadata; declaring 192 * typed-owned fields in both is rejected regardless of setter order. 193 * 194 * @param appResourceMetadata immutable Apps resource metadata 195 * @return this builder 196 * @throws IllegalArgumentException if raw metadata declares a typed-owned field 197 */ 198 @NonNull 199 public Builder appResourceMetadata(@NonNull McpAppResourceMetadata appResourceMetadata) { 200 McpAppMetadataSupport.effectiveResourceMetadata(this.metadata, 201 requireNonNull(appResourceMetadata)); 202 this.appResourceMetadata = appResourceMetadata; 203 return this; 204 } 205 206 /** 207 * Sets protocol extension metadata. 208 * 209 * @param metadata immutable metadata object 210 * @return this builder 211 */ 212 @NonNull 213 public Builder metadata(@NonNull McpJsonObject metadata) { 214 McpAppMetadataSupport.effectiveResourceMetadata(requireNonNull(metadata), 215 this.appResourceMetadata); 216 this.metadata = metadata; 217 return this; 218 } 219 220 /** @return immutable text resource contents */ 221 @NonNull 222 public McpTextResourceContents build() { 223 McpAppMetadataSupport.requireResourceContents(this.uri, this.mimeType, 224 this.metadata, this.appResourceMetadata); 225 return new McpTextResourceContents(this); 226 } 227 } 228}