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 interceptor for every application-owned MCP 027 * handler invocation. 028 * <p> 029 * Soklet invokes the configured interceptor for tool calls, prompt gets, 030 * resource reads, argument completion, and custom resource-list handling, 031 * including handlers discovered from annotations. Framework-owned discovery 032 * and static catalog generation do not invoke application handlers and 033 * therefore do not traverse this hook. 034 * <p> 035 * Interception occurs after admission, rate limiting, handler-queue admission, 036 * and handler-slot acquisition, but before complete application-input 037 * validation and handler invocation. Applications that need an interceptor 038 * chain compose it behind this one hook and own its ordering. 039 * <p> 040 * An interceptor may invoke the continuation and transform its result, or may 041 * short-circuit by returning a method-compatible result without invoking it. 042 * Either result still traverses Soklet's applicable result validation, 043 * tool-output sanitization, output-schema validation, and response generation. 044 * Argument-completion invocations require an 045 * {@link McpArgumentCompletionResult}; a result for another MCP operation is 046 * rejected even when returned by an interceptor. 047 * Custom Skills-list handlers require a {@link McpSkillPage}; automatic 048 * {@code skills/list} and {@code skills/get} responses remain framework-owned 049 * and are not intercepted. Skills-file reads are intercepted only after fresh 050 * owner authorization and canonical resource-result construction. Their 051 * canonical URI, MIME type, contents and representation must remain unchanged; 052 * result metadata may be added, and freshness may be shortened but not widened 053 * beyond the shared-file cache policy and applicable security clamps. 054 * A {@code null} return or an interceptor-authored exception fails closed 055 * without exposing exception-derived data. The continuation exposes a 056 * handler's intentional {@link McpJsonRpcException} as the original object. 057 * An interceptor may inspect and rethrow that same object to preserve the 058 * handler error, or return a method-compatible recovery result. Replacing or 059 * wrapping the exception, or throwing one retained from an earlier invocation, 060 * is an interception failure and does not expose its error to the client. 061 * <p> 062 * Implementations must be safe for concurrent invocation. Each supplied 063 * continuation remains synchronous, one-shot, and bound to the invoking 064 * thread as documented by {@link McpHandlerContinuation}. The feature lookup 065 * is the exact instance supplied to the downstream handler; its lookup and 066 * feature-value contracts remain those documented by 067 * {@link McpInvocationFeatures}. 068 * 069 * @author <a href="https://www.revetkn.com">Mark Allen</a> 070 */ 071@ThreadSafe 072@FunctionalInterface 073public interface McpHandlerInterceptor { 074 /** 075 * Intercepts one application-owned MCP handler invocation. 076 * 077 * @param requestContext immutable request context 078 * @param invocationFeatures invocation features shared with the downstream handler 079 * @param continuation synchronous one-shot downstream continuation 080 * @return recognized, non-null, method-compatible handler result 081 * @throws Exception if application interception fails 082 */ 083 @NonNull 084 McpOperationResult interceptHandler(@NonNull McpRequestContext requestContext, 085 @NonNull McpInvocationFeatures invocationFeatures, 086 @NonNull McpHandlerContinuation continuation) throws Exception; 087 088 /** 089 * Returns the shared interceptor that invokes the downstream continuation 090 * without transforming its result. 091 * 092 * @return shared pass-through interceptor 093 */ 094 @NonNull 095 static McpHandlerInterceptor passThroughInstance() { 096 return DefaultMcpHandlerInterceptor.INSTANCE; 097 } 098} 099 100/** 101 * Thread-safe pass-through MCP handler interceptor. 102 * 103 * @author <a href="https://www.revetkn.com">Mark Allen</a> 104 */ 105@ThreadSafe 106final class DefaultMcpHandlerInterceptor implements McpHandlerInterceptor { 107 @NonNull 108 static final DefaultMcpHandlerInterceptor INSTANCE = 109 new DefaultMcpHandlerInterceptor(); 110 111 private DefaultMcpHandlerInterceptor() { 112 } 113 114 @Override 115 @NonNull 116 public McpOperationResult interceptHandler( 117 @NonNull McpRequestContext requestContext, 118 @NonNull McpInvocationFeatures invocationFeatures, 119 @NonNull McpHandlerContinuation continuation) throws Exception { 120 requireNonNull(requestContext); 121 requireNonNull(invocationFeatures); 122 return requireNonNull(requireNonNull(continuation).proceed(), 123 "The MCP handler continuation returned null."); 124 } 125}