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.server.mcp; 018 019import org.apache.juneau.bean.jsonrpc.*; 020import org.apache.juneau.commons.inject.*; 021import org.apache.juneau.http.Content; 022import org.apache.juneau.marshall.serializer.*; 023import org.apache.juneau.rest.server.*; 024 025/** 026 * Mixin interface that exposes an MCP JSON-RPC endpoint at {@code POST /mcp} on any Juneau REST resource. 027 * 028 * <p> 029 * Implementing classes supply their {@link McpServerConfig} via {@link #getMcpConfig()} and their 030 * protocol revision via {@link #revision()}. In practice a consumer implements a revision-specific 031 * sub-interface (for example {@code org.apache.juneau.rest.server.mcp.v20250618.McpEndpoint}), which supplies {@link #revision()} for them. 032 * 033 * <h5 class='section'>Example:</h5> 034 * <pre> 035 * @Rest(path="/api") 036 * public class MyResource extends BasicRestServlet implements org.apache.juneau.rest.server.mcp.v20250618.McpEndpoint { 037 * @Override 038 * public McpServerConfig getMcpConfig() { 039 * return new McpServerConfig().addTool(new MyEchoTool()); 040 * } 041 * } 042 * </pre> 043 * 044 * <p> 045 * Named {@code *Mixin} (not {@code Abstract*}) because this is an interface, not an abstract class - 046 * {@code Abstract*} is this repo's naming convention for abstract classes only. {@link McpRevision}, the 047 * other core type dated subclasses de-versioning-shadow, deliberately keeps its plain name: end-users 048 * never implement it directly (only a bound revision's own subclass does), so it never faces the 049 * same-simple-name collision this mixin interface was renamed to avoid. 050 */ 051public interface McpEndpointMixin { 052 053 /** 054 * Returns the {@link McpServerConfig} backing this endpoint. 055 * 056 * @return The config. Never {@code null}. 057 */ 058 McpServerConfig getMcpConfig(); 059 060 /** 061 * The MCP protocol revision this endpoint speaks. 062 * 063 * @return The bound revision. Never {@code null}. 064 */ 065 McpRevision revision(); 066 067 /** 068 * Optional subscription broker backing this endpoint's {@code subscriptions/listen} support. 069 * 070 * <p> 071 * Returning <jk>null</jk> (the default) means this endpoint does not support subscriptions: no 072 * {@link McpSubscriptionBroker} or {@link McpSubscriptions} bean is added to the request-scoped 073 * {@link BeanStore} that {@link #handleMcpRequest} builds. 074 * 075 * <p> 076 * A revision binding that implements {@code subscriptions/listen} (for example 077 * {@code org.apache.juneau.rest.server.mcp.v20260728.McpEndpoint}) overrides this to return a 078 * binding-owned broker instance (its own per-binding vs. shared-instance semantics are that 079 * revision's own choice — see its Javadoc). 080 * 081 * @return The subscription broker, or <jk>null</jk> if this endpoint does not support subscriptions. 082 */ 083 default McpSubscriptionBroker subscriptionBroker() { 084 return null; 085 } 086 087 /** 088 * Default MCP JSON-RPC endpoint handler. 089 * 090 * <p> 091 * Implementations may override this method to customize routing (path / annotations) but must 092 * still dispatch through {@link #revision()}. 093 * 094 * <p> 095 * Applies the same serializer policy as {@link AbstractMcpRestServlet} ({@code addBeanTypes} and 096 * {@code uriResolution="NONE"}), so the two neutral HTTP entrypoints stay at parity. 097 * 098 * @param req JSON-RPC request envelope. 099 * @param restReq The current REST request. 100 * @return The response, or {@code null} for notifications. 101 */ 102 @SuppressWarnings({ 103 "resource" // Request-scoped scratch BasicBeanStore; lifetime is bounded by this handler invocation, no foreign resources are captured. 104 }) 105 @SerializerConfig(addBeanTypes = "true", uriResolution = "NONE") 106 @RestPost(path = "/mcp") 107 default Object handleMcpRequest(@Content JsonRpcRequest req, RestRequest restReq) { 108 var bs = new BasicBeanStore(restReq.getContext().getBeanStore()) 109 .addBean(RestRequest.class, restReq); 110 var broker = subscriptionBroker(); 111 if (broker != null) 112 bs.addBean(McpSubscriptionBroker.class, broker).addBean(McpSubscriptions.class, broker); 113 var exchange = new McpExchange(req, n -> restReq.getHeaderParam(n).asString().orElse(null)); 114 var result = revision().dispatch(exchange, getMcpConfig(), bs); 115 if (result instanceof McpResponseResult r) 116 return r.response(); 117 if (result instanceof McpStreamResult r) 118 return r.stream(); 119 return null; 120 } 121}