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.List; 025import java.util.Objects; 026import java.util.Optional; 027 028import static java.util.Objects.requireNonNull; 029 030/** 031 * Immutable output of one completed MCP tool call. 032 * 033 * <p>For either 2025 protocol revision, structured content must be a JSON 034 * object and any declared output schema must have object type. The modern 035 * revision permits other JSON shapes. Raw/advanced handlers share one output 036 * type, so applications must select a representable shape before performing 037 * side effects. The legacy adapter does not silently wrap or discard a 038 * non-object structured value. June tool resource links omit optional icons; 039 * November and modern tool links retain them.</p> 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043@ThreadSafe 044public final class McpToolOutput implements McpCompletePayload { 045 @NonNull 046 private final List<@NonNull McpContentBlock> content; 047 @Nullable 048 private final McpJsonValue structuredContent; 049 private final boolean error; 050 051 /** @return an empty tool-output builder */ 052 @NonNull 053 public static Builder builder() { 054 return new Builder(); 055 } 056 057 /** 058 * Creates successful prose output. 059 * 060 * @param text prose text 061 * @return tool output containing one text block 062 */ 063 @NonNull 064 public static McpToolOutput fromText(@NonNull String text) { 065 return builder().content(List.of(McpTextContent.fromText(text))).build(); 066 } 067 068 /** 069 * Creates successful structured output. 070 * 071 * @param structuredContent structured JSON value 072 * @return tool output carrying structured content 073 */ 074 @NonNull 075 public static McpToolOutput fromStructuredContent( 076 @NonNull McpJsonValue structuredContent) { 077 return builder().structuredContent(structuredContent).build(); 078 } 079 080 /** 081 * Creates error prose output. 082 * 083 * @param text safe client-visible error text 084 * @return error tool output containing one text block 085 */ 086 @NonNull 087 public static McpToolOutput fromErrorText(@NonNull String text) { 088 return builder().content(List.of(McpTextContent.fromText(text))) 089 .error(true).build(); 090 } 091 092 private McpToolOutput(@NonNull Builder builder) { 093 this.content = List.copyOf(builder.content); 094 this.structuredContent = builder.structuredContent; 095 this.error = builder.error; 096 } 097 098 /** @return immutable content blocks in insertion order */ 099 @NonNull 100 public List<@NonNull McpContentBlock> getContent() { 101 return this.content; 102 } 103 104 /** @return structured JSON content, if supplied */ 105 @NonNull 106 public Optional<@NonNull McpJsonValue> getStructuredContent() { 107 return Optional.ofNullable(this.structuredContent); 108 } 109 110 /** @return whether the tool reported an application-level error */ 111 @NonNull 112 public Boolean isError() { 113 return this.error; 114 } 115 116 /** 117 * Returns a builder initialized with every field of this output. 118 * 119 * <p>Content blocks retain their insertion order, structured content is 120 * copied when present, and the application-level error state is preserved. 121 * This is the preferred starting point for a sanitizer that changes only 122 * selected fields while retaining all other tool output. 123 * 124 * @return field-preserving builder initialized from this output 125 */ 126 @NonNull 127 public Builder toBuilder() { 128 Builder builder = builder().content(this.content).error(this.error); 129 if (this.structuredContent != null) 130 builder.structuredContent(this.structuredContent); 131 return builder; 132 } 133 134 /** @return whether every tool-output property is structurally equal */ 135 @Override 136 public boolean equals(@Nullable Object other) { 137 if (this == other) 138 return true; 139 if (!(other instanceof McpToolOutput output)) 140 return false; 141 return this.error == output.error 142 && this.content.equals(output.content) 143 && Objects.equals(this.structuredContent, 144 output.structuredContent); 145 } 146 147 /** @return structural tool-output hash code */ 148 @Override 149 public int hashCode() { 150 return Objects.hash(this.content, this.structuredContent, this.error); 151 } 152 153 /** 154 * Mutable builder for immutable {@link McpToolOutput}. 155 * 156 * @author <a href="https://www.revetkn.com">Mark Allen</a> 157 */ 158 @NotThreadSafe 159 public static final class Builder { 160 @NonNull 161 private List<@NonNull McpContentBlock> content = List.of(); 162 @Nullable 163 private McpJsonValue structuredContent; 164 private boolean error; 165 166 private Builder() { 167 } 168 169 /** 170 * Replaces content blocks in supplied order. 171 * Null or empty clears the property. The complete list is validated and 172 * snapshotted before replacing the prior value. 173 * 174 * @param content content blocks, or null to clear 175 * @return this builder 176 * @throws NullPointerException if a list element is null 177 */ 178 @NonNull 179 public Builder content( 180 @Nullable List<? extends @NonNull McpContentBlock> content) { 181 this.content = content == null ? List.of() 182 : List.copyOf(content); 183 return this; 184 } 185 186 /** 187 * Sets structured JSON content. 188 * 189 * @param structuredContent structured JSON value 190 * @return this builder 191 */ 192 @NonNull 193 public Builder structuredContent( 194 @NonNull McpJsonValue structuredContent) { 195 this.structuredContent = requireNonNull(structuredContent); 196 return this; 197 } 198 199 /** 200 * Sets whether the tool result is an application-level error. 201 * 202 * @param error error state 203 * @return this builder 204 * @throws NullPointerException if {@code error} is null 205 */ 206 @NonNull 207 public Builder error(@NonNull Boolean error) { 208 this.error = requireNonNull(error); 209 return this; 210 } 211 212 /** @return immutable tool output */ 213 @NonNull 214 public McpToolOutput build() { 215 return new McpToolOutput(this); 216 } 217 } 218}