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.function.Consumer;
022
023import org.apache.juneau.bean.mcp.v20260728.ServerCapabilities;
024import org.apache.juneau.rest.server.mcp.BasicMcpSubscriptionBroker;
025import org.apache.juneau.rest.server.mcp.McpSubscriptionBroker;
026
027/**
028 * Binding-owned behavior configuration ("how" a {@code 2026-07-28} MCP endpoint behaves) — the single
029 * v2-only aggregate consolidating every per-binding tunable {@link McpEndpoint}/{@link McpRestServlet} previously
030 * exposed as six separate hooks: {@code capabilities}, {@code instructions}, {@code cache}, {@code mrtr},
031 * {@code subscriptions}, and {@code subscriptionBroker}.
032 *
033 * <p>
034 * <b>Mutable during setup, effectively immutable once published.</b> Same builder-less mutable-setup idiom as
035 * {@link McpCacheConfig}/{@link McpMrtrConfig}/{@link McpSubscriptionsConfig} and the neutral
036 * {@code org.apache.juneau.rest.server.mcp.McpServerConfig} it sits next to: configure fully via the single
037 * {@code createMcpOptions()} (servlet-subclass path) / {@code getMcpOptions()} (mixin path) override, then never
038 * mutate it afterward. The framework memoizes exactly one instance per binding (see {@link McpRestServlet#getMcpOptions()}
039 * and {@link McpEndpoint#getMcpOptions()}), so a request-time reader sees a stable, request-independent snapshot.
040 *
041 * <p>
042 * The three nested configs ({@link #getCache() cache}, {@link #getMrtr() mrtr}, {@link #getSubscriptions()
043 * subscriptions}) are framework-owned instances created once, here, at construction time. Use the
044 * {@link #cache(Consumer)}/{@link #mrtr(Consumer)}/{@link #subscriptions(Consumer)} configure-blocks to mutate
045 * them in place &mdash; callers never {@code new} a replacement nested config, so the "must return a stable
046 * instance" footgun the pre-consolidation hooks warned about cannot recur.
047 *
048 * <h5 class='section'>The subscription broker default:</h5>
049 * <p>
050 * {@link #getSubscriptionBroker()} defaults to <jk>null</jk>, meaning "framework-derived": the binding lazily
051 * derives a {@link BasicMcpSubscriptionBroker} sized from {@link #getSubscriptions()}'s
052 * {@link McpSubscriptionsConfig#getQueueSize() queueSize} the first time a broker is needed, and memoizes that
053 * one derived instance for the lifetime of the binding (a fresh broker per call would silently split live
054 * subscribers and publishers across unrelated registries). Call {@link #setSubscriptionBroker(McpSubscriptionBroker)}
055 * to supply a custom broker instead (for example one shared across multiple bindings on purpose).
056 *
057 * <h5 class='section'>The idleTimeoutMs / heartbeatIntervalMs caveat:</h5>
058 * <p>
059 * When configuring {@link #subscriptions(Consumer)}, keep
060 * {@link McpSubscriptionsConfig#getHeartbeatIntervalMs() heartbeatIntervalMs} below
061 * {@link McpSubscriptionsConfig#getIdleTimeoutMs() idleTimeoutMs} (see that method's Javadoc) &mdash; otherwise
062 * the idle-timeout watchdog can reap a perfectly healthy connection between two heartbeats.
063 *
064 * <h5 class='section'>Example:</h5>
065 * <pre>
066 * McpOptions <jv>options</jv> = <jk>new</jk> McpOptions()
067 *     .setInstructions(<js>"Use tool 'echo' to test."</js>)
068 *     .cache(<jv>c</jv> -&gt; <jv>c</jv>.setToolsList(<jk>new</jk> McpCacheHint().setTtlMs(60_000)))
069 *     .subscriptions(<jv>s</jv> -&gt; <jv>s</jv>.setQueueSize(2048));
070 * </pre>
071 */
072@SuppressWarnings({
073   "java:S115", // Constants use UPPER_snakeCase convention (e.g., MSG_consumerMustNotBeNull)
074   "java:S1192" // Duplicated validation-message literals read more clearly inline than as constants.
075})
076public class McpOptions {
077
078   private ServerCapabilities capabilities;
079   private String instructions;
080   private McpCacheConfig cache = new McpCacheConfig();
081   private McpMrtrConfig mrtr = new McpMrtrConfig();
082   private McpResourceServerConfig resourceServer = new McpResourceServerConfig();
083   private McpSubscriptionsConfig subscriptions = new McpSubscriptionsConfig();
084   private McpSubscriptionBroker subscriptionBroker;
085
086   @SuppressWarnings({
087      "java:S3077" // Volatile is required for correct double-checked locking in resolveSubscriptionBroker(): unlike a plain lazy read, two BasicMcpSubscriptionBroker instances are NOT equivalent (each holds distinct live per-connection subscription state), so exactly one derived broker must ever be created and published or a subscription registered against one instance would never be visible to a publish call against the other.
088   })
089   private volatile McpSubscriptionBroker derivedBroker;
090
091   private final Object lock = new Object();
092
093   /**
094    * Optional explicit capabilities advertisement for {@code server/discover}.
095    *
096    * @return The explicit capabilities, or <jk>null</jk> to auto-derive from the registered tool / prompt /
097    *    resource lists (the default).
098    */
099   public ServerCapabilities getCapabilities() {
100      return capabilities;
101   }
102
103   /**
104    * Sets the explicit {@code server/discover} capabilities advertisement.
105    *
106    * <p>
107    * A non-<jk>null</jk> value bypasses auto-derivation entirely and advertises exactly what is supplied.
108    *
109    * @param value The new value. Can be <jk>null</jk> to auto-derive.
110    * @return This object (for method chaining).
111    */
112   public McpOptions setCapabilities(ServerCapabilities value) {
113      capabilities = value;
114      return this;
115   }
116
117   /**
118    * Optional free-form {@code server/discover} usage instructions advertised to clients.
119    *
120    * @return The instructions, or <jk>null</jk> to omit them from the discovery result (the default).
121    */
122   public String getInstructions() {
123      return instructions;
124   }
125
126   /**
127    * Sets the {@code server/discover} usage instructions.
128    *
129    * @param value The new value. Can be <jk>null</jk> to omit them.
130    * @return This object (for method chaining).
131    */
132   public McpOptions setInstructions(String value) {
133      instructions = value;
134      return this;
135   }
136
137   /**
138    * This binding's cache-hint configuration.
139    *
140    * @return The framework-owned cache configuration. Never <jk>null</jk>.
141    */
142   public McpCacheConfig getCache() {
143      return cache;
144   }
145
146   /**
147    * Replaces this binding's cache configuration outright.
148    *
149    * @param value The new value. Must not be <jk>null</jk>.
150    * @return This object (for method chaining).
151    * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
152    */
153   public McpOptions setCache(McpCacheConfig value) {
154      if (value == null)
155         throw iaex("cache must not be null");
156      cache = value;
157      return this;
158   }
159
160   /**
161    * Mutates this binding's cache configuration in place.
162    *
163    * <p>
164    * Preferred over {@link #setCache(McpCacheConfig)} for incremental configuration since it operates on the
165    * framework-owned instance rather than requiring the caller to construct a replacement.
166    *
167    * @param consumer Callback invoked with the framework-owned {@link McpCacheConfig}. Must not be <jk>null</jk>.
168    * @return This object (for method chaining).
169    * @throws IllegalArgumentException If {@code consumer} is <jk>null</jk>.
170    */
171   public McpOptions cache(Consumer<McpCacheConfig> consumer) {
172      if (consumer == null)
173         throw iaex("consumer must not be null");
174      consumer.accept(cache);
175      return this;
176   }
177
178   /**
179    * This binding's MRTR (Multi-Round-Trip Request) configuration.
180    *
181    * @return The framework-owned MRTR configuration. Never <jk>null</jk>.
182    */
183   public McpMrtrConfig getMrtr() {
184      return mrtr;
185   }
186
187   /**
188    * Replaces this binding's MRTR configuration outright.
189    *
190    * @param value The new value. Must not be <jk>null</jk>.
191    * @return This object (for method chaining).
192    * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
193    */
194   public McpOptions setMrtr(McpMrtrConfig value) {
195      if (value == null)
196         throw iaex("mrtr must not be null");
197      mrtr = value;
198      return this;
199   }
200
201   /**
202    * Mutates this binding's MRTR configuration in place.
203    *
204    * @param consumer Callback invoked with the framework-owned {@link McpMrtrConfig}. Must not be <jk>null</jk>.
205    * @return This object (for method chaining).
206    * @throws IllegalArgumentException If {@code consumer} is <jk>null</jk>.
207    */
208   public McpOptions mrtr(Consumer<McpMrtrConfig> consumer) {
209      if (consumer == null)
210         throw iaex("consumer must not be null");
211      consumer.accept(mrtr);
212      return this;
213   }
214
215   /**
216    * This binding's OAuth 2.1 resource-server (RS) configuration.
217    *
218    * @return The framework-owned RS configuration. Never <jk>null</jk>.  Disabled by default.
219    */
220   public McpResourceServerConfig getResourceServer() {
221      return resourceServer;
222   }
223
224   /**
225    * Replaces this binding's resource-server configuration outright.
226    *
227    * @param value The new value. Must not be <jk>null</jk>.
228    * @return This object (for method chaining).
229    * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
230    */
231   public McpOptions setResourceServer(McpResourceServerConfig value) {
232      if (value == null)
233         throw iaex("resourceServer must not be null");
234      resourceServer = value;
235      return this;
236   }
237
238   /**
239    * Mutates this binding's resource-server configuration in place.
240    *
241    * @param consumer Callback invoked with the framework-owned {@link McpResourceServerConfig}. Must not be
242    *    <jk>null</jk>.
243    * @return This object (for method chaining).
244    * @throws IllegalArgumentException If {@code consumer} is <jk>null</jk>.
245    */
246   public McpOptions resourceServer(Consumer<McpResourceServerConfig> consumer) {
247      if (consumer == null)
248         throw iaex("consumer must not be null");
249      consumer.accept(resourceServer);
250      return this;
251   }
252
253   /**
254    * This binding's {@code subscriptions/listen} configuration.
255    *
256    * @return The framework-owned subscriptions configuration. Never <jk>null</jk>.
257    */
258   public McpSubscriptionsConfig getSubscriptions() {
259      return subscriptions;
260   }
261
262   /**
263    * Replaces this binding's subscriptions configuration outright.
264    *
265    * @param value The new value. Must not be <jk>null</jk>.
266    * @return This object (for method chaining).
267    * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
268    */
269   public McpOptions setSubscriptions(McpSubscriptionsConfig value) {
270      if (value == null)
271         throw iaex("subscriptions must not be null");
272      subscriptions = value;
273      return this;
274   }
275
276   /**
277    * Mutates this binding's subscriptions configuration in place.
278    *
279    * @param consumer Callback invoked with the framework-owned {@link McpSubscriptionsConfig}. Must not be
280    *    <jk>null</jk>.
281    * @return This object (for method chaining).
282    * @throws IllegalArgumentException If {@code consumer} is <jk>null</jk>.
283    */
284   public McpOptions subscriptions(Consumer<McpSubscriptionsConfig> consumer) {
285      if (consumer == null)
286         throw iaex("consumer must not be null");
287      consumer.accept(subscriptions);
288      return this;
289   }
290
291   /**
292    * The explicit subscription broker for this binding, if one was supplied.
293    *
294    * @return The explicit broker, or <jk>null</jk> if none was supplied (the default) &mdash; in which case the
295    *    binding lazily derives and memoizes one (see {@link #resolveSubscriptionBroker()}).
296    */
297   public McpSubscriptionBroker getSubscriptionBroker() {
298      return subscriptionBroker;
299   }
300
301   /**
302    * Sets an explicit subscription broker, bypassing the framework-derived default.
303    *
304    * <p>
305    * Setting this after {@link #resolveSubscriptionBroker()} has already derived and published a default
306    * broker for this instance does not retroactively clear or replace that already-published derived
307    * broker &mdash; {@link #resolveSubscriptionBroker()} always prefers an explicit value set here over a
308    * previously-derived one, but under the never-mutate-after-setup contract this method should only be
309    * called during configuration, before the binding is handed to the framework.
310    *
311    * @param value The new value. Can be <jk>null</jk> to revert to the framework-derived default.
312    * @return This object (for method chaining).
313    */
314   public McpOptions setSubscriptionBroker(McpSubscriptionBroker value) {
315      subscriptionBroker = value;
316      return this;
317   }
318
319   /**
320    * Returns {@link #getSubscriptionBroker()} if explicitly set, otherwise lazily derives and memoizes a
321    * {@link BasicMcpSubscriptionBroker} sized from {@link #getSubscriptions()}'s queue bound.
322    *
323    * <p>
324    * This is the <b>effective</b> broker accessor: unlike {@link #getSubscriptionBroker()} (which returns
325    * <jk>null</jk> by default), this method never returns <jk>null</jk>, deriving a default the first time
326    * one is needed. Internal plumbing for {@link McpEndpoint#subscriptionBroker()} /
327    * {@link McpRestServlet#getSubscriptionBroker()}. Since this {@link McpOptions} instance is itself
328    * memoized once per binding, the derived broker this method publishes is likewise stable for the
329    * binding's lifetime &mdash; publication uses double-checked locking (not a plain lazy read) because two
330    * {@link BasicMcpSubscriptionBroker} instances are <b>not</b> equivalent: each holds distinct live
331    * per-connection subscription state, so a benign publication race would silently split subscribers and
332    * publishers across two unrelated registries.
333    *
334    * @return The subscription broker to use. Never <jk>null</jk>.
335    */
336   public McpSubscriptionBroker resolveSubscriptionBroker() {
337      var explicit = subscriptionBroker;
338      if (explicit != null)
339         return explicit;
340      var result = derivedBroker;
341      if (result == null) {
342         synchronized (lock) {
343            result = derivedBroker;
344            if (result == null) {
345               result = new BasicMcpSubscriptionBroker(subscriptions.getQueueSize());
346               derivedBroker = result;
347            }
348         }
349      }
350      return result;
351   }
352}