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}