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.Objects; 025import java.util.Optional; 026 027import static java.util.Objects.requireNonNull; 028 029/** 030 * Immutable progress update for one active MCP invocation. 031 * 032 * <p>Progress and total values are finite JSON numbers. Soklet associates the 033 * update with the initiating request's opaque progress token; applications do 034 * not copy or manage that token themselves. 035 * 036 * <p>String factories and setters reject unpaired UTF-16 surrogates with 037 * {@link IllegalArgumentException}; well-formed Unicode is preserved exactly. 038 * 039 * @author <a href="https://www.revetkn.com">Mark Allen</a> 040 */ 041@ThreadSafe 042public final class McpProgressUpdate { 043 private final double progress; 044 private final @Nullable Double total; 045 private final @Nullable String message; 046 047 /** 048 * Vends a builder primed with the current progress value. 049 * 050 * @param progress finite progress value 051 * @return progress-update builder 052 * @throws NullPointerException if {@code progress} is null 053 * @throws IllegalArgumentException if {@code progress} is not finite 054 */ 055 @NonNull 056 public static Builder withProgress(@NonNull Double progress) { 057 return new Builder(requireNonNull(progress)); 058 } 059 060 private McpProgressUpdate(@NonNull Builder builder) { 061 this.progress = requireFinite(builder.progress, "Progress"); 062 this.total = builder.total == null ? null 063 : requireFinite(builder.total, "Progress total"); 064 this.message = builder.message; 065 } 066 067 /** @return finite progress value */ 068 @NonNull 069 public Double getProgress() { 070 return this.progress; 071 } 072 073 /** @return finite total value, if supplied */ 074 @NonNull 075 public Optional<@NonNull Double> getTotal() { 076 return Optional.ofNullable(this.total); 077 } 078 079 /** @return human-readable progress message, if supplied */ 080 @NonNull 081 public Optional<@NonNull String> getMessage() { 082 return Optional.ofNullable(this.message); 083 } 084 085 /** @return whether every progress property is structurally equal */ 086 @Override 087 public boolean equals(@Nullable Object other) { 088 if (this == other) 089 return true; 090 if (!(other instanceof McpProgressUpdate update)) 091 return false; 092 return Double.compare(this.progress, update.progress) == 0 093 && Objects.equals(this.total, update.total) 094 && Objects.equals(this.message, update.message); 095 } 096 097 /** @return structural progress-update hash code */ 098 @Override 099 public int hashCode() { 100 return Objects.hash(this.progress, this.total, this.message); 101 } 102 103 private static double requireFinite(double value, @NonNull String description) { 104 if (!Double.isFinite(value)) 105 throw new IllegalArgumentException(description + " must be finite."); 106 // JSON has one zero value. Normalize the Java-only negative-zero spelling. 107 return value == 0.0d ? 0.0d : value; 108 } 109 110 /** 111 * Mutable builder for immutable {@link McpProgressUpdate} values. 112 * 113 * @author <a href="https://www.revetkn.com">Mark Allen</a> 114 */ 115 @NotThreadSafe 116 public static final class Builder { 117 private final double progress; 118 private @Nullable Double total; 119 private @Nullable String message; 120 121 private Builder(double progress) { 122 this.progress = requireFinite(progress, "Progress"); 123 } 124 125 /** 126 * Supplies the total amount of work, when known. 127 * 128 * @param total finite total value 129 * @return this builder 130 * @throws NullPointerException if {@code total} is null 131 * @throws IllegalArgumentException if {@code total} is not finite 132 */ 133 @NonNull 134 public Builder total(@NonNull Double total) { 135 this.total = requireFinite(requireNonNull(total), "Progress total"); 136 return this; 137 } 138 139 /** 140 * Supplies an optional human-readable progress message. 141 * 142 * @param message progress message 143 * @return this builder 144 * @throws NullPointerException if {@code message} is null 145 * @throws IllegalArgumentException if {@code message} contains unpaired UTF-16 surrogates 146 */ 147 @NonNull 148 public Builder message(@NonNull String message) { 149 this.message = McpContentValueSupport.requireWellFormedString(message); 150 return this; 151 } 152 153 /** @return immutable progress update */ 154 @NonNull 155 public McpProgressUpdate build() { 156 return new McpProgressUpdate(this); 157 } 158 } 159}