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 com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata; 029import static java.util.Objects.requireNonNull; 030 031/** 032 * Immutable ordered argument suggestions returned by an MCP completer. 033 * 034 * <p>Values remain exact application-provided strings. Soklet does not sort, 035 * normalize, deduplicate, translate, or perform prefix matching on them. 036 * Unpaired UTF-16 surrogates are rejected at construction. The independent 037 * JSON string/token and response-size limits described by {@link McpJsonString} 038 * remain serialization checks. 039 * 040 * @author <a href="https://www.revetkn.com">Mark Allen</a> 041 */ 042@ThreadSafe 043public final class McpArgumentCompletionResult implements McpOperationResult { 044 private static final int MAX_VALUES = 100; 045 private static final long MAX_SAFE_TOTAL = 9_007_199_254_740_991L; 046 047 @NonNull 048 private final List<@NonNull String> values; 049 @Nullable 050 private final Long total; 051 @Nullable 052 private final Boolean hasMore; 053 @NonNull 054 private final McpJsonObject metadata; 055 056 /** 057 * Creates a result containing only ordered suggestions. 058 * 059 * @param values zero to 100 exact, non-null suggestion strings 060 * @return immutable result 061 * @throws IllegalArgumentException if there are more than 100 values or a value contains an unpaired surrogate 062 */ 063 @NonNull 064 public static McpArgumentCompletionResult fromValues( 065 @NonNull List<@NonNull String> values) { 066 return withValues(values).build(); 067 } 068 069 /** 070 * Begins a result with ordered suggestions and optional fields. 071 * 072 * @param values zero to 100 exact, non-null suggestion strings 073 * @return mutable builder 074 * @throws IllegalArgumentException if there are more than 100 values or a value contains an unpaired surrogate 075 */ 076 @NonNull 077 public static Builder withValues(@NonNull List<@NonNull String> values) { 078 return new Builder(values); 079 } 080 081 private McpArgumentCompletionResult(@NonNull Builder builder) { 082 this.values = builder.values; 083 this.total = builder.total; 084 this.hasMore = builder.hasMore; 085 this.metadata = builder.metadata; 086 } 087 088 /** @return immutable suggestions in application order */ 089 @NonNull 090 public List<@NonNull String> getValues() { 091 return this.values; 092 } 093 094 /** @return exact match count, or empty when unknown or omitted */ 095 @NonNull 096 public Optional<@NonNull Long> getTotal() { 097 return Optional.ofNullable(this.total); 098 } 099 100 /** @return whether additional values exist, or empty when omitted */ 101 @NonNull 102 public Optional<@NonNull Boolean> getHasMore() { 103 return Optional.ofNullable(this.hasMore); 104 } 105 106 /** @return result-level protocol extension metadata */ 107 @NonNull 108 public McpJsonObject getMetadata() { 109 return this.metadata; 110 } 111 112 /** @return whether values, optional fields, and metadata match */ 113 @Override 114 public boolean equals(@Nullable Object other) { 115 if (this == other) 116 return true; 117 if (!(other instanceof McpArgumentCompletionResult result)) 118 return false; 119 return this.values.equals(result.values) 120 && Objects.equals(this.total, result.total) 121 && Objects.equals(this.hasMore, result.hasMore) 122 && this.metadata.equals(result.metadata); 123 } 124 125 /** @return structural hash code */ 126 @Override 127 public int hashCode() { 128 return Objects.hash(this.values, this.total, this.hasMore, 129 this.metadata); 130 } 131 132 /** @return diagnostic rendering without suggestions or metadata */ 133 @Override 134 @NonNull 135 public String toString() { 136 return "McpArgumentCompletionResult{values=<redacted>, total=<redacted>, " 137 + "hasMore=<redacted>, metadata=<redacted>}"; 138 } 139 140 /** 141 * Mutable builder for an immutable argument-completion result. 142 * 143 * @author <a href="https://www.revetkn.com">Mark Allen</a> 144 */ 145 @NotThreadSafe 146 public static final class Builder { 147 @NonNull 148 private final List<@NonNull String> values; 149 @Nullable 150 private Long total; 151 @Nullable 152 private Boolean hasMore; 153 @NonNull 154 private McpJsonObject metadata = McpJsonObject.emptyInstance(); 155 156 private Builder(@NonNull List<@NonNull String> values) { 157 this.values = List.copyOf(requireNonNull(values)); 158 if (this.values.size() > MAX_VALUES) 159 throw new IllegalArgumentException( 160 "MCP completion results must not exceed 100 values."); 161 this.values.forEach(McpContentValueSupport::requireWellFormedString); 162 } 163 164 /** 165 * Sets the exact count of matching values. 166 * 167 * @param total count in the JavaScript-safe integer range 168 * @return this builder 169 */ 170 @NonNull 171 public Builder total(@NonNull Long total) { 172 this.total = requireNonNull(total); 173 return this; 174 } 175 176 /** 177 * Sets whether additional values exist. 178 * 179 * @param hasMore true when results were truncated 180 * @return this builder 181 */ 182 @NonNull 183 public Builder hasMore(@NonNull Boolean hasMore) { 184 this.hasMore = requireNonNull(hasMore); 185 return this; 186 } 187 188 /** 189 * Sets result-level protocol extension metadata. 190 * 191 * @param metadata application metadata without MCP-reserved prefixes 192 * @return this builder 193 */ 194 @NonNull 195 public Builder metadata(@NonNull McpJsonObject metadata) { 196 this.metadata = requireApplicationMetadata(metadata); 197 return this; 198 } 199 200 /** 201 * Validates cross-field consistency and builds an immutable result. 202 * 203 * @return immutable result 204 */ 205 @NonNull 206 public McpArgumentCompletionResult build() { 207 if (this.total != null) { 208 if (this.total < this.values.size() 209 || this.total > MAX_SAFE_TOTAL) 210 throw new IllegalArgumentException( 211 "MCP completion total is out of range."); 212 if (this.hasMore != null 213 && this.hasMore != (this.total > this.values.size())) 214 throw new IllegalArgumentException( 215 "MCP completion total and hasMore disagree."); 216 } 217 return new McpArgumentCompletionResult(this); 218 } 219 } 220}