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.net.URI; 025import java.util.Collections; 026import java.util.EnumSet; 027import java.util.LinkedHashSet; 028import java.util.Objects; 029import java.util.Optional; 030import java.util.Set; 031 032import static java.util.Objects.requireNonNull; 033 034/** 035 * Immutable MCP Apps tool association and audience metadata. 036 * 037 * <p>An app-visible helper need not declare a resource URI. Neither visibility 038 * nor the resource association is an authorization grant or proof that a call 039 * originated from an authorized UI; applications authorize every invocation. 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043@ThreadSafe 044public final class McpAppToolMetadata { 045 046 @Nullable 047 private final URI resourceUri; 048 @NonNull 049 private final Set<@NonNull McpProtocolVersion> protocolVersions; 050 @NonNull 051 private final Set<@NonNull Visibility> visibility; 052 053 /** 054 * Starts metadata for exact protocol revisions of its owning tool. 055 * 056 * @param protocolVersions nonempty revision set 057 * @return mutable builder with both audiences and no resource association 058 */ 059 @NonNull 060 public static Builder withProtocolVersions( 061 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 062 return new Builder(McpProtocolVersion.requiredSet(protocolVersions)); 063 } 064 065 private McpAppToolMetadata(@NonNull Builder builder) { 066 this.resourceUri = builder.resourceUri; 067 this.protocolVersions = builder.protocolVersions; 068 this.visibility = builder.visibility; 069 } 070 071 /** @return concrete normalized ASCII {@code ui://} resource URI, if supplied */ 072 @NonNull 073 public Optional<@NonNull URI> getResourceUri() { 074 return Optional.ofNullable(this.resourceUri); 075 } 076 077 /** @return exact revisions on which the Apps metadata is available */ 078 @NonNull 079 public Set<@NonNull McpProtocolVersion> getProtocolVersions() { 080 return this.protocolVersions; 081 } 082 083 /** @return immutable audiences in enum declaration order; explicit empty stays empty */ 084 @NonNull 085 public Set<@NonNull Visibility> getVisibility() { 086 return this.visibility; 087 } 088 089 /** @return whether the resource association and effective audiences match */ 090 @Override 091 public boolean equals(@Nullable Object other) { 092 if (this == other) 093 return true; 094 if (!(other instanceof McpAppToolMetadata metadata)) 095 return false; 096 return Objects.equals(this.resourceUri, metadata.resourceUri) 097 && this.protocolVersions.equals(metadata.protocolVersions) 098 && this.visibility.equals(metadata.visibility); 099 } 100 101 /** @return structural hash code */ 102 @Override 103 public int hashCode() { 104 return Objects.hash(this.resourceUri, this.protocolVersions, this.visibility); 105 } 106 107 /** @return diagnostic rendering without application configuration */ 108 @Override 109 @NonNull 110 public String toString() { 111 return "McpAppToolMetadata{resourceUri=<redacted>, visibility=<redacted>}"; 112 } 113 114 /** 115 * Closed Apps audience domain for Soklet 4.0. A new audience requires an 116 * explicit domain/API-evolution amendment. 117 * 118 * <p> 119 * This is a closed domain; exhaustive switches are supported, and any new alternative requires an explicit reviewed contract amendment. 120 * 121 * @author <a href="https://www.revetkn.com">Mark Allen</a> 122 */ 123 public enum Visibility { 124 /** Tool is visible to the model. */ 125 MODEL, 126 /** Tool is visible to Apps hosts. */ 127 APP 128 } 129 130 /** 131 * Mutable builder for immutable Apps tool metadata. 132 * 133 * @author <a href="https://www.revetkn.com">Mark Allen</a> 134 */ 135 @NotThreadSafe 136 public static final class Builder { 137 @Nullable 138 private URI resourceUri; 139 @NonNull 140 private final Set<@NonNull McpProtocolVersion> protocolVersions; 141 @NonNull 142 private Set<@NonNull Visibility> visibility = Collections.unmodifiableSet( 143 new LinkedHashSet<>(EnumSet.allOf(Visibility.class))); 144 145 private Builder(@NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 146 this.protocolVersions = requireNonNull(protocolVersions); 147 } 148 149 /** 150 * Associates a concrete UI resource without fetching or dereferencing it. 151 * 152 * @param resourceUri normalized absolute ASCII {@code ui://} URI 153 * @return this builder 154 */ 155 @NonNull 156 public Builder resourceUri(@NonNull URI resourceUri) { 157 this.resourceUri = McpAppMetadataValidation.requireResourceUri(resourceUri); 158 return this; 159 } 160 161 /** 162 * Replaces the effective audiences. Empty means neither audience. 163 * 164 * @param visibility non-null audiences, defensively copied in declaration order 165 * @return this builder 166 */ 167 @NonNull 168 public Builder visibility(@NonNull Set<@NonNull Visibility> visibility) { 169 requireNonNull(visibility); 170 EnumSet<Visibility> snapshot = EnumSet.noneOf(Visibility.class); 171 for (Visibility value : visibility) 172 snapshot.add(requireNonNull(value)); 173 this.visibility = Collections.unmodifiableSet(new LinkedHashSet<>(snapshot)); 174 return this; 175 } 176 177 /** @return immutable metadata snapshot */ 178 @NonNull 179 public McpAppToolMetadata build() { 180 return new McpAppToolMetadata(this); 181 } 182 } 183}