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.Objects;
026import java.util.Optional;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * Immutable MCP implementation metadata.
032 * <p>
033 * Servers use this value to identify themselves to MCP clients. Client
034 * implementation information received from the wire is informational and is
035 * not an authenticated identity.
036 *
037 * <p>String factories and setters reject unpaired UTF-16 surrogates with
038 * {@link IllegalArgumentException}; well-formed Unicode is preserved exactly.
039 *
040 * @author <a href="https://www.revetkn.com">Mark Allen</a>
041 */
042@ThreadSafe
043public final class McpImplementation {
044        @NonNull
045        private final String name;
046        @NonNull
047        private final String version;
048        @Nullable
049        private final String title;
050        @Nullable
051        private final String description;
052        @Nullable
053        private final URI websiteUrl;
054
055        /**
056         * Vends a builder primed with the required implementation name and version.
057         * Client-provided strings are preserved, including blank values. Endpoints
058         * require a nonblank name and version for configured server information.
059         *
060         * @param name    the implementation name
061         * @param version the implementation version
062         * @return a builder for implementation metadata
063         * @throws NullPointerException if {@code name} or {@code version} is null
064         */
065        @NonNull
066        public static Builder withNameAndVersion(@NonNull String name,
067                                                                                                                                        @NonNull String version) {
068                return new Builder(name, version);
069        }
070
071        private McpImplementation(@NonNull Builder builder) {
072                requireNonNull(builder);
073                this.name = builder.name;
074                this.version = builder.version;
075                this.title = blankToNull(builder.title);
076                this.description = blankToNull(builder.description);
077                this.websiteUrl = builder.websiteUrl;
078        }
079
080        /**
081         * The implementation name.
082         *
083         * @return the implementation name, which may be blank for client information
084         */
085        @NonNull
086        public String getName() {
087                return this.name;
088        }
089
090        /**
091         * The implementation version.
092         *
093         * @return the implementation version, which may be blank for client information
094         */
095        @NonNull
096        public String getVersion() {
097                return this.version;
098        }
099
100        /**
101         * The optional human-readable implementation title.
102         *
103         * @return the title, or the empty optional if none was configured
104         */
105        @NonNull
106        public Optional<@NonNull String> getTitle() {
107                return Optional.ofNullable(this.title);
108        }
109
110        /**
111         * The optional human-readable implementation description.
112         *
113         * @return the description, or the empty optional if none was configured
114         */
115        @NonNull
116        public Optional<@NonNull String> getDescription() {
117                return Optional.ofNullable(this.description);
118        }
119
120        /**
121         * The optional absolute website URI for this implementation.
122         *
123         * @return the website URI, or the empty optional if none was configured
124         */
125        @NonNull
126        public Optional<@NonNull URI> getWebsiteUrl() {
127                return Optional.ofNullable(this.websiteUrl);
128        }
129
130        /** @return whether every implementation property is structurally equal */
131        @Override
132        public boolean equals(@Nullable Object other) {
133                if (this == other)
134                        return true;
135                if (!(other instanceof McpImplementation implementation))
136                        return false;
137                return this.name.equals(implementation.name)
138                                && this.version.equals(implementation.version)
139                                && Objects.equals(this.title, implementation.title)
140                                && Objects.equals(this.description, implementation.description)
141                                && Objects.equals(this.websiteUrl, implementation.websiteUrl);
142        }
143
144        /** @return structural implementation hash code */
145        @Override
146        public int hashCode() {
147                return Objects.hash(this.name, this.version, this.title, this.description,
148                                this.websiteUrl);
149        }
150
151        @Nullable
152        private static String blankToNull(@Nullable String value) {
153                return value == null || value.isBlank() ? null : value;
154        }
155
156        /**
157         * Builder for immutable {@link McpImplementation} values.
158         * <p>
159         * This class is intended for use by a single thread.
160         *
161         * @author <a href="https://www.revetkn.com">Mark Allen</a>
162         */
163        @NotThreadSafe
164        public static final class Builder {
165                @NonNull
166                private final String name;
167                @NonNull
168                private final String version;
169                @Nullable
170                private String title;
171                @Nullable
172                private String description;
173                @Nullable
174                private URI websiteUrl;
175
176                private Builder(@NonNull String name, @NonNull String version) {
177                        this.name = McpContentValueSupport.requireWellFormedString(name);
178                        this.version = McpContentValueSupport.requireWellFormedString(version);
179                }
180
181                /**
182                 * Sets a human-readable title. A blank title is treated as absent.
183                 *
184                 * @param title the implementation title
185                 * @return this builder
186                 */
187                @NonNull
188                public Builder title(@NonNull String title) {
189                        this.title = McpContentValueSupport.requireWellFormedString(title);
190                        return this;
191                }
192
193                /**
194                 * Sets a human-readable description. A blank description is treated as
195                 * absent.
196                 *
197                 * @param description the implementation description
198                 * @return this builder
199                 */
200                @NonNull
201                public Builder description(@NonNull String description) {
202                        this.description = McpContentValueSupport.requireWellFormedString(description);
203                        return this;
204                }
205
206                /**
207                 * Sets the implementation website URI.
208                 *
209                 * @param websiteUrl the absolute website URI
210                 * @return this builder
211                 * @throws IllegalArgumentException if the URI is relative
212                 */
213                @NonNull
214                public Builder websiteUrl(@NonNull URI websiteUrl) {
215                        requireNonNull(websiteUrl);
216
217                        if (!websiteUrl.isAbsolute())
218                                throw new IllegalArgumentException(
219                                                "Implementation website URL must be an absolute URI.");
220
221                        this.websiteUrl = websiteUrl;
222                        return this;
223                }
224
225                /**
226                 * Builds immutable implementation metadata.
227                 *
228                 * @return the implementation metadata
229                 */
230                @NonNull
231                public McpImplementation build() {
232                        return new McpImplementation(this);
233                }
234        }
235}