001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020
021import javax.annotation.concurrent.ThreadSafe;
022
023import static java.util.Objects.requireNonNull;
024
025/**
026 * Thread-safe server-level hook that sanitizes completed MCP tool results,
027 * including their payload and application-owned result metadata.
028 *
029 * <p>Soklet invokes the hook after handler/interceptor processing and after
030 * checking that the payload is {@link McpToolOutput}, but before remaining
031 * result validation, output-schema validation, structured-content mirroring,
032 * envelope construction, and serialization. Only the returned result's payload
033 * and metadata are used by subsequent validation, budgets, and writing.
034 * Exceptions, null results, or non-tool payloads fail closed without exposing
035 * original or partial result data or exception-derived details.
036 *
037 * <p>A completed durable tool task is sanitized again on each authorized
038 * detailed {@code tasks/get} request, potentially on different server nodes.
039 * The supplied context belongs to the independently admitted polling request;
040 * the original tool name and raw arguments come from persisted task origin.
041 * The caller-specific result does not replace the persisted result.
042 * Implementations must be safe for concurrent invocation, deterministic and
043 * idempotent for repeated equivalent calls, and free of one-shot side effects.
044 * Applications may compose a sanitizer chain behind this one hook.
045 * Framework-generated tool input-validation and Java binding errors for
046 * {@link McpProtocolVersion#V2025_11_25} and
047 * {@link McpProtocolVersion#V2026_07_28} also traverse this hook with fixed,
048 * instance-free error text. Their raw arguments remain application-owned
049 * input; sanitizers must not copy unsafe input into client-visible feedback.
050 *
051 * <p>For selective changes, start with {@link McpCompleteResult#toBuilder()}
052 * and, when changing tool output, {@link McpToolOutput#toBuilder()}. Result
053 * metadata is out-of-band data, not a secret channel; a host may log or forward
054 * it. Neither this hook nor Apps metadata grants authorization. This hook does
055 * not sanitize task-status metadata, progress notifications, input-required
056 * results, or every JSON-RPC response, and does not translate application JSON.
057 *
058 * @author <a href="https://www.revetkn.com">Mark Allen</a>
059 */
060@ThreadSafe
061@FunctionalInterface
062public interface McpToolResultSanitizer {
063
064        /**
065         * Sanitizes one complete tool result.
066         *
067         * @param requestContext immutable current request context; for completed
068         *                       durable tasks, the authorized polling request
069         * @param toolName original invoked tool name
070         * @param rawArguments immutable original raw tool arguments
071         * @param completeResult unsanitized complete result with a tool payload
072         * @return non-null result with a tool payload, to validate and serialize
073         * @throws Exception if application sanitization fails
074         */
075        @NonNull
076        McpCompleteResult sanitize(@NonNull McpRequestContext requestContext,
077                        @NonNull String toolName,
078                        @NonNull McpJsonObject rawArguments,
079                        @NonNull McpCompleteResult completeResult) throws Exception;
080
081        /**
082         * Returns the shared sanitizer that leaves the supplied result unchanged.
083         * No application-level redaction is performed; normal framework validation
084         * and output limits still apply.
085         *
086         * @return shared unchanged-result sanitizer
087         */
088        @NonNull
089        static McpToolResultSanitizer nonSanitizingInstance() {
090                return NonSanitizingMcpToolResultSanitizer.INSTANCE;
091        }
092}
093
094/** Thread-safe unchanged-result implementation. */
095@ThreadSafe
096final class NonSanitizingMcpToolResultSanitizer implements McpToolResultSanitizer {
097        @NonNull
098        static final NonSanitizingMcpToolResultSanitizer INSTANCE =
099                        new NonSanitizingMcpToolResultSanitizer();
100
101        private NonSanitizingMcpToolResultSanitizer() {}
102
103        @Override
104        @NonNull
105        public McpCompleteResult sanitize(@NonNull McpRequestContext requestContext,
106                        @NonNull String toolName,
107                        @NonNull McpJsonObject rawArguments,
108                        @NonNull McpCompleteResult completeResult) {
109                requireNonNull(requestContext);
110                requireNonNull(toolName);
111                requireNonNull(rawArguments);
112                return requireNonNull(completeResult);
113        }
114}