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 -> 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 — 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 — 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) — 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 — 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 — 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 — 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} — 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}