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}