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 — 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) — 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> -> <jv>c</jv>.setToolsList(<jk>new</jk> McpCacheHint().setTtlMs(60_000))) 069 * .subscriptions(<jv>s</jv> -> <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) — 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 — {@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 — 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}