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.time.Instant; 025import java.util.Collections; 026import java.util.LinkedHashSet; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.Set; 030 031import static java.util.Objects.requireNonNull; 032 033/** 034 * Immutable optional annotations for an MCP content block. 035 * 036 * <p>The annotation vocabulary follows the selected MCP revision and may gain 037 * fields in a future supported profile. Applications should ignore 038 * annotation data they do not use rather than treating today's fields as a 039 * permanent closed set. 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043@ThreadSafe 044public final class McpContentAnnotations { 045 @NonNull 046 private final Set<@NonNull McpRole> audience; 047 @Nullable 048 private final Double priority; 049 @Nullable 050 private final Instant lastModified; 051 052 /** @return an empty content-annotation builder */ 053 @NonNull 054 public static Builder builder() { 055 return new Builder(); 056 } 057 058 private McpContentAnnotations(@NonNull Builder builder) { 059 this.audience = Collections.unmodifiableSet( 060 new LinkedHashSet<>(builder.audience)); 061 this.priority = builder.priority; 062 this.lastModified = builder.lastModified; 063 } 064 065 /** @return immutable intended audience */ 066 @NonNull 067 public Set<@NonNull McpRole> getAudience() { 068 return this.audience; 069 } 070 071 /** @return priority in the inclusive range {@code 0.0..1.0}, if set */ 072 @NonNull 073 public Optional<@NonNull Double> getPriority() { 074 return Optional.ofNullable(this.priority); 075 } 076 077 /** @return last-modified instant, if set */ 078 @NonNull 079 public Optional<@NonNull Instant> getLastModified() { 080 return Optional.ofNullable(this.lastModified); 081 } 082 083 /** @return whether every annotation property is structurally equal */ 084 @Override 085 public boolean equals(@Nullable Object other) { 086 if (this == other) 087 return true; 088 if (!(other instanceof McpContentAnnotations annotations)) 089 return false; 090 return this.audience.equals(annotations.audience) 091 && Objects.equals(this.priority, annotations.priority) 092 && Objects.equals(this.lastModified, annotations.lastModified); 093 } 094 095 /** @return structural annotation hash code */ 096 @Override 097 public int hashCode() { 098 return Objects.hash(this.audience, this.priority, this.lastModified); 099 } 100 101 /** 102 * Mutable builder for immutable {@link McpContentAnnotations}. 103 * 104 * @author <a href="https://www.revetkn.com">Mark Allen</a> 105 */ 106 @NotThreadSafe 107 public static final class Builder { 108 @NonNull 109 private Set<@NonNull McpRole> audience = new LinkedHashSet<>(); 110 @Nullable 111 private Double priority; 112 @Nullable 113 private Instant lastModified; 114 115 private Builder() { 116 } 117 118 /** 119 * Replaces the intended audience. 120 * Passing no roles clears the intended audience. 121 * 122 * @param audience audience roles 123 * @return this builder 124 * @throws NullPointerException if the array or any role is null 125 */ 126 @NonNull 127 public Builder audience( 128 @NonNull McpRole @NonNull... audience) { 129 requireNonNull(audience); 130 Set<McpRole> copied = new LinkedHashSet<>(); 131 for (McpRole role : audience) 132 copied.add(requireNonNull(role)); 133 this.audience = copied; 134 return this; 135 } 136 137 /** 138 * Sets a content priority. 139 * 140 * @param priority priority in the inclusive range {@code 0.0..1.0} 141 * @return this builder 142 * @throws NullPointerException if {@code priority} is null 143 * @throws IllegalArgumentException if the value is non-finite or outside 144 * the permitted range 145 */ 146 @NonNull 147 public Builder priority(@NonNull Double priority) { 148 requireNonNull(priority); 149 if (!Double.isFinite(priority) || priority < 0.0 || priority > 1.0) 150 throw new IllegalArgumentException( 151 "Content priority must be finite and between 0.0 and 1.0."); 152 this.priority = priority; 153 return this; 154 } 155 156 /** 157 * Sets the last-modified time. 158 * 159 * @param lastModified last-modified time 160 * @return this builder 161 */ 162 @NonNull 163 public Builder lastModified(@NonNull Instant lastModified) { 164 this.lastModified = requireNonNull(lastModified); 165 return this; 166 } 167 168 /** @return immutable content annotations */ 169 @NonNull 170 public McpContentAnnotations build() { 171 return new McpContentAnnotations(this); 172 } 173 } 174}