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.v20250618; 018 019import java.io.*; 020import java.util.*; 021import java.util.concurrent.atomic.*; 022 023import org.apache.juneau.bean.jsonrpc.*; 024import org.apache.juneau.bean.mcp.v20250618.*; 025import org.apache.juneau.marshall.collections.*; 026import org.apache.juneau.marshall.json.*; 027import org.apache.juneau.rest.client.mcp.*; 028 029/** 030 * Typed <c>2025-06-18</c> Model Context Protocol (MCP) client facade. 031 * 032 * <p> 033 * Extends {@link AbstractMcpClient}, adding one typed method per <c>2025-06-18</c> JSON-RPC method 034 * (see {@link McpMethods}). Every method builds the corresponding v1 request bean, wraps it in a 035 * {@link JsonRpcRequest}, delegates to the inherited {@link #send(JsonRpcRequest)}, and then either returns the 036 * <c>result</c> re-marshaled into its typed v1 result bean, or throws an {@link McpException} built from the 037 * response's <c>error</c>. 038 * 039 * @since 10.0.0 040 */ 041public class McpClient extends AbstractMcpClient { 042 043 private static final JsonParser RESULT_PARSER = JsonParser.create() 044 .typePropertyName(Content.class, "type") 045 .typePropertyName(ResourceContents.class, "type") 046 .build(); 047 048 private static final JsonSerializer REQUEST_SERIALIZER = JsonSerializer.create() 049 .addBeanTypes() 050 .typePropertyName(CompletionReference.class, "type") 051 .build(); 052 053 private final AtomicLong nextId = new AtomicLong(1); 054 055 /** 056 * Constructor. 057 * 058 * @param builder The builder supplying this client's configuration. Must not be <jk>null</jk>. 059 */ 060 protected McpClient(Builder builder) { 061 super(builder); 062 } 063 064 /** 065 * Creates a new builder. 066 * 067 * @return A new builder. Never <jk>null</jk>. 068 */ 069 public static Builder builder() { 070 return new Builder(); 071 } 072 073 /** 074 * Sends {@value McpMethods#INITIALIZE}. 075 * 076 * @return The initialize result. Never <jk>null</jk> on success unless the server returns a <jk>null</jk> result. 077 * @throws IOException If a transport-level or (de)serialization error occurs. 078 * @throws McpException If the server returned a JSON-RPC error. 079 */ 080 public InitializeResult initialize() throws IOException { 081 var params = new InitializeRequest() 082 .setProtocolVersion(McpProtocol.VERSION_2025_06_18) 083 .setCapabilities(new ClientCapabilities()) 084 .setClientInfo(new Implementation().setName("juneau-rest-client-mcp-v20250618").setVersion(McpProtocol.VERSION_2025_06_18)); 085 return call(McpMethods.INITIALIZE, params, InitializeResult.class); 086 } 087 088 /** 089 * Sends {@value McpMethods#PING} to verify server liveness. 090 * 091 * @throws IOException If a transport-level or (de)serialization error occurs. 092 * @throws McpException If the server returned a JSON-RPC error. 093 */ 094 public void ping() throws IOException { 095 call(McpMethods.PING, null, JsonMap.class); 096 } 097 098 /** 099 * Sends {@value McpMethods#TOOLS_LIST}. 100 * 101 * @return The tools-list result. Never <jk>null</jk> on success unless the server returns a <jk>null</jk> result. 102 * @throws IOException If a transport-level or (de)serialization error occurs. 103 * @throws McpException If the server returned a JSON-RPC error. 104 */ 105 public ListToolsResult listTools() throws IOException { 106 return call(McpMethods.TOOLS_LIST, null, ListToolsResult.class); 107 } 108 109 /** 110 * Sends {@value McpMethods#TOOLS_CALL}. 111 * 112 * @param name The tool name to invoke. 113 * @param arguments The tool arguments. Can be <jk>null</jk>. 114 * @return The call-tool result. Never <jk>null</jk> on success unless the server returns a <jk>null</jk> result. 115 * @throws IOException If a transport-level or (de)serialization error occurs. 116 * @throws McpException If the server returned a JSON-RPC error. 117 */ 118 public CallToolResult callTool(String name, Map<String,Object> arguments) throws IOException { 119 var params = new CallToolRequest().setName(name).setArguments(arguments); 120 return call(McpMethods.TOOLS_CALL, params, CallToolResult.class); 121 } 122 123 /** 124 * Sends {@value McpMethods#PROMPTS_LIST}. 125 * 126 * @return The prompts-list result. Never <jk>null</jk> on success unless the server returns a <jk>null</jk> result. 127 * @throws IOException If a transport-level or (de)serialization error occurs. 128 * @throws McpException If the server returned a JSON-RPC error. 129 */ 130 public ListPromptsResult listPrompts() throws IOException { 131 return call(McpMethods.PROMPTS_LIST, null, ListPromptsResult.class); 132 } 133 134 /** 135 * Sends {@value McpMethods#PROMPTS_GET}. 136 * 137 * @param name The prompt name to fetch. 138 * @param arguments The prompt argument values. Can be <jk>null</jk>. 139 * @return The get-prompt result. Never <jk>null</jk> on success unless the server returns a <jk>null</jk> result. 140 * @throws IOException If a transport-level or (de)serialization error occurs. 141 * @throws McpException If the server returned a JSON-RPC error. 142 */ 143 public GetPromptResult getPrompt(String name, Map<String,Object> arguments) throws IOException { 144 var params = new GetPromptRequest().setName(name).setArguments(arguments); 145 return call(McpMethods.PROMPTS_GET, params, GetPromptResult.class); 146 } 147 148 /** 149 * Sends {@value McpMethods#RESOURCES_LIST}. 150 * 151 * @return The resources-list result. Never <jk>null</jk> on success unless the server returns a <jk>null</jk> result. 152 * @throws IOException If a transport-level or (de)serialization error occurs. 153 * @throws McpException If the server returned a JSON-RPC error. 154 */ 155 public ListResourcesResult listResources() throws IOException { 156 return call(McpMethods.RESOURCES_LIST, null, ListResourcesResult.class); 157 } 158 159 /** 160 * Sends {@value McpMethods#RESOURCES_READ}. 161 * 162 * @param uri The resource URI to read. 163 * @return The read-resource result. Never <jk>null</jk> on success unless the server returns a <jk>null</jk> result. 164 * @throws IOException If a transport-level or (de)serialization error occurs. 165 * @throws McpException If the server returned a JSON-RPC error. 166 */ 167 public ReadResourceResult readResource(String uri) throws IOException { 168 var params = new ReadResourceRequest().setUri(uri); 169 return call(McpMethods.RESOURCES_READ, params, ReadResourceResult.class); 170 } 171 172 /** 173 * Sends {@value McpMethods#RESOURCES_TEMPLATES_LIST}. 174 * 175 * @return The resource-templates-list result. Never <jk>null</jk> on success unless the server returns a 176 * <jk>null</jk> result. 177 * @throws IOException If a transport-level or (de)serialization error occurs. 178 * @throws McpException If the server returned a JSON-RPC error. 179 */ 180 public ListResourceTemplatesResult listResourceTemplates() throws IOException { 181 return call(McpMethods.RESOURCES_TEMPLATES_LIST, null, ListResourceTemplatesResult.class); 182 } 183 184 /** 185 * Sends {@value McpMethods#COMPLETION_COMPLETE}. 186 * 187 * @param ref The completion target (a declared prompt or resource template reference). 188 * @param argument The prompt argument or resource-template variable being completed, and its current partial 189 * value. 190 * @return The completion result. Never <jk>null</jk> on success unless the server returns a <jk>null</jk> result. 191 * @throws IOException If a transport-level or (de)serialization error occurs. 192 * @throws McpException If the server returned a JSON-RPC error. 193 */ 194 public CompleteResult complete(CompletionReference ref, CompletionArgument argument) throws IOException { 195 var params = new CompleteRequest().setRef(ref).setArgument(argument); 196 return call(McpMethods.COMPLETION_COMPLETE, params, CompleteResult.class); 197 } 198 199 private static Object toWireParams(Object params) { 200 if (params == null) 201 return null; 202 return JsonParser.DEFAULT.read(REQUEST_SERIALIZER.write(params), Object.class); 203 } 204 205 /** 206 * Builds and sends a JSON-RPC request for {@code method}, mapping a JSON-RPC {@code error} to a thrown 207 * {@link McpException} and a non-<jk>null</jk> {@code result} to {@code resultType} via {@link #RESULT_PARSER}. 208 * 209 * <p> 210 * {@code params}, if non-<jk>null</jk>, is uniformly pre-flattened through {@link #toWireParams(Object)} 211 * before being placed on the request - the same shape the v2 ({@code 2026-07-28}) adapter's {@code call(...)} 212 * applies unconditionally, so a param bean with a polymorphic field (e.g. {@link CompletionReference}) is 213 * never accidentally sent through {@link org.apache.juneau.rest.client.RestClient}'s 214 * {@code addBeanTypes=false} default serializer without its type discriminator, regardless of which typed 215 * method call reaches this shared path. 216 * 217 * @param <T> The typed v1 result bean type. 218 * @param method The JSON-RPC method name (see {@link McpMethods}). 219 * @param params The method params bean, or <jk>null</jk> for methods that take none. 220 * @param resultType The typed v1 result bean class to re-marshal a non-<jk>null</jk> result into. 221 * @return The typed result, or <jk>null</jk> if the response's {@code result} was <jk>null</jk>. 222 * @throws IOException If a transport-level or (de)serialization error occurs. 223 * @throws McpException If the server returned a JSON-RPC error. 224 */ 225 private <T> T call(String method, Object params, Class<T> resultType) throws IOException { 226 var request = new JsonRpcRequest() 227 .setJsonrpc(McpProtocol.JSON_RPC_2_0) 228 .setId(nextId.getAndIncrement()) 229 .setMethod(method) 230 .setParams(toWireParams(params)); 231 var response = send(request); 232 var error = response.getError(); 233 if (error != null) 234 throw McpException.fromJsonRpcError(error); 235 var result = response.getResult(); 236 if (result == null) 237 return null; 238 return RESULT_PARSER.read(JsonSerializer.DEFAULT.toString(result), resultType); 239 } 240 241 // ----------------------------------------------------------------------------------------------------------------- 242 // Builder 243 // ----------------------------------------------------------------------------------------------------------------- 244 245 /** 246 * Fluent builder for {@link McpClient}. 247 * 248 * @since 10.0.0 249 */ 250 public static class Builder extends AbstractMcpClient.Builder<Builder> { 251 252 /** 253 * Builds the client. 254 * 255 * @return A new {@link McpClient}. Never <jk>null</jk>. 256 */ 257 @SuppressWarnings({ 258 "resource" // factory hands the built client to the caller, who must close it (McpClient implements Closeable). 259 }) 260 public McpClient build() { 261 return new McpClient(this); 262 } 263 } 264}