001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.util.Collections; 025import java.util.LinkedHashMap; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Optional; 030import java.util.Set; 031 032import static java.util.Objects.requireNonNull; 033 034/** 035 * Immutable programmatic registration for one MCP prompt. 036 * 037 * <p>Registration is staged so a handler must be supplied before optional 038 * descriptor metadata can be configured and the registration built. Prompt 039 * arguments are flat strings and do not use JSON Schema. 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043@ThreadSafe 044public final class McpPromptRegistration { 045 @NonNull 046 private final String name; 047 @NonNull 048 private final Set<@NonNull McpProtocolVersion> protocolVersions; 049 @Nullable 050 private final String title; 051 @Nullable 052 private final String description; 053 @NonNull 054 private final List<@NonNull McpIcon> icons; 055 @NonNull 056 private final List<@NonNull McpPromptArgumentDeclaration> arguments; 057 @NonNull 058 private final List<@NonNull McpInputRequestDeclaration> inputRequestDeclarations; 059 @NonNull 060 private final McpRequestStateMode requestStateMode; 061 @NonNull 062 private final McpJsonObject metadata; 063 @NonNull 064 private final McpPromptHandler handler; 065 @Nullable 066 private final McpCompletionHandler completionHandler; 067 @NonNull 068 private final Set<@NonNull McpProtocolVersion> completionProtocolVersions; 069 070 /** 071 * Begins a staged registration for a named prompt. 072 * 073 * @param name nonblank prompt name published to MCP clients 074 * @param protocolVersions nonempty exact revisions serving this prompt 075 * @return required-handler stage 076 * @throws IllegalArgumentException if {@code name} is blank 077 */ 078 @NonNull 079 public static HandlerStage withName(@NonNull String name, 080 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 081 return new HandlerStage(requireName(name), 082 McpProtocolVersion.requiredSet(protocolVersions)); 083 } 084 085 private McpPromptRegistration(@NonNull Builder builder) { 086 this.name = builder.name; 087 this.protocolVersions = builder.protocolVersions; 088 this.title = builder.title; 089 this.description = builder.description; 090 this.icons = List.copyOf(builder.icons); 091 this.arguments = immutableArguments(builder.arguments); 092 this.inputRequestDeclarations = 093 List.copyOf(builder.inputRequestDeclarations); 094 this.requestStateMode = builder.requestStateMode; 095 this.metadata = builder.metadata; 096 this.handler = builder.handler; 097 this.completionHandler = builder.completionHandler; 098 this.completionProtocolVersions = builder.completionProtocolVersions; 099 } 100 101 /** @return published prompt name */ 102 @NonNull 103 public String getName() { 104 return this.name; 105 } 106 107 /** @return exact protocol revisions on which this prompt is available */ 108 @NonNull 109 public Set<@NonNull McpProtocolVersion> getProtocolVersions() { 110 return this.protocolVersions; 111 } 112 113 /** @return human-readable title, if configured */ 114 @NonNull 115 public Optional<@NonNull String> getTitle() { 116 return Optional.ofNullable(this.title); 117 } 118 119 /** @return human-readable description, if configured */ 120 @NonNull 121 public Optional<@NonNull String> getDescription() { 122 return Optional.ofNullable(this.description); 123 } 124 125 /** @return immutable icon list in registration order */ 126 @NonNull 127 public List<@NonNull McpIcon> getIcons() { 128 return this.icons; 129 } 130 131 /** @return immutable argument declarations in registration order */ 132 @NonNull 133 public List<@NonNull McpPromptArgumentDeclaration> getArguments() { 134 return this.arguments; 135 } 136 137 /** 138 * Returns the input requests this prompt operation may emit. 139 * 140 * @return immutable declarations in registration order 141 */ 142 @NonNull 143 public List<@NonNull McpInputRequestDeclaration> 144 getInputRequestDeclarations() { 145 return this.inputRequestDeclarations; 146 } 147 148 /** 149 * Returns the request-state contract for this prompt operation. 150 * 151 * @return request-state mode 152 */ 153 @NonNull 154 public McpRequestStateMode getRequestStateMode() { 155 return this.requestStateMode; 156 } 157 158 /** @return immutable protocol extension metadata */ 159 @NonNull 160 public McpJsonObject getMetadata() { 161 return this.metadata; 162 } 163 164 /** @return prompt handler */ 165 @NonNull 166 public McpPromptHandler getHandler() { 167 return this.handler; 168 } 169 170 /** @return configured argument completer, or empty when omitted */ 171 @NonNull 172 public Optional<@NonNull McpCompletionHandler> getCompletionHandler() { 173 return Optional.ofNullable(this.completionHandler); 174 } 175 176 /** @return exact revisions on which the completion handler is available */ 177 @NonNull 178 public Set<@NonNull McpProtocolVersion> getCompletionProtocolVersions() { 179 return this.completionProtocolVersions; 180 } 181 182 /** 183 * Validates raw string arguments and invokes the prompt handler. 184 */ 185 @NonNull 186 McpOperationResult invoke(@NonNull McpRequestContext request, 187 @NonNull McpJsonObject rawArguments, 188 @NonNull McpInvocationFeatures features) throws Exception { 189 requireNonNull(request); 190 requireNonNull(rawArguments); 191 requireNonNull(features); 192 193 Map<String, McpPromptArgumentDeclaration> definitions = 194 new LinkedHashMap<>(); 195 for (McpPromptArgumentDeclaration argument : this.arguments) 196 definitions.put(argument.getName(), argument); 197 198 Map<String, String> values = new LinkedHashMap<>(); 199 for (Map.Entry<String, McpJsonValue> entry 200 : rawArguments.getMembers().entrySet()) { 201 if (!definitions.containsKey(entry.getKey()) 202 || !(entry.getValue() instanceof McpJsonString string)) 203 throw new McpInvalidPromptArgumentsException(); 204 values.put(entry.getKey(), string.getValue()); 205 } 206 for (McpPromptArgumentDeclaration argument : this.arguments) { 207 if (argument.isRequired() && !values.containsKey(argument.getName())) 208 throw new McpInvalidPromptArgumentsException(); 209 } 210 211 McpPromptGetContext prompt = new DefaultPromptGetContext(values); 212 return requireNonNull(this.handler.handle(request, prompt, features), 213 "The MCP prompt handler returned null."); 214 } 215 216 @NonNull 217 private static String requireName(@NonNull String name) { 218 McpContentValueSupport.requireWellFormedString(name); 219 if (name.isBlank()) 220 throw new IllegalArgumentException( 221 "MCP prompt names must not be blank."); 222 return name; 223 } 224 225 @NonNull 226 private static List<@NonNull McpPromptArgumentDeclaration> immutableArguments( 227 @NonNull List<@NonNull McpPromptArgumentDeclaration> arguments) { 228 List<McpPromptArgumentDeclaration> copied = List.copyOf(arguments); 229 Set<String> names = new LinkedHashSet<>(); 230 for (McpPromptArgumentDeclaration argument : copied) { 231 if (!names.add(argument.getName())) 232 throw new IllegalStateException( 233 "Duplicate MCP prompt argument name: " 234 + argument.getName()); 235 } 236 return copied; 237 } 238 239 /** 240 * Required-handler stage for a named prompt. 241 * 242 * @author <a href="https://www.revetkn.com">Mark Allen</a> 243 */ 244 @NotThreadSafe 245 public static final class HandlerStage { 246 @NonNull 247 private final String name; 248 @NonNull 249 private final Set<@NonNull McpProtocolVersion> protocolVersions; 250 251 private HandlerStage(@NonNull String name, 252 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 253 this.name = McpContentValueSupport.requireWellFormedString(name); 254 this.protocolVersions = requireNonNull(protocolVersions); 255 } 256 257 /** 258 * Supplies the required prompt handler. 259 * 260 * @param handler prompt handler 261 * @return optional-metadata builder 262 */ 263 @NonNull 264 public Builder handler(@NonNull McpPromptHandler handler) { 265 return new Builder(this.name, this.protocolVersions, 266 requireNonNull(handler)); 267 } 268 } 269 270 /** 271 * Mutable builder for an immutable prompt registration. 272 * 273 * @author <a href="https://www.revetkn.com">Mark Allen</a> 274 */ 275 @NotThreadSafe 276 public static final class Builder { 277 @NonNull 278 private final String name; 279 @NonNull 280 private final Set<@NonNull McpProtocolVersion> protocolVersions; 281 @NonNull 282 private final McpPromptHandler handler; 283 @Nullable 284 private String title; 285 @Nullable 286 private String description; 287 @NonNull 288 private List<@NonNull McpIcon> icons = List.of(); 289 @NonNull 290 private List<@NonNull McpPromptArgumentDeclaration> arguments = 291 List.of(); 292 @NonNull 293 private List<@NonNull McpInputRequestDeclaration> 294 inputRequestDeclarations = List.of(); 295 @NonNull 296 private McpRequestStateMode requestStateMode = McpRequestStateMode.NONE; 297 @NonNull 298 private McpJsonObject metadata = McpJsonObject.emptyInstance(); 299 @Nullable 300 private McpCompletionHandler completionHandler; 301 @NonNull 302 private Set<@NonNull McpProtocolVersion> completionProtocolVersions = Set.of(); 303 304 private Builder(@NonNull String name, 305 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions, 306 @NonNull McpPromptHandler handler) { 307 this.name = McpContentValueSupport.requireWellFormedString(name); 308 this.protocolVersions = requireNonNull(protocolVersions); 309 this.handler = requireNonNull(handler); 310 } 311 312 /** @param title human-readable title 313 * @return this builder */ 314 @NonNull 315 public Builder title(@NonNull String title) { 316 this.title = McpContentValueSupport.requireWellFormedString(title); 317 return this; 318 } 319 320 /** @param description human-readable description 321 * @return this builder */ 322 @NonNull 323 public Builder description(@NonNull String description) { 324 this.description = McpContentValueSupport.requireWellFormedString(description); 325 return this; 326 } 327 328 /** 329 * Replaces icon descriptors in supplied order. 330 * Null or empty clears the property. The complete list is validated and 331 * snapshotted before replacing the prior value. 332 * 333 * @param icons icon descriptors, or null to clear 334 * @return this builder 335 * @throws NullPointerException if a list element is null 336 */ 337 @NonNull 338 public Builder icons( 339 @Nullable List<@NonNull McpIcon> icons) { 340 this.icons = icons == null ? List.of() 341 : List.copyOf(icons); 342 return this; 343 } 344 345 /** 346 * Replaces prompt-argument declarations in supplied order. 347 * Null or empty clears the property. The complete list is validated and 348 * snapshotted before replacing the prior value. 349 * 350 * @param argumentDeclarations prompt-argument declarations, or null to clear 351 * @return this builder 352 * @throws NullPointerException if a list element is null 353 */ 354 @NonNull 355 public Builder arguments( 356 @Nullable List<@NonNull McpPromptArgumentDeclaration> argumentDeclarations) { 357 this.arguments = argumentDeclarations == null ? List.of() 358 : List.copyOf(argumentDeclarations); 359 return this; 360 } 361 362 /** 363 * Replaces input-request declarations in supplied order. 364 * Null or empty clears the property. The complete list is validated and 365 * snapshotted before replacing the prior value. 366 * 367 * @param inputRequestDeclarations input-request declarations, or null to clear 368 * @return this builder 369 * @throws NullPointerException if a list element is null 370 */ 371 @NonNull 372 public Builder inputRequestDeclarations( 373 @Nullable List<@NonNull McpInputRequestDeclaration> inputRequestDeclarations) { 374 this.inputRequestDeclarations = inputRequestDeclarations == null ? List.of() 375 : List.copyOf(inputRequestDeclarations); 376 return this; 377 } 378 379 /** 380 * Sets the request-state contract for this prompt operation. 381 * 382 * @param requestStateMode request-state mode 383 * @return this builder 384 */ 385 @NonNull 386 public Builder requestStateMode( 387 @NonNull McpRequestStateMode requestStateMode) { 388 this.requestStateMode = requireNonNull(requestStateMode); 389 return this; 390 } 391 392 /** @param metadata protocol extension metadata 393 * @return this builder */ 394 @NonNull 395 public Builder metadata(@NonNull McpJsonObject metadata) { 396 this.metadata = requireNonNull(metadata); 397 return this; 398 } 399 400 /** 401 * Configures the handler for partial prompt-argument suggestions. 402 * Completion is supported for MCP 2025-06-18, 2025-11-25, and 403 * 2026-07-28. Repeated calls replace the previous handler and revision 404 * selection. 405 * 406 * @param completionHandler non-null argument completer 407 * @param protocolVersions nonempty subset of this prompt's revisions 408 * @return this builder 409 */ 410 @NonNull 411 public Builder completionHandler( 412 @NonNull McpCompletionHandler completionHandler, 413 @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) { 414 requireNonNull(completionHandler); 415 Set<McpProtocolVersion> versions = McpProtocolVersion.requiredSet(protocolVersions); 416 if (!this.protocolVersions.containsAll(versions)) 417 throw new IllegalArgumentException( 418 "MCP prompt completion revisions must be a subset of prompt revisions."); 419 this.completionHandler = completionHandler; 420 this.completionProtocolVersions = versions; 421 return this; 422 } 423 424 /** 425 * Builds an immutable prompt registration. 426 * 427 * @return prompt registration 428 * @throws IllegalStateException if argument names are duplicated 429 */ 430 @NonNull 431 public McpPromptRegistration build() { 432 return new McpPromptRegistration(this); 433 } 434 } 435 436 /** 437 * Immutable prompt argument projection. 438 * 439 * @author <a href="https://www.revetkn.com">Mark Allen</a> 440 */ 441 @ThreadSafe 442 private static final class DefaultPromptGetContext 443 implements McpPromptGetContext { 444 @NonNull 445 private final Map<@NonNull String, @NonNull String> arguments; 446 447 private DefaultPromptGetContext( 448 @NonNull Map<@NonNull String, @NonNull String> arguments) { 449 this.arguments = Collections.unmodifiableMap( 450 new LinkedHashMap<>(requireNonNull(arguments))); 451 } 452 453 @Override 454 @NonNull 455 public Map<@NonNull String, @NonNull String> getArguments() { 456 return this.arguments; 457 } 458 459 @Override 460 @NonNull 461 public Optional<@NonNull String> findArgument(@NonNull String name) { 462 return Optional.ofNullable(this.arguments.get(McpContentValueSupport.requireWellFormedString(name))); 463 } 464 } 465}