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.List;
026import java.util.Optional;
027import java.util.Set;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Immutable programmatic registration for one MCP resource-read route.
033 *
034 * <p>Registration is staged so a handler must be supplied before optional
035 * descriptor metadata can be configured. Exact and template registrations
036 * intentionally expose different builders: only an exact resource may
037 * advertise a size. The registration cache policy owns the fixed scope and
038 * default time to live for reads; the default is
039 * {@link McpCachePolicy#privateNoCacheInstance()}.
040 *
041 * <p>Registration validates address structure and routing, not deployment
042 * safety. The application owns an allowlist based on how each URI is delivered
043 * or dereferenced. Direct-client addresses and handler-only custom addresses
044 * commonly require different scheme, authority, credential, query, and
045 * fragment rules. A filesystem-backed handler additionally owns canonical
046 * root containment, symlink policy, authorization, and safe opening; Soklet
047 * does not map registered URIs to files.
048 *
049 * @author <a href="https://www.revetkn.com">Mark Allen</a>
050 */
051@ThreadSafe
052public final class McpResourceRegistration {
053        @NonNull
054        private final McpResourceAddressType addressType;
055        @Nullable
056        private final URI uri;
057        @Nullable
058        private final String uriTemplate;
059        @NonNull
060        private final String name;
061        @NonNull
062        private final Set<@NonNull McpProtocolVersion> protocolVersions;
063        @Nullable
064        private final String title;
065        @Nullable
066        private final String description;
067        @Nullable
068        private final String mimeType;
069        @NonNull
070        private final List<@NonNull McpIcon> icons;
071        @Nullable
072        private final McpContentAnnotations annotations;
073        @Nullable
074        private final Long sizeInBytes;
075        @NonNull
076        private final McpCachePolicy cachePolicy;
077        @NonNull
078        private final List<@NonNull McpInputRequestDeclaration> inputRequestDeclarations;
079        @NonNull
080        private final McpRequestStateMode requestStateMode;
081        @NonNull
082        private final McpJsonObject metadata;
083        @NonNull
084        private final McpResourceReadHandler handler;
085        @Nullable
086        private final McpCompletionHandler completionHandler;
087        @NonNull
088        private final Set<@NonNull McpProtocolVersion> completionProtocolVersions;
089
090        /**
091         * Begins a staged exact-resource registration.
092         *
093         * <p>Acceptance here does not mean that the URI is safe for direct client
094         * loading or for application-side dereference. Apply a deployment policy
095         * for the address's intended delivery path.
096         *
097         * <p>This method validates the URI's shape. Construction of the enclosing
098         * {@link McpServer} later applies the 1,048,576-byte ASCII wire limit and
099         * complete endpoint route validation.
100         * IPv6 zone identifiers are excluded from the RFC 3986 URI contract.
101         *
102         * @param uri absolute normalized resource URI in ASCII wire form
103         * @param name nonblank resource name
104         * @param protocolVersions nonempty exact revisions serving this resource
105         * @return required-handler stage
106         * @throws IllegalArgumentException if the URI is relative, not normalized,
107         * not in ASCII wire form, contains an IPv6 zone identifier, or the name is blank
108         */
109        @NonNull
110        public static ExactHandlerStage withUriAndName(@NonNull URI uri,
111                        @NonNull String name,
112                        @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) {
113                return new ExactHandlerStage(requireExactUri(uri), requireName(name),
114                                McpProtocolVersion.requiredSet(protocolVersions));
115        }
116
117        /**
118         * Begins a staged URI-template resource registration.
119         *
120         * <p>This method performs inexpensive local validation. Construction of the
121         * enclosing {@link McpServer} performs complete RFC 6570 Level 1 parsing and
122         * route validation, including the template-size, variable-name,
123         * variable-count, and overlap-work limits documented in the MCP guide. It
124         * also enforces the limit of 256 URI templates per endpoint at server
125         * construction. IPv6 zone identifiers are excluded from the RFC 3986 URI
126         * contract, including identifiers containing template variables.
127         *
128         * <p>Level 1 simple variables do not consume a raw slash. For example,
129         * {@code file:///{path}} matches {@code file:///src%2Fmain.rs} with variable
130         * value {@code src/main.rs}, but does not match {@code file:///src/main.rs}.
131         * Captured values are UTF-8 percent-decoded exactly once; {@code %252F}
132         * becomes the literal string {@code %2F}. Reserved expansion
133         * ({@code {+path}}), explode and prefix modifiers are unsupported.
134         *
135         * <p>Neither validation step authorizes expanded URIs or establishes that
136         * their scheme is safe for the application's delivery intent.
137         *
138         * @param uriTemplate nonblank URI template containing simple
139         * {@code {variable}} expressions
140         * @param name nonblank resource name
141         * @param protocolVersions nonempty exact revisions serving this resource
142         * @return required-handler stage
143         * @throws IllegalArgumentException if the template fails local structural
144         * validation or the name is blank
145         */
146        @NonNull
147        public static TemplateHandlerStage withUriTemplateAndName(
148                        @NonNull String uriTemplate, @NonNull String name,
149                        @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) {
150                return new TemplateHandlerStage(
151                                requireBasicUriTemplate(uriTemplate), requireName(name),
152                                McpProtocolVersion.requiredSet(protocolVersions));
153        }
154
155        private McpResourceRegistration(@NonNull BuilderState state) {
156                this.addressType = state.addressType;
157                this.uri = state.uri;
158                this.uriTemplate = state.uriTemplate;
159                this.name = state.name;
160                this.protocolVersions = state.protocolVersions;
161                this.title = state.title;
162                this.description = state.description;
163                this.mimeType = state.mimeType;
164                this.icons = List.copyOf(state.icons);
165                this.annotations = state.annotations;
166                this.sizeInBytes = state.sizeInBytes;
167                this.cachePolicy = state.cachePolicy;
168                this.inputRequestDeclarations =
169                                List.copyOf(state.inputRequestDeclarations);
170                this.requestStateMode = state.requestStateMode;
171                this.metadata = com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata(state.metadata);
172                this.handler = state.handler;
173                this.completionHandler = state.completionHandler;
174                this.completionProtocolVersions = state.completionProtocolVersions;
175        }
176
177        /** @return whether this registration uses an exact URI or URI template */
178        @NonNull
179        public McpResourceAddressType getAddressType() {
180                return this.addressType;
181        }
182
183        /** @return exact resource URI, or empty for a template registration */
184        @NonNull
185        public Optional<@NonNull URI> getUri() {
186                return Optional.ofNullable(this.uri);
187        }
188
189        /** @return resource URI template, or empty for an exact registration */
190        @NonNull
191        public Optional<@NonNull String> getUriTemplate() {
192                return Optional.ofNullable(this.uriTemplate);
193        }
194
195        /** @return nonblank resource name */
196        @NonNull
197        public String getName() {
198                return this.name;
199        }
200
201        /** @return exact protocol revisions on which this resource is available */
202        @NonNull
203        public Set<@NonNull McpProtocolVersion> getProtocolVersions() {
204                return this.protocolVersions;
205        }
206
207        /** @return human-readable title, if configured */
208        @NonNull
209        public Optional<@NonNull String> getTitle() {
210                return Optional.ofNullable(this.title);
211        }
212
213        /** @return human-readable description, if configured */
214        @NonNull
215        public Optional<@NonNull String> getDescription() {
216                return Optional.ofNullable(this.description);
217        }
218
219        /** @return resource MIME type, if configured */
220        @NonNull
221        public Optional<@NonNull String> getMimeType() {
222                return Optional.ofNullable(this.mimeType);
223        }
224
225        /** @return immutable icon list in insertion order */
226        @NonNull
227        public List<@NonNull McpIcon> getIcons() {
228                return this.icons;
229        }
230
231        /** @return content annotations, if configured */
232        @NonNull
233        public Optional<@NonNull McpContentAnnotations> getAnnotations() {
234                return Optional.ofNullable(this.annotations);
235        }
236
237        /**
238         * Returns the exact resource's advertised size.
239         *
240         * @return nonnegative byte count, or empty when omitted or templated
241         */
242        @NonNull
243        public Optional<@NonNull Long> getSizeInBytes() {
244                return Optional.ofNullable(this.sizeInBytes);
245        }
246
247        /** @return fixed scope and default time to live for resource reads */
248        @NonNull
249        public McpCachePolicy getCachePolicy() {
250                return this.cachePolicy;
251        }
252
253        /**
254         * Returns the input requests this resource-read operation may emit.
255         *
256         * @return immutable declarations in registration order
257         */
258        @NonNull
259        public List<@NonNull McpInputRequestDeclaration>
260                        getInputRequestDeclarations() {
261                return this.inputRequestDeclarations;
262        }
263
264        /**
265         * Returns the request-state contract for this resource-read operation.
266         *
267         * @return request-state mode
268         */
269        @NonNull
270        public McpRequestStateMode getRequestStateMode() {
271                return this.requestStateMode;
272        }
273
274        /** @return immutable protocol extension metadata */
275        @NonNull
276        public McpJsonObject getMetadata() {
277                return this.metadata;
278        }
279
280        /** @return resource-read handler */
281        @NonNull
282        public McpResourceReadHandler getHandler() {
283                return this.handler;
284        }
285
286        /**
287         * Returns the template's argument completer, when configured.
288         *
289         * @return configured completer, or empty for exact resources and templates
290         * without one
291         */
292        @NonNull
293        public Optional<@NonNull McpCompletionHandler> getCompletionHandler() {
294                return Optional.ofNullable(this.completionHandler);
295        }
296
297        /** @return exact revisions on which the completion handler is available */
298        @NonNull
299        public Set<@NonNull McpProtocolVersion> getCompletionProtocolVersions() {
300                return this.completionProtocolVersions;
301        }
302
303        @NonNull
304        private static URI requireExactUri(@NonNull URI uri) {
305                return McpResourceValueSupport.requireAbsoluteNormalizedUri(uri);
306        }
307
308        @NonNull
309        private static String requireName(@NonNull String name) {
310                McpContentValueSupport.requireWellFormedString(name);
311                if (name.isBlank())
312                        throw new IllegalArgumentException(
313                                        "MCP resource names must not be blank.");
314                return name;
315        }
316
317        @NonNull
318        private static String requireMimeType(@NonNull String mimeType) {
319                return McpContentValueSupport.requireMimeType(mimeType);
320        }
321
322        @NonNull
323        private static String requireBasicUriTemplate(
324                        @NonNull String uriTemplate) {
325                McpContentValueSupport.requireWellFormedString(uriTemplate);
326                if (uriTemplate.isBlank())
327                        throw new IllegalArgumentException(
328                                        "MCP resource URI templates must not be blank.");
329
330                boolean foundVariable = false;
331                for (int index = 0; index < uriTemplate.length(); index++) {
332                        char character = uriTemplate.charAt(index);
333                        if (character == '}')
334                                throw invalidUriTemplate();
335                        if (character != '{')
336                                continue;
337
338                        int close = uriTemplate.indexOf('}', index + 1);
339                        if (close < 0)
340                                throw invalidUriTemplate();
341                        String variable = uriTemplate.substring(index + 1, close);
342                        if (variable.isEmpty() || variable.indexOf('{') >= 0)
343                                throw invalidUriTemplate();
344                        foundVariable = true;
345                        index = close;
346                }
347                if (!foundVariable)
348                        throw new IllegalArgumentException(
349                                        "MCP resource URI templates must contain a variable expression.");
350                return uriTemplate;
351        }
352
353        @NonNull
354        private static IllegalArgumentException invalidUriTemplate() {
355                return new IllegalArgumentException(
356                                "MCP resource URI templates must use balanced {variable} expressions.");
357        }
358
359        /**
360         * Required-handler stage for an exact resource.
361         *
362         * @author <a href="https://www.revetkn.com">Mark Allen</a>
363         */
364        @NotThreadSafe
365        public static final class ExactHandlerStage {
366                @NonNull
367                private final URI uri;
368                @NonNull
369                private final String name;
370                @NonNull
371                private final Set<@NonNull McpProtocolVersion> protocolVersions;
372
373                private ExactHandlerStage(@NonNull URI uri, @NonNull String name,
374                                @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) {
375                        this.uri = requireNonNull(uri);
376                        this.name = McpContentValueSupport.requireWellFormedString(name);
377                        this.protocolVersions = requireNonNull(protocolVersions);
378                }
379
380                /**
381                 * Supplies the required resource-read handler.
382                 *
383                 * @param handler resource-read handler
384                 * @return exact-resource optional-metadata builder
385                 */
386                @NonNull
387                public ExactBuilder handler(@NonNull McpResourceReadHandler handler) {
388                        return new ExactBuilder(new BuilderState(this.uri, null,
389                                        this.name, this.protocolVersions, requireNonNull(handler)));
390                }
391        }
392
393        /**
394         * Required-handler stage for a URI-template resource.
395         *
396         * @author <a href="https://www.revetkn.com">Mark Allen</a>
397         */
398        @NotThreadSafe
399        public static final class TemplateHandlerStage {
400                @NonNull
401                private final String uriTemplate;
402                @NonNull
403                private final String name;
404                @NonNull
405                private final Set<@NonNull McpProtocolVersion> protocolVersions;
406
407                private TemplateHandlerStage(@NonNull String uriTemplate,
408                                @NonNull String name,
409                                @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) {
410                        this.uriTemplate = McpContentValueSupport.requireWellFormedString(uriTemplate);
411                        this.name = McpContentValueSupport.requireWellFormedString(name);
412                        this.protocolVersions = requireNonNull(protocolVersions);
413                }
414
415                /**
416                 * Supplies the required resource-read handler.
417                 *
418                 * @param handler resource-read handler
419                 * @return template-resource optional-metadata builder
420                 */
421                @NonNull
422                public TemplateBuilder handler(@NonNull McpResourceReadHandler handler) {
423                        return new TemplateBuilder(new BuilderState(null,
424                                        this.uriTemplate, this.name, this.protocolVersions,
425                                        requireNonNull(handler)));
426                }
427        }
428
429        /**
430         * Mutable builder for an immutable exact-resource registration.
431         *
432         * @author <a href="https://www.revetkn.com">Mark Allen</a>
433         */
434        @NotThreadSafe
435        public static final class ExactBuilder {
436                @NonNull
437                private final BuilderState state;
438
439                private ExactBuilder(@NonNull BuilderState state) {
440                        this.state = requireNonNull(state);
441                }
442
443                /** @param title human-readable title
444                 * @return this builder */
445                @NonNull
446                public ExactBuilder title(@NonNull String title) {
447                        this.state.title = McpContentValueSupport.requireWellFormedString(title);
448                        return this;
449                }
450
451                /** @param description human-readable description
452                 * @return this builder */
453                @NonNull
454                public ExactBuilder description(@NonNull String description) {
455                        this.state.description = McpContentValueSupport.requireWellFormedString(description);
456                        return this;
457                }
458
459                /** @param mimeType syntactically valid resource MIME type
460                 * @return this builder
461                 * @throws IllegalArgumentException if {@code mimeType} is malformed */
462                @NonNull
463                public ExactBuilder mimeType(@NonNull String mimeType) {
464                        this.state.mimeType = requireMimeType(mimeType);
465                        return this;
466                }
467
468                /**
469                 * Replaces icon descriptors in supplied order.
470                 * Null or empty clears the property. The complete list is validated and
471                 * snapshotted before replacing the prior value.
472                 *
473                 * @param icons icon descriptors, or null to clear
474                 * @return this builder
475                 * @throws NullPointerException if a list element is null
476                 */
477                @NonNull
478                public ExactBuilder icons(
479                                @Nullable List<@NonNull McpIcon> icons) {
480                        this.state.icons = icons == null ? List.of()
481                                        : List.copyOf(icons);
482                        return this;
483                }
484
485                /** @param annotations content annotations
486                 * @return this builder */
487                @NonNull
488                public ExactBuilder annotations(
489                                @NonNull McpContentAnnotations annotations) {
490                        this.state.annotations = requireNonNull(annotations);
491                        return this;
492                }
493
494                /**
495                 * Sets the exact resource's size in bytes.
496                 *
497                 * @param sizeInBytes nonnegative byte count
498                 * @return this builder
499                 * @throws NullPointerException if {@code sizeInBytes} is null
500                 * @throws IllegalArgumentException if {@code sizeInBytes} is negative
501                 */
502                @NonNull
503                public ExactBuilder sizeInBytes(@NonNull Long sizeInBytes) {
504                        requireNonNull(sizeInBytes);
505                        if (sizeInBytes < 0)
506                                throw new IllegalArgumentException(
507                                                "MCP resource sizes must not be negative.");
508                        this.state.sizeInBytes = sizeInBytes;
509                        return this;
510                }
511
512                /** @param cachePolicy fixed scope and default time to live
513                 * @return this builder */
514                @NonNull
515                public ExactBuilder cachePolicy(@NonNull McpCachePolicy cachePolicy) {
516                        this.state.cachePolicy = requireNonNull(cachePolicy);
517                        return this;
518                }
519
520                /**
521                 * Replaces input-request declarations in supplied order.
522                 * Null or empty clears the property. The complete list is validated and
523                 * snapshotted before replacing the prior value.
524                 *
525                 * @param inputRequestDeclarations input-request declarations, or null to clear
526                 * @return this builder
527                 * @throws NullPointerException if a list element is null
528                 */
529                @NonNull
530                public ExactBuilder inputRequestDeclarations(
531                                @Nullable List<@NonNull McpInputRequestDeclaration> inputRequestDeclarations) {
532                        this.state.inputRequestDeclarations = inputRequestDeclarations == null ? List.of()
533                                        : List.copyOf(inputRequestDeclarations);
534                        return this;
535                }
536
537                /**
538                 * Sets the request-state contract for this resource operation.
539                 *
540                 * @param requestStateMode request-state mode
541                 * @return this builder
542                 */
543                @NonNull
544                public ExactBuilder requestStateMode(
545                                @NonNull McpRequestStateMode requestStateMode) {
546                        this.state.requestStateMode = requireNonNull(requestStateMode);
547                        return this;
548                }
549
550                /** @param metadata protocol extension metadata
551                 * @return this builder */
552                @NonNull
553                public ExactBuilder metadata(@NonNull McpJsonObject metadata) {
554                        this.state.metadata = requireNonNull(metadata);
555                        return this;
556                }
557
558                /** @return immutable exact-resource registration */
559                @NonNull
560                public McpResourceRegistration build() {
561                        return new McpResourceRegistration(this.state);
562                }
563        }
564
565        /**
566         * Mutable builder for an immutable URI-template resource registration.
567         *
568         * <p>Template registrations do not advertise a size because the value can
569         * vary for each concrete expansion.
570         *
571         * @author <a href="https://www.revetkn.com">Mark Allen</a>
572         */
573        @NotThreadSafe
574        public static final class TemplateBuilder {
575                @NonNull
576                private final BuilderState state;
577
578                private TemplateBuilder(@NonNull BuilderState state) {
579                        this.state = requireNonNull(state);
580                }
581
582                /** @param title human-readable title
583                 * @return this builder */
584                @NonNull
585                public TemplateBuilder title(@NonNull String title) {
586                        this.state.title = McpContentValueSupport.requireWellFormedString(title);
587                        return this;
588                }
589
590                /** @param description human-readable description
591                 * @return this builder */
592                @NonNull
593                public TemplateBuilder description(@NonNull String description) {
594                        this.state.description = McpContentValueSupport.requireWellFormedString(description);
595                        return this;
596                }
597
598                /** @param mimeType syntactically valid resource MIME type
599                 * @return this builder
600                 * @throws IllegalArgumentException if {@code mimeType} is malformed */
601                @NonNull
602                public TemplateBuilder mimeType(@NonNull String mimeType) {
603                        this.state.mimeType = requireMimeType(mimeType);
604                        return this;
605                }
606
607                /**
608                 * Replaces icon descriptors in supplied order.
609                 * Null or empty clears the property. The complete list is validated and
610                 * snapshotted before replacing the prior value.
611                 *
612                 * @param icons icon descriptors, or null to clear
613                 * @return this builder
614                 * @throws NullPointerException if a list element is null
615                 */
616                @NonNull
617                public TemplateBuilder icons(
618                                @Nullable List<@NonNull McpIcon> icons) {
619                        this.state.icons = icons == null ? List.of()
620                                        : List.copyOf(icons);
621                        return this;
622                }
623
624                /** @param annotations content annotations
625                 * @return this builder */
626                @NonNull
627                public TemplateBuilder annotations(
628                                @NonNull McpContentAnnotations annotations) {
629                        this.state.annotations = requireNonNull(annotations);
630                        return this;
631                }
632
633                /** @param cachePolicy fixed scope and default time to live
634                 * @return this builder */
635                @NonNull
636                public TemplateBuilder cachePolicy(
637                                @NonNull McpCachePolicy cachePolicy) {
638                        this.state.cachePolicy = requireNonNull(cachePolicy);
639                        return this;
640                }
641
642                /**
643                 * Replaces input-request declarations in supplied order.
644                 * Null or empty clears the property. The complete list is validated and
645                 * snapshotted before replacing the prior value.
646                 *
647                 * @param inputRequestDeclarations input-request declarations, or null to clear
648                 * @return this builder
649                 * @throws NullPointerException if a list element is null
650                 */
651                @NonNull
652                public TemplateBuilder inputRequestDeclarations(
653                                @Nullable List<@NonNull McpInputRequestDeclaration> inputRequestDeclarations) {
654                        this.state.inputRequestDeclarations = inputRequestDeclarations == null ? List.of()
655                                        : List.copyOf(inputRequestDeclarations);
656                        return this;
657                }
658
659                /**
660                 * Sets the request-state contract for this resource operation.
661                 *
662                 * @param requestStateMode request-state mode
663                 * @return this builder
664                 */
665                @NonNull
666                public TemplateBuilder requestStateMode(
667                                @NonNull McpRequestStateMode requestStateMode) {
668                        this.state.requestStateMode = requireNonNull(requestStateMode);
669                        return this;
670                }
671
672                /** @param metadata protocol extension metadata
673                 * @return this builder */
674                @NonNull
675                public TemplateBuilder metadata(@NonNull McpJsonObject metadata) {
676                        this.state.metadata = requireNonNull(metadata);
677                        return this;
678                }
679
680                /**
681                 * Configures the handler for partial URI-template variable suggestions.
682                 * Completion is supported for MCP 2025-06-18, 2025-11-25, and
683                 * 2026-07-28. Repeated calls replace the previous handler and revision
684                 * selection.
685                 *
686                 * @param completionHandler non-null argument completer
687                 * @param protocolVersions nonempty subset of this resource's revisions
688                 * @return this builder
689                 */
690                @NonNull
691                public TemplateBuilder completionHandler(
692                                @NonNull McpCompletionHandler completionHandler,
693                                @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) {
694                        requireNonNull(completionHandler);
695                        Set<McpProtocolVersion> versions = McpProtocolVersion.requiredSet(protocolVersions);
696                        if (!this.state.protocolVersions.containsAll(versions))
697                                throw new IllegalArgumentException(
698                                                "MCP resource completion revisions must be a subset of resource revisions.");
699                        this.state.completionHandler = completionHandler;
700                        this.state.completionProtocolVersions = versions;
701                        return this;
702                }
703
704                /** @return immutable URI-template resource registration */
705                @NonNull
706                public McpResourceRegistration build() {
707                        return new McpResourceRegistration(this.state);
708                }
709        }
710
711        @NotThreadSafe
712        private static final class BuilderState {
713                @NonNull
714                private final McpResourceAddressType addressType;
715                @Nullable
716                private final URI uri;
717                @Nullable
718                private final String uriTemplate;
719                @NonNull
720                private final String name;
721                @NonNull
722                private final Set<@NonNull McpProtocolVersion> protocolVersions;
723                @Nullable
724                private String title;
725                @Nullable
726                private String description;
727                @Nullable
728                private String mimeType;
729                @NonNull
730                private List<@NonNull McpIcon> icons = List.of();
731                @Nullable
732                private McpContentAnnotations annotations;
733                @Nullable
734                private Long sizeInBytes;
735                @NonNull
736                private McpCachePolicy cachePolicy =
737                                McpCachePolicy.privateNoCacheInstance();
738                @NonNull
739                private List<@NonNull McpInputRequestDeclaration>
740                                inputRequestDeclarations = List.of();
741                @NonNull
742                private McpRequestStateMode requestStateMode = McpRequestStateMode.NONE;
743                @NonNull
744                private McpJsonObject metadata = McpJsonObject.emptyInstance();
745                @NonNull
746                private final McpResourceReadHandler handler;
747                @Nullable
748                private McpCompletionHandler completionHandler;
749                @NonNull
750                private Set<@NonNull McpProtocolVersion> completionProtocolVersions = Set.of();
751
752                private BuilderState(@Nullable URI uri,
753                                @Nullable String uriTemplate, @NonNull String name,
754                                @NonNull Set<@NonNull McpProtocolVersion> protocolVersions,
755                                @NonNull McpResourceReadHandler handler) {
756                        if ((uri == null) == (uriTemplate == null))
757                                throw new IllegalArgumentException(
758                                                "Exactly one MCP resource address must be supplied.");
759                        this.addressType = uri == null
760                                        ? McpResourceAddressType.URI_TEMPLATE
761                                        : McpResourceAddressType.URI;
762                        this.uri = uri;
763                        this.uriTemplate = uriTemplate;
764                        this.name = McpContentValueSupport.requireWellFormedString(name);
765                        this.protocolVersions = requireNonNull(protocolVersions);
766                        this.handler = requireNonNull(handler);
767                }
768        }
769}