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}