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}