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}