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}