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; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.util.*; 022 023import org.apache.juneau.commons.inject.*; 024 025/** 026 * Aggregate configuration consumed by the bound {@link McpRevision}. 027 * 028 * <p> 029 * Applications register a single {@link McpServerConfig} (typically as a bean in their {@code RestContext} 030 * bean store) listing the tools, prompts, resources, and resource templates to expose, plus optional server 031 * metadata and a pagination strategy. 032 * 033 * <p> 034 * This type is revision-neutral: it holds no field typed with any protocol revision's wire beans, and 035 * in particular holds no capabilities field in any form. The protocol version is owned by 036 * {@link McpRevision#protocolVersion()}, and an explicit capabilities advertisement is owned by a 037 * revision-specific hook on that revision's servlet or endpoint mixin. 038 */ 039@SuppressWarnings({ 040 "java:S135", // Handler-validation and completer-lookup loops (validatePrompts, validateResourceTemplates, resolveResourceTemplate, completer*) use independent continue guard clauses 041 "java:S3077", // The volatile fields hold immutable List/Map snapshots that are replaced wholesale when resource templates are published, so volatile only publishes a fully-built reference 042 "java:S3776" // validateResourceTemplates(), resolveResourceTemplate() and the completer lookups each match handlers against several URI/argument rules in one pass 043}) 044public class McpServerConfig { 045 046 private String name; 047 private String version; 048 private String instructions; 049 private List<McpToolHandler> tools = l(); 050 private List<McpPromptHandler> prompts = l(); 051 private List<McpResourceHandler> resources = l(); 052 private List<McpResourceTemplateHandler> resourceTemplates = l(); 053 054 private volatile List<McpResourceTemplateHandler> validatedResourceTemplatesSnapshot = List.of(); 055 private volatile Map<McpResourceTemplateHandler, McpUriTemplateMatcher> compiledResourceTemplateMatchers = Map.of(); 056 private McpCursor cursor = McpCursor.SINGLE_PAGE; 057 058 /** 059 * Server name reported in {@code initialize}. 060 * 061 * <p> 062 * When both this and {@link #getVersion()} are {@code null}, the bound revision substitutes its 063 * own default server identity. 064 * 065 * @return The name, or {@code null} if not set. 066 */ 067 public String getName() { 068 return name; 069 } 070 071 /** 072 * Sets the server name. 073 * 074 * @param value The new value. Can be <jk>null</jk> to unset the property. 075 * @return This object. 076 */ 077 public McpServerConfig setName(String value) { 078 name = value; 079 return this; 080 } 081 082 /** 083 * Server version reported in {@code initialize}. 084 * 085 * @return The version, or {@code null} if not set. 086 */ 087 public String getVersion() { 088 return version; 089 } 090 091 /** 092 * Sets the server version. 093 * 094 * @param value The new value. Can be <jk>null</jk> to unset the property. 095 * @return This object. 096 */ 097 public McpServerConfig setVersion(String value) { 098 version = value; 099 return this; 100 } 101 102 /** 103 * Optional instructions surfaced via {@code initialize}. 104 * 105 * @return The instructions, or {@code null} if not set. 106 */ 107 public String getInstructions() { 108 return instructions; 109 } 110 111 /** 112 * Sets initialize instructions. 113 * 114 * @param instructions The new value. 115 * @return This object (for method chaining). 116 */ 117 public McpServerConfig setInstructions(String instructions) { 118 this.instructions = instructions; 119 return this; 120 } 121 122 /** 123 * Registered tool handlers. 124 * 125 * @return Mutable list of handlers. Never {@code null}. 126 */ 127 public List<McpToolHandler> getTools() { 128 return tools; 129 } 130 131 /** 132 * Sets the tool handler list. 133 * 134 * @param tools The new value (or {@code null} to clear). 135 * @return This object (for method chaining). 136 */ 137 public McpServerConfig setTools(List<McpToolHandler> tools) { 138 this.tools = tools == null ? l() : new ArrayList<>(tools); 139 return this; 140 } 141 142 /** 143 * Convenience: append one or more tool handlers. 144 * 145 * @param handlers Handlers to add. 146 * @return This object (for method chaining). 147 */ 148 public McpServerConfig addTool(McpToolHandler... handlers) { 149 Collections.addAll(this.tools, handlers); 150 return this; 151 } 152 153 /** 154 * Convenience: adapt and append a single typed tool handler. 155 * 156 * <p> 157 * The handler is bridged into a raw {@link McpToolHandler} via 158 * {@link McpTypedHandlers#adaptTool(McpTypedToolHandler)} before it is stored. This overload is intentionally 159 * singular: a typed varargs overload would make {@code addTool()} ambiguous. To register multiple typed 160 * handlers, use chained {@code addTool(...)} calls, or adapt them explicitly with 161 * {@link McpTypedHandlers#adaptTool(McpTypedToolHandler)} and pass the results to 162 * {@link #addTool(McpToolHandler...)}. 163 * 164 * @param handler The typed handler to adapt and add. 165 * @return This object (for method chaining). 166 */ 167 public McpServerConfig addTool(McpTypedToolHandler<?,?> handler) { 168 tools.add(McpTypedHandlers.adaptTool(handler)); 169 return this; 170 } 171 172 /** 173 * Registered prompt handlers. 174 * 175 * @return Mutable list of handlers. Never {@code null}. 176 */ 177 public List<McpPromptHandler> getPrompts() { 178 return prompts; 179 } 180 181 /** 182 * Sets the prompt handler list. 183 * 184 * <p> 185 * The complete resulting registry is validated before publication; on failure, the previously published 186 * registry is left unchanged. See {@link #addPrompt(McpPromptHandler...)} for the validated conditions. 187 * 188 * @param prompts The new value (or {@code null} to clear). 189 * @return This object (for method chaining). 190 * @throws IllegalArgumentException If the resulting registry would be invalid. 191 */ 192 public McpServerConfig setPrompts(List<McpPromptHandler> prompts) { 193 var candidate = prompts == null ? new ArrayList<McpPromptHandler>() : new ArrayList<>(prompts); 194 validatePrompts(candidate); 195 this.prompts = candidate; 196 return this; 197 } 198 199 /** 200 * Convenience: append one or more prompt handlers. 201 * 202 * <p> 203 * The complete resulting registry - the handlers already registered plus {@code handlers} - is validated 204 * before publication; on failure, the previously published registry is left unchanged. Fails fast with 205 * {@link IllegalArgumentException} for a single prompt whose declared {@link McpPromptArgument} list either: 206 * <ul> 207 * <li>repeats a non-{@code null} argument name; or 208 * <li>attaches a non-{@code null} {@link McpPromptArgument#getCompleter() completer} to a {@code null} or 209 * blank argument name. 210 * </ul> 211 * 212 * @param handlers Handlers to add. 213 * @return This object (for method chaining). 214 * @throws IllegalArgumentException If the resulting registry would be invalid. 215 */ 216 public McpServerConfig addPrompt(McpPromptHandler... handlers) { 217 var candidate = new ArrayList<>(this.prompts); 218 Collections.addAll(candidate, handlers); 219 validatePrompts(candidate); 220 this.prompts = candidate; 221 return this; 222 } 223 224 /** 225 * Validates one candidate prompt registry in full, throwing {@link IllegalArgumentException} on the first 226 * invalid entry, so a misconfigured prompt argument is caught at registration time rather than during 227 * {@code completion/complete} dispatch. See {@link #addPrompt(McpPromptHandler...)} for the validated 228 * conditions. 229 * 230 * <p> 231 * A {@code null} handler, a handler whose {@link McpPromptHandler#descriptor()} returns {@code null}, or a 232 * descriptor with {@code null} {@link McpPromptSpec#getArguments() arguments} carries nothing to validate 233 * here and is silently skipped; those are not this validation's concern. 234 */ 235 private static void validatePrompts(List<McpPromptHandler> candidate) { 236 for (var i = 0; i < candidate.size(); i++) { 237 var handler = candidate.get(i); 238 if (handler == null) 239 continue; 240 var descriptor = handler.descriptor(); 241 if (descriptor == null) 242 continue; 243 var arguments = descriptor.getArguments(); 244 if (arguments == null) 245 continue; 246 var seenNames = new HashSet<String>(); 247 for (var j = 0; j < arguments.size(); j++) { 248 var argument = arguments.get(j); 249 if (argument == null) 250 continue; 251 var name = argument.getName(); 252 if (argument.getCompleter() != null && ib(name)) 253 throw iaex( 254 "Invalid prompt registration at index %s (prompt '%s'), argument index %s: a completer must not be attached to a null or blank argument name", 255 i, descriptor.getName(), j); 256 if (name != null && ! seenNames.add(name)) 257 throw iaex("Invalid prompt registration at index %s (prompt '%s'): duplicate argument name '%s'", 258 i, descriptor.getName(), name); 259 } 260 } 261 } 262 263 /** 264 * Registered resource handlers. 265 * 266 * @return Mutable list of handlers. Never {@code null}. 267 */ 268 public List<McpResourceHandler> getResources() { 269 return resources; 270 } 271 272 /** 273 * Sets the resource handler list. 274 * 275 * @param resources The new value (or {@code null} to clear). 276 * @return This object (for method chaining). 277 */ 278 public McpServerConfig setResources(List<McpResourceHandler> resources) { 279 this.resources = resources == null ? l() : new ArrayList<>(resources); 280 return this; 281 } 282 283 /** 284 * Convenience: append one or more resource handlers. 285 * 286 * @param handlers Handlers to add. 287 * @return This object (for method chaining). 288 */ 289 public McpServerConfig addResource(McpResourceHandler... handlers) { 290 Collections.addAll(this.resources, handlers); 291 return this; 292 } 293 294 /** 295 * Registered resource-template handlers. 296 * 297 * <p> 298 * Every read of this property (re)validates the current registry if the list has been mutated directly 299 * since the last validated {@link #setResourceTemplates(List)}, {@link #addResourceTemplate(McpResourceTemplateHandler...)}, 300 * {@link #addResourceTemplate(McpResourceTemplateSpec...)}, or {@link #setResourceTemplateSpecs(List)} call, 301 * so direct-list corruption is caught on first consumption rather than silently propagating to dispatch. 302 * A registry that has not been mutated since its last successful validation is returned without 303 * recompiling its templates. 304 * 305 * @return Mutable list of handlers. Never {@code null}. 306 * @throws IllegalArgumentException If the list was mutated directly into an invalid state (see 307 * {@link #addResourceTemplate(McpResourceTemplateHandler...)} for the validated conditions). 308 */ 309 public List<McpResourceTemplateHandler> getResourceTemplates() { 310 ensureResourceTemplatesValid(); 311 return resourceTemplates; 312 } 313 314 /** 315 * Sets the resource-template handler list. 316 * 317 * <p> 318 * The complete resulting registry is validated before publication; on failure, the previously published 319 * registry is left unchanged. See {@link #addResourceTemplate(McpResourceTemplateHandler...)} for the 320 * validated conditions. 321 * 322 * @param value The new value (or {@code null} to clear). 323 * @return This object (for method chaining). 324 * @throws IllegalArgumentException If the resulting registry would be invalid. 325 */ 326 public McpServerConfig setResourceTemplates(List<McpResourceTemplateHandler> value) { 327 var candidate = value == null ? new ArrayList<McpResourceTemplateHandler>() : new ArrayList<>(value); 328 publishResourceTemplates(candidate); 329 return this; 330 } 331 332 /** 333 * Sets the resource-template registry from bare descriptors, replacing each with a built-in listing-only 334 * handler equivalent to what {@link #addResourceTemplate(McpResourceTemplateSpec...)} wraps. 335 * 336 * <p> 337 * The complete resulting registry is validated before publication; on failure, the previously published 338 * registry is left unchanged. 339 * 340 * @param value The new descriptor list (or {@code null} to clear). 341 * @return This object (for method chaining). 342 * @throws IllegalArgumentException If the resulting registry would be invalid. 343 */ 344 public McpServerConfig setResourceTemplateSpecs(List<McpResourceTemplateSpec> value) { 345 return setResourceTemplates(value == null ? null : value.stream().map(McpServerConfig::listingOnlyTemplate).toList()); 346 } 347 348 /** 349 * Convenience: append one or more resource-template handlers. 350 * 351 * <p> 352 * The complete resulting registry - the handlers already registered plus {@code values} - is validated 353 * before publication; on failure, the previously published registry is left unchanged. Fails fast with 354 * {@link IllegalArgumentException} naming the offending registration index and template for: 355 * <ul> 356 * <li>a {@code null} handler; 357 * <li>a handler whose {@link McpResourceTemplateHandler#descriptor()} returns {@code null}; 358 * <li>a descriptor with a {@code null} or blank {@code uriTemplate}; 359 * <li>a malformed {@code uriTemplate} (delegated to {@link McpUriTemplateMatcher#compile(String)}); and 360 * <li>an exact-duplicate {@code uriTemplate} string within the resulting registry. 361 * </ul> 362 * A syntactically valid {@code uriTemplate} outside {@link McpUriTemplateMatcher}'s reverse-matchable 363 * subset is a legal registration, not an error. 364 * 365 * @param values Handlers to add. 366 * @return This object (for method chaining). 367 * @throws IllegalArgumentException If the resulting registry would be invalid. 368 */ 369 public McpServerConfig addResourceTemplate(McpResourceTemplateHandler... values) { 370 var candidate = new ArrayList<>(resourceTemplates); 371 Collections.addAll(candidate, values); 372 publishResourceTemplates(candidate); 373 return this; 374 } 375 376 /** 377 * Convenience: append one or more resource-template descriptors as built-in listing-only handlers. 378 * 379 * <p> 380 * Each descriptor is wrapped in a handler whose {@link McpResourceTemplateHandler#descriptor()} returns 381 * that same descriptor instance, whose {@link McpResourceTemplateHandler#read(String, Map, BeanStore) read} 382 * always returns {@code null}, and whose {@link McpResourceTemplateHandler#completer(String) completer} 383 * always returns {@code null}. The resulting registry is validated exactly as in 384 * {@link #addResourceTemplate(McpResourceTemplateHandler...)}. 385 * 386 * @param values Descriptors to add. 387 * @return This object (for method chaining). 388 * @throws IllegalArgumentException If the resulting registry would be invalid. 389 */ 390 public McpServerConfig addResourceTemplate(McpResourceTemplateSpec... values) { 391 var wrapped = new McpResourceTemplateHandler[values.length]; 392 for (var i = 0; i < values.length; i++) 393 wrapped[i] = listingOnlyTemplate(values[i]); 394 return addResourceTemplate(wrapped); 395 } 396 397 private static McpResourceTemplateHandler listingOnlyTemplate(McpResourceTemplateSpec spec) { 398 return new McpResourceTemplateHandler() { 399 @Override public McpResourceTemplateSpec descriptor() { return spec; } 400 @Override public McpResourceOutcome read(String uri, Map<String,String> variables, BeanStore ctx) { return null; } 401 }; 402 } 403 404 /** 405 * Validates {@code candidate} and, on success, publishes it as the current registry and its already-valid 406 * snapshot (so the very next {@link #getResourceTemplates()} call does not repeat the validation just 407 * performed here). On failure the previously published registry is left untouched. 408 */ 409 private void publishResourceTemplates(List<McpResourceTemplateHandler> candidate) { 410 var compiled = validateResourceTemplates(candidate); 411 resourceTemplates = candidate; 412 validatedResourceTemplatesSnapshot = List.copyOf(candidate); 413 compiledResourceTemplateMatchers = compiled; 414 } 415 416 /** 417 * Revalidates the live {@link #resourceTemplates} list if it differs from the last validated snapshot, 418 * catching mutation performed directly through the mutable list returned by {@link #getResourceTemplates()}. 419 * A registry that is unchanged since its last successful validation is left alone: templates are not 420 * recompiled on every call. 421 */ 422 private void ensureResourceTemplatesValid() { 423 var current = resourceTemplates; 424 if (validatedResourceTemplatesSnapshot.equals(current)) 425 return; 426 var compiled = validateResourceTemplates(current); 427 validatedResourceTemplatesSnapshot = List.copyOf(current); 428 compiledResourceTemplateMatchers = compiled; 429 } 430 431 /** 432 * Validates one candidate resource-template registry in full, throwing {@link IllegalArgumentException} 433 * on the first invalid entry. See {@link #addResourceTemplate(McpResourceTemplateHandler...)} for the 434 * validated conditions. 435 * 436 * <p> 437 * As a side benefit of validation already compiling every entry's {@link McpUriTemplateMatcher} once, 438 * this returns an identity-keyed map from each validated handler to its compiled matcher, so callers can 439 * publish it as {@link #compiledResourceTemplateMatchers} and reuse the compiled matcher on the 440 * per-request paths ({@link #resolveResourceTemplate(String)}, {@link #hasAnyCompleter()}, 441 * {@link #templateCompleter(String, String)}) instead of recompiling from the {@code uriTemplate} string 442 * on every call. 443 */ 444 private static Map<McpResourceTemplateHandler, McpUriTemplateMatcher> validateResourceTemplates(List<McpResourceTemplateHandler> candidate) { 445 var seenTemplates = new HashSet<String>(); 446 var compiled = new IdentityHashMap<McpResourceTemplateHandler, McpUriTemplateMatcher>(); 447 for (var i = 0; i < candidate.size(); i++) { 448 var handler = candidate.get(i); 449 if (handler == null) 450 throw iaex("Invalid resource-template registration at index %s: handler must not be null", i); 451 var descriptor = handler.descriptor(); 452 if (descriptor == null) 453 throw iaex("Invalid resource-template registration at index %s: descriptor() must not return null", i); 454 var uriTemplate = descriptor.getUriTemplate(); 455 if (ib(uriTemplate)) 456 throw iaex("Invalid resource-template registration at index %s (template '%s'): uriTemplate must not be null or blank", 457 i, uriTemplate); 458 McpUriTemplateMatcher matcher; 459 try { 460 matcher = McpUriTemplateMatcher.compile(uriTemplate); 461 } catch (IllegalArgumentException e) { 462 throw iaex(e, "Invalid resource-template registration at index %s (template '%s'): %s", i, uriTemplate, e.getMessage()); 463 } 464 if (! seenTemplates.add(uriTemplate)) 465 throw iaex("Invalid resource-template registration at index %s (template '%s'): duplicate uriTemplate", i, uriTemplate); 466 compiled.put(handler, matcher); 467 } 468 return Collections.unmodifiableMap(compiled); 469 } 470 471 /** 472 * Resolves the completer for one declared variable of a registered resource-template handler, enforcing 473 * in one neutral place - rather than in each dated adapter - that an undeclared variable name never 474 * receives a completer even if the handler itself misbehaves and returns one. 475 * 476 * @param handler The resource-template handler. Must not be {@code null}, and its 477 * {@link McpResourceTemplateHandler#descriptor()}.{@code uriTemplate} must be well-formed (as enforced by 478 * registration validation). 479 * @param variableName The variable name from a {@code completion/complete} request. 480 * @return The handler's completer for that variable, or {@code null} if the variable is not declared by 481 * the template or the handler supplies no completer for it. 482 */ 483 static McpCompleter resourceTemplateCompleter(McpResourceTemplateHandler handler, String variableName) { 484 return resourceTemplateCompleter(handler, variableName, McpUriTemplateMatcher.compile(handler.descriptor().getUriTemplate())); 485 } 486 487 /** 488 * Same contract as {@link #resourceTemplateCompleter(McpResourceTemplateHandler, String)}, but takes an 489 * already-compiled {@code matcher} (typically the one cached in {@link #compiledResourceTemplateMatchers} 490 * for a registered handler) instead of recompiling the template from its {@code uriTemplate} string. 491 */ 492 static McpCompleter resourceTemplateCompleter(McpResourceTemplateHandler handler, String variableName, McpUriTemplateMatcher matcher) { 493 var declared = matcher.variableNames(); 494 return declared.contains(variableName) ? handler.completer(variableName) : null; 495 } 496 497 /** 498 * Resolves the concrete {@code resources/read} request URI {@code uri} against the registered 499 * resource-template registry, selecting the single winning template handler using the deterministic 500 * specificity ranking shared identically by both dated adapters. 501 * 502 * <p> 503 * A candidate is every reverse-matchable ({@link McpUriTemplateMatcher#isReverseMatchable()}) template 504 * handler whose {@link McpUriTemplateMatcher#match(String)} succeeds against {@code uri}; a 505 * non-reverse-matchable or non-matching template handler is never a candidate. Successful candidates are 506 * ranked, highest priority first, by exactly: 507 * <ol> 508 * <li>greater {@link McpUriTemplateMatcher#literalOctetCount() literalOctetCount}; 509 * <li>then fewer {@link McpUriTemplateMatcher#variableCount() variableCount}; and 510 * <li>then earlier registration index (candidates are scanned in registration order and a later 511 * candidate only displaces the current winner on a strictly better key, so the first-registered 512 * candidate wins a complete tie). 513 * </ol> 514 * No other tie-breaker - operator type, capture width, descriptor name/title, or match-time value 515 * length - is consulted. 516 * 517 * <p> 518 * This method does not consult exact {@link McpResourceHandler} registrations. Exact lookup is 519 * unconditionally higher precedence than any template match; callers must perform exact lookup first 520 * and invoke this method only on an exact miss. 521 * 522 * @param uri The concrete request URI. Can be {@code null}, in which case this method returns 523 * {@code null}. 524 * @return The winning match (handler plus its immutable, insertion-ordered decoded variable map), or 525 * {@code null} if no registered reverse-matchable template matches {@code uri}. 526 */ 527 public ResourceTemplateMatch resolveResourceTemplate(String uri) { 528 if (uri == null) 529 return null; 530 McpResourceTemplateHandler winner = null; 531 Map<String,String> winnerVariables = null; 532 McpUriTemplateMatcher winnerMatcher = null; 533 var templates = getResourceTemplates(); 534 var matchers = compiledResourceTemplateMatchers; 535 for (var handler : templates) { 536 if (handler == null) 537 continue; 538 var descriptor = handler.descriptor(); 539 if (descriptor == null) 540 continue; 541 var matcher = matchers.get(handler); 542 if (matcher == null) 543 matcher = McpUriTemplateMatcher.compile(descriptor.getUriTemplate()); 544 if (! matcher.isReverseMatchable()) 545 continue; 546 var variables = matcher.match(uri); 547 if (variables == null) 548 continue; 549 if (winner == null 550 || matcher.literalOctetCount() > winnerMatcher.literalOctetCount() 551 || (matcher.literalOctetCount() == winnerMatcher.literalOctetCount() 552 && matcher.variableCount() < winnerMatcher.variableCount())) { 553 winner = handler; 554 winnerVariables = variables; 555 winnerMatcher = matcher; 556 } 557 } 558 return winner == null ? null : new ResourceTemplateMatch(winner, winnerVariables); 559 } 560 561 /** 562 * The result of {@link #resolveResourceTemplate(String)}: the winning resource-template handler paired 563 * with the variables decoded from the concrete request URI. 564 * 565 * @param handler The winning handler. Never {@code null}. 566 * @param variables The immutable, insertion-ordered map of the handler's declared variables to their 567 * decoded values, captured from the request URI. Never {@code null}. 568 */ 569 public record ResourceTemplateMatch(McpResourceTemplateHandler handler, Map<String,String> variables) {} 570 571 /** 572 * Resolves the completer registered for one declared argument of a registered prompt, by exact prompt name 573 * and exact argument name. 574 * 575 * <p> 576 * This is one half of the neutral cross-target completion lookup shared identically by both dated 577 * adapters; see {@link #templateCompleter(String, String)} for the resource-template half and 578 * {@link #completer(McpCompletionRef, String)} for the unified entry point. 579 * 580 * @param promptName The exact prompt name to look up. Can be {@code null}, in which case this method 581 * returns {@code null}. 582 * @param argumentName The exact argument name to look up. Can be {@code null}, in which case this method 583 * returns {@code null}. 584 * @return The argument's completer, or {@code null} if the prompt is unknown, the argument is undeclared, 585 * or the argument declares no completer. Never invokes the completer. 586 */ 587 public McpCompleter promptCompleter(String promptName, String argumentName) { 588 if (promptName == null || argumentName == null) 589 return null; 590 for (var handler : getPrompts()) { 591 if (handler == null) 592 continue; 593 var descriptor = handler.descriptor(); 594 if (descriptor == null || neq(promptName, descriptor.getName())) 595 continue; 596 var arguments = descriptor.getArguments(); 597 return arguments == null ? null : completerForArgument(arguments, argumentName); 598 } 599 return null; 600 } 601 602 /** 603 * Scans one prompt's declared arguments for the exact-named argument's completer, extracted from 604 * {@link #promptCompleter(String, String)} to keep that method's cognitive complexity within the 605 * project's guard-clause-density threshold. 606 */ 607 private static McpCompleter completerForArgument(List<McpPromptArgument> arguments, String argumentName) { 608 for (var argument : arguments) 609 if (argument != null && eq(argumentName, argument.getName())) 610 return argument.getCompleter(); 611 return null; 612 } 613 614 /** 615 * Resolves the completer registered for one declared variable of a registered resource-template, by exact 616 * registered {@code uriTemplate} string and exact variable name. 617 * 618 * <p> 619 * Matching is against the exact registered template string, not a reverse-match against a concrete URI: a 620 * {@code completion/complete} reference names the declaration being completed, never a concrete/expanded 621 * URI. This is one half of the neutral cross-target completion lookup shared identically by both dated 622 * adapters; see {@link #promptCompleter(String, String)} for the prompt half and 623 * {@link #completer(McpCompletionRef, String)} for the unified entry point. Delegates the "variable must be 624 * declared by the template" check to {@link #resourceTemplateCompleter(McpResourceTemplateHandler, String)} 625 * rather than re-parsing. 626 * 627 * @param uriTemplate The exact registered {@code uriTemplate} to look up. Can be {@code null}, in which 628 * case this method returns {@code null}. 629 * @param variableName The exact variable name to look up. Can be {@code null}, in which case this method 630 * returns {@code null}. 631 * @return The variable's completer, or {@code null} if the template is unregistered, the variable is 632 * undeclared, or the handler declares no completer for it. Never invokes the completer. 633 */ 634 public McpCompleter templateCompleter(String uriTemplate, String variableName) { 635 if (uriTemplate == null || variableName == null) 636 return null; 637 var templates = getResourceTemplates(); 638 var matchers = compiledResourceTemplateMatchers; 639 for (var handler : templates) { 640 if (handler == null) 641 continue; 642 var descriptor = handler.descriptor(); 643 if (descriptor == null || ! uriTemplate.equals(descriptor.getUriTemplate())) 644 continue; 645 var matcher = matchers.get(handler); 646 return matcher != null ? resourceTemplateCompleter(handler, variableName, matcher) : resourceTemplateCompleter(handler, variableName); 647 } 648 return null; 649 } 650 651 /** 652 * Unified neutral completion lookup: resolves the completer for a {@code completion/complete} target, 653 * dispatching by {@link McpCompletionRef.Kind} to {@link #promptCompleter(String, String)} or 654 * {@link #templateCompleter(String, String)}. 655 * 656 * <p> 657 * Both dated adapters call this same method for both target kinds, so template/prompt resolution policy is 658 * defined exactly once, in this neutral module. 659 * 660 * @param ref The completion reference identifying the exact prompt name or registered template string. 661 * Can be {@code null}, in which case this method returns {@code null}. 662 * @param argumentName The exact argument/variable name to look up. Can be {@code null}, in which case this 663 * method returns {@code null}. 664 * @return The resolved completer, or {@code null} if {@code ref} is {@code null}/has no {@link 665 * McpCompletionRef#getKind() kind}, or if the target-specific lookup finds none. Never invokes the 666 * completer. 667 */ 668 public McpCompleter completer(McpCompletionRef ref, String argumentName) { 669 if (ref == null || ref.getKind() == null) 670 return null; 671 if (ref.getKind() == McpCompletionRef.Kind.PROMPT) 672 return promptCompleter(ref.getTarget(), argumentName); 673 return templateCompleter(ref.getTarget(), argumentName); 674 } 675 676 /** 677 * Reports whether {@code completions} capability should be advertised: whether at least one registered 678 * prompt argument or resource-template variable currently declares a non-{@code null} completer. 679 * 680 * <p> 681 * This never invokes a completer; it only detects registration, so both dated adapters can use it for 682 * auto-derived capability advertisement without side effects. 683 * 684 * @return {@code true} if at least one prompt-argument completer or resource-template variable completer 685 * is registered. 686 */ 687 public boolean hasAnyCompleter() { 688 for (var handler : getPrompts()) { 689 if (handler == null) 690 continue; 691 var descriptor = handler.descriptor(); 692 var arguments = descriptor == null ? null : descriptor.getArguments(); 693 if (arguments == null) 694 continue; 695 for (var argument : arguments) 696 if (argument != null && argument.getCompleter() != null) 697 return true; 698 } 699 var templates = getResourceTemplates(); 700 var matchers = compiledResourceTemplateMatchers; 701 for (var handler : templates) { 702 if (handler == null) 703 continue; 704 var descriptor = handler.descriptor(); 705 var uriTemplate = descriptor == null ? null : descriptor.getUriTemplate(); 706 if (uriTemplate == null) 707 continue; 708 var matcher = matchers.get(handler); 709 var variableNames = matcher != null ? matcher.variableNames() : McpUriTemplateMatcher.compile(uriTemplate).variableNames(); 710 for (var variableName : variableNames) 711 if (handler.completer(variableName) != null) 712 return true; 713 } 714 return false; 715 } 716 717 /** 718 * Pagination strategy for {@code list} dispatchers (tools / prompts / resources / resource templates). 719 * 720 * @return The cursor. Never {@code null} (defaults to {@link McpCursor#SINGLE_PAGE}). 721 */ 722 public McpCursor getCursor() { 723 return cursor; 724 } 725 726 /** 727 * Sets the pagination strategy. 728 * 729 * @param cursor The new value (or {@code null} to reset to {@link McpCursor#SINGLE_PAGE}). 730 * @return This object (for method chaining). 731 */ 732 public McpServerConfig setCursor(McpCursor cursor) { 733 this.cursor = cursor == null ? McpCursor.SINGLE_PAGE : cursor; 734 return this; 735 } 736}