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 com.soklet.converter.TypeReference; 020import com.soklet.internal.mcp.protocol.McpMirroredHeaderPlan; 021import com.soklet.internal.mcp.schema.McpRuntimeToolInputSchemaBridge; 022import com.soklet.internal.mcp.schema.McpRuntimeTypedSchemaBridge; 023import org.jspecify.annotations.NonNull; 024import org.jspecify.annotations.Nullable; 025 026import javax.annotation.concurrent.NotThreadSafe; 027import javax.annotation.concurrent.ThreadSafe; 028import java.lang.reflect.Type; 029import java.util.List; 030import java.util.Optional; 031import java.util.Set; 032import java.util.regex.Pattern; 033 034import static java.util.Objects.requireNonNull; 035 036/** 037 * Immutable programmatic registration for one MCP tool. 038 * 039 * <p>Registration is staged: choose a name, choose typed, fixed raw-JSON, or 040 * authored-schema raw-JSON arguments, provide a handler, configure optional 041 * metadata, and explicitly call {@code build()}. Typed and authored schemas 042 * and their binding plans compile synchronously while the required inputs are 043 * still in hand. A typed-output stage can select an always-complete handler, 044 * an inline advanced handler, or a statically task-required operation handler 045 * while retaining the same output contract. 046 * 047 * @param <A> bound argument type 048 * @author <a href="https://www.revetkn.com">Mark Allen</a> 049 */ 050@ThreadSafe 051public final class McpToolRegistration<A> { 052 private static final int MAXIMUM_NAME_LENGTH = 128; 053 @NonNull 054 private static final Pattern NAME_PATTERN = 055 Pattern.compile("[A-Za-z0-9_.-]+"); 056 @NonNull 057 private static final McpToolSchema JSON_OBJECT_SCHEMA = new McpToolSchema( 058 McpJsonObject.builder().put("type", "object").build()); 059 @NonNull 060 private static final McpMirroredHeaderPlan EMPTY_MIRRORED_HEADER_PLAN = 061 McpMirroredHeaderPlan.empty(); 062 063 @NonNull 064 private final String name; 065 @NonNull 066 private final Set<@NonNull McpProtocolVersion> protocolVersions; 067 @Nullable 068 private final String title; 069 @Nullable 070 private final String description; 071 @NonNull 072 private final List<@NonNull McpIcon> icons; 073 @NonNull 074 private final Type argumentType; 075 @NonNull 076 private final McpToolSchema inputSchema; 077 @NonNull 078 private final McpMirroredHeaderPlan mirroredHeaderPlan; 079 @Nullable 080 private final Type outputType; 081 @Nullable 082 private final McpToolSchema outputSchema; 083 @Nullable 084 private final McpRuntimeTypedSchemaBridge<?> outputSchemaBridge; 085 @Nullable 086 private final McpToolAnnotations toolAnnotations; 087 @Nullable 088 private final McpAppToolMetadata appToolMetadata; 089 @Nullable 090 private final String rateLimiterName; 091 @Nullable 092 private final McpRateLimiter rateLimiter; 093 private final boolean structuredContentMirroredAsText; 094 private final boolean taskRequired; 095 @NonNull 096 private final List<@NonNull McpInputRequestDeclaration> inputRequestDeclarations; 097 @NonNull 098 private final McpRequestStateMode requestStateMode; 099 @NonNull 100 private final McpJsonObject metadata; 101 @NonNull 102 private final McpToolHandler<A> handler; 103 @NonNull 104 private final ArgumentDecoder<A> argumentDecoder; 105 106 /** 107 * Begins a staged registration for a named tool. 108 * 109 * <p>The next stage selects typed, fixed raw-JSON, or authored-schema 110 * raw-JSON arguments. Supplying both 111 * argument and output types selects a typed-output path whose next stage 112 * chooses inline completion or a statically task-required operation; 113 * supplying only an argument type selects the untyped advanced 114 * {@link McpOperationResult} path. No stage exposes {@code build()} before 115 * a handler has been supplied. 116 * 117 * @param name published MCP tool name 118 * @param protocolVersions nonempty exact revisions serving this tool 119 * @return argument-shape selection stage 120 * @throws IllegalArgumentException if the name is not 1-128 characters 121 * from {@code A-Z}, {@code a-z}, {@code 0-9}, underscore, hyphen, and dot 122 */ 123 @NonNull 124 public static ArgumentTypeStage withName(@NonNull String name, 125 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 126 return new ArgumentTypeStage(requireName(name), 127 McpProtocolVersion.requiredSet(protocolVersions)); 128 } 129 130 private McpToolRegistration(@NonNull RegistrationState<A> state) { 131 this.name = state.name; 132 this.protocolVersions = state.protocolVersions; 133 this.title = state.title; 134 this.description = state.description; 135 this.icons = List.copyOf(state.icons); 136 this.argumentType = state.argumentType; 137 this.inputSchema = state.inputSchema; 138 this.mirroredHeaderPlan = state.mirroredHeaderPlan; 139 this.outputType = state.outputType; 140 this.outputSchema = state.outputSchema; 141 this.outputSchemaBridge = state.outputSchemaBridge; 142 this.toolAnnotations = state.toolAnnotations; 143 this.appToolMetadata = state.appToolMetadata; 144 this.rateLimiterName = state.rateLimiterName; 145 this.rateLimiter = state.rateLimiter; 146 this.structuredContentMirroredAsText = 147 state.structuredContentMirroredAsText; 148 this.taskRequired = state.taskRequired; 149 this.inputRequestDeclarations = 150 List.copyOf(state.inputRequestDeclarations); 151 this.requestStateMode = state.requestStateMode; 152 this.metadata = state.metadata; 153 this.handler = state.handler; 154 this.argumentDecoder = state.argumentDecoder; 155 } 156 157 /** @return published MCP tool name */ 158 @NonNull 159 public String getName() { 160 return this.name; 161 } 162 163 /** @return exact protocol revisions on which this tool is available */ 164 @NonNull 165 public Set<@NonNull McpProtocolVersion> getProtocolVersions() { 166 return this.protocolVersions; 167 } 168 169 /** @return human-readable title, if configured */ 170 @NonNull 171 public Optional<@NonNull String> getTitle() { 172 return Optional.ofNullable(this.title); 173 } 174 175 /** @return human-readable description, if configured */ 176 @NonNull 177 public Optional<@NonNull String> getDescription() { 178 return Optional.ofNullable(this.description); 179 } 180 181 /** @return immutable icon list in registration order */ 182 @NonNull 183 public List<@NonNull McpIcon> getIcons() { 184 return this.icons; 185 } 186 187 /** @return declared Java argument type */ 188 @NonNull 189 public Type getArgumentType() { 190 return this.argumentType; 191 } 192 193 /** @return derived, fixed, or application-authored input schema */ 194 @NonNull 195 public McpToolSchema getInputSchema() { 196 return this.inputSchema; 197 } 198 199 /** 200 * Returns the immutable internal custom-header validation plan retained from 201 * the compiled input schema. 202 */ 203 @NonNull 204 McpMirroredHeaderPlan getMirroredHeaderPlan() { 205 return this.mirroredHeaderPlan; 206 } 207 208 /** @return generated output schema for a typed-output registration */ 209 @NonNull 210 public Optional<@NonNull McpToolSchema> getOutputSchema() { 211 return Optional.ofNullable(this.outputSchema); 212 } 213 214 /** 215 * Returns the Java output type from which Soklet derived a schema. 216 * 217 * @return declared output type for a typed-output registration 218 */ 219 @NonNull 220 public Optional<@NonNull Type> getOutputType() { 221 return Optional.ofNullable(this.outputType); 222 } 223 224 /** @return advisory tool annotations, if configured */ 225 @NonNull 226 public Optional<@NonNull McpToolAnnotations> getToolAnnotations() { 227 return Optional.ofNullable(this.toolAnnotations); 228 } 229 230 /** 231 * Returns typed Apps association and audience metadata, if configured. 232 * 233 * <p>Raw application metadata remains separately available from 234 * {@link #getMetadata()}; this accessor does not parse raw Apps fields. 235 * 236 * @return typed Apps tool metadata 237 */ 238 @NonNull 239 public Optional<@NonNull McpAppToolMetadata> getAppToolMetadata() { 240 return Optional.ofNullable(this.appToolMetadata); 241 } 242 243 /** 244 * Returns the named rate-limiter override. 245 * 246 * <p>At most one of this value and {@link #getRateLimiter()} is present. 247 * 248 * @return limiter name, or empty for a direct or inherited limiter 249 */ 250 @NonNull 251 public Optional<@NonNull String> getRateLimiterName() { 252 return Optional.ofNullable(this.rateLimiterName); 253 } 254 255 /** 256 * Returns the direct rate-limiter override. 257 * 258 * <p>At most one of this value and {@link #getRateLimiterName()} is 259 * present. 260 * 261 * @return direct limiter, or empty for a named or inherited limiter 262 */ 263 @NonNull 264 public Optional<@NonNull McpRateLimiter> getRateLimiter() { 265 return Optional.ofNullable(this.rateLimiter); 266 } 267 268 /** 269 * Indicates whether structured content is also mirrored as canonical JSON 270 * text in the tool's content array. 271 * 272 * @return {@code true} when mirroring is enabled 273 */ 274 @NonNull 275 public Boolean isStructuredContentMirroredAsText() { 276 return this.structuredContentMirroredAsText; 277 } 278 279 /** 280 * Returns the input requests this advanced operation may emit. 281 * 282 * @return immutable declarations in registration order 283 */ 284 @NonNull 285 public List<@NonNull McpInputRequestDeclaration> 286 getInputRequestDeclarations() { 287 return this.inputRequestDeclarations; 288 } 289 290 /** 291 * Returns the request-state contract for this operation. 292 * 293 * @return request-state mode 294 */ 295 @NonNull 296 public McpRequestStateMode getRequestStateMode() { 297 return this.requestStateMode; 298 } 299 300 /** @return immutable protocol extension metadata */ 301 @NonNull 302 public McpJsonObject getMetadata() { 303 return this.metadata; 304 } 305 306 /** 307 * Returns the normalized advanced handler. 308 * 309 * <p>For an always-complete typed registration, Soklet supplies an adapter that 310 * converts the Java result with the same compiled binding that generated 311 * {@link #getOutputSchema()}. 312 * 313 * @return normalized handler 314 */ 315 @NonNull 316 public McpToolHandler<@NonNull A> getHandler() { 317 return this.handler; 318 } 319 320 /** 321 * Validates and decodes raw arguments, invokes the normalized handler, and 322 * returns a recognized result. This package-private seam keeps all 323 * type-erasure handling inside registration while exposing only public MCP 324 * values to Soklet's package-peer server implementation. 325 */ 326 @NonNull 327 McpOperationResult invoke(@NonNull McpRequestContext request, 328 @NonNull McpJsonObject rawArguments, 329 @NonNull McpInvocationFeatures features) throws Exception { 330 return invokeDecoded(request, decodeArguments(rawArguments), features); 331 } 332 333 /** 334 * Validates and decodes raw arguments without invoking the tool handler. 335 * Typed argument decoding may invoke application record constructors. 336 * This package-private seam lets the server lazily establish that a task 337 * origin is durable-safe and reuse the same decoded value when the handler 338 * continuation proceeds. 339 */ 340 @NonNull 341 McpToolArguments<@NonNull A> decodeArguments( 342 @NonNull McpJsonObject rawArguments) { 343 requireNonNull(rawArguments); 344 A arguments; 345 try { 346 arguments = requireNonNull( 347 this.argumentDecoder.decode(rawArguments), 348 "The MCP argument decoder returned null."); 349 } catch (IllegalArgumentException exception) { 350 throw new McpInvalidToolArgumentsException(exception); 351 } 352 return new DefaultToolArguments<>(arguments, rawArguments); 353 } 354 355 /** 356 * Invokes the normalized handler with arguments already validated and 357 * decoded by {@link #decodeArguments(McpJsonObject)}. 358 */ 359 @NonNull 360 McpOperationResult invokeDecoded(@NonNull McpRequestContext request, 361 @NonNull McpToolArguments<@NonNull A> toolArguments, 362 @NonNull McpInvocationFeatures features) throws Exception { 363 requireNonNull(request); 364 requireNonNull(toolArguments); 365 requireNonNull(features); 366 return requireNonNull(this.handler.handle(request, toolArguments, features), 367 "The MCP tool handler returned null."); 368 } 369 370 /** 371 * Validates structured output against the compiled schema retained by a 372 * typed-output registration. Untyped advanced registrations have no output 373 * schema and therefore accept every structured JSON value here. 374 */ 375 boolean isStructuredOutputValid(@NonNull McpJsonValue structuredOutput) { 376 requireNonNull(structuredOutput); 377 return this.outputSchemaBridge == null 378 || this.outputSchemaBridge.isValid(structuredOutput); 379 } 380 381 /** 382 * Indicates whether Soklet must preflight the Tasks capability before 383 * invoking this registration's application handler. 384 */ 385 boolean isTaskRequired() { 386 return this.taskRequired; 387 } 388 389 @NonNull 390 private static String requireName(@NonNull String name) { 391 McpContentValueSupport.requireWellFormedString(name); 392 if (name.length() < 1 || name.length() > MAXIMUM_NAME_LENGTH 393 || !NAME_PATTERN.matcher(name).matches()) 394 throw new IllegalArgumentException( 395 "MCP tool names must contain 1-128 characters from " 396 + "[A-Za-z0-9_.-]."); 397 return name; 398 } 399 400 /** 401 * Staged selection of a tool's argument and output shapes. 402 * 403 * @author <a href="https://www.revetkn.com">Mark Allen</a> 404 */ 405 @NotThreadSafe 406 public static final class ArgumentTypeStage { 407 @NonNull 408 private final String name; 409 @NonNull 410 private final Set<@NonNull McpProtocolVersion> protocolVersions; 411 412 private ArgumentTypeStage(@NonNull String name, 413 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 414 this.name = McpContentValueSupport.requireWellFormedString(name); 415 this.protocolVersions = requireNonNull(protocolVersions); 416 } 417 418 /** 419 * Selects class-token argument and output types. 420 * 421 * @param argumentType argument type 422 * @param outputType structured output type 423 * @param <T> argument type 424 * @param <R> output type 425 * @return typed handler-selection stage 426 */ 427 @NonNull 428 public <T, R> CompleteHandlerStage<@NonNull T, @NonNull R> argumentAndOutputTypes( 429 @NonNull Class<@NonNull T> argumentType, 430 @NonNull Class<@NonNull R> outputType) { 431 return typedStage(argumentType, outputType); 432 } 433 434 /** 435 * Selects a class-token argument type and generic output type. 436 * 437 * @param argumentType argument type 438 * @param outputType structured output type token 439 * @param <T> argument type 440 * @param <R> output type 441 * @return typed handler-selection stage 442 */ 443 @NonNull 444 public <T, R> CompleteHandlerStage<@NonNull T, @NonNull R> argumentAndOutputTypes( 445 @NonNull Class<@NonNull T> argumentType, 446 @NonNull TypeReference<@NonNull R> outputType) { 447 requireNonNull(outputType); 448 return typedStage(argumentType, outputType.getType()); 449 } 450 451 /** 452 * Selects a generic argument type and class-token output type. 453 * 454 * @param argumentType argument type token 455 * @param outputType structured output type 456 * @param <T> argument type 457 * @param <R> output type 458 * @return typed handler-selection stage 459 */ 460 @NonNull 461 public <T, R> CompleteHandlerStage<@NonNull T, @NonNull R> argumentAndOutputTypes( 462 @NonNull TypeReference<@NonNull T> argumentType, 463 @NonNull Class<@NonNull R> outputType) { 464 requireNonNull(argumentType); 465 return typedStage(argumentType.getType(), outputType); 466 } 467 468 /** 469 * Selects generic argument and output types. 470 * 471 * @param argumentType argument type token 472 * @param outputType structured output type token 473 * @param <T> argument type 474 * @param <R> output type 475 * @return typed handler-selection stage 476 */ 477 @NonNull 478 public <T, R> CompleteHandlerStage<@NonNull T, @NonNull R> argumentAndOutputTypes( 479 @NonNull TypeReference<@NonNull T> argumentType, 480 @NonNull TypeReference<@NonNull R> outputType) { 481 requireNonNull(argumentType); 482 requireNonNull(outputType); 483 return typedStage(argumentType.getType(), outputType.getType()); 484 } 485 486 /** 487 * Selects a class-token argument type for an advanced handler. 488 * 489 * @param argumentType argument type 490 * @param <T> argument type 491 * @return advanced handler-selection stage 492 */ 493 @NonNull 494 public <T> OperationHandlerStage<@NonNull T> argumentType( 495 @NonNull Class<@NonNull T> argumentType) { 496 return operationStage(argumentType); 497 } 498 499 /** 500 * Selects a generic argument type for an advanced handler. 501 * 502 * @param argumentType argument type token 503 * @param <T> argument type 504 * @return advanced handler-selection stage 505 */ 506 @NonNull 507 public <T> OperationHandlerStage<@NonNull T> argumentType( 508 @NonNull TypeReference<@NonNull T> argumentType) { 509 requireNonNull(argumentType); 510 return operationStage(argumentType.getType()); 511 } 512 513 /** 514 * Selects raw JSON-object arguments for an advanced handler. 515 * 516 * <p>The registration publishes and enforces the fixed 517 * {@code {"type":"object"}} schema. 518 * 519 * @return raw-JSON handler-selection stage 520 */ 521 @NonNull 522 public OperationHandlerStage<@NonNull McpJsonObject> 523 jsonObjectArguments() { 524 return new OperationHandlerStage<>(this.name, this.protocolVersions, McpJsonObject.class, 525 JSON_OBJECT_SCHEMA, EMPTY_MIRRORED_HEADER_PLAN, 526 rawArguments -> rawArguments); 527 } 528 529 /** 530 * Selects raw JSON-object arguments governed by an authored Soklet MCP 531 * Tool Schema Profile 1 input schema. 532 * 533 * <p>The schema is compiled and validated synchronously. The exact 534 * immutable document is published for the tool, and every invocation is 535 * evaluated against the compiled schema before the handler receives its 536 * validated {@link McpJsonObject}. This path supports Profile 1 schema 537 * features that cannot be expressed by Soklet's Java-type derivation, 538 * including authored constraints and {@code x-mcp-header} declarations. 539 * Soklet never resolves network references. 540 * 541 * @param inputSchema authored Profile 1 input schema 542 * @return advanced handler-selection stage 543 * @throws NullPointerException if {@code inputSchema} is {@code null} 544 * @throws IllegalArgumentException if the document exceeds a production 545 * limit, is outside Profile 1, is not a direct object-root tool input, or 546 * has an invalid mirrored-header declaration 547 */ 548 @NonNull 549 public OperationHandlerStage<@NonNull McpJsonObject> inputSchema( 550 @NonNull McpJsonObject inputSchema) { 551 McpRuntimeToolInputSchemaBridge bridge = 552 McpRuntimeToolInputSchemaBridge.compileToolInput( 553 requireNonNull(inputSchema)); 554 return new OperationHandlerStage<>(this.name, this.protocolVersions, McpJsonObject.class, 555 new McpToolSchema(bridge.getSchemaDocument()), 556 bridge.getMirroredHeaderPlan(), bridge::decode); 557 } 558 559 @NonNull 560 private <T, R> CompleteHandlerStage<T, R> typedStage( 561 @NonNull Type argumentType, @NonNull Type outputType) { 562 requireNonNull(argumentType); 563 requireNonNull(outputType); 564 McpRuntimeTypedSchemaBridge<T> inputBridge = 565 McpRuntimeTypedSchemaBridge.compileToolInput(argumentType); 566 McpRuntimeTypedSchemaBridge<R> outputBridge = 567 McpRuntimeTypedSchemaBridge.compileToolOutput(outputType); 568 return new CompleteHandlerStage<>(this.name, this.protocolVersions, argumentType, 569 outputType, inputBridge, outputBridge); 570 } 571 572 @NonNull 573 private <T> OperationHandlerStage<T> operationStage( 574 @NonNull Type argumentType) { 575 requireNonNull(argumentType); 576 McpRuntimeTypedSchemaBridge<T> inputBridge = 577 McpRuntimeTypedSchemaBridge.compileToolInput(argumentType); 578 McpToolSchema inputSchema = 579 new McpToolSchema(inputBridge.getSchemaDocument()); 580 return new OperationHandlerStage<>(this.name, this.protocolVersions, argumentType, 581 inputSchema, inputBridge.getMirroredHeaderPlan(), 582 inputBridge::decode); 583 } 584 } 585 586 /** 587 * Handler-selection stage for a typed-output path. 588 * 589 * <p>{@link #handler(McpCompleteToolHandler)} selects an operation that 590 * always completes inline with a typed value. 591 * {@link #inlineOperationHandler(McpToolHandler)} selects an inline advanced 592 * operation that may return explicit content or an {@code isError} tool 593 * result. {@link #operationHandler(McpToolHandler)} selects an advanced 594 * operation that retains the same eventual output schema and is statically 595 * known to require Tasks. 596 * 597 * @param <A> argument type 598 * @param <R> structured output type 599 * @author <a href="https://www.revetkn.com">Mark Allen</a> 600 */ 601 @NotThreadSafe 602 public static final class CompleteHandlerStage<A, R> { 603 @NonNull 604 private final String name; 605 @NonNull 606 private final Set<@NonNull McpProtocolVersion> protocolVersions; 607 @NonNull 608 private final Type argumentType; 609 @NonNull 610 private final Type outputType; 611 @NonNull 612 private final McpRuntimeTypedSchemaBridge<A> inputBridge; 613 @NonNull 614 private final McpRuntimeTypedSchemaBridge<R> outputBridge; 615 616 private CompleteHandlerStage(@NonNull String name, 617 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions, 618 @NonNull Type argumentType, @NonNull Type outputType, 619 @NonNull McpRuntimeTypedSchemaBridge<A> inputBridge, 620 @NonNull McpRuntimeTypedSchemaBridge<R> outputBridge) { 621 this.name = McpContentValueSupport.requireWellFormedString(name); 622 this.protocolVersions = requireNonNull(protocolVersions); 623 this.argumentType = requireNonNull(argumentType); 624 this.outputType = requireNonNull(outputType); 625 this.inputBridge = requireNonNull(inputBridge); 626 this.outputBridge = requireNonNull(outputBridge); 627 } 628 629 /** 630 * Supplies the required complete handler. 631 * 632 * @param handler complete handler 633 * @return optional-metadata builder 634 */ 635 @NonNull 636 public CompleteBuilder<@NonNull A> handler( 637 @NonNull McpCompleteToolHandler<@NonNull A, @NonNull R> handler) { 638 requireNonNull(handler); 639 McpToolHandler<A> normalizedHandler = (request, arguments, features) -> { 640 R result = requireNonNull( 641 handler.handle(request, arguments, features), 642 "The MCP complete tool handler returned null."); 643 McpJsonValue structuredContent = 644 this.outputBridge.encodeForDeferredValidation(result); 645 return McpCompleteResult.fromToolStructuredContent( 646 structuredContent); 647 }; 648 RegistrationState<A> state = new RegistrationState<>(this.name, this.protocolVersions, 649 this.argumentType, 650 new McpToolSchema(this.inputBridge.getSchemaDocument()), 651 this.inputBridge.getMirroredHeaderPlan(), 652 this.outputType, 653 new McpToolSchema(this.outputBridge.getSchemaDocument()), 654 this.outputBridge, 655 normalizedHandler, this.inputBridge::decode, false); 656 return new CompleteBuilder<>(state); 657 } 658 659 /** 660 * Supplies an inline advanced handler while retaining the typed output 661 * schema. 662 * 663 * <p>The handler returns the {@link McpOperationResult} spine directly, 664 * which permits explicit content and {@code isError} tool results. This 665 * path is not statically task-required; a successful result must include 666 * structured content satisfying the retained output schema.</p> 667 * 668 * @param handler inline advanced handler 669 * @return optional-metadata builder 670 */ 671 @NonNull 672 public OperationBuilder<@NonNull A> inlineOperationHandler( 673 @NonNull McpToolHandler<@NonNull A> handler) { 674 return operationBuilder(handler, false); 675 } 676 677 /** 678 * Supplies the required advanced handler for a task-required operation 679 * whose eventual structured output has type {@code R}. 680 * 681 * <p>Unlike {@link #handler(McpCompleteToolHandler)}, this handler returns 682 * the {@link McpOperationResult} spine directly, including 683 * {@link McpTaskCreatedResult}. Soklet retains the selected output type and 684 * schema for deferred completed-result validation and treats this as a 685 * task-required registration. A client that did not negotiate the Tasks 686 * extension is rejected before the application handler is invoked. 687 * 688 * @param handler advanced task-required handler 689 * @return optional-metadata builder 690 */ 691 @NonNull 692 public OperationBuilder<@NonNull A> operationHandler( 693 @NonNull McpToolHandler<@NonNull A> handler) { 694 return operationBuilder(handler, true); 695 } 696 697 @NonNull 698 private OperationBuilder<@NonNull A> operationBuilder( 699 @NonNull McpToolHandler<@NonNull A> handler, 700 boolean taskRequired) { 701 RegistrationState<A> state = new RegistrationState<>(this.name, this.protocolVersions, 702 this.argumentType, 703 new McpToolSchema(this.inputBridge.getSchemaDocument()), 704 this.inputBridge.getMirroredHeaderPlan(), 705 this.outputType, 706 new McpToolSchema(this.outputBridge.getSchemaDocument()), 707 this.outputBridge, 708 requireNonNull(handler), this.inputBridge::decode, 709 taskRequired); 710 return new OperationBuilder<>(state); 711 } 712 } 713 714 /** 715 * Handler-selection stage for the advanced operation-result path. 716 * 717 * <p>This path does not statically require Tasks because a handler may choose 718 * an inline result. A handler that conditionally creates a task must first 719 * check {@link McpInvocationFeatures#getTaskCreationContext()}. To declare that every 720 * invocation requires Tasks while retaining a typed eventual output schema, 721 * use {@link CompleteHandlerStage#operationHandler(McpToolHandler)}. 722 * 723 * @param <A> argument type 724 * @author <a href="https://www.revetkn.com">Mark Allen</a> 725 */ 726 @NotThreadSafe 727 public static final class OperationHandlerStage<A> { 728 @NonNull 729 private final String name; 730 @NonNull 731 private final Set<@NonNull McpProtocolVersion> protocolVersions; 732 @NonNull 733 private final Type argumentType; 734 @NonNull 735 private final McpToolSchema inputSchema; 736 @NonNull 737 private final McpMirroredHeaderPlan mirroredHeaderPlan; 738 @NonNull 739 private final ArgumentDecoder<A> argumentDecoder; 740 741 private OperationHandlerStage(@NonNull String name, 742 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions, 743 @NonNull Type argumentType, @NonNull McpToolSchema inputSchema, 744 @NonNull McpMirroredHeaderPlan mirroredHeaderPlan, 745 @NonNull ArgumentDecoder<A> argumentDecoder) { 746 this.name = McpContentValueSupport.requireWellFormedString(name); 747 this.protocolVersions = requireNonNull(protocolVersions); 748 this.argumentType = requireNonNull(argumentType); 749 this.inputSchema = requireNonNull(inputSchema); 750 this.mirroredHeaderPlan = requireNonNull(mirroredHeaderPlan); 751 this.argumentDecoder = requireNonNull(argumentDecoder); 752 } 753 754 /** 755 * Supplies the required advanced handler. 756 * 757 * <p>This dynamic path is not preflighted as task-required. Before creating 758 * durable work, a handler that may return {@link McpTaskCreatedResult} must 759 * require or inspect {@link McpInvocationFeatures#getTaskCreationContext()}. 760 * 761 * @param handler advanced handler 762 * @return optional-metadata builder 763 */ 764 @NonNull 765 public OperationBuilder<@NonNull A> handler( 766 @NonNull McpToolHandler<@NonNull A> handler) { 767 RegistrationState<A> state = new RegistrationState<>(this.name, this.protocolVersions, 768 this.argumentType, this.inputSchema, this.mirroredHeaderPlan, 769 null, null, null, 770 requireNonNull(handler), this.argumentDecoder, false); 771 return new OperationBuilder<>(state); 772 } 773 } 774 775 /** 776 * Builder for an advanced handler that returns the 777 * {@link McpOperationResult} spine directly. 778 * 779 * @param <A> argument type 780 * @author <a href="https://www.revetkn.com">Mark Allen</a> 781 */ 782 @NotThreadSafe 783 public static final class OperationBuilder<A> { 784 @NonNull 785 private final RegistrationState<A> state; 786 787 private OperationBuilder(@NonNull RegistrationState<A> state) { 788 this.state = requireNonNull(state); 789 } 790 791 /** @param title human-readable title 792 * @return this builder */ 793 @NonNull 794 public OperationBuilder<@NonNull A> title(@NonNull String title) { 795 this.state.title = McpContentValueSupport.requireWellFormedString(title); 796 return this; 797 } 798 799 /** @param description human-readable description 800 * @return this builder */ 801 @NonNull 802 public OperationBuilder<@NonNull A> description(@NonNull String description) { 803 this.state.description = McpContentValueSupport.requireWellFormedString(description); 804 return this; 805 } 806 807 /** 808 * Replaces icon descriptors in supplied order. 809 * Null or empty clears the property. The complete list is validated and 810 * snapshotted before replacing the prior value. 811 * 812 * @param icons icon descriptors, or null to clear 813 * @return this builder 814 * @throws NullPointerException if a list element is null 815 */ 816 @NonNull 817 public OperationBuilder<@NonNull A> icons( 818 @Nullable List<@NonNull McpIcon> icons) { 819 this.state.icons = icons == null ? List.of() 820 : List.copyOf(icons); 821 return this; 822 } 823 824 /** @param toolAnnotations advisory tool annotations 825 * @return this builder */ 826 @NonNull 827 public OperationBuilder<@NonNull A> toolAnnotations( 828 @NonNull McpToolAnnotations toolAnnotations) { 829 this.state.toolAnnotations = requireNonNull(toolAnnotations); 830 return this; 831 } 832 833 /** 834 * Sets typed Apps association and audience metadata. 835 * 836 * <p>Typed configuration owns {@code ui.resourceUri} and 837 * {@code ui.visibility}; raw declarations of either field conflict even 838 * when their values agree. Unrelated raw metadata is retained. An 839 * associated UI resource is checked when the endpoint is built. 840 * 841 * @param appToolMetadata typed Apps metadata 842 * @return this builder 843 * @throws NullPointerException if the metadata is null 844 * @throws IllegalArgumentException if raw metadata declares an owned field 845 */ 846 @NonNull 847 public OperationBuilder<@NonNull A> appToolMetadata( 848 @NonNull McpAppToolMetadata appToolMetadata) { 849 requireNonNull(appToolMetadata); 850 if (!this.state.protocolVersions.containsAll( 851 appToolMetadata.getProtocolVersions())) 852 throw new IllegalArgumentException( 853 "MCP Apps revisions must be a subset of tool revisions."); 854 McpAppMetadataSupport.effectiveToolMetadata(this.state.metadata, 855 appToolMetadata); 856 this.state.appToolMetadata = appToolMetadata; 857 return this; 858 } 859 860 /** 861 * Sets a named rate-limiter override. 862 * 863 * <p>Named and direct setter calls are last-call-wins. 864 * 865 * @param rateLimiterName nonblank name in the server limiter registry 866 * @return this builder 867 */ 868 @NonNull 869 public OperationBuilder<@NonNull A> rateLimiterName( 870 @NonNull String rateLimiterName) { 871 this.state.rateLimiterName = requireNonBlank(rateLimiterName, 872 "Rate-limiter name"); 873 this.state.rateLimiter = null; 874 return this; 875 } 876 877 /** 878 * Sets a direct rate-limiter override. 879 * 880 * <p>Named and direct setter calls are last-call-wins. 881 * 882 * @param rateLimiter direct application limiter 883 * @return this builder 884 */ 885 @NonNull 886 public OperationBuilder<@NonNull A> rateLimiter( 887 @NonNull McpRateLimiter rateLimiter) { 888 this.state.rateLimiter = requireNonNull(rateLimiter); 889 this.state.rateLimiterName = null; 890 return this; 891 } 892 893 /** 894 * Controls canonical JSON text mirroring for structured tool content. 895 * 896 * <p>Mirroring is enabled by default. Pass {@code false} to opt out. 897 * The default is defined by Soklet's supported MCP profile and does not 898 * automatically follow newer MCP revisions. 899 * 900 * @param structuredContentMirroredAsText whether mirroring is enabled 901 * @return this builder 902 * @throws NullPointerException if {@code structuredContentMirroredAsText} 903 * is null 904 */ 905 @NonNull 906 public OperationBuilder<@NonNull A> structuredContentMirroredAsText( 907 @NonNull Boolean structuredContentMirroredAsText) { 908 this.state.structuredContentMirroredAsText = 909 requireNonNull(structuredContentMirroredAsText); 910 return this; 911 } 912 913 /** 914 * Replaces input-request declarations in supplied order. 915 * Null or empty clears the property. The complete list is validated and 916 * snapshotted before replacing the prior value. 917 * 918 * @param inputRequestDeclarations input-request declarations, or null to clear 919 * @return this builder 920 * @throws NullPointerException if a list element is null 921 */ 922 @NonNull 923 public OperationBuilder<@NonNull A> inputRequestDeclarations( 924 @Nullable List<@NonNull McpInputRequestDeclaration> inputRequestDeclarations) { 925 this.state.inputRequestDeclarations = inputRequestDeclarations == null ? List.of() 926 : List.copyOf(inputRequestDeclarations); 927 return this; 928 } 929 930 /** 931 * Sets the request-state contract for this advanced operation. 932 * 933 * @param requestStateMode request-state mode 934 * @return this builder 935 */ 936 @NonNull 937 public OperationBuilder<@NonNull A> requestStateMode( 938 @NonNull McpRequestStateMode requestStateMode) { 939 this.state.requestStateMode = requireNonNull(requestStateMode); 940 return this; 941 } 942 943 /** @param metadata protocol extension metadata 944 * @return this builder 945 * @throws IllegalArgumentException if raw Apps metadata is malformed or 946 * conflicts with typed Apps metadata */ 947 @NonNull 948 public OperationBuilder<@NonNull A> metadata(@NonNull McpJsonObject metadata) { 949 requireNonNull(metadata); 950 McpAppMetadataSupport.effectiveToolMetadata(metadata, 951 this.state.appToolMetadata); 952 this.state.metadata = metadata; 953 return this; 954 } 955 956 /** @return immutable tool registration */ 957 @NonNull 958 public McpToolRegistration<@NonNull A> build() { 959 return new McpToolRegistration<>(this.state); 960 } 961 } 962 963 /** 964 * Builder for a tool that always completes with a supported structured Java 965 * result. 966 * 967 * <p>The staged type declaration is the sole source for schema derivation 968 * and intrinsic conversion. This builder intentionally has no output-type, 969 * output-schema, or converter override. 970 * 971 * @param <A> argument type 972 * @author <a href="https://www.revetkn.com">Mark Allen</a> 973 */ 974 @NotThreadSafe 975 public static final class CompleteBuilder<A> { 976 @NonNull 977 private final RegistrationState<A> state; 978 979 private CompleteBuilder(@NonNull RegistrationState<A> state) { 980 this.state = requireNonNull(state); 981 } 982 983 /** @param title human-readable title 984 * @return this builder */ 985 @NonNull 986 public CompleteBuilder<@NonNull A> title(@NonNull String title) { 987 this.state.title = McpContentValueSupport.requireWellFormedString(title); 988 return this; 989 } 990 991 /** @param description human-readable description 992 * @return this builder */ 993 @NonNull 994 public CompleteBuilder<@NonNull A> description( 995 @NonNull String description) { 996 this.state.description = McpContentValueSupport.requireWellFormedString(description); 997 return this; 998 } 999 1000 /** 1001 * Replaces icon descriptors in supplied order. 1002 * Null or empty clears the property. The complete list is validated and 1003 * snapshotted before replacing the prior value. 1004 * 1005 * @param icons icon descriptors, or null to clear 1006 * @return this builder 1007 * @throws NullPointerException if a list element is null 1008 */ 1009 @NonNull 1010 public CompleteBuilder<@NonNull A> icons( 1011 @Nullable List<@NonNull McpIcon> icons) { 1012 this.state.icons = icons == null ? List.of() 1013 : List.copyOf(icons); 1014 return this; 1015 } 1016 1017 /** @param toolAnnotations advisory tool annotations 1018 * @return this builder */ 1019 @NonNull 1020 public CompleteBuilder<@NonNull A> toolAnnotations( 1021 @NonNull McpToolAnnotations toolAnnotations) { 1022 this.state.toolAnnotations = requireNonNull(toolAnnotations); 1023 return this; 1024 } 1025 1026 /** 1027 * Sets typed Apps association and audience metadata. 1028 * 1029 * <p>Typed configuration owns {@code ui.resourceUri} and 1030 * {@code ui.visibility}; raw declarations of either field conflict even 1031 * when their values agree. Unrelated raw metadata is retained. An 1032 * associated UI resource is checked when the endpoint is built. 1033 * 1034 * @param appToolMetadata typed Apps metadata 1035 * @return this builder 1036 * @throws NullPointerException if the metadata is null 1037 * @throws IllegalArgumentException if raw metadata declares an owned field 1038 */ 1039 @NonNull 1040 public CompleteBuilder<@NonNull A> appToolMetadata( 1041 @NonNull McpAppToolMetadata appToolMetadata) { 1042 requireNonNull(appToolMetadata); 1043 if (!this.state.protocolVersions.containsAll( 1044 appToolMetadata.getProtocolVersions())) 1045 throw new IllegalArgumentException( 1046 "MCP Apps revisions must be a subset of tool revisions."); 1047 McpAppMetadataSupport.effectiveToolMetadata(this.state.metadata, 1048 appToolMetadata); 1049 this.state.appToolMetadata = appToolMetadata; 1050 return this; 1051 } 1052 1053 /** 1054 * Sets a named rate-limiter override. 1055 * 1056 * <p>Named and direct setter calls are last-call-wins. 1057 * 1058 * @param rateLimiterName nonblank name in the server limiter registry 1059 * @return this builder 1060 */ 1061 @NonNull 1062 public CompleteBuilder<@NonNull A> rateLimiterName( 1063 @NonNull String rateLimiterName) { 1064 this.state.rateLimiterName = requireNonBlank(rateLimiterName, 1065 "Rate-limiter name"); 1066 this.state.rateLimiter = null; 1067 return this; 1068 } 1069 1070 /** 1071 * Sets a direct rate-limiter override. 1072 * 1073 * <p>Named and direct setter calls are last-call-wins. 1074 * 1075 * @param rateLimiter direct application limiter 1076 * @return this builder 1077 */ 1078 @NonNull 1079 public CompleteBuilder<@NonNull A> rateLimiter( 1080 @NonNull McpRateLimiter rateLimiter) { 1081 this.state.rateLimiter = requireNonNull(rateLimiter); 1082 this.state.rateLimiterName = null; 1083 return this; 1084 } 1085 1086 /** 1087 * Controls canonical JSON text mirroring for structured tool content. 1088 * 1089 * <p>Mirroring is enabled by default. Pass {@code false} to opt out. 1090 * The default is defined by Soklet's supported MCP profile and does not 1091 * automatically follow newer MCP revisions. 1092 * 1093 * @param structuredContentMirroredAsText whether mirroring is enabled 1094 * @return this builder 1095 * @throws NullPointerException if {@code structuredContentMirroredAsText} 1096 * is null 1097 */ 1098 @NonNull 1099 public CompleteBuilder<@NonNull A> structuredContentMirroredAsText( 1100 @NonNull Boolean structuredContentMirroredAsText) { 1101 this.state.structuredContentMirroredAsText = 1102 requireNonNull(structuredContentMirroredAsText); 1103 return this; 1104 } 1105 1106 /** @param metadata protocol extension metadata 1107 * @return this builder 1108 * @throws IllegalArgumentException if raw Apps metadata is malformed or 1109 * conflicts with typed Apps metadata */ 1110 @NonNull 1111 public CompleteBuilder<@NonNull A> metadata( 1112 @NonNull McpJsonObject metadata) { 1113 requireNonNull(metadata); 1114 McpAppMetadataSupport.effectiveToolMetadata(metadata, 1115 this.state.appToolMetadata); 1116 this.state.metadata = metadata; 1117 return this; 1118 } 1119 1120 /** @return immutable tool registration */ 1121 @NonNull 1122 public McpToolRegistration<@NonNull A> build() { 1123 return new McpToolRegistration<>(this.state); 1124 } 1125 } 1126 1127 @NonNull 1128 private static String requireNonBlank(@NonNull String value, 1129 @NonNull String description) { 1130 requireNonNull(value); 1131 McpContentValueSupport.requireWellFormedString(value); 1132 if (value.isBlank()) 1133 throw new IllegalArgumentException( 1134 description + " must not be blank."); 1135 return value; 1136 } 1137 1138 @ThreadSafe 1139 @FunctionalInterface 1140 private interface ArgumentDecoder<A> { 1141 @NonNull 1142 A decode(@NonNull McpJsonObject rawArguments); 1143 } 1144 1145 @ThreadSafe 1146 private static final class DefaultToolArguments<A> 1147 implements McpToolArguments<A> { 1148 @NonNull 1149 private final A arguments; 1150 @NonNull 1151 private final McpJsonObject rawArguments; 1152 1153 private DefaultToolArguments(@NonNull A arguments, 1154 @NonNull McpJsonObject rawArguments) { 1155 this.arguments = requireNonNull(arguments); 1156 this.rawArguments = requireNonNull(rawArguments); 1157 } 1158 1159 @Override 1160 @NonNull 1161 public A getConvertedArguments() { 1162 return this.arguments; 1163 } 1164 1165 @Override 1166 @NonNull 1167 public McpJsonObject getRawArguments() { 1168 return this.rawArguments; 1169 } 1170 } 1171 1172 @NotThreadSafe 1173 private static final class RegistrationState<A> { 1174 @NonNull 1175 private final String name; 1176 @NonNull 1177 private final Set<@NonNull McpProtocolVersion> protocolVersions; 1178 @NonNull 1179 private final Type argumentType; 1180 @NonNull 1181 private final McpToolSchema inputSchema; 1182 @NonNull 1183 private final McpMirroredHeaderPlan mirroredHeaderPlan; 1184 @Nullable 1185 private final Type outputType; 1186 @Nullable 1187 private final McpToolSchema outputSchema; 1188 @Nullable 1189 private final McpRuntimeTypedSchemaBridge<?> outputSchemaBridge; 1190 @NonNull 1191 private final McpToolHandler<A> handler; 1192 @NonNull 1193 private final ArgumentDecoder<A> argumentDecoder; 1194 @Nullable 1195 private String title; 1196 @Nullable 1197 private String description; 1198 @NonNull 1199 private List<@NonNull McpIcon> icons = List.of(); 1200 @Nullable 1201 private McpToolAnnotations toolAnnotations; 1202 @Nullable 1203 private McpAppToolMetadata appToolMetadata; 1204 @Nullable 1205 private String rateLimiterName; 1206 @Nullable 1207 private McpRateLimiter rateLimiter; 1208 private boolean structuredContentMirroredAsText = true; 1209 private final boolean taskRequired; 1210 @NonNull 1211 private List<@NonNull McpInputRequestDeclaration> 1212 inputRequestDeclarations = List.of(); 1213 @NonNull 1214 private McpRequestStateMode requestStateMode = McpRequestStateMode.NONE; 1215 @NonNull 1216 private McpJsonObject metadata = McpJsonObject.emptyInstance(); 1217 1218 private RegistrationState(@NonNull String name, 1219 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions, 1220 @NonNull Type argumentType, @NonNull McpToolSchema inputSchema, 1221 @NonNull McpMirroredHeaderPlan mirroredHeaderPlan, 1222 @Nullable Type outputType, @Nullable McpToolSchema outputSchema, 1223 @Nullable McpRuntimeTypedSchemaBridge<?> outputSchemaBridge, 1224 @NonNull McpToolHandler<A> handler, 1225 @NonNull ArgumentDecoder<A> argumentDecoder, 1226 boolean taskRequired) { 1227 this.name = McpContentValueSupport.requireWellFormedString(name); 1228 this.protocolVersions = requireNonNull(protocolVersions); 1229 this.argumentType = requireNonNull(argumentType); 1230 this.inputSchema = requireNonNull(inputSchema); 1231 this.mirroredHeaderPlan = requireNonNull(mirroredHeaderPlan); 1232 this.outputType = outputType; 1233 this.outputSchema = outputSchema; 1234 this.outputSchemaBridge = outputSchemaBridge; 1235 this.handler = requireNonNull(handler); 1236 this.argumentDecoder = requireNonNull(argumentDecoder); 1237 this.taskRequired = taskRequired; 1238 if ((this.outputType == null) != (this.outputSchema == null) 1239 || (this.outputSchema == null) 1240 != (this.outputSchemaBridge == null)) 1241 throw new IllegalArgumentException( 1242 "Output type, schema, and bridge must be present together."); 1243 if (this.taskRequired && this.outputType == null) 1244 throw new IllegalArgumentException( 1245 "A task-required registration must declare an output type."); 1246 } 1247 } 1248}