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 &mdash; 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 &mdash; 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} &mdash; 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} &mdash; 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}