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 * https://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 */ 016package com.soklet; 017 018import com.soklet.internal.mcp.skills.McpSkillRuntimeBridge; 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.net.URI; 025import java.util.List; 026import java.util.Locale; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.Set; 030 031import static java.util.Objects.requireNonNull; 032 033/** 034 * Immutable association between a Skills snapshot and its root resource URI. 035 * 036 * <p>Construction validates the generated manifest but does not publish endpoint 037 * resources. The URI identifies the root {@code SKILL.md}, not a directory. 038 * Locale and cache settings do not grant authorization to read the bundle. 039 * 040 * @author <a href="https://www.revetkn.com">Mark Allen</a> 041 */ 042@ThreadSafe 043public final class McpSkillRegistration { 044 @NonNull private final URI uri; 045 @NonNull private final McpSkillBundle skillBundle; 046 @NonNull private final Set<@NonNull McpProtocolVersion> protocolVersions; 047 @Nullable private final Locale locale; 048 @NonNull private final McpCachePolicy cachePolicy; 049 @NonNull private final List<@NonNull Resource> resources; 050 private final McpSkillRuntimeBridge.@NonNull Registration runtimeRegistration; 051 052 private McpSkillRegistration(@NonNull Builder builder) { 053 this.uri = builder.uri; 054 this.skillBundle = builder.skillBundle; 055 this.protocolVersions = builder.protocolVersions; 056 this.locale = builder.locale; 057 this.cachePolicy = builder.cachePolicy; 058 this.runtimeRegistration = McpSkillRuntimeBridge.register(this.uri, 059 this.skillBundle.runtimeBundle(), this.cachePolicy); 060 this.resources = this.runtimeRegistration.resources().stream().map(Resource::new).toList(); 061 } 062 063 /** 064 * Starts registration of a bundle at its root document URI. 065 * 066 * @param uri absolute normalized hierarchical ASCII URI ending in literal 067 * {@code /SKILL.md}, without query, fragment, dot segments, or encoded separators 068 * @param skillBundle validated immutable snapshot whose name matches the URI's 069 * final skill-directory segment 070 * @param protocolVersions nonempty exact revisions serving this Skill 071 * @return mutable builder with no locale and private zero-TTL cache policy 072 * @throws NullPointerException if either argument is null 073 */ 074 @NonNull 075 public static Builder withUriAndSkillBundle(@NonNull URI uri, 076 @NonNull McpSkillBundle skillBundle, 077 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 078 return new Builder(uri, skillBundle, 079 McpProtocolVersion.requiredSet(protocolVersions)); 080 } 081 082 /** @return original root {@code SKILL.md} URI */ 083 @NonNull 084 public URI getUri() { return this.uri; } 085 086 /** @return the supplied immutable bundle */ 087 @NonNull 088 public McpSkillBundle getSkillBundle() { return this.skillBundle; } 089 090 /** @return exact protocol revisions on which this Skill is available */ 091 @NonNull 092 public Set<@NonNull McpProtocolVersion> getProtocolVersions() { 093 return this.protocolVersions; 094 } 095 096 /** @return configured locale, or empty when omitted */ 097 @NonNull 098 public Optional<@NonNull Locale> getLocale() { return Optional.ofNullable(this.locale); } 099 100 /** @return configured cache policy, private zero-TTL when omitted */ 101 @NonNull 102 public McpCachePolicy getCachePolicy() { return this.cachePolicy; } 103 104 /** 105 * Returns framework-derived file manifest entries without copying file bytes. 106 * 107 * @return cached immutable list in canonical bundle path order, root document first 108 */ 109 @NonNull 110 public List<McpSkillRegistration.@NonNull Resource> getResources() { return this.resources; } 111 112 McpSkillRuntimeBridge.@NonNull Registration runtimeRegistration() { return this.runtimeRegistration; } 113 114 /** @return whether URI identity, exact bundle content, locale, and cache policy match */ 115 @Override 116 public boolean equals(@Nullable Object other) { 117 return this == other || other instanceof McpSkillRegistration registration 118 && this.uri.equals(registration.uri) && this.skillBundle.equals(registration.skillBundle) 119 && this.protocolVersions.equals(registration.protocolVersions) 120 && Objects.equals(this.locale, registration.locale) && this.cachePolicy.equals(registration.cachePolicy); 121 } 122 123 /** @return structural registration hash code */ 124 @Override 125 public int hashCode() { return Objects.hash(this.uri, this.skillBundle, 126 this.protocolVersions, this.locale, this.cachePolicy); } 127 128 /** @return diagnostic rendering without registration or bundle content */ 129 @Override 130 @NonNull 131 public String toString() { return "McpSkillRegistration[redacted]"; } 132 133 /** 134 * Immutable, framework-generated resource identity and content fingerprint. 135 * 136 * @author <a href="https://www.revetkn.com">Mark Allen</a> 137 */ 138 @ThreadSafe 139 public static final class Resource { 140 @NonNull private final URI uri; 141 @NonNull private final String digest; 142 @NonNull private final Long sizeInBytes; 143 144 private Resource(McpSkillRuntimeBridge.@NonNull Resource resource) { 145 this.uri = resource.uri(); 146 this.digest = resource.digest(); 147 this.sizeInBytes = resource.sizeInBytes(); 148 } 149 150 /** @return canonical absolute file URI */ 151 @NonNull 152 public URI getUri() { return this.uri; } 153 154 /** @return {@code sha256:} followed by lowercase hexadecimal SHA-256 of served bytes */ 155 @NonNull 156 public String getDigest() { return this.digest; } 157 158 /** @return exact raw file size in bytes */ 159 @NonNull 160 public Long getSizeInBytes() { return this.sizeInBytes; } 161 162 /** @return whether URI identity, digest, and byte size match */ 163 @Override 164 public boolean equals(@Nullable Object other) { 165 return this == other || other instanceof Resource resource 166 && this.uri.equals(resource.uri) && this.digest.equals(resource.digest) 167 && this.sizeInBytes.equals(resource.sizeInBytes); 168 } 169 170 /** @return structural resource hash code */ 171 @Override 172 public int hashCode() { return Objects.hash(this.uri, this.digest, this.sizeInBytes); } 173 174 /** @return diagnostic rendering without URI, digest, or byte size */ 175 @Override 176 @NonNull 177 public String toString() { return "McpSkillRegistration.Resource[redacted]"; } 178 } 179 180 /** 181 * Mutable builder for an immutable Skills registration. 182 * 183 * @author <a href="https://www.revetkn.com">Mark Allen</a> 184 */ 185 @NotThreadSafe 186 public static final class Builder { 187 @NonNull private final URI uri; 188 @NonNull private final McpSkillBundle skillBundle; 189 @NonNull private final Set<@NonNull McpProtocolVersion> protocolVersions; 190 @Nullable private Locale locale; 191 @NonNull private McpCachePolicy cachePolicy = McpCachePolicy.privateNoCacheInstance(); 192 193 private Builder(@NonNull URI uri, @NonNull McpSkillBundle skillBundle, 194 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 195 this.uri = requireNonNull(uri, "A Skills root URI is required."); 196 this.skillBundle = requireNonNull(skillBundle, "A Skills bundle is required."); 197 this.protocolVersions = requireNonNull(protocolVersions); 198 } 199 200 /** 201 * Selects the locale associated with this registration. 202 * 203 * @param locale non-null registration locale 204 * @return this builder 205 * @throws NullPointerException if the locale is null 206 */ 207 @NonNull 208 public Builder locale(@NonNull Locale locale) { 209 this.locale = requireNonNull(locale, "A Skills locale is required."); 210 return this; 211 } 212 213 /** 214 * Replaces the registration's cache policy. 215 * 216 * @param cachePolicy non-null policy; public scope is appropriate only for 217 * content that does not vary by private identity or authorization 218 * @return this builder 219 * @throws NullPointerException if the policy is null 220 */ 221 @NonNull 222 public Builder cachePolicy(@NonNull McpCachePolicy cachePolicy) { 223 this.cachePolicy = requireNonNull(cachePolicy, "A Skills cache policy is required."); 224 return this; 225 } 226 227 /** 228 * Validates URI/name agreement and bounded manifest/resource representations. 229 * 230 * <p>Preflight uses representative response wrappers. It cannot account for 231 * arbitrary request IDs, server metadata, or multi-entry endpoint pages. 232 * Endpoint-wide checks and final request serialization remain separate; 233 * construction is not a guarantee that every later response envelope fits. 234 * 235 * @return immutable registration snapshot 236 * @throws IllegalArgumentException if the URI or generated response representation is invalid 237 */ 238 @NonNull 239 public McpSkillRegistration build() { return new McpSkillRegistration(this); } 240 } 241}