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}