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}