001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client.mcp;
018
019
020import java.util.concurrent.atomic.*;
021
022import org.apache.juneau.bean.jsonrpc.*;
023import org.apache.juneau.commons.inject.*;
024import static org.apache.juneau.commons.utils.Shorts.*;
025
026/**
027 * Dispatches server-initiated JSON-RPC requests delivered over an MCP client's duplex event-stream channel to a
028 * single registered {@link McpServerRequestHandler}.
029 *
030 * <p>
031 * Requests are handed to the registered handler untouched, with their {@code params} left as the generic,
032 * revision-neutral wire representation - never projected into a typed request bean - since the set of
033 * server-initiated methods (for example {@code sampling/createMessage}; illustrative only, as sampling is
034 * deprecated per SEP-2577) is revision- and deployment-specific and this dispatcher has no knowledge of it.
035 *
036 * @since 10.0.0
037 */
038public class McpDuplexDispatcher {
039
040   private final AtomicReference<McpServerRequestHandler> handler = new AtomicReference<>();
041
042   /**
043    * Registers (or de-registers) the handler invoked by {@link #dispatch(JsonRpcRequest, BeanStore)}.
044    *
045    * @param value The handler to register. Passing <jk>null</jk> de-registers any previously-registered handler -
046    *    a subsequent {@link #dispatch(JsonRpcRequest, BeanStore)} call then behaves as if no handler was ever
047    *    registered (see the "no handler" behavior documented there).
048    * @return This object.
049    */
050   public McpDuplexDispatcher register(McpServerRequestHandler value) {
051      handler.set(value);
052      return this;
053   }
054
055   /**
056    * Dispatches a single server-initiated request to the registered handler.
057    *
058    * <p>
059    * When no handler is registered: a notification (a <jk>null</jk> {@link JsonRpcRequest#getId() id}) is
060    * silently dropped (returns <jk>null</jk>); a request is rejected with a {@code -32601} {@link McpException}.
061    *
062    * @param request The inbound request. Must not be <jk>null</jk>.
063    * @param ctx The request-scoped bean store to hand to the handler. Can be <jk>null</jk>, in which case
064    *    {@link BasicBeanStore#INSTANCE} is used instead.
065    * @return The handler's result, or <jk>null</jk> for a notification with no registered handler.
066    * @throws McpException If no handler is registered and {@code request} is not a notification, or if the
067    *    handler itself throws (an {@link McpException} thrown by the handler propagates as-is; any other
068    *    exception is wrapped as a {@code -32603} internal-error {@link McpException} with the original
069    *    exception preserved as its cause).
070    */
071   public Object dispatch(JsonRpcRequest request, BeanStore ctx) {
072      reqnn("request", request);
073      var h = handler.get();
074      if (h == null) {
075         if (JsonRpcResponse.notification(request.getId()))
076            return null;
077         throw new McpException(-32601, f("No server-request handler registered for method '%s'", request.getMethod()));
078      }
079      try {
080         return h.handle(request, ctx == null ? BasicBeanStore.INSTANCE : ctx);
081      } catch (McpException e) {
082         throw e;
083      } catch (Exception e) {
084         var e2 = new McpException(-32603, "Internal error handling server request '" + request.getMethod() + "'");
085         e2.initCause(e);
086         throw e2;
087      }
088   }
089}