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.Immutable;
023import javax.annotation.concurrent.NotThreadSafe;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.Objects;
027import java.util.Optional;
028
029import static java.util.List.copyOf;
030import static java.util.Objects.requireNonNull;
031
032/**
033 * Immutable tool result used by v1 MCP tool handlers.
034 *
035 * @author <a href="https://www.revetkn.com">Mark Allen</a>
036 */
037@Immutable
038public final class McpToolResult {
039        @NonNull
040        private final List<@NonNull McpTextContent> content;
041        @Nullable
042        private final Object structuredContent;
043        @NonNull
044        private final Boolean error;
045
046        private McpToolResult(@NonNull List<@NonNull McpTextContent> content,
047                                                                                                @Nullable Object structuredContent,
048                                                                                                @NonNull Boolean error) {
049                requireNonNull(content);
050                requireNonNull(error);
051
052                this.content = copyOf(content);
053                this.structuredContent = structuredContent;
054                this.error = error;
055        }
056
057        /**
058         * Provides the text content blocks returned by the tool.
059         *
060         * @return the tool content blocks
061         */
062        @NonNull
063        public List<@NonNull McpTextContent> getContent() {
064                return this.content;
065        }
066
067        /**
068         * Provides optional app-owned structured content to be marshaled into the MCP response.
069         *
070         * @return the structured content, if present
071         */
072        @NonNull
073        public Optional<Object> getStructuredContent() {
074                return Optional.ofNullable(this.structuredContent);
075        }
076
077        /**
078         * Indicates whether the tool result represents an error result.
079         *
080         * @return {@code true} if the tool result is an error result
081         */
082        @NonNull
083        public Boolean isError() {
084                return this.error;
085        }
086
087        /**
088         * Creates a builder for {@link McpToolResult}.
089         *
090         * @return a new builder
091         */
092        @NonNull
093        public static Builder builder() {
094                return new Builder();
095        }
096
097        /**
098         * Creates an error result containing a single text message.
099         *
100         * @param message the error message
101         * @return an error tool result
102         */
103        @NonNull
104        public static McpToolResult fromErrorMessage(@NonNull String message) {
105                requireNonNull(message);
106                return builder()
107                                .isError(true)
108                                .content(McpTextContent.fromText(message))
109                                .build();
110        }
111
112        @Override
113        public boolean equals(@Nullable Object other) {
114                if (this == other)
115                        return true;
116
117                if (!(other instanceof McpToolResult mcpToolResult))
118                        return false;
119
120                return getContent().equals(mcpToolResult.getContent())
121                                && Objects.equals(this.structuredContent, mcpToolResult.structuredContent)
122                                && isError().equals(mcpToolResult.isError());
123        }
124
125        @Override
126        public int hashCode() {
127                return Objects.hash(getContent(), this.structuredContent, isError());
128        }
129
130        @Override
131        public String toString() {
132                return "McpToolResult{content=%s, structuredContent=%s, error=%s}".formatted(getContent(), this.structuredContent, isError());
133        }
134
135        /**
136         * Builder used to construct {@link McpToolResult} instances.
137         */
138        @NotThreadSafe
139        public static final class Builder {
140                @NonNull
141                private final List<@NonNull McpTextContent> content;
142                @Nullable
143                private Object structuredContent;
144                @NonNull
145                private Boolean error;
146
147                private Builder() {
148                        this.content = new ArrayList<>();
149                        this.structuredContent = null;
150                        this.error = false;
151                }
152
153                /**
154                 * Appends a content block to the result.
155                 *
156                 * @param content the content block to append
157                 * @return this builder
158                 */
159                @NonNull
160                public Builder content(@NonNull McpTextContent content) {
161                        requireNonNull(content);
162                        this.content.add(content);
163                        return this;
164                }
165
166                /**
167                 * Appends multiple content blocks to the result in order.
168                 *
169                 * @param content the content blocks to append
170                 * @return this builder
171                 */
172                @NonNull
173                public Builder content(@NonNull List<@NonNull McpTextContent> content) {
174                        requireNonNull(content);
175                        this.content.addAll(content);
176                        return this;
177                }
178
179                /**
180                 * Sets the structured content value to be marshaled into the tool result.
181                 *
182                 * @param structuredContent the structured content value, possibly {@code null}
183                 * @return this builder
184                 */
185                @NonNull
186                public Builder structuredContent(@Nullable Object structuredContent) {
187                        this.structuredContent = structuredContent;
188                        return this;
189                }
190
191                /**
192                 * Sets whether the result should be flagged as an error result.
193                 *
194                 * @param error the error flag
195                 * @return this builder
196                 */
197                @NonNull
198                public Builder isError(@NonNull Boolean error) {
199                        requireNonNull(error);
200                        this.error = error;
201                        return this;
202                }
203
204                /**
205                 * Builds the immutable tool result.
206                 *
207                 * @return the built tool result
208                 */
209                @NonNull
210                public McpToolResult build() {
211                        return new McpToolResult(this.content, this.structuredContent, this.error);
212                }
213        }
214}