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}