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.client.mcp.v20260728; 018 019 020import java.util.*; 021 022import org.apache.juneau.bean.mcp.v20260728.*; 023import org.apache.juneau.marshall.marshaller.*; 024import static org.apache.juneau.commons.utils.Shorts.*; 025 026/** 027 * Static helper for reading server→client MCP {@code 2026-07-28} SEP-2322 elicitation requests out of 028 * {@link McpClient#callRaw}'s raw {@code Map<String,Object>} result (MRTR, the Multi-Round-Trip Requests 029 * loop, client side). 030 * 031 * <p> 032 * Paired with {@link ElicitationResponses}, which encodes the client's answers back into the wire shape a resume 033 * call expects — the same {@code ElicitationRequests}/{@code ElicitationResponses} split the server side 034 * already uses (server {@code ElicitationRequests} builds the pause signal; server {@code ElicitationResponses} 035 * reads the resumed answers). 036 * 037 * <p> 038 * Works at the raw map/JSON level rather than exposing typed overloads per concrete request bean, because 039 * {@code CallToolRequest}/{@code GetPromptRequest}/{@code ReadResourceRequest} share no common 040 * "has-inputResponses-and-requestState" interface (each independently declares its own pair of fields; their 041 * actual common base, {@code RequestParams<T>}, carries only {@code _meta}). A caller resuming, say, a 042 * {@code CallToolRequest} calls {@link ElicitationResponses#toInputResponses(Map)} for the raw payload, then 043 * makes its own {@code .setInputResponses(...).setRequestState(...)} call on the concrete bean it already knows 044 * it holds. 045 * 046 * @since 10.0.0 047 */ 048public final class ElicitationRequests { 049 050 private ElicitationRequests() {} 051 052 /** 053 * Whether a {@link McpClient#callRaw} result represents a paused elicitation (or any other MRTR pause). 054 * 055 * @param raw The raw result from {@link McpClient#callRaw}. Must not be <jk>null</jk>. 056 * @return <jk>true</jk> if {@code raw.get("resultType")} is {@code "input_required"}. 057 * @throws IllegalArgumentException If {@code raw} is <jk>null</jk>. 058 */ 059 public static boolean isInputRequired(Map<String,Object> raw) { 060 reqnn("raw", raw); 061 return eq(raw.get("resultType"), "input_required"); 062 } 063 064 /** 065 * Decodes {@code raw}'s {@code inputRequests} into typed {@link ElicitRequest}s. 066 * 067 * <p> 068 * Each raw value is generic JSON (a {@code Map} after wire deserialization, per {@code InputRequiredResult}'s 069 * lossless-map contract), converted via the same {@code Json.to(Json.of(value), type)} technique 070 * {@code McpMrtrResumeContext.continuationAs(Class)} demonstrates server-side. A value that is present but 071 * <jk>null</jk> decodes to a <jk>null</jk> entry (see the return doc); this mirrors 072 * {@code ElicitationResponses.all(McpMrtrResumeContext)}'s null-passthrough on the server side. 073 * 074 * <p> 075 * <b>Every</b> entry is decoded as an {@link ElicitRequest}, unconditionally — per 076 * {@code InputRequiredResult}'s Javadoc, {@code inputRequests} entries may in principle be arbitrary 077 * sub-request kinds (sampling/roots/elicitation), not only elicitation. A non-elicitation entry (one that 078 * shares no field names with {@link ElicitRequest}) simply decodes to an all-<jk>null</jk>-fields 079 * {@link ElicitRequest}, since the underlying {@code Json.to} conversion tolerates unknown/missing bean 080 * properties; it is the caller's responsibility to only call this for elicitation-flavored MRTR pauses. 081 * 082 * @param raw The raw result from {@link McpClient#callRaw}. Must not be <jk>null</jk>. 083 * @return A keyed map of typed requests. Never <jk>null</jk> (empty if {@code raw} carried none). Values 084 * may be <jk>null</jk> when the corresponding raw entry is absent/<jk>null</jk>. 085 * @throws IllegalArgumentException If {@code raw} is <jk>null</jk>. 086 * @throws ClassCastException If {@code raw.get("inputRequests")} is present but is not, at runtime, a 087 * {@code Map} — which can only happen against a malformed/non-conforming remote response, since a 088 * well-formed one always deserializes {@code inputRequests} to a {@code Map} per JSON semantics. 089 * @throws RuntimeException If any entry's decoded shape cannot be converted to {@link ElicitRequest}. 090 */ 091 @SuppressWarnings({ 092 "unchecked" // raw.get("inputRequests") deserializes to a Map<String,Object> from JSON for any well-formed remote response; a malformed one surfaces as ClassCastException here (see @throws), not a silently-wrong cast. 093 }) 094 public static Map<String,ElicitRequest> requests(Map<String,Object> raw) { 095 reqnn("raw", raw); 096 var rawRequests = (Map<String,Object>) raw.get("inputRequests"); 097 Map<String,ElicitRequest> out = new LinkedHashMap<>(); 098 if (rawRequests != null) 099 rawRequests.forEach((id, value) -> out.put(id, value == null ? null : Json.to(Json.of(value), ElicitRequest.class))); 100 return out; 101 } 102 103 /** 104 * Returns {@code raw}'s echoed continuation token. 105 * 106 * @param raw The raw result from {@link McpClient#callRaw}. Must not be <jk>null</jk>. 107 * @return The token, or <jk>null</jk> if absent. 108 * @throws IllegalArgumentException If {@code raw} is <jk>null</jk>. 109 * @throws ClassCastException If {@code raw.get("requestState")} is present but is not, at runtime, a 110 * {@code String} — only possible against a malformed/non-conforming remote response. 111 */ 112 public static String requestState(Map<String,Object> raw) { 113 reqnn("raw", raw); 114 return (String) raw.get("requestState"); 115 } 116}