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 java.util.*;
020
021import org.apache.juneau.bean.jsonrpc.*;
022
023/**
024 * Shared internal plumbing for revision {@link McpRevision#dispatch} implementations.
025 *
026 * <p>
027 * Untyped-parameter coercion and typed-argument extraction. Not a general-purpose public API: the
028 * methods are {@code public} only because a revision adapter lives in a different package from this
029 * one ({@code org.apache.juneau.rest.server.mcp.v20250618} vs. {@code org.apache.juneau.rest.server.mcp}),
030 * and Java package-private visibility does not span that boundary.
031 *
032 * <p>
033 * The {@code -32602} code these methods raise is the <b>JSON-RPC 2.0 standard</b> "Invalid params"
034 * code, not a per-revision choice, which is why it is fixed here rather than routed through
035 * {@link McpRevision#errorCode(McpErrorKind)}. A revision that needs a different code for malformed
036 * parameters must coerce parameters itself instead of using these helpers.
037 */
038@SuppressWarnings({
039   "unchecked" // Cast is safe: type parameter verified by MCP protocol contract.
040})
041public final class McpParamUtils {
042
043   private static final int CODE_INVALID_PARAMS = -32602;
044
045   private McpParamUtils() {}
046
047   /**
048    * Coerces a JSON-RPC {@code params} value to a string-keyed map.
049    *
050    * @param params The raw params value. Can be <jk>null</jk>.
051    * @return The params as a map; an empty map when {@code params} is <jk>null</jk>. Never <jk>null</jk>.
052    * @throws McpException If {@code params} is present but is not a JSON object.
053    */
054   public static Map<String,Object> asMap(Object params) {
055      if (params == null)
056         return Map.of();
057      if (params instanceof Map)
058         return (Map<String,Object>) params;
059      throw new McpException(CODE_INVALID_PARAMS, "Params must be an object");
060   }
061
062   /**
063    * Reads a parameter as a string.
064    *
065    * @param args The params map. Never <jk>null</jk>.
066    * @param key The parameter name.
067    * @return The value's {@code toString()}, or <jk>null</jk> if absent.
068    */
069   public static String strParam(Map<String,Object> args, String key) {
070      var v = args.get(key);
071      return v == null ? null : v.toString();
072   }
073
074   /**
075    * Reads a parameter as a nested string-keyed map.
076    *
077    * @param args The params map. Never <jk>null</jk>.
078    * @param key The parameter name.
079    * @return The nested map; an empty map when the parameter is absent. Never <jk>null</jk>.
080    * @throws McpException If the parameter is present but is not a JSON object.
081    */
082   public static Map<String,Object> mapParam(Map<String,Object> args, String key) {
083      var v = args.get(key);
084      if (v == null)
085         return Map.of();
086      if (v instanceof Map)
087         return (Map<String,Object>) v;
088      throw new McpException(CODE_INVALID_PARAMS, "Param '" + key + "' must be an object");
089   }
090
091   /**
092    * Reads a parameter as a string, rejecting any non-string JSON value rather than coercing it via
093    * {@code toString()}.
094    *
095    * <p>
096    * Unlike {@link #strParam(Map, String)}, which accepts any JSON scalar and stringifies it, this is for
097    * parameters whose protocol contract requires the wire value to already be a JSON string - for example a
098    * {@code completion/complete} argument value, where a numeric or boolean value must be rejected outright,
099    * not silently stringified. Shared identically by both dated {@code completion/complete} dispatch arms.
100    *
101    * @param args The params map. Never <jk>null</jk>.
102    * @param key The parameter name.
103    * @return The string value, or <jk>null</jk> if absent.
104    * @throws McpException If the parameter is present but is not a JSON string.
105    */
106   public static String strictStrParam(Map<String,Object> args, String key) {
107      var v = args.get(key);
108      if (v == null)
109         return null;
110      if (v instanceof String v2)
111         return v2;
112      throw new McpException(CODE_INVALID_PARAMS, "Param '" + key + "' must be a string");
113   }
114
115   /**
116    * Reads a parameter as a string-to-string map, rejecting any entry whose value is not a JSON string.
117    *
118    * <p>
119    * Shared identically by both dated {@code completion/complete} dispatch arms to parse
120    * {@code context.arguments}.
121    *
122    * @param args The params map. Never <jk>null</jk>.
123    * @param key The parameter name.
124    * @return The nested map with every value verified to be a string; an empty map when the parameter is
125    *    absent. Never <jk>null</jk>.
126    * @throws McpException If the parameter is present but is not a JSON object, or if any of its values is
127    *    not a JSON string.
128    */
129   public static Map<String,String> strictStrMapParam(Map<String,Object> args, String key) {
130      var nested = mapParam(args, key);
131      var result = new LinkedHashMap<String,String>();
132      for (var entry : nested.entrySet()) {
133         var value = entry.getValue();
134         if (! (value instanceof String value2))
135            throw new McpException(CODE_INVALID_PARAMS, "Param '" + key + "' values must be strings");
136         result.put(entry.getKey(), value2);
137      }
138      return result;
139   }
140}