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}