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}