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.v20260728;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.concurrent.atomic.AtomicReference;
022
023import org.apache.juneau.commons.inject.Bean;
024import org.apache.juneau.rest.server.*;
025import org.apache.juneau.rest.server.mcp.McpSubscriptionBroker;
026import org.apache.juneau.rest.server.tracing.TraceContextExtractor;
027
028import jakarta.servlet.http.HttpServletRequest;
029import jakarta.servlet.http.HttpServletResponse;
030
031/**
032 * Abstract MCP servlet bound to protocol revision {@code 2026-07-28}.
033 *
034 * <p>
035 * Subclass this (rather than the neutral {@link org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet}) to
036 * expose a {@code 2026-07-28} endpoint at {@code POST /}; the revision binding is a compile-time choice
037 * made by which class you extend.
038 *
039 * <p>
040 * URI and polymorphic-type serializer policy ({@code addBeanTypes} and {@code uriResolution="NONE"}) is
041 * inherited centrally from the neutral {@link org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet}; the only
042 * revision-specific behavior added here is notification empty-body processing, below.
043 *
044 * <p>
045 * Registers {@link McpNotificationResponseProcessor} so a {@code null} dispatch result (a JSON-RPC
046 * notification) renders as a genuinely empty HTTP body instead of the framework's default four-byte
047 * {@code null} JSON literal.
048 *
049 * <h5 class='section'>Example:</h5>
050 * <pre>
051 * @Rest(path="/mcp")
052 * public class MyMcpServlet extends McpRestServlet {
053 *     @Override
054 *     protected McpServerConfig createMcpConfig() {
055 *         return new McpServerConfig().setName("my-server").setVersion("1.0.0").addTool(new MyEchoTool());
056 *     }
057 *     @Override                                            // optional
058 *     protected McpOptions createMcpOptions() {
059 *         return new McpOptions()
060 *             .setInstructions("Use tool 'echo' to test.")
061 *             .cache(c -&gt; c.setToolsList(new McpCacheHint().setTtlMs(60_000)))
062 *             .subscriptions(s -&gt; s.setQueueSize(2048));
063 *     }
064 * }
065 * </pre>
066 *
067 * @serial exclude
068 */
069@SuppressWarnings({
070   "java:S110" // Inherent to extending the RestServlet hierarchy.
071})
072@Rest(responseProcessors = McpNotificationResponseProcessor.class)
073public abstract class McpRestServlet extends org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet {
074   private static final long serialVersionUID = 1L;
075
076   private final transient AtomicReference<McpOptions> mcpOptions = new AtomicReference<>();
077
078   @Override /* AbstractMcpRestServlet */
079   protected final org.apache.juneau.rest.server.mcp.McpRevision revision() {
080      var o = getMcpOptions();
081      return new McpRevision(o.getCapabilities(), o.getCache(), o.getInstructions(), o.getMrtr());
082   }
083
084   /**
085    * Publishes this revision's stable {@link TraceContextExtractor} so an active {@code TracerHook}
086    * (via {@code RestOpInvoker}) recognizes this endpoint's resolved {@code JsonRpcRequest} argument
087    * and its {@code params._meta} carrier before span creation.
088    *
089    * <p>
090    * A no-op when no {@code TracerHook} bean is registered &mdash; see the no-tracer fast-path
091    * contract documented on {@code TracerHook}.
092    *
093    * @return This revision's shared {@link TraceContextExtractor}. Never <jk>null</jk>.
094    */
095   @Bean
096   public final TraceContextExtractor mcpTraceContextExtractor() {
097      return McpRevision.TRACE_CONTEXT_EXTRACTOR;
098   }
099
100   /**
101    * Factory method for the behavior configuration published by {@link #getMcpOptions()}.
102    *
103    * <p>
104    * Override to customize any of capabilities/instructions/cache/mrtr/subscriptions/broker in one place. The
105    * default returns a plain {@link McpOptions} (all defaults).
106    *
107    * <p>
108    * Runs <b>eagerly during servlet/{@code RestContext} initialization, not lazily on first request</b>:
109    * {@link #getMcpOptions()} is itself a {@code @Bean} hook, so {@code RestContext}'s constructor invokes it
110    * &mdash; and therefore this method &mdash; while walking the resource's {@code @Bean} methods at startup,
111    * strictly before the embedded server's {@code start()} returns. Do not write an override that depends on
112    * runtime request state or on a port/address only known after {@code start()} (e.g. a Jetty ephemeral
113    * port); that state does not exist yet when this hook runs.
114    *
115    * <p>
116    * <b>Must be side-effect-free and idempotent</b>, exactly like
117    * {@link org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet#createMcpConfig()}: {@link #getMcpOptions()}
118    * calls this hook at most once per servlet instance and publishes the result via a lock-free
119    * {@link java.util.concurrent.atomic.AtomicReference#compareAndSet}, so under a first-access race it may be
120    * invoked by more than one thread, but only one resulting instance is ever published and returned.
121    *
122    * @return A non-<jk>null</jk> options instance.
123    */
124   protected McpOptions createMcpOptions() {
125      return new McpOptions();
126   }
127
128   /**
129    * Returns this servlet's binding-owned {@link McpOptions}, built eagerly during initialization.
130    *
131    * <p>
132    * Because this method is annotated {@code @Bean}, {@code RestContext}'s constructor invokes it (and
133    * therefore {@link #createMcpOptions()}) while the servlet is initializing &mdash; strictly before the
134    * embedded server's {@code start()} returns. This is <b>not</b> deferred to the first incoming HTTP
135    * request, despite the internal caching below; a {@link #createMcpOptions()} override must not depend on
136    * runtime request/port state, since none exists yet at that point. The {@link AtomicReference}-backed
137    * publish below exists only to make repeat/racing invocations of this {@code @Bean} method idempotent
138    * &mdash; every call after the first returns the same instance, mirroring exactly how
139    * {@link org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet#getMcpConfig() getMcpConfig()} memoizes
140    * &mdash; not to defer construction itself.
141    * Also published as a {@code @Bean} so downstream plumbing ({@link #getSubscriptionBroker()},
142    * {@code subscriptions/listen} dispatch) can resolve it uniformly through the {@code BeanStore}. This
143    * {@code @Bean} publication is authoritative and takes precedence over any Spring {@code @Bean McpOptions}
144    * in the app context: a Spring user who wants their configuration honored must inject it INTO
145    * {@link #createMcpOptions()}'s override (for example an {@code @Autowired} field read from there), not
146    * publish a bare {@code @Bean McpOptions} in their Spring context and expect it to be picked up instead.
147    *
148    * @return The options. Never <jk>null</jk>.
149    * @throws IllegalStateException If {@link #createMcpOptions()} returns <jk>null</jk>.
150    */
151   @Bean
152   public final McpOptions getMcpOptions() {
153      var o = mcpOptions.get();
154      if (o == null) {
155         var no = createMcpOptions();
156         if (no == null)
157            throw isex("createMcpOptions() returned null");
158         o = mcpOptions.compareAndSet(null, no) ? no : mcpOptions.get(); // HTT: the CAS-loses branch requires a genuine concurrent first-access race; untestable deterministically.
159      }
160      return o;
161   }
162
163   /**
164    * Returns this servlet's lazily-published, binding-owned subscription broker.
165    *
166    * <p>
167    * Reads {@link #getMcpOptions()}'s memoized {@link McpOptions#getSubscriptionBroker()}: an explicitly
168    * configured broker is returned as-is; otherwise one is derived and memoized per-binding, sized from
169    * {@link McpOptions#getSubscriptions()}'s queue bound (see {@link McpOptions#resolveSubscriptionBroker()}).
170    *
171    * @return The subscription broker. Never <jk>null</jk>.
172    */
173   @Override
174   public final McpSubscriptionBroker getSubscriptionBroker() {
175      return getMcpOptions().resolveSubscriptionBroker();
176   }
177
178   /**
179    * OAuth 2.1 resource-server bearer gate for the MCP {@code POST /} endpoint.
180    *
181    * <p>
182    * A no-op unless {@link McpResourceServerConfig#isEnabled() RS auth is enabled}; when enabled, every request that is
183    * a {@code POST}, or that claims one (the dedicated MCP servlet's only writable surface) must present a valid bearer
184    * token or it is rejected with {@code 401} / {@code 403} and a {@code WWW-Authenticate: Bearer ...} challenge (see
185    * {@link McpResourceServerSupport#gateMcpServlet(McpResourceServerConfig, HttpServletRequest, HttpServletResponse)}).
186    * The gate treats a {@code POST} claim on any override channel ({@code ?method=} or any of the four override
187    * headers, any value) as a {@code POST}, rather than reading only the raw servlet method, so a method-override cannot
188    * slip past it.  The well-known {@code GET} routes below are
189    * intentionally left unauthenticated so a client can discover the PRM document.
190    *
191    * @param req The raw HTTP request.
192    * @param res The raw HTTP response (used to emit the challenge header on rejection).
193    */
194   @RestStartCall
195   public final void mcpResourceServerAuth(HttpServletRequest req, HttpServletResponse res) {
196      McpResourceServerSupport.gateMcpServlet(getMcpOptions().getResourceServer(), req, res);
197   }
198
199   /**
200    * Serves the RFC 9728 Protected Resource Metadata document at the root well-known location (SEP-2351 fallback).
201    *
202    * <p>
203    * This route (and its path-inserted sibling) is registered unconditionally, but returns {@code 404} whenever RS auth
204    * is {@link McpResourceServerConfig#isEnabled() disabled} &mdash; so a non-opted-in servlet claims the well-known
205    * namespace but exposes no observable metadata.
206    *
207    * @param req The REST request.
208    * @return The PRM document.
209    */
210   @RestGet(path = "/.well-known/oauth-protected-resource")
211   public McpProtectedResourceMetadata mcpProtectedResourceMetadataRoot(RestRequest req) {
212      return McpResourceServerSupport.metadataForWellKnownRoot(getMcpOptions().getResourceServer(), req);
213   }
214
215   /**
216    * Serves the RFC 9728 Protected Resource Metadata document at the path-inserted well-known location (SEP-2351), but
217    * only when the requested suffix matches this resource's expected well-known path; otherwise {@code 404}.
218    *
219    * @param req The REST request.
220    * @return The PRM document.
221    */
222   @RestGet(path = "/.well-known/oauth-protected-resource/*")
223   public McpProtectedResourceMetadata mcpProtectedResourceMetadataPathInserted(RestRequest req) {
224      return McpResourceServerSupport.metadataForWellKnownPathInserted(getMcpOptions().getResourceServer(), req);
225   }
226}