001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.mcp;
018
019import java.util.*;
020
021import org.apache.juneau.bean.jsonrpc.*;
022
023/**
024 * Revision-neutral, as-returned-by-the-application result of an {@link McpCompleter} invocation.
025 *
026 * <p>
027 * Supersedes the wire-level {@code CompleteResult}/{@code Completion} beans. An instance of this class
028 * is the completer's raw, unvalidated output; call {@link #normalize(McpCompletionResult)} exactly once
029 * per completion dispatch to obtain the {@link Normalized} shape both dated adapters map to their wire
030 * result.
031 *
032 * <h5 class='section'>Normalization contract:</h5>
033 * <p>
034 * Applied by {@link #normalize(McpCompletionResult)} before wire mapping:
035 * <ul>
036 *    <li><jk>null</jk> {@link #getValues() values} normalizes to an empty list;
037 *    <li>order is preserved (application-owned ranking) and duplicates are preserved;
038 *    <li>a <jk>null</jk> element within {@code values} is an <b>internal handler failure</b>;
039 *    <li>the first {@value #MAX_VALUES} values are emitted; truncation forces {@code hasMore=true};
040 *    <li>a supplied non-negative {@link #getTotal() total} is preserved; a negative {@code total} is an
041 *       <b>internal handler failure</b>;
042 *    <li>without truncation, a nullable {@link #getHasMore() hasMore} is passed through unchanged; and
043 *    <li>a completer returning <jk>null</jk> itself (rather than an {@code McpCompletionResult} with
044 *       <jk>null</jk> fields) is also an <b>internal handler failure</b>.
045 * </ul>
046 *
047 * <h5 class='section'>Internal handler failure representation:</h5>
048 * <p>
049 * Each of the three "internal handler failure" cases above is surfaced identically: as an
050 * {@link McpException} carrying {@link #CODE_INTERNAL_ERROR} ({@code -32603}), the JSON-RPC 2.0
051 * standard "Internal error" code. That code is fixed by the JSON-RPC specification, not chosen per
052 * revision, which is why it is a local constant here rather than routed through
053 * {@code McpRevision#errorCode(McpErrorKind)} - the same reasoning {@code McpParamUtils} documents for
054 * its {@code -32602} constant. Both dated adapters' existing top-level {@code catch (McpException e)}
055 * dispatch handling (see {@code McpRevision#dispatch}) already converts any {@link McpException} thrown
056 * during {@code completion/complete} handling straight into a JSON-RPC error response with this code, so
057 * adapters do not need any additional mapping step beyond calling
058 * {@link #normalize(McpCompletionResult)} and letting a thrown {@link McpException} propagate.
059 */
060public class McpCompletionResult {
061
062   /**
063    * The maximum number of completion values a {@link Normalized} result ever carries. Juneau performs
064    * no ranking, filtering, deduplication, or pagination of its own; this is strictly a truncation cap.
065    */
066   public static final int MAX_VALUES = 100;
067
068   /**
069    * JSON-RPC error code raised by {@link #normalize(McpCompletionResult)} for any internal handler
070    * failure. See the class Javadoc "Internal handler failure representation" section.
071    */
072   public static final int CODE_INTERNAL_ERROR = -32603;
073
074   private List<String> values;
075   private Integer total;
076   private Boolean hasMore;
077
078   /**
079    * Creates the neutral empty-completion result, the shape both dated adapters map to
080    * {@code {"completion":{"values":[]}}}.
081    *
082    * @return A new result with an empty {@link #getValues() values} list and no {@link #getTotal()
083    *    total}/{@link #getHasMore() hasMore}. Never <jk>null</jk>.
084    */
085   public static McpCompletionResult empty() {
086      return new McpCompletionResult().setValues(List.of());
087   }
088
089   /**
090    * The completion suggestions, in application-ranked order.
091    *
092    * @return The values, or <jk>null</jk> if not set. Normalized to an empty list by
093    *    {@link #normalize(McpCompletionResult)}.
094    */
095   public List<String> getValues() {
096      return values;
097   }
098
099   /**
100    * Sets the completion suggestions.
101    *
102    * @param value The new value. Can be <jk>null</jk> to unset the property.
103    * @return This object.
104    */
105   public McpCompletionResult setValues(List<String> value) {
106      values = value;
107      return this;
108   }
109
110   /**
111    * The total number of completions available, if known, independent of how many values are emitted.
112    *
113    * @return The total, or <jk>null</jk> if not set/unknown.
114    */
115   public Integer getTotal() {
116      return total;
117   }
118
119   /**
120    * Sets the total number of completions available.
121    *
122    * @param value The new value. Must be non-negative if non-<jk>null</jk>; see
123    *    {@link #normalize(McpCompletionResult)}. Can be <jk>null</jk> to unset the property.
124    * @return This object.
125    */
126   public McpCompletionResult setTotal(Integer value) {
127      total = value;
128      return this;
129   }
130
131   /**
132    * Whether more completions exist beyond {@link #getValues() values}.
133    *
134    * @return The flag, or <jk>null</jk> if not set/unknown.
135    */
136   public Boolean getHasMore() {
137      return hasMore;
138   }
139
140   /**
141    * Sets whether more completions exist beyond {@link #getValues() values}.
142    *
143    * @param value The new value. Can be <jk>null</jk> to unset the property.
144    * @return This object.
145    */
146   public McpCompletionResult setHasMore(Boolean value) {
147      hasMore = value;
148      return this;
149   }
150
151   /**
152    * Applies the normalization contract documented on this class to a completer's raw return value.
153    *
154    * @param result The raw result returned by an {@link McpCompleter}. Can be <jk>null</jk>.
155    * @return The normalized shape ready for wire mapping. Never <jk>null</jk>.
156    * @throws McpException With {@link #CODE_INTERNAL_ERROR} if {@code result} is <jk>null</jk>, if
157    *    {@code result}'s values contain a <jk>null</jk> element, or if {@code result}'s total is negative.
158    */
159   public static Normalized normalize(McpCompletionResult result) {
160      if (result == null)
161         throw new McpException(CODE_INTERNAL_ERROR, "Completer returned a null McpCompletionResult.");
162
163      var raw = result.getValues();
164      var source = raw == null ? List.<String>of() : raw;
165      for (var v : source)
166         if (v == null)
167            throw new McpException(CODE_INTERNAL_ERROR, "Completer result values must not contain a null element.");
168
169      var total = result.getTotal();
170      if (total != null && total < 0)
171         throw new McpException(CODE_INTERNAL_ERROR, "Completer result total must not be negative: " + total);
172
173      var hasMore = result.getHasMore();
174      List<String> values;
175      if (source.size() > MAX_VALUES) {
176         values = List.copyOf(source.subList(0, MAX_VALUES));
177         hasMore = Boolean.TRUE;
178      } else {
179         values = List.copyOf(source);
180      }
181      return new Normalized(values, total, hasMore);
182   }
183
184   /**
185    * The normalized shape of a completion result, ready for wire mapping.
186    *
187    * @param values The (possibly truncated) completion values, in order, with duplicates preserved.
188    *    Immutable. Never <jk>null</jk>; empty when the completer supplied none.
189    * @param total The completer-supplied total, or <jk>null</jk> if unknown/unset.
190    * @param hasMore Whether more completions exist. <jk>true</jk> when truncation occurred; otherwise
191    *    the completer-supplied value, which may be <jk>null</jk>.
192    */
193   public record Normalized(List<String> values, Integer total, Boolean hasMore) {}
194}