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 org.apache.juneau.commons.inject.Bean;
020import org.apache.juneau.rest.server.RestGet;
021import org.apache.juneau.rest.server.RestStartCall;
022import org.apache.juneau.rest.server.mcp.McpSubscriptionBroker;
023import org.apache.juneau.rest.server.tracing.TraceContextExtractor;
024
025import jakarta.servlet.http.HttpServletRequest;
026import jakarta.servlet.http.HttpServletResponse;
027
028/**
029 * Mixin interface that exposes a {@code 2026-07-28} MCP endpoint at {@code POST /mcp} on any Juneau REST
030 * resource.
031 *
032 * <p>
033 * The mixin path and the servlet-subclass path ({@link McpRestServlet}) are at parity: both bind the revision
034 * at compile time and both read the same {@link McpOptions} shape for behavior configuration
035 * (capabilities/instructions/cache/mrtr/subscriptions/broker) via a single override.
036 *
037 * <h5 class='section'>Example:</h5>
038 * <pre>
039 * @Rest(path="/api")
040 * public class MyResource extends BasicRestServlet implements McpEndpoint {
041 *     @Override
042 *     public McpServerConfig getMcpConfig() {
043 *         return new McpServerConfig().addTool(new MyEchoTool());
044 *     }
045 *     @Override                                        // optional
046 *     public McpOptions getMcpOptions() {
047 *         return new McpOptions().mrtr(m -&gt; m.setTtlMs(600_000));
048 *     }
049 * }
050 * </pre>
051 */
052public interface McpEndpoint extends org.apache.juneau.rest.server.mcp.McpEndpointMixin {
053
054   @Override /* McpEndpointMixin */
055   default org.apache.juneau.rest.server.mcp.McpRevision revision() {
056      var o = McpEndpointOptionsCache.resolve(this);
057      return new McpRevision(o.getCapabilities(), o.getCache(), o.getInstructions(), o.getMrtr());
058   }
059
060   /**
061    * Publishes this revision's stable {@link TraceContextExtractor} so an active {@code TracerHook}
062    * (via {@code RestOpInvoker}) recognizes this endpoint's resolved {@code JsonRpcRequest} argument
063    * and its {@code params._meta} carrier before span creation.
064    *
065    * <p>
066    * A no-op when no {@code TracerHook} bean is registered &mdash; see the no-tracer fast-path
067    * contract documented on {@code TracerHook}.
068    *
069    * @return This revision's shared {@link TraceContextExtractor}. Never <jk>null</jk>.
070    */
071   @Bean
072   default TraceContextExtractor mcpTraceContextExtractor() {
073      return McpRevision.TRACE_CONTEXT_EXTRACTOR;
074   }
075
076   /**
077    * This endpoint's behavior configuration &mdash; capabilities, instructions, cache hints, MRTR, subscriptions,
078    * and the subscription broker, all in one place.
079    *
080    * <p>
081    * The default returns a per-binding-fresh {@link McpOptions} (all defaults; no live-state sharing across
082    * endpoint instances). Override to customize any of the six concepts.
083    *
084    * <p>
085    * <b>This method is invoked exactly once per endpoint instance</b> and the result is memoized for that
086    * instance's lifetime by the framework internals that read it ({@link #revision()},
087    * {@link #subscriptionBroker()}, the {@code @Bean}-published accessor) &mdash; so an override may freely
088    * construct and configure a fresh instance on every call, exactly as shown above; it never needs to cache the
089    * result itself, and returning a fresh instance per call cannot reintroduce the "must return a stable
090    * instance" footgun the pre-consolidation per-concept hooks warned about.
091    *
092    * @return This endpoint's options. Must not be <jk>null</jk>.
093    */
094   default McpOptions getMcpOptions() {
095      return new McpOptions();
096   }
097
098   /**
099    * Publishes this endpoint's memoized {@link McpOptions} into this resource's {@code RestContext} bean store,
100    * the same {@code @Bean}-method mechanism already used by {@link #mcpTraceContextExtractor()}.
101    *
102    * <p>
103    * This is what lets {@code McpRevision.dispatch(...)}'s {@code subscriptions/listen} branch resolve the
104    * real, possibly-overridden subscriptions configuration via {@code ctx.getBean(McpOptions.class)} instead of
105    * always falling through to that call's defensive {@code new McpSubscriptionsConfig()} default &mdash; the
106    * neutral core cannot add this bean itself ({@code McpOptions} is a v2 type, and the neutral module must not
107    * import it), so this v2-side {@code @Bean} method is the wiring's only seam.
108    *
109    * @return This endpoint's memoized options. Never <jk>null</jk>.
110    */
111   @Bean
112   default McpOptions mcpOptionsBean() {
113      return McpEndpointOptionsCache.resolve(this);
114   }
115
116   /**
117    * Subscription broker for this endpoint's bound revision.
118    *
119    * <p>
120    * Reads {@link #getMcpOptions()}'s memoized {@link McpOptions#getSubscriptionBroker()}: an explicitly
121    * configured broker is returned as-is; otherwise one is derived and memoized per-binding, sized from
122    * {@link McpOptions#getSubscriptions()}'s queue bound (see {@link McpOptions#resolveSubscriptionBroker()}).
123    * Because {@link McpOptions} itself is memoized once per endpoint instance ({@link McpEndpointOptionsCache}),
124    * two distinct endpoint instances resolve to two distinct broker registries by default &mdash; there is no
125    * accidental JVM-wide sharing the way the pre-consolidation mixin default had.
126    *
127    * @return The subscription broker. Never <jk>null</jk>.
128    */
129   @Override
130   default McpSubscriptionBroker subscriptionBroker() {
131      return McpEndpointOptionsCache.resolve(this).resolveSubscriptionBroker();
132   }
133
134   /**
135    * OAuth 2.1 resource-server bearer gate for the mixin's MCP {@code POST /mcp} endpoint.
136    *
137    * <p>
138    * A no-op unless {@link McpResourceServerConfig#isEnabled() RS auth is enabled}; when enabled, a request that
139    * <b>the router resolves</b> to the MCP {@code POST /mcp} dispatch operation must present a valid bearer token or it is
140    * rejected with {@code 401} / {@code 403} and a {@code WWW-Authenticate: Bearer ...} challenge.  The gate is scoped to
141    * the MCP dispatch <i>operation</i> (identified by resolved-operation identity, or by that operation's own router
142    * matchers &mdash; see {@link McpResourceServerSupport#resolvesToMcpDispatch}) so other {@code POST} operations on the
143    * host resource are unaffected, and it cannot drift from the router: a trailing slash, percent-encoding, a
144    * {@code POST} claim on any override channel ({@code ?method=} or an override header), or a host-chosen {@code @Mixin(path/paths)} re-mount cannot slip past it
145    *.  The well-known {@code GET} routes are left unauthenticated for PRM discovery.
146    *
147    * @param ctx The host {@link org.apache.juneau.rest.server.RestContext} handling this request.
148    * @param req The raw HTTP request.
149    * @param res The raw HTTP response (used to emit the challenge header on rejection).
150    */
151   @RestStartCall
152   default void mcpResourceServerAuth(org.apache.juneau.rest.server.RestContext ctx, HttpServletRequest req, HttpServletResponse res) {
153      McpResourceServerSupport.gateMcpEndpoint(McpEndpointOptionsCache.resolve(this).getResourceServer(), ctx, req, res);
154   }
155
156   /**
157    * Serves the RFC 9728 Protected Resource Metadata document at the root well-known location (SEP-2351 fallback).
158    *
159    * <p>
160    * This route (and its path-inserted sibling) is registered unconditionally, but returns {@code 404} whenever RS auth
161    * is {@link McpResourceServerConfig#isEnabled() disabled} &mdash; so a non-opted-in endpoint claims the well-known
162    * namespace but exposes no observable metadata.
163    *
164    * @param req The REST request.
165    * @return The PRM document.
166    */
167   @RestGet(path = "/.well-known/oauth-protected-resource")
168   default McpProtectedResourceMetadata mcpProtectedResourceMetadataRoot(org.apache.juneau.rest.server.RestRequest req) {
169      return McpResourceServerSupport.metadataForWellKnownRoot(McpEndpointOptionsCache.resolve(this).getResourceServer(), req);
170   }
171
172   /**
173    * Serves the RFC 9728 Protected Resource Metadata document at the path-inserted well-known location (SEP-2351), but
174    * only when the requested suffix matches this resource's expected well-known path; otherwise {@code 404}.
175    *
176    * @param req The REST request.
177    * @return The PRM document.
178    */
179   @RestGet(path = "/.well-known/oauth-protected-resource/*")
180   default McpProtectedResourceMetadata mcpProtectedResourceMetadataPathInserted(org.apache.juneau.rest.server.RestRequest req) {
181      return McpResourceServerSupport.metadataForWellKnownPathInserted(McpEndpointOptionsCache.resolve(this).getResourceServer(), req);
182   }
183}