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.Optional; 025 026import static com.soklet.McpContentValueSupport.annotationsEqual; 027import static com.soklet.McpContentValueSupport.annotationsHashCode; 028import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata; 029import static java.util.Objects.requireNonNull; 030 031/** 032 * Immutable textual MCP content block. 033 * 034 * <p>Construction rejects unpaired UTF-16 surrogates with 035 * {@link IllegalArgumentException}. Well-formed text is retained exactly, 036 * without normalization or replacement.</p> 037 * 038 * <p>Text must fit the independent decoded-string and escaped-token JSON 039 * limits described by {@link McpJsonString}, as well as the complete response 040 * byte limit. Creating this content block does not prevalidate its eventual 041 * serialized response.</p> 042 * 043 * @author <a href="https://www.revetkn.com">Mark Allen</a> 044 */ 045@ThreadSafe 046public final class McpTextContent implements McpContentBlock { 047 @NonNull 048 private final String text; 049 @Nullable 050 private final McpContentAnnotations annotations; 051 @NonNull 052 private final McpJsonObject metadata; 053 054 /** 055 * Creates unannotated text content. 056 * 057 * @param text text value 058 * @return immutable text content 059 * @throws IllegalArgumentException if the text contains an unpaired surrogate 060 */ 061 @NonNull 062 public static McpTextContent fromText(@NonNull String text) { 063 return withText(text).build(); 064 } 065 066 /** 067 * Vends a builder primed with the text value. 068 * 069 * @param text text value 070 * @return text-content builder 071 * @throws IllegalArgumentException if the text contains an unpaired surrogate 072 */ 073 @NonNull 074 public static Builder withText(@NonNull String text) { 075 return new Builder(text); 076 } 077 078 private McpTextContent(@NonNull Builder builder) { 079 this.text = builder.text; 080 this.annotations = builder.annotations; 081 this.metadata = requireApplicationMetadata(builder.metadata); 082 } 083 084 /** @return text value */ 085 @NonNull 086 public String getText() { 087 return this.text; 088 } 089 090 /** @return content annotations, if supplied */ 091 @Override 092 @NonNull 093 public Optional<@NonNull McpContentAnnotations> getAnnotations() { 094 return Optional.ofNullable(this.annotations); 095 } 096 097 /** @return immutable extension metadata */ 098 @Override 099 @NonNull 100 public McpJsonObject getMetadata() { 101 return this.metadata; 102 } 103 104 /** @return whether every content property is structurally equal */ 105 @Override 106 public boolean equals(@Nullable Object other) { 107 if (this == other) 108 return true; 109 if (!(other instanceof McpTextContent content)) 110 return false; 111 return this.text.equals(content.text) 112 && annotationsEqual(this.annotations, content.annotations) 113 && this.metadata.equals(content.metadata); 114 } 115 116 /** @return structural content hash code */ 117 @Override 118 public int hashCode() { 119 int result = this.text.hashCode(); 120 result = 31 * result + annotationsHashCode(this.annotations); 121 result = 31 * result + this.metadata.hashCode(); 122 return result; 123 } 124 125 /** 126 * Mutable builder for immutable {@link McpTextContent}. 127 * 128 * @author <a href="https://www.revetkn.com">Mark Allen</a> 129 */ 130 @NotThreadSafe 131 public static final class Builder { 132 @NonNull 133 private final String text; 134 @Nullable 135 private McpContentAnnotations annotations; 136 @NonNull 137 private McpJsonObject metadata = McpJsonObject.emptyInstance(); 138 139 private Builder(@NonNull String text) { 140 this.text = McpContentValueSupport.requireWellFormedString(text); 141 } 142 143 /** @param annotations content annotations 144 * @return this builder */ 145 @NonNull 146 public Builder annotations(@NonNull McpContentAnnotations annotations) { 147 this.annotations = requireNonNull(annotations); 148 return this; 149 } 150 151 /** @param metadata protocol extension metadata 152 * @return this builder */ 153 @NonNull 154 public Builder metadata(@NonNull McpJsonObject metadata) { 155 this.metadata = requireNonNull(metadata); 156 return this; 157 } 158 159 /** @return immutable text content */ 160 @NonNull 161 public McpTextContent build() { 162 return new McpTextContent(this); 163 } 164 } 165}